[HarekazeCTF2019]Avatar Uploader 1 题解


打开题目发现是一个登入界面,试下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格式存储的。当时上传总是被卡,搞了半天才发现是这里的问题,给大家避避雷。

声明:大K|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - [HarekazeCTF2019]Avatar Uploader 1 题解


I'm scared this is all i will ever be...I feel trapped in my own life...I think i've figured it out but in reality i'm as lost as ever...I wish i could choose the memories that stay...please,stay.