打开题目发现是一个登入界面,试下123,发现不符合格式,看下源码发现有个正则
^[0-9A-Za-z_]{4,16}$
随便试下uuuu_123, 成功登入。
是一个文件上传界面,随便传下一句话,发现均被拦截,我们来审下题目给的upload.php
<?php
error_reporting(0);
require_once('config.php');
require_once('lib/util.php');
require_once('lib/session.php');
$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);
// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
error('No file was uploaded.');
}
// check file size
if ($_FILES['file']['size'] > 256000) {
error('Uploaded file is too large.');
}
// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}
// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}
// ok
$filename = bin2hex(random_bytes(4)) . '.png';
move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename);
$session->set('avatar', $filename);
flash('info', 'Your avatar has been successfully updated!');
redirect('/');
发现想要打印flag需要满足$size[2] !== IMAGETYPE_PNG,而size数组是getimagesize返回的内容。
梳理下思路,我们现在要绕过finfo_file函数的检测,同时要使得$size[2]!==IMAGETYPE_PNG。关于finfo_file函数,我们看一下PHP官方文档:
# finfo_file() 示例
<?php
$finfo = finfo_open(FILEINFO_MIME_TYPE); // 返回 mime 类型,也被称为 mime 类型扩展。
foreach (glob("*") as $filename) {
echo finfo_file($finfo, $filename) . "\n";
}
finfo_close($finfo);
?>
可以看到这里是返回了文件的MIME类型,但获取MIME类型的方式不是我们熟知的在请求报文中提取Content-type字段,而是进行了一个open操作,猜测是根据文件的二进制内容进行判断。
去查找了下资料,发现finfo_file函数是通过文件幻术头来判断MIME的。那么我们可以只保留文件幻术头,后面的全都删掉,使得图片能通过finfo_file函数检测,同时能使得getimagesize函数因检测不到图像的宽高等信息而出错,无法给$size[2]赋值,从而满足$size[2]!==IMAGETYPE_PNG。
上文中getimagesize函数出错的具体原理是什么?
正常情况下,getimagesize返回的数组格式为
Array
(
[0] => 290 //图像宽度的像素值
[1] => 69 //图像高度的像素值
[2] => 3 //图像的类型
[3] => width="290" height="69" //一个宽度和高度的字符串,可以直接用于 HTML 的 <image> 标签
[bits] => 8 //图像的每种颜色的位数,二进制格式
[mime] => image/png //图像的 MIME 信息
)
其中宽度的像素值和高度的像素值部分的获取,对于PNG格式文件来说,是通过解析16进制文件的第二行得到的。
假设我们有这样一PNG文件
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452
00000010: 0000 07c0 0000 0450 0806 0000 0075 56d2
其中第一行的0000 000d表示 IHDR 块的数据长度是 13 字节,4948 4452是 IHDR 块的标识符(对应 ASCII 的 "IHDR")。接下来第二行就是IHDR块的具体内容,存储了图像的基本信息,如宽度、高度、位深度、颜色类型等。
例如上面的00000010这第二行,解析一下就是:
0000 07c0:这是图像的宽度,使用 4 个字节表示。07C0 是十六进制,转换为十进制是 1984,即图像的宽度为 1984 像素
0000 0450:这是图像的高度,使用 4 个字节表示。0450 是十六进制,转换为十进制是 1104,即图像的高度为 1104 像素
08:这是位深度,表示每个像素的位数。这里是 8 位
06:这是颜色类型,06 表示使用 RGBA(红绿蓝透明度)四通道颜色
00:这是压缩方法,PNG 只支持 0 表示的压缩方式(基于 Deflate)
00:这是过滤方法,PNG 只支持 0 表示的过滤方法
00:这是交错方法,0 表示没有使用交错(逐行扫描)
总而言之,第二行是IHDR的具体内容,包含关键的宽高信息,若我们将这第二行删除,getimagesize函数无法获取到宽高信息,也就无法正常返回size数组,自然会导致出错。
再提一嘴,某绿色图标即时通讯软件自带的截图功能,以png格式保存下来的图片是伪png,我们去看16进制数据的时候可以发现底层还是JPEG格式存储的。当时上传总是被卡,搞了半天才发现是这里的问题,给大家避避雷。
Comments | NOTHING