PHP代码审计得这样由浅入深地学

安全 应用安全
一篇关于PHP语言CMS的代码审计文章,详细记录代码审计的完整过程,学习代码审计,不妨从这边文章入手,认真阅读完,相信一定可以有所收获!

 以Emlog 6.0 beta版本为引,一篇关于PHP语言CMS的代码审计文章,详细记录代码审计的完整过程,学习代码审计,不妨从这边文章入手,认真阅读完,相信一定可以有所收获!

[[396282]]

序言

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

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

0x00 Emlog 6.0 beta

 

EMLOG 6.0

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

Emlog 6.0 beta下载地址:

https://www.emlog.net/download

由于官方限制论坛会员(注册付费)才可下载,这里提供一个原版下载地址:https://www.lanzous.com/i1l5gad

文件校验:

  1. 文件: C:\Users\stdy\Desktop\emlog_6.0.0.zip 
  2. 大小: 607725 字节 
  3. 修改时间: 2018年8月6日, 20:53:50 
  4. MD5: 7844FE6FEAE7AF68052DC878B8811FAC 
  5. SHA1: E06A050D2A0AA879DB9F5CFCAA4703B6AC7B8352 
  6. CRC32: 4963E489 

博主的博客就是基于此套博客系统,其实很多圈内大佬都在使用,对于本款CMS的审计文章却并没有,笔者就来以此CMS作为PHP代码审计的封笔之作。

0x01 初步测试

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

 

安装成功

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

登陆成功后:

 

后台界面

闲话一句,感觉6.0比5.3.1版本好看太多了~

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

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

 

emlog结构

因此我们主要会分析 admin 和 include 文件夹下的文件。

数据库表:

 

DATABASE

在根目录的init.php 文件中

 

报错等级

报错等级指定为7:

  1. <?php 
  2. //禁用错误报告 
  3. error_reporting(0); 
  4.  
  5. //报告运行时错误 
  6. error_reporting(E_ERROR | E_WARNING | E_PARSE); 
  7.  
  8. //报告所有错误 
  9. error_reporting(E_ALL); 
  10.  
  11. error_reporting(7); 
  12. /* 
  13. 设置php错误检测级别 
  14. E_ERROR - 致命性运行时错 (1) 
  15. E_WARNING - 运行时警告(非致命性错)(2) 
  16. E_PARSE - 编译时解析错误 (4) 
  17. 1+2+4 = 7 
  18. */ 
  19. ?> 

0x02 使用漏洞扫描器

可能有朋友就会说你为什么要使用“漏扫”呐?不是代码审计吗?

这里要纠正一下这个观点,漏扫其实就是一个自动化黑盒测试,在本地环境下,我们不会影响任何的业务。

通过漏扫出的漏洞能够方便我们快速定位漏洞位置,这样是一种高效的方式,这也是在团队里的成员通过漏扫Get了百度的几个高危漏洞给笔者的启示。

这里使用了一款重型扫描器 AWVS ,得到的报告如下:

 

结果

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

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

0x03 文章编辑器储存性XSS

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

 

编辑器XSS

成功发布后,来到首页

 

emlogXSS

进入文章页后

 

文章页XSS

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

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

 

emlog会员/投稿

修复建议:参考其他CMS做好文章内容关键词的检测,并做好过滤或者转义

0x04 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:

 

SWF XSS

0x05 反射型XSS

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

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

 

payload

效果:

 

COOKIE XSS

其次验证了 CSRF 漏洞,这个是前台的搜索框的CSRF根本没什么价值

然后是管理员添加友情链接的XSS,经过验证并不存在,后台函数会限制字数

然后就是我们开始进行原始的代码审计工作了,主要借用了Seay代码审计工具和Rips,这种审计工具主要依靠正则匹配可能导致危险的php函数来作为可能存在漏洞的判断,半自动化的方式,在一定程度上缓解了代码审计的压力。

0x06 基本函数

首先看了一下文件操作相关的函数,发现经常用到 View::getView 这一方法,

