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) { |