SOCKS Proxy Tunnelling Security: Detecting Abuse and Hardening Legitimate Deployments

SOCKS Proxy Tunnelling Security: Detecting Abuse and Hardening Legitimate Deployments

The Problem

SOCKS5 is a transparent, protocol-agnostic proxy: it forwards raw TCP (and optionally UDP) streams on behalf of a client. That neutrality makes it genuinely useful — remote development environments, privacy tooling, enterprise forward proxies, and database jump hosts all rely on it. The same neutrality makes it the most popular pivoting mechanism in post-exploitation toolkits.

When a red teamer lands a foothold on an internet-accessible host, their next problem is reach: the database server, the build system, and the AD domain controller are all inside network segments that are not reachable from outside. A SOCKS5 listener on the foothold host — started in seconds using Cobalt Strike’s socks command, ssh -D, or chisel — transforms that host into a transparent relay into the internal network. All the attacker needs is a client configured to proxy through it.

The core problem is that SOCKS traffic is indistinguishable from legitimate usage at the protocol level: the handshake is three packets, authentication is optional, and after the CONNECT the payload is whatever the client sends. Defenders cannot rely on deep packet inspection of the proxied stream. Detection depends on behavioural anomalies: unusual ports, unexpected processes listening, egress traffic to unknown IP:port pairs, and correlation between authentication events and outbound tunnels.

Common gaps in environments where SOCKS abuse goes undetected:

  • SSH forwarding is not disabled on internal hosts. AllowTcpForwarding defaults to yes in OpenSSH. Any user with an SSH session can open a SOCKS5 tunnel with ssh -D 1080 user@host without any additional privileges or software installation.
  • No egress filtering on internal hosts. Internal servers are trusted to reach the internet directly. An attacker’s chisel client connects outbound to a cloud VPS on port 443, rendering the SOCKS listener invisible to perimeter rules that only inspect inbound traffic.
  • Port 1080 is not monitored. SOCKS has an IANA-assigned port (1080) that many organisations leave open or unmonitored on internal segments.
  • Legitimate SOCKS proxies are unauthenticated. Corporate SOCKS5 proxies for developer access are deployed with no authentication because it was “just for internal use.” An attacker on any internal host can proxy through them without credentials.
  • DNS-over-HTTPS bypasses DNS controls. When SOCKS tunnels are blocked, attackers pivot to DoH to bypass DNS-based egress filtering, then use TLS-wrapped tunnels through HTTPS ports.

Target systems: Linux hosts running OpenSSH 8.9+; environments using chisel, frp, or ngrok for legitimate tunnelling; Squid 6+ for forward proxy deployments; iptables/nftables for egress enforcement.

Threat Model

  • Adversary 1 — SSH dynamic forwarding for lateral movement: An attacker with SSH access to a DMZ host runs ssh -D 1080 to create a SOCKS5 listener. They configure ProxyChains on their attack box and use nmap, CrackMapExec, or Impacket through the tunnel to attack internal hosts unreachable from outside.
  • Adversary 2 — Cobalt Strike SOCKS5 pivot: A Beacon implant on a compromised workstation executes the socks 9050 command. The C2 operator routes all internal reconnaissance and lateral movement through the Beacon’s SOCKS listener using the CS pivot listener feature.
  • Adversary 3 — Chisel reverse tunnel: An attacker drops chisel on a foothold host inside a highly restricted network. The chisel client connects outbound over TCP/443 to an attacker-controlled VPS running the chisel server. The VPS now has a SOCKS5 proxy that routes into the internal network over the single allowed outbound connection.
  • Adversary 4 — Unauthenticated corporate SOCKS abuse: A rogue insider or lateral-movement attacker discovers the corporate SOCKS5 proxy at 10.0.0.50:1080, which requires no authentication. They use it to reach administrative interfaces on other network segments without triggering host-level port-scan alerts.
  • Adversary 5 — DoH exfiltration after SOCKS is blocked: After SOCKS tunnels are detected and blocked, an attacker configures the browser or malware to use DNS-over-HTTPS to a resolver they control, bypassing all DNS-based sinkholing and egress controls.
  • Access level: Adversaries 1–4 have network or authenticated SSH access to at least one internal host. Adversary 5 has code execution on a workstation.
  • Objective: Reach internal network segments, access management interfaces, exfiltrate data, evade detection.
  • Blast radius: A single SOCKS tunnel from one compromised host potentially grants access to every service reachable by that host — the equivalent of placing the attacker directly on the internal network.

Configuration

Step 1: Harden SSH Forwarding on All Hosts

The fastest win is removing the SOCKS-via-SSH capability entirely from hosts where it is not needed. This is almost everywhere: most servers do not need to act as SSH proxy endpoints for clients.

# /etc/ssh/sshd_config — apply to all internal servers and internet-facing hosts

# Disable all TCP forwarding. This blocks both -L (local), -R (remote),
# and -D (dynamic/SOCKS) port forwarding. One setting, eliminates SOCKS-via-SSH.
AllowTcpForwarding no

# Belt-and-suspenders: PermitTunnel controls TUN/TAP device tunnelling (VPN-style).
# SSH can also create a layer-3 tunnel between two hosts — block it.
PermitTunnel no

# X11 forwarding can be abused independently of TCP forwarding;
# it also creates a secondary channel that DLP controls miss.
X11Forwarding no

# StreamLocalForwarding controls Unix domain socket forwarding.
# Attackers can proxy through Unix sockets just as easily as TCP.
AllowStreamLocalForwarding no

# Disable agent forwarding so a compromised relay host cannot harvest
# credentials from forwarded SSH agents.
AllowAgentForwarding no

# Restrict who can log in at all (defence in depth).
AllowUsers deploy ci-runner     # Explicit allowlist; deny everything else.

Apply and verify:

# After editing sshd_config, test before restarting.
sshd -t

# Restart on systemd systems.
systemctl restart sshd

# Verify the restrictions are active — attempt a -D tunnel; it must fail.
ssh -D 1080 user@target-host -N
# Expected: channel 0: open failed: administratively prohibited: open failed

For the bastion host (covered in detail in SSH Bastion Host and Jump Server Hardening), the same settings apply with one exception: AllowTcpForwarding must remain no on the bastion itself, and -J ProxyJump connections must go directly to targets without enabling SOCKS.

Step 2: Detect SOCKS Listeners via Process and Network Monitoring

Configuration-level hardening only addresses SSH-based SOCKS. Attackers who have code execution can start a SOCKS listener using any binary — chisel, Golang custom implants, Python’s socks5 module, or Metasploit’s auxiliary/server/socks5. Detection requires runtime monitoring.

# On Linux: list processes listening on TCP ports that could be SOCKS listeners.
# SOCKS5 handshake starts with version byte 0x05; listeners are typically
# on high-numbered ephemeral ports or well-known 1080/9050.
ss -tlnp | awk '$4 ~ /:1080$|:9050$|:1081$|:8080$/ {print}'

# Broader approach: all processes listening on non-standard ports.
ss -tlnp | awk 'NR>1 {split($4,a,":"); port=a[length(a)]; if (port>1024 && port!=8080 && port!=8443 && port!=9090) print}'

# On a monitored endpoint, look for chisel, frp, ngrok in process table.
ps aux | grep -E 'chisel|frp|ngrok|ncat|socat|proxychains'

# Check for recently added listening sockets (Linux audit approach).
# Requires auditd with execve and connect rules.
ausearch -k socks_listen --start today

# Auditd rule to track processes binding TCP ports (add to /etc/audit/rules.d/socks.rules).
# -a always,exit -F arch=b64 -S bind -k tcp_bind
# Falco rule: detect unexpected processes binding to port 1080 or 9050.
# /etc/falco/rules.d/socks_detection.yaml

- rule: SOCKS Proxy Port Binding
  desc: A process bound to common SOCKS ports (1080, 9050, 1081).
  condition: >
    evt.type = bind
    and fd.sport in (1080, 9050, 1081, 1082)
    and not proc.name in (dante-server, ss5, microsocks)
  output: >
    Unexpected SOCKS port bind (user=%user.name cmd=%proc.cmdline
    port=%fd.sport container=%container.name)
  priority: WARNING
  tags: [network, tunnelling, lateral-movement]

