Defeating poor port knocking configurations

I was thinking about port knocking the other day (yep, that’s how I roll) and while I consider it to be a valid security layer, it occurred to me that it would be pretty easy to set up a poor implementation of it that was susceptible to being gamed. Here’s how that thought process went.

Caveat: This is a proof of concept and has many points against it which I outline at the end of this post.

For the uninitiated, port knocking is a process whereby some port on a server can be fire-walled off until some pre-determined set of ports are ‘knocked’ on, and then the firewall can be reconfigured to open some other port. A practical example is a server where you need SSH access, but you don’t want to leave the SSH daemon running wide open to the world all the time. You can use a port knocking daemon like knockd, coupled with an IPTables firewall to protect that port. The normal configuration would be to have the SSH daemon running on some arbitrary port and have the firewall dropping connections to that port until a valid set of ports are knocked on, and then the IPTables would be rewritten, usually temporarily, to allow connections to the SSH port.

The security layer this provides is two fold:

  • The server does not present this port as open during scanning, and
  • Access to the port is temporary (an optional, but desirable configuration)

When configuring the ports that will unblock the desired port (the port sequence), there are three things that can be set:

  • The number of ports.
  • The numbers of the ports.
  • The timeout that is allowed for a port knocker to complete the correct sequence.

When put together, this is a pretty daunting task. An attacker would have to know how many ports to knock (from 1 to ?) and what ports to knock (from 1 to 65,535) and pull that all off within the set sequence timeout.

But, as they say, the devil is in the details. If the configured port sequence has ports close (enough) together and ascends or descends, then the configuration is all but ruined. If an attacker were to set her sites on a server she knows runs a service, but presents no ports for that service, she might deduce that a port knocker is in use. The easiest attack to start with would be a sequential ascending or descending port knocking palooza.

An example of a defeatable (is that a word?) port knocking configuration is ports 7003, 7459 and 8001 using the default knockd.conf timeout of 5 seconds. The attack stems from the fact that the knockd daemon does not pay attention to knocks on ports that are not part of a sequence it is configured to look for. This makes sense as it would be computationally expensive to monitor every port for activity and compare it against a potentially complex list of sequences. But it can be gamed a bit like so:

As a proof of concept I set up the following test:

The knockd daemon config:

The port knocking client config:

Notice that there are only 3 ports in my unblock sequence, but 6 ports in my client’s knocking config. Since the knockd daemon does not care about the 2020, 2025 or 4045 knocks, my sequence still triggers the start_command as shown in the log below:

The log:

Note: the reason this attack succeeded despite my port range being outside the 1250 maximum is because I did not knock every port.

As a proof of concept this is nice, but is it really a practical attack vector in the real world? The answer is a solid “maybe”. It’s not going to attract any lulz low-hanging fruit script kiddies, but if you’re being targetted, it might be possible for a bad actor to win.

Here are some mitigating factors that make this vector potentially useless, or at least extremely costly:

  • The fastest I could knock ports successfully in my test environment was 4ms between knocks. Any faster and the daemon would not register them. I don’t know if this is a limitation of my testing environment and therefore perhaps other systems or clients might do better.
    • The default seq_timeout for knockd.conf is 5 seconds. At 4ms between knocks I can knock on 1250 ports per 5-second seq_timeout which means it would only take me 52 iterations to exhaust the 65,335 ports available. Less than 5 minutes.
  • The port sequence would have to span a maximum of 1250 ports for this to work and they would also have to fall within a specific range of each iteration. Meaning, if I knocked ports 1 to 1250 in the 5 second timeout, and the unlock sequence included port 1251, it would not succeed even if I fired up another knock client immediately and started at port 1251.
  • This attack will only work if the ports are in ascending or descending order. As soon as there is a combination of ports like 1000, 2000, 1500 the attack cannot succeed.
  • An attacker probably won’t know how many ports to knock.
  • There is no notification on the client side that a knock sequence was successful.
  • Even if the attacker knew the sequence was successful, she may not know what port has been opened.

If you assume that better systems can get the knock delay down to 1ms, that means an attacker could hit 5000 ports per default 5-second timeout which is a paltry 13 iterations (65 seconds) to run every port and it also greatly expands the allowable range of ports in each iteration.

Based on this, it becomes easier to see how to harden your port knocking configuration:

  • Set the seq_timeout as low as your tests show you can. The 5 second default may be more than you need.
  • Choose as many ports as you can cram in to your seq_timeout
  • Make sure your port sequence is not ascending or descending
  • Ensure you configure a stop_command to close the port after a few seconds in the cmd_timeout
  • Run whatever daemons you’re protecting on non-standard ports so even if an attacker is successful, they won’t find SSH sitting on port 22. It takes a long time to scan a machine after each port knock attempt so using non-standard ports can save the day (because you set a stop_command in a short cmd_timeout, right?)
  • Use normal security precautions (such as key-only login and no-root login on SSH daemons, for examples).

There are more reasons why this attack would fail than there are reasons why it would succeed, but it’s also obvious that a poor configuration could allow a bad guy in. Spending a few minutes thinking about the realities of what your configuration should be instead of taking the default configuration could save you some pain down the road.