在include/lib/view.php 文件中,源码如下:

  1. <?php 
  2. /** 
  3.  * 视图控制 
  4.  * @copyright (c) Emlog All Rights Reserved 
  5.  */ 
  6.  
  7. class View { 
  8.  public static function getView($template, $ext = '.php') { 
  9.   if (!is_dir(TEMPLATE_PATH)) { 
  10.    emMsg('当前使用的模板已被删除或损坏,请登录后台更换其他模板。', BLOG_URL . 'admin/template.php'); 
  11.   } 
  12.   return TEMPLATE_PATH . $template . $ext; 
  13.  } 
  14.  
  15.  public static function output() { 
  16.   $content = ob_get_clean(); 
  17.         ob_start(); 
  18.   echo $content; 
  19.   ob_end_flush(); 
  20.   exit; 
  21.  } 

同时作为权限控制的 LoginAuth::checkToken(),在 \include\lib\loginauth.php下约209行开始

  1. /** 
  2. * 生成token,防御CSRF攻击 
  3. */ 
  4. public static function genToken() { 
  5.  $token_cookie_name = 'EM_TOKENCOOKIE_' . md5(substr(AUTH_KEY, 16, 32) . UID); 
  6.  if (isset($_COOKIE[$token_cookie_name])) { 
  7.   return $_COOKIE[$token_cookie_name]; 
  8.  } else { 
  9.   $token = md5(getRandStr(16)); 
  10.   setcookie($token_cookie_name, $token, 0, '/'); 
  11.   return $token; 
  12.  } 
  13.  
  14. /** 
  15. * 检查token,防御CSRF攻击 
  16. */ 
  17. public static function checkToken(){ 
  18.  $token = isset($_REQUEST['token']) ? addslashes($_REQUEST['token']) : ''
  19.  if ($token != self::genToken()) { 
  20.   emMsg('权限不足,token error'); 
  21.  } 

验证了Rips扫描出的文件包含问题(第一次使用Rips),发现无法复现,因为Rips扫描的时候是以文件形式,并没有参照程序的严格逻辑,导致的误报!

来到 \admin\admin_log.php 文件,从第78行开始:

  1. //操作文章 
  2. if ($action == 'operate_log') { 
  3.     $operate = isset($_REQUEST['operate']) ? $_REQUEST['operate'] : ''
  4.     $pid = isset($_POST['pid']) ? $_POST['pid'] : ''
  5.     $logs = isset($_POST['blog']) ? array_map('intval', $_POST['blog']) : array(); 
  6.     $sort = isset($_POST['sort']) ? intval($_POST['sort']) : ''
  7.     $author = isset($_POST['author']) ? intval($_POST['author']) : ''
  8.     $gid = isset($_GET['gid']) ? intval($_GET['gid']) : ''
  9.  
  10.     LoginAuth::checkToken(); 
  11.  
  12.     if ($operate == '') { 
  13.         emDirect("./admin_log.php?pid=$pid&error_b=1"); 
  14.     } 
  15.     if (empty($logs) && empty($gid)) { 
  16.         emDirect("./admin_log.php?pid=$pid&error_a=1"); 
  17.     } 
  18.  
  19.     switch ($operate) { 
  20.         case 'del'
  21.             foreach ($logs as $val) 
  22.             { 
  23.                 doAction('before_del_log', $val); 
  24.                 $Log_Model->deleteLog($val); 
  25.                 doAction('del_log', $val); 
  26.             } 
  27.             $CACHE->updateCache(); 
  28.             if ($pid == 'draft'
  29.             { 
  30.                 emDirect("./admin_log.php?pid=draft&active_del=1"); 
  31.             } else
  32.                 emDirect("./admin_log.php?active_del=1"); 
  33.             } 
  34.             break; 
  35.         case 'top'
  36.             foreach ($logs as $val) 
  37.             { 
  38.                 $Log_Model->updateLog(array('top'=>'y'), $val); 
  39.             } 
  40.             emDirect("./admin_log.php?active_up=1"); 
  41.             break; 
  42.         case 'sortop'
  43.             foreach ($logs as $val) 
  44.             { 
  45.                 $Log_Model->updateLog(array('sortop'=>'y'), $val); 
  46.             } 
  47.             emDirect("./admin_log.php?active_up=1"); 
  48.             break; 
  49.         case 'notop'
  50.             foreach ($logs as $val) 
  51.             { 
  52.                 $Log_Model->updateLog(array('top'=>'n''sortop'=>'n'), $val); 
  53.             } 
  54.             emDirect("./admin_log.php?active_down=1"); 
  55.             break; 
  56.         case 'hide'
  57.             foreach ($logs as $val) 
  58.             { 
  59.                 $Log_Model->hideSwitch($val, 'y'); 
  60.             } 
  61.             $CACHE->updateCache(); 
  62.             emDirect("./admin_log.php?active_hide=1"); 
  63.             break; 
  64.  
  65.         ...//中间的代码要验证管理身份,故省略 
  66.  
  67.         case 'uncheck'
  68.             if (ROLE != ROLE_ADMIN) 
  69.             { 
  70.                 emMsg('权限不足!','./'); 
  71.             } 
  72.             $Log_Model->checkSwitch($gid, 'n'); 
  73.             $CACHE->updateCache(); 
  74.             emDirect("./admin_log.php?active_unck=1"); 
  75.             break; 
  76.     } 

那么我们尝试越权删除文章http://www.test.com/admin/admin_log.php?action=operate_log&operate=del&blog=29&token=994132a26661c8c244a91063c4701a7e 失败了提示权限不足,来到\include\model\log_model.php 发现

  1. /** 
  2.  * 删除文章 
  3.  * 
  4.  * @param int $blogId 
  5.  */ 
  6. function deleteLog($blogId) { 
  7.  $author = ROLE == ROLE_ADMIN ? '' : 'and author=' . UID; 
  8.  $this->db->query("DELETE FROM " . DB_PREFIX . "blog where gid=$blogId $author");  //这里和上一句限制了作者只能删除自己的文章 
  9.  if ($this->db->affected_rows() < 1) { 
  10.   emMsg('权限不足!''./'); 
  11.  } 
  12.  // 评论 
  13.  $this->db->query("DELETE FROM " . DB_PREFIX . "comment where gid=$blogId"); 
  14.  // 标签 
  15.  $this->db->query("UPDATE " . DB_PREFIX . "tag SET gid= REPLACE(gid,',$blogId,',',') WHERE gid LIKE '%" . $blogId . "%' "); 
  16.  $this->db->query("DELETE FROM " . DB_PREFIX . "tag WHERE gid=',' "); 
  17.  // 附件 
  18.  $query = $this->db->query("select filepath from " . DB_PREFIX . "attachment where blogid=$blogId "); 
  19.  while ($attach = $this->db->fetch_array($query)) { 
  20.   if (file_exists($attach['filepath'])) { 
  21.    $fpath = str_replace('thum-''', $attach['filepath']); 
  22.    if ($fpath != $attach['filepath']) { 
  23.     @unlink($fpath); 
  24.    } 
  25.    @unlink($attach['filepath']); 
  26.   } 
  27.  } 
  28.  $this->db->query("DELETE FROM " . DB_PREFIX . "attachment where blogid=$blogId"); 

这个越权漏洞不存在,同时看了下面的函数判断也是做了类似的处理

到这里其实我们对于整个 CMS 的架构已经较为熟悉了,基本能根据对应函数功能,直接手动找到对应的函数位置。

令人伤心的是,通过 Rips 代码审计工具得到的结果,一个都没复现成功...

0x07 Seay辅助审计

相信很多人都知道法师的这款工具,主要还是因为中文,用着方便,但是完全依靠正则的方式去匹配函数,只能发现那些函数直接的控制漏洞,逻辑漏洞有时候可以根据逆推可以发现,但这种情况很少。

使用这款工具扫描出来共120个可能的情况(根据经验98%以上都是没法复现的),然后一个个排查,有的例如SQL语句反单引号这样的,很容易就可以判断给忽略,就不需要考虑。

在 /admin/store.php 看到这样一串代码:

 

store.php

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

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

(1). SQL注入

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

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

 

IP参数

然后到 admin/comment.php 中查看

 

comment.php

再看 delCommentByIp($ip) 函数

 

IP参数sql

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

验证如下:

 

SQL注入

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

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

 

data.php

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

 

CSRF+任意删除

这里就成功删除了文件

(3).TAG SQL注入

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

 

tag sql

来看deleteTag()函数:

 

deletetag

又调用了getBlogIdsFromTagId()函数,同样没有过滤

 

getBlogIdsFromTagId

因此使用抓包验证一下:

 

验证

但是其他语句利用时候并没有回显,笔者不知道什么原因,没仔细探究,但是可以采用时间盲注的方式。

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

0x08 手工测试

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

(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行开始

  1. /** 
  2.  * 写用于登录验证cookie 
  3.  * 
  4.  * @param int $user_id User ID 
  5.  * @param bool $remember Whether to remember the user or not 
  6.  */ 
  7. public static function setAuthCookie($user_login, $ispersis = false) { 
  8.  if ($ispersis) { 
  9.   $expiration  = time() + 3600 * 24 * 30 * 12; 
  10.  } else { 
  11.   $expiration = null
  12.  } 
  13.  $auth_cookie_name = AUTH_COOKIE_NAME; 
  14.  $auth_cookie = self::generateAuthCookie($user_login, $expiration); 
  15.  setcookie($auth_cookie_name, $auth_cookie, $expiration,'/'); 
  16.  
  17. /** 
  18.  * 生成登录验证cookie 
  19.  * 
  20.  * @param int $user_id user login 
  21.  * @param int $expiration Cookie expiration in seconds 
  22.  * @return string Authentication cookie contents 
  23.  */ 
  24. private static function generateAuthCookie($user_login, $expiration) { 
  25.  $key = self::emHash($user_login . '|' . $expiration); 
  26.  $hash = hash_hmac('md5', $user_login . '|' . $expiration, $key); 
  27.  $cookie = $user_login . '|' . $expiration . '|' . $hash; 
  28.  return $cookie; 

可以看到此处的cookie都可以直接计算得到,只需要知道根目录下config.php中的

  1. //auth key 
  2. define('AUTH_KEY','dx1&CH^En86GZnxd9CLO7GwC0Q5eYHKM450f598bbd148b6a62f7d263623e31c3'); 
  3. //cookie name 
  4. define('AUTH_COOKIE_NAME','EM_AUTHCOOKIE_VzfVniPWDqd1LM3BFocnrcjpAGH4lUbz'); 

即可。

(4).侧边栏存储性XSS

为了同样是为了支持HTML代码的输出,没有转义对应的脚本代码标签,导致了存储性的XSS存在

侧边栏XSS

0x09 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 "" into outfile 'D:\\Server\\htdocs\\safe\\dyboy.php';

然后导入备份恢复本地数据即可

这样就在网站个目录生成了一个dyboy.php的shell

0x0A 总结

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

 

责任编辑:姜华 来源: DYBOY
相关推荐

2013-03-25 11:51:42

php漏洞代码审计php

2013-06-05 09:51:04

2021-05-12 08:13:31

Zabbix 5.2php-fpm监控

2015-07-31 09:27:56

安全编码代码审计Python

2012-03-13 09:54:42

JavaScript

2015-06-03 09:07:46

白盒审计PHPPHP自动化审计

2016-10-25 08:49:34

数据通信行业技能图

2017-05-05 09:45:13

编程语言学习代码

2020-09-27 10:55:10

代码Java字符串

2022-12-06 08:29:01

2022-12-01 09:12:49

CodeQL自动化审计

2020-12-02 08:30:27

Java Synchroniz并发

2009-10-26 13:45:39

linux Makef

2010-03-25 17:20:00

CentOS入门

2020-10-25 19:58:04

Pythonic代码语言

2019-10-17 11:42:18

前端程序员算法

2011-06-15 16:58:26

PHP

2019-07-03 15:14:00

Oracle存储结构

2013-06-19 09:27:51

2011-06-09 16:23:18

SEO
点赞
收藏

51CTO技术栈公众号