- rule: Chisel or FRP Process Execution
  desc: Known tunnelling binaries executed.
  condition: >
    evt.type = execve
    and proc.name in (chisel, frpc, frps, ngrok, bore, rathole)
  output: >
    Tunnelling binary executed (user=%user.name cmdline=%proc.cmdline
    parent=%proc.pname container=%container.name)
  priority: HIGH
  tags: [network, tunnelling, covert-channel]

Step 3: Network-Level SOCKS5 Signature Detection

The SOCKS5 handshake has a fixed structure detectable by IDS rules when it appears on non-proxy ports. The client greeting is a \x05 version byte followed by the number of authentication methods and the method list. This is distinctive enough to write a Suricata rule.

# /etc/suricata/rules/socks5_tunnelling.rules

# Detect SOCKS5 client greeting on non-standard ports.
# Signature: version=0x05, nmethods=0x01-0x03, followed by method bytes.
alert tcp $HOME_NET any -> any !1080 (
    msg:"SOCKS5 Handshake on Non-Standard Port";
    flow:established,to_server;
    dsize:3<>10;
    content:"|05|"; offset:0; depth:1;
    pcre:"/^\x05[\x01-\x03][\x00-\x02]+/";
    threshold: type both, track by_src, count 3, seconds 60;
    classtype:policy-violation;
    sid:9000100; rev:2;
)

# Detect SOCKS5 handshake specifically on port 443 — tunnelling through HTTPS port.
alert tcp $HOME_NET any -> $EXTERNAL_NET 443 (
    msg:"SOCKS5 Handshake on Port 443 — Possible Chisel/C2 Tunnel";
    flow:established,to_server;
    dsize:3<>10;
    content:"|05|"; offset:0; depth:1;
    pcre:"/^\x05[\x01-\x03]/";
    classtype:trojan-activity;
    sid:9000101; rev:1;
)

# Detect chisel's default WebSocket upgrade header — chisel uses WebSocket over HTTP.
alert http $HOME_NET any -> $EXTERNAL_NET any (
    msg:"Chisel Tunnel WebSocket Upgrade Detected";
    flow:established,to_server;
    http.header;
    content:"Upgrade: websocket"; nocase;
    http.uri;
    content:"/chisel";
    classtype:trojan-activity;
    sid:9000102; rev:1;
)

For environments running Zeek, the SOCKS analyser fires automatically on port 1080 traffic. Add a custom script to flag SOCKS connections on non-standard ports:

# /opt/zeek/share/zeek/site/socks_nonstandard_port.zeek

event socks_request(c: connection, version: count, request: SOCKS::Request) {
    if ( c$id$resp_p != 1080/tcp && c$id$resp_p != 1081/tcp ) {
        NOTICE([$note=Weird::Activity,
                $msg=fmt("SOCKS%d request on non-standard port %s",
                         version, c$id$resp_p),
                $conn=c,
                $identifier=cat(c$id$orig_h)]);
    }
}

Step 4: Egress Filtering as the Primary Defence

SSH hardening and IDS rules are detective controls. The preventive control is egress filtering: internal hosts should not be able to initiate arbitrary TCP connections to the internet. All outbound traffic should go through an authenticated forward proxy where it is logged and inspected.

# nftables ruleset for a Linux server — egress policy.
# Principle: allow only DNS to known resolvers, HTTP/HTTPS to the corporate proxy,
# and SSH to the bastion. Block everything else outbound.

nft -f /etc/nftables.d/egress-policy.nft
# /etc/nftables.d/egress-policy.nft

