PRELOADER

当前文章 : 《第三届强网杯一道web题(upload)详解》

8/20/2019 —— 

前言

这次强网杯真的很难啊(个人觉得),web题要是没有师兄援助的话可能就只做出一道最简单的,真的是看各路神仙打架了。。不说了,先好好的复现一次吧。

upload

一拿到题目打开就是一个登录注册页面,

顺手先测了下有没有sql注入,发现没有后注册了一个,登录

是一个上传页面,然后就是在burp里测试各种绕过上传

期间开着dirsearch对站点进行目录扫描

测试发现了每个账户都只能上传一次图片,每次重新尝试绕过都要重新注册一个新账号,这个有点繁琐

后来发现扫出来了一个备份文件www.tar.gz

下载下来后打开果然是网站源码,看来就是代码审计里(代码审计实在不是强项)

没办法,只能硬着头皮啃了,还好源代码也不算多,关键也是几个文件。

关键的函数也只有几处,先来看下Profile.php中的upload_img函数,当我们上传头像的时候根据路由后台会直接调用这里的upload_img函数

首先,先检查用户是否登录,调用了login_check函数,我们跟进login_check函数

可以看到这里有个unserialize函数,第一时间先想到反序列化漏洞,继续看,简单来说这个检查函数就是先把cookie中的user取出来,接着base64解码,反序列化后的值与数据库中根据id查询出来的值进行对比,相同即返回1,不同即返回0;

返回upload_img函数,接着往下

接着检查是否上传了文件, 若有文件被上传,filename_tmp为在服务端储存的临时文件名,filename为客户端文件的原名称 的md5值并且加上了“.png”为后缀,接着调用ext_check()函数对后缀进行检查,若后缀为png则返回1,否则返回0。(当时我一直认为该检查函数有点多此一举的感觉,事实证明这在后面的反序列中是需要绕过的)

继续往下,当ext()检查函数返回1后进入if语句,先getimagesize()函数简单判断传入的文件是否为图片类型,

接着复制临时文件filename_tmp到md5值+png后缀的filename文件,然后再删除临时文件filename_tmp

这里我首先想到了条件竞争,后来想了想也否定了。

往下,把filename赋给img后,调用update_img()函数,跟进

该函数先从数据库根据id取出img的值(当我们第一上传图片后,数据库中该img就有相对应的值了),检查img的值是否为空,为空则表明是第一次上传,然后提示“Upload img successful!”,返回home目录;

如果不为空,即之前已经上传过一次图片了,则报错’Upload file failed!’,返回index目录;

看到这里也明白了为什么之前会有一个账户只能上传一次的错觉,原因就是这里,其实一个账户可以不止上传一次,前端虽然看起来返回失败了,后端还是上传上去了。

这里对upload()函数已经有了一个大概了解。接着看同样是Profile.php文件下的update_cookie()函数

很短,只有两行。看第一行,其实就是把filename的路径(也就是我们图片上传后的路径)的值赋给profile[img],

然后第二行就是对profile进行序列化,base64加密,再更新客户端中的cookie。

再来看看Profile.php最后的两个函数

这两个都是魔术方法,先来看第一个__get (),它的作用是当其他类读取其不可访问属性的值时自动触发。在这里,就是当其他类读取了profile类中不可访问属性的值时自动返回except数组的内容。

第二个是__call()魔术方法,其实跟 第一个是类似的,只不过它是对象中调用一个不可访问方法时触发 。也就是说,这两个魔术方法里的内容合起来,只要构造得到,就可以随意调用该类中的某一个方法。

接下来我们要看到Register.php中的最后一个函数,也是一个魔术方法:

__destruct ()函数的作用是当对象的所有引用都被删除或者当对象被显式销毁时执行。同时看这里它执行的内容,可以调用index方法。

到这里,一条反序列的攻击链已经呼之欲出了,接下来就是要思考怎么整合这些可利用的点并构造反序列化了。

慢慢来捋一遍思路:

首先,无论我们打开哪个页面,都先调用login_check(函数来检查是否已登录,而login_check()函数中就把cookie中的user值提出来并反序列化,我们就得从这里先入手。

那么在构造序列化的时候要先new哪一个类呢?

在register类的最后一个魔术方法是一个比较好的选择,因为我们可以控制它去访问profile类中的index方法。

而profile类中由于index方法,自动触发__call()魔术方法,

先if语句判断index的值是否存在,而index在profile中是不存在的,所以又自动触发了__get()魔术方法,我们可以控制except数组返回的值为upload_img,从而来调用upload_img()函数,成功地调用到了upload函数后,再让

filename_tmp的值改为我们已经上传的图片的路径+文件名,再把filename改为php后缀的文件,就可以让图片里的代码成功解析了。

总结:所以总的来说就是通过上传包含有恶意代码的图片,通过修改cookie来达到反序列化的一系列操作,把png后缀的文件改为php文件从而来实现了真正的上传绕过。

附上:构造反序列化的php代码

<?php
namespace app\web\controller;
require __DIR__ . '/../thinkphp/base.php';
use think\Controller;

class Register
{
    public $checker;
    public function __construct()
    {
        $this->checker = new Profile();
    }
}
class Profile
{
    public $except;
    public $filename;
    public $filename_tmp;
    public $ext;
    public $checker;
    public function __construct()
    {
        $this->filename="../public/upload/98acc62aa02eda032d1caed497ce72a0/6d74cc7548a0ddef1eafc6a6224e9d43.php";
        $this->filename_tmp = "../public/upload/98acc62aa02eda032d1caed497ce72a0/6d74cc7548a0ddef1eafc6a6224e9d43.png";
        $this->ext = "1";
        $this->checker = "0";
        $this->except=array("index"=>"upload_img");
    }

}

echo base64_encode((serialize(new Register())));

?>