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

Биб­лиоте­ка Ignition нуж­на для кас­томиза­ции сооб­щений об ошиб­ках, что полез­но во вре­мя раз­работ­ки и отладки. Ignition дос­тупна и исполь­зует­ся в Laravel «из короб­ки», а так­же встре­чает­ся в дру­гих про­ектах.

Уяз­вимость воз­можна из‑за некор­рек­тной обра­бот­ки парамет­ров POST-зап­роса. Бла­года­ря это­му зло­умыш­ленник может отпра­вить про­изволь­ные дан­ные в качес­тве аргу­мен­тов фун­кций file_get_contents и file_put_contents. Спе­циаль­но сфор­мирован­ная цепоч­ка таких зап­росов при­водит к воз­можнос­ти выпол­нить код на целевой сис­теме.

info

Баг обна­ружил Чарльз Фол (Charles Fol) из Ambionics Security. Уяз­вимос­ти прис­воен иден­тифика­тор CVE-2021-3129 и кри­тичес­кий ста­тус, так как для успешной экс­плу­ата­ции не нуж­на авто­риза­ция. Баг при­сутс­тву­ет в Ignition 2.5.2 и ниже.

 

Стенд

В качес­тве стен­да будем исполь­зовать кон­тей­нер Docker на осно­ве Debian 10.

docker pull debian
docker run -ti --name="laravelrce" -p8080:80 debian /bin/bash

Ус­тановим необ­ходимые пакеты. В качес­тве веб‑сер­вера я буду исполь­зовать nginx.

apt update
apt install -y nano curl unzip nginx php-fpm php-common php-mbstring php-xmlrpc php-soap php-gd php-xml php-mysql php-cli php-zip php-curl php-pear php-dev python xxd libfcgi

За­тем нуж­но про­инстал­лировать Composer.

curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

Че­рез него соз­дадим про­ект на осно­ве фрей­мвор­ка Laravel. Для удобс­тва помес­тим фай­лы в дирек­торию /var/www.

cd /var/www && rm -rf html && composer create-project laravel/laravel . "v8.4.2" && sed -i -E 's|"facade/ignition": ".+?"|"facade/ignition": "2.5.1"|g' composer.json && composer update && mv public html

Те­перь отре­дак­тиру­ем кон­фиги nginx. Вклю­чаем обра­бот­ку скрип­тов PHP и нас­тра­иваем необ­ходимый для Laravel редирект.

sed -i -E 's|index index|index index.php index|g' /etc/nginx/sites-enabled/default
sed -i -E 's|try_files \$uri.*|try_files \$uri \$uri/ /index.php?\$query_string;|g' /etc/nginx/sites-enabled/default
sed -i -E 's|#location ~ \\\.php\$ \{|location ~ \\.php\$ {\n\t\tinclude snippets/fastcgi-php.conf;\n\t\tfastcgi_pass 127.0.0.1:9000;\n\t}|g' /etc/nginx/sites-enabled/default

Ме­няем нас­трой­ку демона PHP-FPM, что­бы он работал по TCP и висел на 9000-м пор­те.

sed -i -E 's|listen = .*|listen = 127.0.0.1:9000|g' /etc/php/7.3/fpm/pool.d/www.conf

Для изу­чения деталей работы Ignition нам так­же пот­ребу­ется прос­тень­кий кон­трол­лер. Добавим роут test.

/var/www/routes/web.php
19: Route::get('/test', function () {
20: return view('test');
21: });

Те­перь нуж­но соз­дать view (пред­став­ление) это­го роута. Для опи­сания пред­став­лений в Laravel исполь­зует­ся шаб­лониза­тор Blade.

/www/resources/views/test.blade.php
<!DOCTYPE html>
<html>
<body>
Hello, {{ $name }}.
</body>
</html>

где $name — это перемен­ная, которую нуж­но передать во view.

С этим разоб­рались, оста­лось ска­чать сор­цы фрей­мвор­ка. Их мож­но взять пря­мо из Docker.

docker cp laravelrce:/var/www ./

Те­перь все готово и мож­но прис­тупать к раз­бору уяз­вимос­ти.

 

Детали уязвимости