table inet egress_filter {
    chain output {
        type filter hook output priority 0; policy drop;

        # Allow established/related return traffic.
        ct state established,related accept

        # Loopback.
        oif lo accept

        # DNS only to approved resolvers — block DoH bypass (see Step 6).
        ip daddr { 10.0.0.53, 10.0.0.54 } udp dport 53 accept
        ip daddr { 10.0.0.53, 10.0.0.54 } tcp dport 53 accept

        # NTP to internal server.
        ip daddr 10.0.0.1 udp dport 123 accept

        # HTTP/HTTPS only via corporate proxy — not direct to internet.
        ip daddr 10.10.0.50 tcp dport { 3128, 8080 } accept

        # SSH to bastion only.
        ip daddr 10.0.0.10 tcp dport 22 accept

        # Log and drop everything else outbound.
        log prefix "egress-drop: " level warn
        drop
    }
}

Apply and persist:

nft -f /etc/nftables.d/egress-policy.nft
systemctl enable nftables
systemctl restart nftables

# Test: a direct outbound connection must be blocked.
curl --max-time 5 https://example.com
# Expected: curl: (28) Connection timed out

# Test: connection through the proxy must succeed.
https_proxy=http://10.10.0.50:3128 curl --max-time 10 https://example.com
# Expected: 200 OK

Step 5: Authenticated SOCKS5 — Hardening Legitimate Deployments

When SOCKS5 is needed — remote database access for DBAs, developer tooling, or legacy application compatibility — deploy it with authentication and destination restrictions rather than unauthenticated.

Dante is the most widely deployed SOCKS5 server on Linux. A hardened configuration:

# /etc/danted.conf

logoutput: syslog /var/log/danted.log

# Listen only on the internal interface; never on 0.0.0.0.
internal: eth0 port = 1080

# Route outbound through the external interface.
external: eth1

# Authentication: require username/password (SOCKS5 RFC 1929 sub-negotiation).
# This is passed in cleartext unless the transport is already encrypted (SSH tunnel).
socksmethod: username

# PAM authentication against system accounts or an external directory.
user.privileged: root
user.notprivileged: nobody

# Allow only specific authenticated users to connect.
client pass {
    from: 10.0.0.0/8 to: 0.0.0.0/0
    log: connect disconnect error
}

# Restrict what destinations clients can reach.
# Allow only specific internal subnets — not the internet.
socks pass {
    from: 0.0.0.0/0 to: 10.20.0.0/16     # Approved database segment.
    socksmethod: username
    log: connect disconnect iooperation
}

socks pass {
    from: 0.0.0.0/0 to: 10.30.0.0/16     # Approved analytics segment.
    socksmethod: username
    log: connect disconnect iooperation
}

# Block everything else.
socks block {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    log: connect error
}
# Enable and start Dante.
systemctl enable danted
systemctl start danted

# Verify authentication is required — this must fail without credentials.
curl --socks5 10.0.0.50:1080 http://10.20.0.5:5432
# Expected: Failed to connect to SOCKS5 proxy

# Verify authenticated access works.
curl --socks5-hostname user:password@10.0.0.50:1080 http://10.20.0.5:5432

Step 6: Block DNS-over-HTTPS to Prevent Bypass

When SOCKS tunnels are detected and blocked, sophisticated attackers fall back to DoH to exfiltrate data and resolve C2 infrastructure while bypassing DNS-based controls. DoH sends DNS queries inside HTTPS to a resolver (typically 1.1.1.1, 8.8.8.8, 9.9.9.9) on port 443 — indistinguishable from web traffic at the port level.

The primary mitigation is to block direct outbound HTTPS to known DoH resolvers and force all DNS through internal resolvers:

# Extend /etc/nftables.d/egress-policy.nft — block DoH resolvers.

table inet doh_block {
    set doh_resolvers {
        type ipv4_addr
        elements = {
            1.1.1.1,         # Cloudflare DoH
            1.0.0.1,         # Cloudflare DoH
            8.8.8.8,         # Google DoH
            8.8.4.4,         # Google DoH
            9.9.9.9,         # Quad9 DoH
            149.112.112.112  # Quad9 DoH
        }
    }

    chain output {
        type filter hook output priority 1; policy accept;

        # Block direct HTTPS to DoH resolvers from servers.
        ip daddr @doh_resolvers tcp dport 443 \
            log prefix "doh-block: " level warn drop
    }
}

At the network level, use a DNS RPZ to catch DoH resolver domains before they resolve:

# /etc/named/rpz/doh-block.db — bind RPZ zone (see DNS RPZ article for full setup)

$TTL 60
@    IN    SOA    ns1.internal. admin.internal. ( 2026050701 3600 900 86400 60 )
     IN    NS     ns1.internal.

; Known DoH providers — return NXDOMAIN to break DoH auto-detection.
dns.cloudflare.com         CNAME    rpz-drop.
dns.google                 CNAME    rpz-drop.
dns.quad9.net              CNAME    rpz-drop.
mozilla.cloudflare-dns.com CNAME    rpz-drop.
doh.pub                    CNAME    rpz-drop.

Detect DoH usage in network logs by looking for HTTPS connections directly to the IP addresses above without a corresponding DNS lookup (the IP was hardcoded, bypassing your resolver):

# Zeek: flag HTTPS connections to DoH resolver IPs.
# In zeek conn.log, look for connections to DoH IPs on port 443
# that have no corresponding DNS query.
zcat /var/log/zeek/conn.*.log.gz | \
    zeek-cut ts id.orig_h id.resp_h id.resp_p proto | \
    awk '$4 == "443" && $3 ~ /^(1\.1\.1\.1|8\.8\.8\.8|9\.9\.9\.9)$/ {print}'

Step 7: Deploy an Authenticated Forward Proxy (Squid)

The architectural answer to uncontrolled egress is to require all outbound HTTP/HTTPS to route through an authenticated forward proxy. This eliminates direct TCP from servers to the internet, making chisel-style reverse tunnels dependent on getting traffic out through the proxy — which is logged and access-controlled.

# /etc/squid/squid.conf — authenticated forward proxy with LDAP auth.

# Bind to internal interface only.
http_port 10.10.0.50:3128

# LDAP authentication — users must authenticate with AD credentials.
auth_param basic program /usr/lib/squid/basic_ldap_auth \
    -b "dc=corp,dc=example,dc=com" \
    -D "cn=squid-bind,ou=service-accounts,dc=corp,dc=example,dc=com" \
    -w /etc/squid/ldap-bind-password \
    -f "sAMAccountName=%s" \
    -h ldap.corp.example.com

auth_param basic realm "Corporate Web Proxy — credentials required"
auth_param basic credentialsttl 1 hour

# Require authentication for all access.
acl authenticated proxy_auth REQUIRED

# Allowlist: only approved destination domains.
# Servers rarely need to browse the internet; lock them to known-good destinations.
acl approved_destinations dstdomain \
    .ubuntu.com \
    .debian.org \
    .pypi.org \
    .npmjs.com \
    .github.com \
    .ghcr.io \
    .amazonaws.com \
    .storage.googleapis.com

# Block CONNECT tunnel to non-HTTPS ports — prevents SOCKS-over-proxy tricks.
acl SSL_ports port 443
acl CONNECT method CONNECT
http_access deny CONNECT !SSL_ports

# Allow authenticated users to access approved destinations.
http_access allow authenticated approved_destinations

# Deny everything else.
http_access deny all

# Log all connections: timestamp, user, method, URL, response code, bytes.
access_log /var/log/squid/access.log squid

# TLS bump to inspect HTTPS — requires deploying the Squid CA cert to all clients.
# Without bumping, HTTPS destinations are logged (via SNI) but content is not inspected.
ssl_bump peek all
ssl_bump bump approved_destinations
ssl_bump terminate all

# Large disk cache for performance.
cache_dir ufs /var/cache/squid 10000 16 256

Force all server traffic through the proxy using a PAC file or firewall redirect:

# Transparent proxy redirect: intercept outbound HTTP and HTTPS, redirect to Squid.
# Requires iptables REDIRECT (or TPROXY for HTTPS).
# HTTP (port 80) — transparent intercept.
iptables -t nat -A OUTPUT -p tcp --dport 80 \
    ! -d 10.0.0.0/8 -j REDIRECT --to-port 3128

