| 191 |
rodolico |
1 |
#! /usr/bin/env perl
|
|
|
2 |
|
|
|
3 |
=head1 NAME
|
|
|
4 |
|
|
|
5 |
backdoor - Network Backdoor Management Script
|
|
|
6 |
|
|
|
7 |
=head1 SYNOPSIS
|
|
|
8 |
|
|
|
9 |
backdoor [options] # Check remote sites and bring up/down accordingly
|
|
|
10 |
backdoor [options] up # Manually bring up the backdoor interface
|
|
|
11 |
backdoor [options] down # Manually bring down the backdoor interface
|
|
|
12 |
|
|
|
13 |
Options:
|
|
|
14 |
--test Test mode - show commands without executing them
|
|
|
15 |
--verbose Display interface status after changes
|
|
|
16 |
--help Show this help message
|
|
|
17 |
|
|
|
18 |
=head1 DESCRIPTION
|
|
|
19 |
|
|
|
20 |
This script manages a network backdoor interface by checking remote sites for
|
|
|
21 |
specific MD5 checksums. If a matching checksum is found, the backdoor interface
|
|
|
22 |
is brought up with configured routes. If no match is found, the interface is
|
|
|
23 |
brought down.
|
|
|
24 |
|
|
|
25 |
The script can also be run with manual 'up' or 'down' commands to directly
|
|
|
26 |
control the backdoor interface without checking remote sites.
|
|
|
27 |
|
|
|
28 |
Configuration is managed via a YAML file, which will be created with default
|
|
|
29 |
values if it doesn't exist on first run.
|
|
|
30 |
|
|
|
31 |
=head1 CONFIGURATION
|
|
|
32 |
|
|
|
33 |
The configuration file is located at: ./backdoor.conf.yaml
|
|
|
34 |
|
|
|
35 |
If the configuration file does not exist, it will be created with default values
|
|
|
36 |
on first run. You must edit this file with your specific network settings before
|
|
|
37 |
using the script.
|
|
|
38 |
|
|
|
39 |
Configuration options:
|
|
|
40 |
- TEST: Set to 1 to enable test mode (show commands without executing)
|
|
|
41 |
Also changes configuration file to cwd/backdoor.conf.yaml
|
|
|
42 |
- verbose: Set to 1 to display interface status after changes
|
|
|
43 |
- ip: IP address to assign to the backdoor interface
|
|
|
44 |
- netmask: Network mask for the interface
|
|
|
45 |
- nic: Network interface card name (e.g., eth0, eth1)
|
|
|
46 |
- gateway: Gateway/router IP for routing
|
|
|
47 |
- allowedSubnets: Hash of allowed subnets/IPs with netmask and ports
|
|
|
48 |
Key is the subnet/IP (can be DNS name), value contains:
|
|
|
49 |
- netmask: CIDR notation (e.g., 24, 32)
|
|
|
50 |
- ports: Port numbers to allow
|
|
|
51 |
- sitesToCheck: Hash of URLs to check with their expected MD5 checksums
|
|
|
52 |
Key is the URL, value is the expected checksum
|
|
|
53 |
|
|
|
54 |
Note: Command-line options (--test, --verbose) override configuration file settings.
|
|
|
55 |
|
|
|
56 |
=head1 REQUIREMENTS
|
|
|
57 |
|
|
|
58 |
- Perl 5.x
|
|
|
59 |
- YAML::Tiny module
|
|
|
60 |
- Getopt::Long module (core Perl)
|
|
|
61 |
- Pod::Usage module (core Perl)
|
|
|
62 |
- wget command-line tool
|
|
|
63 |
- ifconfig and ip route commands (iproute2 package)
|
|
|
64 |
- Root/sudo privileges for network configuration
|
|
|
65 |
|
|
|
66 |
=head1 EXAMPLES
|
|
|
67 |
|
|
|
68 |
# Run in test mode to see what commands would be executed
|
|
|
69 |
backdoor --test
|
|
|
70 |
|
|
|
71 |
# Check remote sites and manage interface accordingly with verbose output
|
|
|
72 |
backdoor --verbose
|
|
|
73 |
|
|
|
74 |
# Manually bring up the interface without checking remote sites
|
|
|
75 |
backdoor up
|
|
|
76 |
|
|
|
77 |
# Manually bring down the interface in test mode
|
|
|
78 |
backdoor --test down
|
|
|
79 |
|
|
|
80 |
# Show help documentation
|
|
|
81 |
backdoor --help
|
|
|
82 |
|
|
|
83 |
=head1 OPERATION
|
|
|
84 |
|
|
|
85 |
The script operates in two modes:
|
|
|
86 |
|
|
|
87 |
1. Automatic Mode (default): Checks configured remote sites for MD5 checksums.
|
|
|
88 |
If a matching checksum is found, the backdoor interface is brought up.
|
|
|
89 |
Otherwise, it is brought down.
|
|
|
90 |
|
|
|
91 |
2. Manual Mode: When 'up' or 'down' is specified as an argument, the script
|
|
|
92 |
directly controls the interface without checking remote sites.
|
|
|
93 |
|
|
|
94 |
The script is idempotent - it will not attempt to bring up an interface that
|
|
|
95 |
is already up, or bring down an interface that is already down.
|
|
|
96 |
|
|
|
97 |
=head1 LICENSE
|
|
|
98 |
|
|
|
99 |
Copyright (c) 2025, R. W. Rodolico
|
|
|
100 |
All rights reserved.
|
|
|
101 |
|
|
|
102 |
Redistribution and use in source and binary forms, with or without
|
|
|
103 |
modification, are permitted provided that the following conditions are met:
|
|
|
104 |
|
|
|
105 |
1. Redistributions of source code must retain the above copyright notice, this
|
|
|
106 |
list of conditions and the following disclaimer.
|
|
|
107 |
|
|
|
108 |
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
|
109 |
this list of conditions and the following disclaimer in the documentation
|
|
|
110 |
and/or other materials provided with the distribution.
|
|
|
111 |
|
|
|
112 |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
113 |
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
114 |
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
115 |
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
|
116 |
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
|
117 |
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
|
118 |
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
|
119 |
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
|
120 |
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
|
121 |
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
122 |
|
|
|
123 |
=head1 AUTHOR
|
|
|
124 |
|
|
|
125 |
R. W. Rodolico <rodo@dailydata.net>
|
|
|
126 |
|
|
|
127 |
=head1 VERSION
|
|
|
128 |
|
|
|
129 |
Version 1.0.0 - December 2025
|
|
|
130 |
|
|
|
131 |
=cut
|
|
|
132 |
|
|
|
133 |
use strict;
|
|
|
134 |
use warnings;
|
|
|
135 |
use YAML::Tiny;
|
|
|
136 |
use Getopt::Long;
|
|
|
137 |
use Pod::Usage;
|
|
|
138 |
|
|
|
139 |
my $configFile = './backdoor.conf.yaml'; # Path to YAML configuration file
|
|
|
140 |
|
|
|
141 |
# Default configuration
|
|
|
142 |
# $config is a hashref used globally by routines; no parameters are passed in many cases
|
|
|
143 |
my $config = {
|
|
|
144 |
TEST => 0, # Set to 1 to test without executing commands
|
|
|
145 |
verbose => 0, # Set to 1 to display interface status after changes
|
|
|
146 |
ip => '0.0.0.0', # the IP to assign when bringing up
|
|
|
147 |
netmask => '255.255.255.0', # the netmask to use
|
|
|
148 |
nic => 'eth1', # the network interface card to use
|
|
|
149 |
gateway => '0.0.0.0', # the upstream router/gateway
|
|
|
150 |
# Allowed Subnet/IPs for backdoor access. The value is what ports to open.
|
|
|
151 |
# may also be a DNS resolvable URL (uses realIP subroutine to resolve)
|
|
|
152 |
allowedSubnets => {
|
|
|
153 |
'0.0.0.0' => { netmask => 24, ports => '22' },
|
|
|
154 |
},
|
|
|
155 |
# key is site to check via wget, value is the expected md5 checksum
|
|
|
156 |
sitesToCheck => {
|
|
|
157 |
'https://example.com/key1' => 'f9620d8865581a9402a49e25b3504ff7',
|
|
|
158 |
'https://example.org/key2' => 'a196c22275cd5e54400bd63d5954cf01',
|
|
|
159 |
},
|
|
|
160 |
};
|
|
|
161 |
|
|
|
162 |
# Subroutines for network management
|
|
|
163 |
|
|
|
164 |
# determines the real IP address of a domain name or returns the IP if given an IP
|
|
|
165 |
# Uses the 'host' command to resolve domain names to IP addresses
|
|
|
166 |
#
|
|
|
167 |
# Parameters:
|
|
|
168 |
# $ip - an IP address or domain name
|
|
|
169 |
#
|
|
|
170 |
# Returns: the resolved IP address or an empty string on failure
|
|
|
171 |
sub realIP {
|
|
|
172 |
my $ip = shift;
|
|
|
173 |
# if it is a real IP address, just return it
|
|
|
174 |
return $ip if $ip =~ m/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
|
|
175 |
# Otherwise, use host to grab the IP address
|
|
|
176 |
$ip = `host -t A $ip | rev | cut -d' ' -f1 | rev`;
|
|
|
177 |
chomp $ip;
|
|
|
178 |
return $ip;
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
# checks if the backdoor interface is currently up
|
|
|
182 |
# Searches for the configured IP in the interface output
|
|
|
183 |
#
|
|
|
184 |
# Returns: 1 if up, 0 if down
|
|
|
185 |
sub currentStatus {
|
|
|
186 |
my $out = `ifconfig $config->{nic} | grep $config->{ip}`;
|
|
|
187 |
return $out =~ m/$config->{ip}/;
|
|
|
188 |
}
|
|
|
189 |
|
|
|
190 |
# runs a list of commands
|
|
|
191 |
# Executes system commands, respecting the $config->{TEST} flag
|
|
|
192 |
#
|
|
|
193 |
# Parameters:
|
|
|
194 |
# @commands - list of shell commands to execute
|
|
|
195 |
#
|
|
|
196 |
# Returns: nothing
|
|
|
197 |
sub runCommand {
|
|
|
198 |
while ( my $command = shift ) {
|
|
|
199 |
print "$command\n" if $config->{TEST};
|
|
|
200 |
`$command` unless $config->{TEST};
|
|
|
201 |
}
|
|
|
202 |
}
|
|
|
203 |
|
|
|
204 |
# brings up the backdoor interface if it is down
|
|
|
205 |
# Configures the network interface with the specified IP/netmask and
|
|
|
206 |
# adds routes for all allowed subnets through the configured gateway
|
|
|
207 |
#
|
|
|
208 |
# Returns: nothing (exits early if already up)
|
|
|
209 |
sub bringUp {
|
|
|
210 |
my $currentStatus = ¤tStatus();
|
|
|
211 |
return if $currentStatus; # Already up
|
|
|
212 |
|
|
|
213 |
print "Open Sesame\n";
|
|
|
214 |
# configure the interface and bring it up
|
|
|
215 |
&runCommand( "ifconfig $config->{nic} $config->{ip} netmask $config->{netmask}" );
|
|
|
216 |
foreach my $allowedSubnet ( keys %{$config->{allowedSubnets}} ) {
|
|
|
217 |
my $subnetIP = &realIP( $allowedSubnet );
|
|
|
218 |
my $netmask = $config->{allowedSubnets}->{$allowedSubnet}->{netmask} || 32;
|
|
|
219 |
$subnetIP .= "/$netmask";
|
|
|
220 |
&runCommand( "ip route add $subnetIP via $config->{gateway} dev $config->{nic}" );
|
|
|
221 |
}
|
|
|
222 |
# display the status
|
|
|
223 |
&runCommand( "ifconfig $config->{nic}" ) if $config->{verbose};
|
|
|
224 |
&runCommand( "route -n" ) if $config->{verbose};
|
|
|
225 |
|
|
|
226 |
}
|
|
|
227 |
|
|
|
228 |
# brings down the backdoor interface if it is up
|
|
|
229 |
# Removes all configured routes and brings down the network interface
|
|
|
230 |
#
|
|
|
231 |
# Returns: nothing (exits early if already down)
|
|
|
232 |
sub bringDown {
|
|
|
233 |
my $currentStatus = ¤tStatus();
|
|
|
234 |
return unless $currentStatus; # Already down
|
|
|
235 |
|
|
|
236 |
print "You shall not pass!\n";
|
|
|
237 |
# first, remove the routes
|
|
|
238 |
foreach my $allowedSubnet ( keys %{$config->{allowedSubnets}} ) {
|
|
|
239 |
my $subnetIP = &realIP( $allowedSubnet );
|
|
|
240 |
my $netmask = $config->{allowedSubnets}->{$allowedSubnet}->{netmask} || 32;
|
|
|
241 |
$subnetIP .= "/$netmask";
|
|
|
242 |
&runCommand( "ip route del $subnetIP" );
|
|
|
243 |
}
|
|
|
244 |
# reset and bring down the interface
|
|
|
245 |
&runCommand( "ifconfig $config->{nic} 192.168.1.1 netmask 255.255.255.255" );
|
|
|
246 |
&runCommand( "ifconfig $config->{nic} down" );
|
|
|
247 |
# display the status
|
|
|
248 |
&runCommand( "ifconfig $config->{nic}" ) if $config->{verbose};
|
|
|
249 |
&runCommand( "route -n" ) if $config->{verbose};
|
|
|
250 |
}
|
|
|
251 |
|
|
|
252 |
# Subroutine to check remote sites
|
|
|
253 |
# Checks each URL in the sitesToCheck hash for its expected MD5 checksum
|
|
|
254 |
#
|
|
|
255 |
# Parameters:
|
|
|
256 |
# $sitesToCheck - hashref of URL => expected_md5_checksum
|
|
|
257 |
#
|
|
|
258 |
# Returns:
|
|
|
259 |
# 1 if any site returns the expected checksum
|
|
|
260 |
# 0 if no matches are found
|
|
|
261 |
sub checkRemote {
|
|
|
262 |
my ( $sitesToCheck ) = @_;
|
|
|
263 |
|
|
|
264 |
foreach my $url ( keys %$sitesToCheck ) {
|
|
|
265 |
my $expectedChecksum = $sitesToCheck->{$url};
|
|
|
266 |
my $output = `wget -q -O - $url | md5sum`;
|
|
|
267 |
$output =~ m/^([a-f0-9]+)/i;
|
|
|
268 |
if ( $1 eq $expectedChecksum ) {
|
|
|
269 |
return 1; # Match found
|
|
|
270 |
}
|
|
|
271 |
}
|
|
|
272 |
|
|
|
273 |
return 0; # No matches
|
|
|
274 |
}
|
|
|
275 |
|
|
|
276 |
#==============================================================================
|
|
|
277 |
# Main Program Logic
|
|
|
278 |
#==============================================================================
|
|
|
279 |
|
|
|
280 |
# Read config file or create it with defaults if it doesn't exist
|
|
|
281 |
# Path to YAML configuration file. It can be in cwd or /etc/backdoor/
|
|
|
282 |
$configFile = -f './backdoor.conf.yaml' ? './backdoor.conf.yaml' : '/etc/backdoor/backdoor.conf.yaml';
|
|
|
283 |
if ( -e $configFile ) {
|
|
|
284 |
# Config file exists, read it
|
|
|
285 |
my $yaml = YAML::Tiny->read( $configFile );
|
|
|
286 |
my $fileConfig = $yaml->[0];
|
|
|
287 |
# Merge file config with current config
|
|
|
288 |
foreach my $key ( keys %$fileConfig ) {
|
|
|
289 |
$config->{$key} = $fileConfig->{$key};
|
|
|
290 |
}
|
|
|
291 |
} else {
|
|
|
292 |
# Config file doesn't exist, create it with defaults
|
|
|
293 |
my $yaml = YAML::Tiny->new( $config );
|
|
|
294 |
makedir( '/etc/backdoor', 0755 ) unless -d '/etc/backdoor';
|
|
|
295 |
$yaml->write( $configFile );
|
|
|
296 |
die "Created default config file at $configFile. Please edit it and rerun.\n";
|
|
|
297 |
}
|
|
|
298 |
|
|
|
299 |
# Parse command-line options (these override config file settings)
|
|
|
300 |
my $help = 0;
|
|
|
301 |
GetOptions(
|
|
|
302 |
'test' => \$config->{TEST},
|
|
|
303 |
'verbose' => \$config->{verbose},
|
|
|
304 |
'help|?' => \$help,
|
|
|
305 |
) or pod2usage(2);
|
|
|
306 |
|
|
|
307 |
pod2usage(1) if $help;
|
|
|
308 |
|
|
|
309 |
# Validate that all required configuration fields are present
|
|
|
310 |
die "Configuration file $configFile is missing required fields.\n"
|
|
|
311 |
unless exists $config->{ip}
|
|
|
312 |
&& exists $config->{netmask}
|
|
|
313 |
&& exists $config->{nic}
|
|
|
314 |
&& exists $config->{gateway}
|
|
|
315 |
&& exists $config->{sitesToCheck};
|
|
|
316 |
|
|
|
317 |
# Process command-line argument if provided
|
|
|
318 |
my $command = $ARGV[0] ? lc shift : '';
|
|
|
319 |
if ( $command eq 'up' ) {
|
|
|
320 |
&bringUp();
|
|
|
321 |
exit 0;
|
|
|
322 |
} elsif ( $command eq 'down' ) {
|
|
|
323 |
&bringDown();
|
|
|
324 |
exit 0;
|
|
|
325 |
}
|
|
|
326 |
|
|
|
327 |
# Check remote sites and manage backdoor interface accordingly
|
|
|
328 |
if ( checkRemote( $config->{sitesToCheck} ) ) {
|
|
|
329 |
&bringUp();
|
|
|
330 |
exit 0;
|
|
|
331 |
} else {
|
|
|
332 |
&bringDown();
|
|
|
333 |
exit 0;
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
1;
|