Для начала про­верим, вклю­чена ли Ignition. Мож­но отпра­вить зап­рос, для которо­го нет обра­бот­чика у кон­крет­ного роута. Нап­ример, неп­лохо работа­ет DELETE на index.php.

Отправка некорректного типа запроса DELETE на index.php
От­прав­ка некор­рек­тно­го типа зап­роса DELETE на index.php

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

http://laravelrce.vh:8080/_ignition/execute-solution
Кастомизированное сообщение об ошибке от Ignition
Кас­томизи­рован­ное сооб­щение об ошиб­ке от Ignition

Ес­ли видишь кра­сивую кар­тинку с сооб­щени­ем об ошиб­ке, как на скрин­шоте, — зна­чит, все идет по пла­ну. Помимо кас­томизи­рован­ных стра­ниц с сооб­щени­ем об ошиб­ке, Ignition поз­воля­ет соз­давать так называ­емые solutions. Это неболь­шие фраг­менты кода, они помога­ют решить проб­лемы, с которы­ми стал­кива­ются раз­работ­чики. Нап­ример, вер­немся к нашему роуту test. В шаб­лоне мы исполь­зуем перемен­ную $name, но не переда­ем ее, поэто­му Laravel вер­нет ошиб­ку.

Сообщение об ошибке в Laravel, если не найдена переменная в шаблоне
Со­обще­ние об ошиб­ке в Laravel, если не най­дена перемен­ная в шаб­лоне

Об­рати вни­мание на кноп­ку Make variable optional. При нажатии к сер­веру ухо­дит инте­рес­ный зап­рос.

POST /_ignition/execute-solution HTTP/1.1
Host: laravelrce.vh:8080
Accept: application/json
Content-Type: application/json
{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"name","viewFile":"/var/www/resources/views/test.blade.php"}}

В парамет­ре solution ука­зыва­ется класс, который нуж­но выпол­нить. Шту­ка любопыт­ная, но ука­зать про­изволь­ный класс там не получит­ся, так как Ignition тре­бует, что­бы вызыва­емый класс реали­зовы­вал интерфейс RunnableSolution.

/vendor/facade/ignition/src/SolutionProviders/SolutionProviderRepository.php
83: public function getSolutionForClass(string $solutionClass): ?Solution
84: {
85: if (! class_exists($solutionClass)) {
86: return null;
87: }
88:
89: if (! in_array(Solution::class, class_implements($solutionClass))) {
90: return null;
91: }
92:
93: return app($solutionClass);
94: }
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
8: class MakeViewVariableOptionalSolution implements RunnableSolution

Гля­нем код MakeViewVariableOptionalSolution, что­бы узнать, что он дела­ет. Сна­чала вызыва­ется метод makeOptional.

/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
65: public function run(array $parameters = [])
66: {
67: $output = $this->makeOptional($parameters);

Он чита­ет файл, путь до которо­го был передан в парамет­ре viewFile.

/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
73: public function makeOptional(array $parameters = [])
74: {
75: $originalContents = file_get_contents($parameters['viewFile']);

За­тем передан­ная в variableName перемен­ная изме­няет­ся с вида $name на $name ?? ''.

/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
76: $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);

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

/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
78: $originalTokens = token_get_all(Blade::compileString($originalContents));
79: $newTokens = token_get_all(Blade::compileString($newContents));
80:
81: $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
82:
83: if ($expectedTokens !== $newTokens) {
84: return false;
85: }

Ес­ли это не так, то makeOptional вер­нет false, в про­тив­ном слу­чае (вари­ант, ког­да все прош­ло глад­ко) содер­жимое шаб­лона переза­писы­вает­ся.

/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
73: public function makeOptional(array $parameters = [])
74: {
...
87: return $newContents;
88: }
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
65: public function run(array $parameters = [])
66: {
...
68: if ($output !== false) {
69: file_put_contents($parameters['viewFile'], $output);
70: }

Ес­ли отбро­сить все лиш­нее, то выпол­няет­ся прос­тое чте­ние и запись фай­ла.

75: $originalContents = file_get_contents($parameters['viewFile']);
69: file_put_contents($parameters['viewFile'], $output);

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

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

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Оставить мнение