Форензика, SQL-инъекция и многострадальный котик: разбор задания №3 online-этапа NeoQUEST-2020

  • Tutorial


Сегодня рассмотрим самое противоречивое задание NeoQUEST-2020: первая его часть является рекордсменом по количеству прошедших его участников, а вторая часть покорилась лишь единицам. Что же в нем такого особенного? Разбираемся под катом!


Тебе когда-нибудь приходилось исследовать дамп оперативной памяти? А знаешь ли ты, сколько всего интересного можно оттуда извлечь? (спойлер: все, включая результаты работы запущенных скриптов, недавние фотографии, а также историю браузера).
А как насчет экзотичных SQL-инъекций? Ты загружаешь определенную картинку на сайт, а тебе выдается информация из закрытой базы данных — разве это не идеальное преступление?
В первой части статьи мы рассмотрим средства для анализа дампа оперативной памяти и научимся извлекать оттуда нужную нам информацию, а во второй части статьи наглядно покажем процесс манипуляции тегами картинки для проведения хитрой инъекции:)

Итак, задании предлагается скачать архив, который содержит бинарный файл размером 1 Гб с говорящим названием memdump.bin, исходя из чего можно предположить, что это дамп оперативной памяти. Для его исследования будем использовать фреймворк volatility. Volatility поддерживает различные плагины, которые упрощают поиск и извлечение из дампа различной полезной для шантажа аналитика информации. Для начала воспользуемся командой imageinfo и получим информацию про дамп:



Судя по выводу команды imageinfo, мы имеем дело с дампом памяти Windows 7. Далее посмотрим список запущенных процессов в системе на момент снятия дампа с помощью команды pslist:



В полученном списке можно заметить несколько интересных процессов. Начнем исследование с процесса chrome.exe — в браузере всегда можно найти что-нибудь интригующее. Для volatility существуют дополнительные плагины, которые автоматизируют извлечение информации из процессов. Например, плагин chromehistory позволит нам извлечь из дампа оперативной памяти историю посещений браузера:



