Knocking ports from your browser
Jan 3, 2015After switching my SSH server over from some random high port to tcp/22
, I
have been seeing loads of brute-force activity trying to gain access. Even
though those attempts are futile, I don’t like the attention. Besides, who knows
what vulnerabilities may crop up yet.
In search for a good protection measure, I decided to avoid exposure of critical services through a layer of port knocking. Traditionally, one relies on special network traffic (sequence of connection attempts, specific packet with encrypted payload, …) to temporarily open up additional ports. However, generating such traffic is not user friendly (ie. requires some tool or script) and it can be hard getting it through strict firewalls.
I wanted to knock ports using some protocol or traffic, which is:
- Secure: it should be hard to discover or sniff the knocking sequence;
- User-friendly: not relying on auxiliary tools;
- Firewall-friendly.
My solution to this is a simple HTML/PHP form, served over password- protected HTTPS. No firewall would block such traffic, and it is both extremely user-friendly and very secure.
Implementation
The bottom part of the system is the firewall, allowing for certain ports to be opened upon an external signal. I’m using FireHOL, a lightweight iptables-based firewall with a user-friendly syntax:
interface eth0 internet not "${UNROUTABLE_IPS}"
policy drop
protection strong 10/sec 10
server ssh accept with knock admin
The important part here is with knock $NAME
, which will build a special chain
called knock_$NAME
. Adding rules to this chain will allow access to the ssh
service, deleting them will revoke access (keeping established connections
intact).
Next up is some HTML form allowing the user to select a set of services to unlock. The forms also provides an IP field, which can be useful when unlocking from a different address (for example, your phone).
<form action="<?=htmlentities($_SERVER['PHP_SELF']); ?>"
method="post">
<table>
<tr>
<td><label for='service[]'>Service:</label></td>
<td><select name="service">
<option value="admin">administrative</option>
</select></td>
</tr>
<tr>
<td><label for='source'>Source IP:</label></td>
<td><input type="text" name="source"
value="<?=(isset($_POST['source'])
? $_POST['source']
: $_SERVER['REMOTE_ADDR'])?>">
</td>
</tr>
<tr>
<td><label for='duration'>Duration:</label></td>
<td><input type="text" name="duration"
value="<?=(isset($_POST['duration'])
? $_POST['duration']
: 15)?>">
</td>
</tr>
<tr>
<td></td>
<td><input type="submit" name="knock"
value="Knock"></td>
</td>
</table>
</form>
In my browser, this form looks like this: {% image php-port-knocker/form.png alt:“Screenshot of form” title:“State-of-the-art UI design” %}
Within the same PHP file, we process the data submitted by the user:
<?php
if (isset($_POST['knock'])) {
// Build command line
$error = NULL;
$command = ["/usr/bin/sudo", "/usr/local/sbin/knock"];
if (!empty($_POST['source']))
$command =
array_merge($command,
["-s", escapeshellarg($_POST['source'])]);
if (!empty($_POST['duration']))
$command =
array_merge($command,
["-d", escapeshellarg($_POST['duration'])]);
if (!empty($_POST['service']))
array_push($command,
escapeshellarg($_POST['service']));
else
$error = "Missing service...";
// Perform call
if (!$error) {
$output = [];
$rv = 0;
exec(join(" ", $command), $output, $rv);
if ($rv != 0)
$error = join('', $output);
}
if ($error)
echo("<p>Error: " . join('', $output) . ".</p>");
else
echo("<p>Service knock completed successfully.</p>");
}
?>
Even though the form is only privately accessible, password protected and served strictly over HTTPS, we still take some basic security precautions by checking and escaping all user inputs.
As another layer of security, I do not allow the PHP script to invoke iptables
directly. Instead, I have written an auxiliary knock
Perl script which takes
care of adding and removing rules to iptables
:
use strict;
use warnings;
use POSIX;
use Getopt::Std;
# Parse user-input
my $usage = "Usage: $0 [-s SOURCE] [-d DELAY] SERVICE\n";
my %options;
getopts("s:d:",\%options) || die $usage;
my $service = shift || die "Missing service.\n$usage";
die "Invalid service.\n" unless ($service =~ m/^[a-zA-Z]+$/);
my $delay = $options{d} || 15;
die "Invalid delay.\n" unless ($delay =~ m/^[0-9]+$/);
my @flags;
if (defined $options{s}) {
push(@flags, ("-s", $options{s}));
}
push(@flags, ("-j", "ACCEPT"));
defined (my $kid = fork) or die "Cannot fork: $!\n";
my $mode;
if ($kid) {
$mode = "-A";
} else {
daemonize();
sleep($delay);
$mode = "-D";
}
system("/sbin/iptables", $mode, "knock_$service", @flags);
exit 0;
sub daemonize {
POSIX::setsid or die "setsid: $!";
chdir "/";
umask 0;
foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024)) {
POSIX::close $_
}
open(STDIN, "</dev/null");
open(STDOUT, ">/dev/null");
open(STDERR, ">&STDOUT");
}
By using such a script, a hypothetical malicious user can only control a limited
subset of iptables
commands, restricted to knock_*
chains. Note that we use
the list-form of Perl’s system()
to prevent command injection, and take care
of sanitizing user input yet again.
Lastly, the webserver needs to be able to call this script with elevated
permissions. Adding www-data
to the sudo
group (or equivalent) would not be
wise, as that would allow an attacker to gain root through the webserver user.
Instead, we still use sudo
, but put a very specific rule in
/etc/sudoers.d/knock
allowing the webserver to use sudo
for our script but
nothing else:
www-data ALL=NOPASSWD: /usr/local/sbin/knock
Conclusion
Port knocking is a powerful tool for hiding services, and combined with some simple scripts it is easy to use from an ordinary browser. Security is tricky though, so it is best to hide the interface from public access.