9 May 2023
Introducing resocks - An Encrypted Back-Connect SOCKS Proxy for Network Pivoting
Compromising a host in a company’s perimeter often creates the opportunity to pivot into an internal network. From there on, each additional compromised system may grant us access into further subnets. Pivoting like this is second nature to pentesters, but let’s step back and look at this scenario in detail in order to understand why we created our newest open-source tool resocks - a secure reverse SOCKS5 proxy (reverse as in reverse shell, not as in reverse proxy).
Suppose we want to route our traffic through a compromised system into adjacent networks that we can’t reach directly. While there are countless techniques to achieve this, many come with significant downsides. Forwarding ports is cumbersome and may allow other parties to also access the forwarded ports. As penetration testers, we don’t want to expose our customers’ networks like this. SSH supports dynamic port forwarding, but it is often not available on Windows hosts and reverse SSH connections to traverse NAT require some thought and setup. For example, the trust-on-first-use approach for verifying the SSH host key is not an option when you assume a machine-in-the-middle which may intercept the connection. Instead, we wanted a solution with as little friction as possible. This is where resocks comes into play.
The program resocks is written in Go and compiles to a single binary that is deployed on the attack system as well as the compromised host. It can be run in one of two modes: listener and relay. First, on the attack system resocks is run in listener mode, opening a TCP port to accept incoming connections. On the compromised host (called “relay host” from now on), resocks is run in relay mode and instructed to connect back to the attack system, establishing a secure channel that can traverse NAT.
When the secure channel is established successfully, the listener instance running on the attack system opens up a SOCKS5 server port and routes all traffic through the secure channel to the relay host and from there to the target. Think of it as combining the back-connect technique used by reverse shells with the dynamic forward function of SSH.
You might have noticed we keep saying “secure” channel. resocks secures the channel using a common connection key that can be generated ad-hoc. However, just generally claiming that something is secure has actually very little meaning unless we’ve defined a clear threat model which we aim to defend against. So let’s do that!
Threat Model: Attacking the Attackers
Establishing a clear threat model is one of the most important steps in the development of security-related applications. We cannot recommend enough to both formulate and communicate a threat model. Unfortunately, especially offensive tools often lack a threat model and do not consider the attackers using such tools (such as pentesters) being themselves targeted by other malicious attackers.
In contrast, resocks was designed with the following attacker models in mind:
- A: Malicious Observer: Attackers with network access between the listener and the relay host should not be able to see the SOCKS5 traffic that is routed through the secure channel.
- B: Malicious Listener: Attackers should not be able to run their own resocks listener and redirect a resocks connection to it, as this would grant them access to the relay host’s network and services.
- C: Malicious Relay: Attackers should not be able to connect to an existing listener in order to be able to receive the traffic that was meant to be routed through the relay host.
In a pentest, all three scenarios would endanger the customer. Scenarios A and C allow attackers to observe the pentesters at work. This endangers the customer, as the network traffic may contain vital hints about the vulnerabilities that are exploited by the pentesters. Scenario B allows attackers themselves to directly attack internal networks of the customer through the relay deployed by the pentesters. This demonstrates that even offensive tools used by pentesters can be worthwhile targets for actual attackers.
One thing these attacker models have in common is that they assume an attacker somewhere in between the listener and the relay. However, attackers could also already have access to the host on which the listener or the relay runs. This creates the following two additional attacker models:
- D: Malicious User on Listener System: Malicious users on the system hosting the listener are generally able to connect to the SOCKS5 proxy or extract the connection key.
- E: Malicious User on Relay System: A malicious user on the system hosting the relay can generally extract the connection key.
In contrast to attacker models A, B and C, resocks was not designed to defend against D and E and only provides somewhat limited defense-in-depth measures against such attackers.
Securing the Tunnel
Defending against a malicious observer of the traffic between listener and relay (model A) obviously mandates encryption of the traffic. When taking malicious listeners or relays into account, we need to establish trust between the legitimate listener and relay. These are the primary concerns from a security perspective, however, they have to be balanced with ease-of-use, convenience and implementation complexity.
One of the first things that comes into mind when looking at these security demands is TLS. With client certificates in addition to server certificates, TLS can be used to establish a mutually trusted connection (commonly called mTLS). Additionally, modern TLS offers perfect forward secrecy. However, manual certificate management is certainly not convenient to do in practice. A certificate would either have to be deployed alongside the relay or embedded into the relay binary. The former method introduces more initial steps required to use resocks on a newly compromised host while the latter requires users to re-build resocks on-demand. Both scenarios are obviously deal breakers for ease-of-use and convenience. In order to limit implementation complexity, we also decided against implementing a custom protocol.
Finally, we came up with a way to tame the setup cost of TLS: A certificate
generation scheme based on a compact shared key. We first generate a so-called
connection key that we share between the listener and relay. The connection key
acts as a seed for an ed25519
key that is used to create a self-signed CA certificate. So you can say that the
connection key is the ed25519
key.
Why do we use ed25519
specifically? In contrast to for example RSA, it does
not rely on entropy for signing, which enables us to deterministically generate
the same (byte-identical) CA certificate on both the listener and the relay
given the same connection key. Under the hood, TLS with ed25519
uses an
algorithm called Edwards-curve Digital Signature Algorithm (EdDSA
),
which was created as a successor to the
Elliptic Curve Digital Signature Algorithm (ECDSA
):
As with other discrete-log-based signature schemes, EdDSA uses a secret value called a nonce unique to each signature. In the signature schemes DSA and ECDSA, this nonce is traditionally generated randomly for each signature—and if the random number generator is ever broken and predictable when making a signature, the signature can leak the private key, as happened with the Sony PlayStation 3 firmware update signing key.
In contrast, EdDSA chooses the nonce deterministically as the hash of a part of the private key and the message. Thus, once a private key is generated, EdDSA has no further need for a random number generator in order to make signatures, and there is no danger that a broken random number generator used to make a signature will reveal the private key.
Additionally, the connection key is small enough to be conveniently passed as a command-line argument. In the next step, both the listener and the relay independently generate completely random keys to be used for independently generated client or server certificates. Finally, both components can use the identical CA key to sign their client or server certificates. As a result, the client and server certificates can be used to establish a mutually trusted connection.
We found that simply passing the 256-bit seed values for ed25519
keys as a 43
character string to both components is convenient enough in practice. As a
result, we have begun to use this approach to secure the traffic of other tools
we have developed for internal use. We also published our Go implementation
kbtls (Key-Based TLS)
as a library which is used by
resocks. While we are unaware of
any security risks caused by this technique, it does use TLS in a slightly
unconventional way, so please let us know if you are aware of any security
concerns.
With this out of the way, let’s take a look at how resocks can improve your life as a pentester.
Designing resocks
The corner stone of resocks' user experience is the listener. One of the key concerns of the listener is to convey the current state of resocks. Is the listener waiting on reverse connections from the relay system? Or is the relay already connected and the SOCKS proxy is ready to use? Initial testing showed that a traditional logging-style output did not convey the current state effectively in scenarios where the relay system frequently disconnects and re-connects (which sometimes happens during a pentest). Based on this experience we chose a stateful display. After all, what could convey state better than a stateful display? Here’s what it looks like in practice:
The next user experience issue is the connection key. Of course, having to
specify a matching connection key still introduces some friction, even if it is
way better than manual certificate management. That’s why
resocks offers multiple ways to
handle the connection key. As shown above, resocks listen
displays a newly
generated connection key by default that can be copied and passed to the relay
as follows:
# relay: connect back to listener at 10.0.2.2
$ resocks 10.0.2.2 --key "ilKync+DujFREmiXlYB+/+UpXsUhuFJIeOwFloZWItk"
connected to 10.0.2.2:4080
However, this connection key becomes invalid when the listener is restarted, as
it generates a new connection key by default. A static key could also be passed
to the listener via --key
to avoid generating a new key, but a static key can
also be conveniently stored in an environment variable:
$ export RESOCKS_KEY="$(resocks generate)"
In order to avoid having to set the same environment variable on the remote relay system, resocks can also just embed a pre-generated connection key during compilation as follows:
$ go build -ldflags="-X main.defaultConnectionKey=$(resocks generate)"
This binary can then be used to start both the listener and the relay without having to pass the connection key for each run.
Remember the threat models we discussed before? We stated that
resocks was not explicitly
designed to protect against scenarios D and E: A malicious user on the listener
or relay system. For some specific cases of these scenarios, alternate ways to
specify the connection key can offer some protection. If the malicious user can
see process listings including command-line arguments, the connection key will
be exposed if it is passed via the --key
option. In this case, an environment
variable or a connection key compiled into the binary may be a better choice as
long as the binary is not readable for the attacker. Unfortunately, if the
malicious actor uses the same account or a high-privileged account you are out
of luck.
One of the reasons we developed
resocks in the first place was
that the proxy feature of Metasploit’s
meterpreter
was often quite unreliable and could crash the whole meterpreter
session. This experience made us think about resilience (for example against
unreliable network connections) during the development of
resocks. For instance, we added
the option --reconnect-after
to the relay to make the relay recover from
intermittent connection issues. With the following command you can start a relay
that re-connects after one second when the connection is interrupted:
$ resocks 10.0.2.2 --reconnect-after 1s --key "ilKync+DujFREmiXlYB+/+UpXsUhuFJIeO..."
The listener will wait for the relay to reconnect by default unless you specify
--abort-on-disconnect
. By default, the reconnect feature is disabled in order
to avoid situations where a misconfigured
resocks instance keeps on living
forever attempting to connect.
One of the advantages of using Go for
resocks is that it can easily be
cross-compiled for almost any operating system without any special setup. Just
specify the target operating system using the $GOOS
environment variable during
compilation:
$ GOOS=windows go build
For a list of available operating system ($GOOS
) and architecture ($GOARCH
)
combinations, run the following command:
$ go tool dist list
Of course, we have not tested all of these targets, but we expect that resocks works at least as a relay in most cases.
Open Source at RedTeam Pentesting
We are always proud to be able to contribute to the InfoSec community by releasing our offensive tools. If you like resocks, check out our name resolution spoofer pretender (blog post) or our HTTP fuzzer monsoon (blog post) and stay tuned for future releases which we announce here and on our social media.