В истории хрома можно заметить сразу несколько интересных вещей. Во-первых, ссылка на вторую часть задания, а также поисковые запросы про метаданные в PNG изображениях, что также является подсказкой ко второй части, но к этому обратимся позже. На текущем же этапе для нас интересны запросы про промокод на скидку к сайту и pdf-файлы, что намекает на объект дальнейших поисков (также на это нас могут натолкнуть запущенные процессы Acrobat Reader`a). Попробуем найти документы формата pdf в дампе с помощью команды filescan:



Удача! Похоже, в дампе присутствует некий promo.pdf. Попробуем вытащить его, используя команду dumpfiles:



На этом этапе могут возникнут небольшие сложности из-за того, некоторые программы просмотра pdf-документов могут не распознать только что извлеченный файл (необходимо просто удалить лишние байты в конце файла). Но разве страшны нам какие-то лишние байты?:) В конце концов открываем документ и видим QR-код, прочитав который, мы получаем флаг от первой части задания (а заодно и промокод на получение подарочка от команды NeoQUEST — ведь мы дарим памятные призы всем прошедшим хотя бы одно задание участникам!).



Далее попробуем получить второй флаг. Вернемся к сайту, ссылку на который нашли в истории браузера. Для доступа на сайт требуется ввести промокод. Вспоминаем историю поиска в браузере и описание самого документа (USE THIS TO JOIN), из чего можно предположить, что первый флаг и является нужным нам промокодом. Вводим его в поле и попадаем на сайт, где нам предлагают загрузить картинку:



Судя по тому, как часто сайт зависал, наши участники решили опытным путем проверить загрузку всех возможных типов документов. Энное количество времени спустя понимаем, что на сайт можно загружать только изображения в формате PNG. Следующей головоломкой является сообщение «Not enough data to store this image, sorry!», возникающее при попытке загрузить какую-нибудь картинку.

Возвращаясь к истории браузера, вспоминаем про запросы вида «add metadata to png file». Путем несложных умозаключений делаем вывод, что, скорее всего, для загрузки изображения на сайт в его метаданные должны быть внесены определенные значения. Начать можно с изучения формата PNG (описание есть, например, тут). Сначала проверяем список стандартных ключевых слов для метаданных PNG изображений, например: «Author», «Description» и т.д. Используя, например, утилиту convert из набора ImageMagick, добавим метаданные тестовому изображению:

convert test.png -set 'Title' '1' -set 'Author' '2' -set 'Description' '3' -set 'Copyright' '4' -set 'Creation Time' '5' -set 'Software' '6' -set 'Disclaimer' '7' -set 'Warning' '8' -set 'Source' '9' -set 'Comment' '0' out.png

Пробуем загрузить полученное изображение на сайт и радуемся успеху:



Оказывается, что необходимыми полями метаданных являются Title, Description, Author и Copyright.
Подсказку к этому, к слову, можно отыскать и в дампе памяти: можно найти упоминание файла mr_cat.png, используя volatility.



Наши участники решили, что именно с помощью мистера Кота нужно провести инъекцию на сайт (на нашу почту приходила куча сообщений с мистером Котом в сжатом, перевернутом, отраженном и иных неприглядных видах), но котик здесь вообще не причем! Он просто отвлекает внимание и намекает участникам: ну же, загляни в меня, внутри есть кое-что интересное! Внутри дампа изображение сохранилось лишь частично; однако в его остатках можно заметить нужные заполненные поля метаданных:



Идем дальше: теперь мы можем загружать файлы на сайт; однако что это дает и как искать флаг? Логично предположить, что раз для загрузки требуются метаданные, то, возможно, они каким-то образом используются для хранения загруженных файлов (например, как ключи в базе данных). Ура! Мы приближаемся к десерту: необходимо проверить сайт на возможность проведения SQL-инъекции. Выясняем, что если в один из параметров подставить двойную кавычку ("), то сайт вместо загрузки изображения оповестит нас об ошибке:



Теперь мы видим сам SQL-запрос и понимаем, какими полями можем оперировать, чтобы сконструировать инъекцию для извлечения данных из используемой БД. Что касается местоположения флага, то в данном случае логично проверить самое первое изображение, которое было загружено в базу данных. Также при эксплуатации инъекции следует обратить внимание на то, что в запросе выполняется INSERT. Это заставляет конструировать специфичные запросы, потому что нельзя одновременно с операцией вставки (INSERT) явно выполнять операцию выбора (SELECT) из той же таблицы, в которую производится вставка.

Составим запрос, с помощью которого вытащим описание (Description) первого изображения в базе данных:

convert test1.png -set 'Title' '1' -set 'Author' '",(SELECT description FROM (SELECT * FROM picture) AS x limit 0,1) ) — -' -set 'Description' '3' -set 'Copyright' '4' test.png

Загрузим полученное изображение на сайт и получим в поле Copyright описание, которое представляет собой флаг:



Ура, задание пройдено! Надеемся, что мы убедили всех в миллионный раз перепроверять пользовательский ввод данных, а также беречь как зеницу ока от создания дампа свою оперативную память:)
Закончим статью цитатой великих мудрецов:

НеоБИТ
Компания

Комментарии 6

    0
    Внутри дампа изображение сохранилось лишь частично

    Вроде целиком, там просто адреса чанков немного побитые были. Из дампа реконструировать картинку пришлось скриптом, а не через стандартные плагины volatility.

      0
      А как Вы вытащили cat.png? Команда
      volatility --profile=Win7SP1x86 -f memdump.bin -Q 0x000000003fbfed28 dumpfiles -D ./
      вообще ничего не сохранила. (где 0x000000003fbfed28 — адрес \Device\HarddiskVolume2\Users\user\Desktop\mr_cat.png\mr_cat.png.png)
        0

        Она не сохраняет, потому что адреса невалидные (насколько я помню, там добавлено 0x200000000 было). Я использовал флаг --unsafe и --summary-file чтобы получить эти адреса, а затем:


        const fs = require('fs');
        
        const CHUNKS = [0x0681b000, 0x0a31c000, 0x2581d000, 0x0c6de000, 0x37a1f000, 0x178e0000, 0x2e6a1000, 0x08462000, 0x05da3000, 0x3c6e4000, 0x176a5000, 0x3ce26000, 0x17ce7000, 0x20da8000, 0x066e9000, 0x20b6a000, 0x2e8ab000, 0x0aaec000, 0x1d66d000, 0x24c6e000, 0x26fef000, 0x287b0000, 0x14631000, 0x18432000, 0x2f633000, 0x2a634000, 0x08d75000, 0x208b6000, 0x105f7000, 0x213b8000, 0x2a279000, 0x0983a000, 0x2027b000, 0x2ed3c000, 0x180fd000, 0x264fe000, 0x0f13f000, 0x3b400000, 0x28d81000, 0x32d42000, 0x078c3000, 0x29044000, 0x240c5000, 0x10606000, 0x02147000, 0x018c8000, 0x2dfc9000, 0x1adca000, 0x0464b000, 0x0b78c000, 0x14b8d000, 0x1a00e000, 0x0640f000, 0x0f610000, 0x14191000, 0x06712000, 0x2e3d3000, 0x0fd94000, 0x06095000, 0x33cd6000, 0x0b217000, 0x3aa18000, 0x10619000, 0x3951a000, 0x3161b000, 0x33e9c000, 0x1571d000, 0x1771e000, 0x1a1df000, 0x3bf20000, 0x3b2e1000, 0x18b22000, 0x00863000, 0x220a4000, 0x1b0a5000, 0x05526000, 0x0bc27000, 0x2a0e8000, 0x15c29000, 0x18caa000, 0x223ab000, 0x3706c000, 0x112ad000, 0x352ae000, 0x36a6f000, 0x015f0000, 0x2fc31000, 0x19ef2000, 0x15fb3000, 0x11c34000, 0x343b5000, 0x21cf6000, 0x29ab7000, 0x0f738000, 0x09cf9000, 0x389ba000, 0x314bb000, 0x02ebc000, 0x2df3d000, 0x2453e000, 0x27e7f000, 0x2eb01000, 0x0ef01000, 0x2a942000, 0x12083000, 0x1e944000, 0x06485000, 0x2fc86000, 0x29e87000, 0x17648000, 0x031c9000, 0x0f60a000, 0x1190b000, 0x0004c000, 0x3b90d000, 0x3f7ce000, 0x31a4f000, 0x1ea50000, 0x0df11000, 0x0fa92000, 0x0d5d3000, 0x052d4000, 0x1ac55000, 0x35c16000, 0x2b697000, 0x0fed8000, 0x33419000, 0x216da000, 0x2f01b000, 0x3545c000, 0x1b59d000, 0x098de000, 0x207df000, 0x130e0000, 0x1f821000, 0x0fb22000, 0x01323000, 0x0c0a4000, 0x3f925000, 0x2cee6000, 0x085a7000, 0x036a8000, 0x2cae9000, 0x28f6a000, 0x09eab000, 0x1af2c000, 0x1bbed000, 0x3b82e000, 0x0582f000, 0x2c3f0000, 0x22eb1000, 0x3a9b2000, 0x12db3000]
        
        const read = (fd, len, offset) => new Promise((resolve) => fs.read(fd, Buffer.alloc(len), 0, len, offset, (err, bytesRead, buffer) => {
          console.log(`Read ${bytesRead} bytes, err = ${err}`);
          resolve(buffer);
        }));
        
        (async function main(){
          const fd = fs.openSync('h:\\neoquest\\3\\volatility\\memdump.bin');
          const buffers = [];
          for (const offset of CHUNKS) {
            const buf = await read(fd, 0x1000, offset);
            buffers.push(buf);
          }
        
          fs.writeFileSync('h:\\neoquest\\3\\recovered.png', Buffer.concat(buffers), 'binary');
        })();
      0
      А с чем связано у вас такое поведение?
      Если я ввожу
      ...select author from (select * from picture) as X limit 1)…
      То получаю ответ: Copyright: Man from the Ship
      А если: select author from (select author from picture) as X limit 1)
      то выводит Copyright:-
      Хотя по идеи должно и так и так выводить первую запись.
        0

        Достаточно задать alias.


        fake3", (SELECT file_name FROM picture pict WHERE pict.id=1), "fake1", "fake2") --

          0
          Да, верно, но мне просто интересно почему разные результаты в двух вариантах, хотя должны быть одинаковыми. Если вставить код в Author:
          1. fake3", (SELECT description FROM (SELECT * FROM picture) AS pic LIMIT 1)) — 2. fake3", (SELECT description FROM (SELECT description FROM picture) AS pic LIMIT 1)) — Со звездочкой работает как и задумано — выводит флаг, а вот во втором варианте выводит просто дефис. Хотя мы выбираем так же один столбец Description, а потом из него выбираем первое значение. Может глюк MySQL какой?! На локальной базе вывод одинаковый у обоих запросов)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое