The learning of php unserialize
概述
当unserialize()
的参数可控时, 可以传入一个构造好的序列串控制类内部的变量甚至函数, 如果类内部存在比较危险的函数include() eval()
等等, 将会造成严重后果
如何利用
当服务端代码中存在某个类的时候, 可以通过unserialize()
创建一个该类的对象, 而我们可控的就是类中的属性, 还可以利用一些技巧去绕过一些魔术方法或者是利用一些魔术方法. 通过控制魔术方法和属性达到我们想要的攻击效果.
一般流程
寻找参数可控的
unserialize()
寻找可以用于命令执行, 文件包含等等的函数在危险类
- 找到一个控制链控制危险类中的危险函数
魔术方法
1 | __wakeup() //执行unserialize()时,先会调用这个函数 |
需要注意的是, __toString()
这个方法触发的方式比较多:
echo ($obj) / print($obj)
打印时会触发- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行
==
比较时(PHP
进行==
比较的时候会转换参数类型) - 反序列化对象参与格式化
SQL
语句,绑定参数时 - 反序列化对象在经过
php
字符串函数,如strlen()
、addslashes()
时 - 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有
toString
返回的字符串的时候toString
会被调用 - 反序列化的对象作为 class_exists() 的参数的时候
关于绕过的Trick
绕过__wake()
版本:
php 5< 5.6.25
php 7> 7.0.10
利用方法:当序列化的字符串中的属性个数大于类中实际的属性个数时, 就会跳过__wake()函数
绕过正则表达式
加号绕过, 在反序列化的时候, 数字前面的加号会被当成是正号而正则匹配的时候则不会这样识别
比如说
O:4:"test":1:{s:1:"a";s:3:"abc";}
, 有时候题目会通过正则O\:\d
来匹配O:4
, 这时将序列改
成O:+4:"test":1:{s:1:"a";s:3:"abc";}
就能绕过该匹配
变量引用
利用变量引用使得类中两个变量始终相等
php原生类反序列化
SoapClient反序列化利用
概述
php安装php-soap
扩展之后, 可以反序列化原生类SoapClient
, 来发送http post
请求来实现SSRF
通过反序列化SoapClient
对象后调用不存在的方法触发__call()
方法
这个__call()
方法会向指定URL
发送HTTP POST
请求, 因为User-Agent
字段可控, 所以可以通过CRLF
来构造请求头实现自定义POST DATA
如果要在自己的环境复现则需要更改php.ini
中的extension=soap
利用方法
举个栗子
1 |
|
当SoapClient
对象被逆序列化出来并调用一个不存在的tan90_function()
方法时.__call()
方法会向location
也就是http://127.0.0.1/test.php
发送一个HTTP POST
请求, 而请求头中会有:
1 | User-Agent: kirito-zbds |
可以看到, 请求头中的这些字段会变成可控的, 但这样只能通过内网访问test.php
, 并不能任意POST
数据. 要想POST
数据, 就要用到CRLF
了.
什么是CRLF
CRLF
就是\r\n
的缩写, HTTP
协议中header
之间是用一个\r\n
来分隔的, 而HTTP header
和HTTP body
之间是用\r\n\r\n
也就是两个\r\n
来分割的. 如果请求头中的字段可控, 那么可以手动添加\r\n
用以在请求中添加字段甚至是添加HTTP body
用以POST data
.
比如在上面的例子中, 我们把$ua
改为
1 | $ua = "kirito-zbds\r\nContent-Type:application-www-form-urlencoded\r\nContent-Length:17\r\n\r\ndata=this_is_data" |
那么HTTP POST
的请求头中就会出现这么一串
1 | User-Agent: kirito-zbds |
这样就成功伪造出HTTP POST
请求并传输参数了
php-session反序列化
又是session
, 上次在文件包含也有session
, 其实这里的反序列化也要用到那边的知识
概述
关于session.upload_progress
这个php-session
反序列化其实跟session.uplaod_progress
有关, 详细的可以看文件包含那部分的笔记
这里只简单的说一下, 服务端的php.ini
默认配置session.use_strict_mode = 0
, 这样用户可以自己设置Cookies: PHPSESSID = xxxxxx
, 服务端将会创建一个/tmp/sess_xxxxxx
文件
并且, 如果用户向服务端传送文件, 那么服务端将会在/tmp/sess_xxxxxx
中写入上传进度, 最重要的是. 如果用户的POST DATA
中含有PHP_SESSION_UPLOAD_PROGRESS=xxx
, 那么服务端将会在/tmp/sess_xxxxxx
中写入
1 | upload_progress_xxx | 上传进度 |
关于session.serialize_handler
在执行php
代码时可以设置这么两个东西
1 | ini_set('session.serialize_handler','php_serialize'); |
这里设置的是php处理session
的方式,php
或php_serialize
. 默认是php
这两个方式的不同就在于, 存取session
文件的格式不同.
下面从一个例子来详细的看一下这两个方式的区别
1 |
|
php
方式的存取的格式是:键名|serialize_string
, 当我传?a=a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}
session
文件中存的是k1rit0|s:45:"a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}";
而如果用的是php_serialize
方式,存取的格式就是serialize_string
, session
文件中存的便是a:1:{s:6:"k1rit0";s:45:"a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}";}
利用方法
根据上面的两个session
存取方法, 如果我们先以php_serialize
储存 再按php
的方式读取, 就可以反序列化一些恶意的序列. 就拿上面的例子来说, 先以php_serialize
储存?a=|a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}
,注意那个|
这个时候来看看session
里是啥
1 | a:1:{s:6:"k1rit0";s:46:"|a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}";} |
如果这个时候, 按php
的方式读取这个session
文件, 就会被识别成两部分:
|
前面的a:1:{s:6:"k1rit0";s:46:"
|
后面的a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}";}
|
后面的前一部分`a:2:{s:4:"name";s:6:"kirito";s:3:"age";i:14;}
, 会被识别成一个完整的序列化串
这里有个session_start()的小知识, 如果php代码中存在session_start()而且处理session的方式是php, 那么在执行
session_start()时会反序列化session中|
后的序列化串, 并赋值给$_SESSION变量
如果php
代码中某些类中有__wake()
或__destruct()
, 并且这两个魔法函数中存在危险的函数, 则可以通过session
反序列化控制这些危险函数.
至于如何在session
文件中写入恶意序列化串呢? 那就要用到session.upload_progress
中的PHP_SESSION_UPLOAD_PROGRESS
了, 具体怎么用, 去看文件包含那篇笔记吧
反序列化字符逃逸
如果存在对序列化后的字符进行替换, 并且替换后字符长度会变, 则有可能存在字符逃逸
替换后字符变长
1 | function change($str) |
比如说上面这个函数, 把输入的字符串中的o
替换成oo
如果有这么个数组array("name" => "kirito","level" => "18");
,
序列化之后就是a:2:{s:4:"name";s:6:"kirito";s:5:"level";s:2:"18";}
再用上面的函数处理序列化字符串, 就会变成a:2:{s:4:"name";s:6:"kiritoo";s:5:"level";s:2:"18";}
, 这里就会出现一个错误s:6:"kiritoo"
, 长度应该为6的字符变成7了
逃逸逃逸, 目的就是逃出双引号, 如果我们的数组是
1 | array( |
序列化之后就是a:2:{s:4:"name";s:55:"kiritooooooooooooooooooooooooo";s:5:"level";s:3:"999";}";s:5:"level";s:2:"18";}
这里name
里面本身就有双引号, 但是不能实现逃逸, 是因为反序列化时候, 会读取指定长度的字符后下一个字符是否是"
就比如这里的s:55:"kiritooooooooooooooooooooooooo";s:5:"level";s:3:"999";}"
,
读取55个字符kiritooooooooooooooooooooooooo";s:5:"level";s:3:"999";}
后, 就剩下结尾的"
, 所以中间的"
并不影响反序列化
但如果将序列化之后的字符串进行替换操作, 将会变成s:55:"kiritoooooooooooooooooooooooooooooooooooooooooooooooooo";s:5:"level";s:3:"999";}
, 现在再来看看读取55个字符后是啥, 我们发现kiritoooooooooooooooooooooooooooooooooooooooooooooooooo
刚好就是55个字符, 而后面刚好就是一个"
, 这意味着在反序列化的时候, 后面的;s:5:"level";s:3:"999";}
则会逃逸出去, 被识别成序列化字符串的另一部分.(具体原因就是因为多出的o
的长度刚好就是";s:5:"level";s:3:"999";}
)的长度
最后, 我们来看看总的字符串处理完是怎样的
1 | a:2:{s:4:"name";s:55:"kiritoooooooooooooooooooooooooooooooooooooooooooooooooo";s:5:"level";s:3:"999";}";s:5:"level";s:2:"18";} |
反序列化有个特点, 当判断反序列化结束后, 后面的字符将会自动忽略, 那么这里反序列化就会只处理a:2:{s:4:"name";s:55:"kiritoooooooooooooooooooooooooooooooooooooooooooooooooo";s:5:"level";s:3:"999";}
后面的";s:5:"level";s:2:"18";}
会被自动忽略, 这样我们就成功通过控制name
, 逃逸并修改了level
将处理后的字符串反序列化的结果:
1 | array(2) { |