# HTTPS transparent requires TPROXY; simpler approach is to block direct HTTPS
# and require explicit proxy configuration on applications.
iptables -A OUTPUT -p tcp --dport 443 \
    ! -d 10.0.0.0/8 \
    ! -d 10.10.0.50 -j DROP

Step 8: Detect Reverse Tunnel Indicators (Chisel, frp, ngrok)

Reverse tunnels are harder to detect than local SOCKS listeners because the malicious network activity is outbound and the listener is on the attacker’s infrastructure. Detection focuses on three areas:

Process signatures:

# Check for known tunnelling binaries by name and by hash.
# Maintain a hash database; rotate hashes as attackers recompile.
find /tmp /var/tmp /dev/shm /run -type f -executable 2>/dev/null | \
    while read f; do
        sha256sum "$f"
    done | grep -f /etc/security/tunnelling-tool-hashes.txt

# Known default chisel server domains and patterns in outbound connections.
ss -tnp | awk '{print $5}' | sort -u | \
    grep -vE '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' | \
    while read ip; do
        host "$ip" 2>/dev/null
    done | grep -iE 'ngrok|chisel|frp|bore|rathole|tunnel'

Beacon pattern detection in flow data:

Reverse tunnels produce highly regular connection patterns: a chisel client or frp client reconnects on a fixed interval and maintains a persistent websocket or TCP connection with small keepalive payloads. In Zeek conn.log or NetFlow data, look for:

  • Long-duration outbound TCP connections (hours) to cloud provider IP ranges on port 443.
  • Connection to the same IP:port pair repeated every N seconds with identical byte counts — keepalive pattern.
  • Outbound traffic to IP ranges owned by DigitalOcean, Linode, Vultr, or Hetzner (cheap VPS providers common in attacker infrastructure) that does not match known CDN or service patterns.
# Zeek: long-duration outbound connections to non-CDN IPs on port 443.
# Legitimate web connections are short; persistent tunnels are long.
zcat /var/log/zeek/conn.*.log.gz | \
    zeek-cut ts id.orig_h id.resp_h id.resp_p duration | \
    awk '$4 == "443" && $5 > 3600 {print}' | \
    # Filter out known CDN and corporate IP ranges.
    grep -vE '(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|[known-cdn-ranges])'

TLS certificate fingerprinting:

Chisel’s server uses a self-signed certificate by default. Its JA3 server fingerprint is consistent when it is not customised. JA4 fingerprints (which are more robust to evasion than JA3) for common tunnelling tools are published by FoxIO and can be loaded into Zeek or Suricata for detection.

# Extract JA3S hashes from Zeek ssl.log and compare against known bad set.
zcat /var/log/zeek/ssl.*.log.gz | \
    zeek-cut ts id.orig_h id.resp_h ja3s | \
    grep -f /etc/security/known-tunnel-ja3s.txt

Operational Checklist

The controls above span several teams and system types. In priority order:

  1. Audit all sshd_config files today. Grep for AllowTcpForwarding across your fleet. Any value other than no is a live SOCKS-via-SSH capability. Fix it before any other item on this list.
  2. Enumerate processes listening on port 1080/9050. On every host: ss -tlnp sport = :1080. Any unexpected listener is an incident.
  3. Deploy egress firewall rules on internal servers. Internal servers should not reach the internet directly. If they do, that is the proximate enabler for reverse tunnels.
  4. Require authentication on any SOCKS5 proxy you own. Unauthenticated SOCKS5 on an internal network is a free lateral-movement relay for any attacker who lands anywhere on that segment.
  5. Block DoH resolver IPs at the perimeter. DoH is the next bypass after SOCKS is blocked. Blocking it forces DNS through your internal resolvers where RPZ filtering applies.
  6. Deploy Squid with LDAP authentication as the mandatory internet egress point. Without it, blocking every other egress mechanism is a whack-a-mole exercise.
  7. Add Falco or auditd rules to catch tunnelling binary execution. The process list is the most reliable indicator; network signatures can be evaded by recompiling tools.

Cross-References