HexBrain

VK.com · Report #142135

XSS в upload.php

Validstatus
$1,500bounty
38upvotes
9 Feb 2017disclosed

How it was found

Добрый вечер! Раскрутил интересную xss на upload.php.

Демо тут или тут.

Как все было? Увидел интересный экшн upload.php?act=transport, который служит для загрузки нарисованного граффити в документы. Глаз зацепился за вызов функции eval в строке 25. Обратите внимание на параметр callback, значением которого в данном случае является строка getUploadSvg. На этой странице определена функция sendData (строки 11-52) и, помимо этого, выполняется следующий кусок кода (строки 55-64):

var _ua = navigator.userAgent;
var locDomain = location.host.toString().match(/[a-zA-Z]+\.[a-zA-Z]+\.?$/)[0];
if (/opera/i.test(_ua) || !/msie 6/i.test(_ua) || document.domain != locDomain) {
  document.domain = locDomain;
}
if (window.parent) {
  parent.getUploadSvg(function(data) {
    sendData('/c415824/upload.php?transport=iframe&act=add_doc&hash=8dfd93e60c78ddb4a9cf914c27f7642c&rhash=8171a35e59a63aab65846a26345ddbf6&aid=1&mid=17274528&pda=', data);
  });
}

, где есть два условных оператора с бессмысленными условиями 👳🏾. В первом проставляется document.domain для кроссдоменного выполнения сценариев, а во втором все самое интересное - вызывается функция родительского окна parent.getUploadSvg, где getUploadSvg это значение GET-параметра callback (как уже было написано выше) и его значение можно менять на любое другое (предположительно подходящее под паттерн /^[a-zA-Z0-9]*$/). То есть функции parent.{значение параметра callback} передается один аргумент, являющийся анонимной функцией. Эта анонимная функция принимает один параметр data, а в её теле происходит вызов функции sendData (строка 62):

sendData('/c415824/upload.php?transport=iframe&act=add_doc&hash=8dfd93e60c78ddb4a9cf914c27f7642c&rhash=8171a35e59a63aab65846a26345ddbf6&aid=1&mid=17274528&pda=', data);

Тут первый аргумент это строка, содержащая относительный URL, в который попадают почти все исходные GET-параметры, проходя перед выводом санитизацию. Второй параметр это переменная data, о которой писал чуть выше.

Разберем саму функцию sendData. Она принимает три аргумента url, file и sequel . По понятным причинам, нам интересны только первые два. Стоит обратить внимание на строку 16

var plain = /iphone|ipod|ipad|opera mini|opera mobi/i.test(navigator.userAgent);

и блок кода условного оператора в строках 29-41

if (plain) {
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

  if (dataUrl.length > 15000) {
    fdataUrl = dataUrl.substr(0, 15000);
    ndataUrl = dataUrl.substr(15000);
    dataUrl = fdataUrl;
    more = 1;
  }

  xhr.send(ajx2q({Data: dataUrl, more: more, sequel: sequel || 0}));
} else {

Остановились мы на этом потому, что в строке 40 происходит вызов функции ajx2q , которая не определена и, поэтому, сохранение граффити в документы не работает для юзерагентов удовлетворяющих регулярному выражению /iphone|ipod|ipad|opera mini|opera mobi/i. Следовательно и хак не будет работать. Если не обращать внимания на сей досадный факт, то можно заметить, что в функции sendData с помощью XMLHttpRequest происходит POST-запрос на URL, параметрами которого мы можем манипулировать. В случае успеха, результат запроса передается в функцию eval, что круто если каким-то образом заставить сервер выдавать нужный нам ответ на этот POST-запрос. Поскольку мы можем манипулировать параметрами URL, на который будет произведен запрос, я начал думать над тем как заставить сервер отвечать на запрос моим js-кодом. Тут я вспомнил о том, что существуют такие штуки как imagejs и share.php. Первое нам интересно потому что позволяет встроить js-код прямо файлы картинок (Gif, Bitmap, WebP, Netbpm Anymap, Progressive Graphics) и файлы будут валидны. А в share.php как-раз есть проксирование изображений (пример). Тут я столкнулся с другой проблемой - хэши. Это была боль, но методом тыка я понял, что если передать скрипту upload_fails.php все необходимые параметры и добавить к ним role=share, то можно получить необходимые валидные хэши, используя которые можно скрафтить конечный URL. На этом этапе использовалась особенность санитизации переданных параметров (при изучении демо обратите внимание на точки в названиях параметров, они там не случайно). Осталось разобраться с последней проблемой. Заключается она в том, что sendData просто так не вызывается. Надо найти такую js-функцию, для которой выполняются следующие условия:

  • название функции должно содержать только те символы, которые можно передать через GET-параметр callback и функция должна быть глобальной;
  • функция должна принимать в качестве аргумента другую callback-функцию и вызвать её передавая в качестве аргумента какой-нибудь объект.

Немного for (... in window) и такая функция была найдена - requestAnimationFrame (документация).

Далее реализовал демо, которое можно посмотреть тут или тут. Выполняется alert(document.cookie);.

Таким образом, получили xss для браузеров, строка user-agent которых не попадают под паттерн /iphone|ipod|ipad|opera mini|opera mobi/i и в которых имплементирована функция requestAnimationFrame (или с вендорными префиксами mozRequestAnimationFrame, webkitRequestAnimationFrame, msRequestAnimationFrame).

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original