46

对Emlog 6.0 Beta的完整代码审计过程

 5 years ago
source link: http://www.freebuf.com/articles/web/179978.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

*本文作者:DYBOY,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

Emlog 6.0 beta版本,这可能是最后一篇关于PHP语言CMS的代码审计文章,此次将详细记录完整的审计过程。

文章基本上完整记录小东的对此 CMS 审计过程,或许显得繁琐,但代码审计的过程就是这样,发现可能项,然后精心构造去验证,这过程中我们会遇到很多次碰壁,坚持测试,思维活跃一些,基本都会有所收获,诚挚希望后来者能够耐心阅读下去,当然最好也能够有所启发。

大家需要注意的一点是,代码审计是为了学习并在SDL中避免发生类似的错误,同时也是帮助开源系统修复相关问题,并不是去为了获得什么0day~

0×00 Emlog 6.0 beta

aYNRZr7.png!web

官网地址: https://www.emlog.net/

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 初步测试

首先,我们得先安装!安装成功后的首页界面:

rIreUvV.png!web

默认后台登陆地址: ./admin/

登陆成功后:

ameMnq3.png!web

闲话一句,感觉 6.05.3.1 版本好看太多了~

安装过后,我们应该尽可能全面搜集关于此 CMS 的信息,这对于我们审计代码有很大的帮助。

所以,分析得到此 CMS 的大致结构, Emlog 是一个 MVC 的设计模式,大致的结构如图:

iIFfa2B.png!web

因此我们主要会分析 admininclude 文件夹下的文件。

数据库表:

JzEfiiN.png!web

在根目录的 init.php 文件中

qMR3iyN.png!web

报错等级指定为 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 ,得到的报告如下:

fAJjQvI.png!web

不过在本地扫描时,使用的是 XAMPP windows10 PHP5.6 的环境,所以导致漏洞报告中很多误报,漏扫主要扫描出了几个 XSS漏洞CSRF漏洞

所以我们首先验证这两类的漏洞

0×03 文章编辑器储存性XSS

在后台的编辑器处,编辑文章 ./admin/admin_log.php

iaE7zya.png!web

成功发布后,来到首页

FjiiUfQ.png!web

进入文章页后

fAniyqQ.png!web

都弹窗了,这里大家可能要说没法儿利用,但是 emlog 设计了 会员/作者 功能,在 emlog 中的某些模版中可以前台注册会员,会员登录后可以编辑发表文章,评论等等功能。 Emlog 官方还提供了文章投稿插件,都是调用了官方默认的 Kindeditor 编辑器,这个编辑器自带 HTML编辑模式 ,就算不带这个模式,攻击者也可以抓包修改达到攻击目的。

为什么前台没过滤呐?为了文章有支持 HTML 代码输出,所以对于 kindeditor 的保存输出内容并没有转义。

i2meMva.png!web

修复建议:参考其他 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

mauuEbe.png!web

0×05 反射型XSS

此处的 XSS 主要发生在 cookie 上,因为某些页面如 admin/admin_log , admin/sort.php , admin/link.php 页面需要在表单中添加了 hidden 属性的 token 值,而这个 token 值直接从用户的 cookie 中取得,导致了一个反射型 XSS

拦截抓包修改 cookie 中的 token 值如下:

neAb2ar.png!web

效果:

n2UVBvj.png!web

其次验证了 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 看到这样一串代码:

QjMVFnm.png!web

这里我的思考是,如果在 emlog 官网有 URL 跳转链接的话,那么就可以构造下载远程任意的文件到网站,但是测试了官网没有跳转链接,那么我们尝试下载别的插件(链接跳转等),或者有黑客精心构造了一个插件或者模版,然后再利用,这也算是一个可行的方案。

此处需要管理员权限,作为代码审计的一个参考思路,不是要发现什么 0day ,而是希望大家能够在代码审计方面有所收获。

(1). SQL注入

对于 SQL注入Seay工具 一直都没准过,这里小东推荐方式,使用全局搜索 $_GET[$_PSOT[ ,然后看看是否代入了 SQL 查询,然后一一验证。

然后我发现了这样一个没有过滤IP参数

vmEnmaj.png!web

然后到 admin/comment.php 中查看

ii6Bnue.png!web

再看 delCommentByIp($ip) 函数

B7fI7zF.png!web

由此我们可以确定了 SQL 注入的存在

验证如下:

2iiABjb.png!web

(2).一个CSRF+任意文件删除

$_GET[] 型分析完以后,就寻找 $_POST[] 的,然后在 admin/data.php 文件中找到了如下代码

VnauEnJ.png!web

这里我们发现,并没有验证 toknen ,那么可以构造 csrf 页面,这里小东就不演示了,直接 BURP 验证一下任意文件删除吧,关于 CSRF ,只要没有调用上面基础函数部分说到的 LoginAuth::checkToken() 方法的,都存在 CSRF

imqYzmQ.png!web

这里就成功删除了文件

(3).TAG SQL注入

在POST参数中发现此处并没有过滤,同时在 deleteTag() 函数中,代入了 SQL 查询,因此又是一个 SQL注入

7VRZvii.png!web

但是此处并没有回显。可以采用时间盲注的方式

至此,利用工具的半自动化审计已经结束,下面准备手工测试

0×08 手工测试

手工测试也不是单纯的翻文件,应当以灰盒测试为主导,从 逻辑权限敏感信息 等方面入手

(1).后台登陆存在暴力破解风险

在这里,我之前提到过的验证码未及时销毁的历史问题还存在,此处不再详细叙述,请参考 https://blog.csdn.net/dyboy2017/article/details/78433748

(2).报错信息导致物理路径泄漏

大家不要以为这是小事情,当 sql注入 存在的时候,我们有机会是可以直接写 shell 文件,安全无小事

一个低权限的方式,在游客的条件下测试一下

JzUFnu7.png!web

payload: http://www.test.com/admin/attachment.php?action[]=

原因是: addslashes() expects parameter 1

(3).Cookie可计算

include/lib/loginauth.php134 行开始

/**
 * 写用于登录验证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 存在

IFBfe2A.png!web

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.phpshell

0×10 总结

EMLOG 是一个非常小巧轻快的博客系统,运行占用资源非常低,所以非常适合博主用作博客用途,其实只要不开启会员功能,没有弱口令就没有什么大的威胁。以此文章作为 PHP代码审计的终稿 ,文章所述方法同样适用于其他的 CMS代码审计 和分析,创作不易,也希望本文章能对大家能有所启示。

*本文作者:DYBOY,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK