0x0 MVC简介
在审计代码之前我们先来了解下 什么是MVC。
MVC模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
- 模型Model – 管理大部分的业务逻辑和所有的数据库逻辑。模型提供了连接和操作数据库的抽象层。
- 控制器Controller - 负责响应用户请求、准备数据,以及决定如何展示数据。
- 视图View – 负责渲染数据,通过HTML方式呈现给用户。
mvc的运行流程:
- controller获取用户的请求
- controller根据获取的请求调用相应的Model完成状态的读写操作
- controller将Model处理的数据传递给View
- View将数据渲染将结果呈现给用户
0x1 了解目录结构
1 2 3 4 5 6 7 8 9 10 11 12
| -- YxtCMF_v6.1 -- admin 后台静态文件 -- application 应用目录 -- data 数据配置文件 -- Expand 静态储存扩展 -- plugins 插件 -- public 资源文件目录 -- themes 主题 -- ueditor 编辑器 -- update 升级目录 -- Uploads 上传目录 -- yxtedu 核心目录根据Thinkphp3.2.3开发的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ├─ThinkPHP 框架系统目录(可以部署在非web目录下面) │ ├─Common 核心公共函数目录 │ ├─Conf 核心配置目录 │ ├─Lang 核心语言包目录 │ ├─Library 框架类库目录 │ │ ├─Think 核心Think类库包目录 │ │ ├─Behavior 行为类库目录 │ │ ├─Org Org类库包目录 │ │ ├─Vendor 第三方类库目录 │ │ ├─ ... 更多类库目录 │ ├─Mode 框架应用模式目录 │ ├─Tpl 系统模板目录 │ ├─LICENSE.txt 框架授权协议文件 │ ├─logo.png 框架LOGO文件 │ ├─README.txt 框架README文件 │ └─ThinkPHP.php 框架入口文件
|
mvc架构的cms通常会有URL路由
例如我们访问http://serverName/index.php/login
等同于访问http://serverName/index.php/user/login/index
对应的controller类是:application\User\Controller\IndexController.class.php
了解相应的路由规则可以有效的定位漏洞的触发点
0x2 入口文件
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
| <?php if (ini_get('magic_quotes_gpc')) { function stripslashesRecursive(array $array) { foreach ($array as $k => $v) { if (is_string($v)) { $array[$k] = stripslashes($v); } else if (is_array($v)) { $array[$k] = stripslashesRecursive($v); } } return $array; } $_GET = stripslashesRecursive($_GET); $_POST = stripslashesRecursive($_POST); } define("APP_DEBUG",false); define('SITE_PATH', dirname(__file__) . "/"); define('APP_PATH', SITE_PATH . 'application/'); define('SPAPP_PATH', SITE_PATH . 'yxtedu/'); define('SPAPP', './application/'); define('SPSTATIC', SITE_PATH . 'statics/'); define("RUNTIME_PATH", SITE_PATH . "data/runtime/"); define("HTML_PATH", SITE_PATH . "data/runtime/Html/"); define("THINKCMF_CORE_TAGLIBS", 'cx,Common\Lib\Taglib\TagLibSpadmin,Common\Lib\Taglib\TagLibHome'); if (!file_exists("data/install.lock")) { if (strtolower($_GET['g']) != "install") { header("Location:./index.php?g=install"); exit(); } } require SPAPP_PATH . 'Core/ThinkPHP.php';
|
line 2-21
判断了是否开启了magic_quotes_gpc
,如果开启了则会对GET、POST
获得的数据进行处理。处理使用的函数就是stripslashes
返回一个去除转义反斜线后的字符串(\'
转换为 '
等等)。双反斜线(\\
)被转换为单个反斜线(\
)。
此处感觉多此一举,如果是开启了magic_quotes_gpc
,程序直接使用了
$_GET、$_POST
传输的数据则会存在安全问题。
下面的流程就是引入ThinkPHP
的核心文件和判断是否已经安装
0x3 审计
因为此程序采用的mvc模式 所以 审计我们只要把重点放在controller这里即可
有些读者可能不知该如何寻找出漏洞,在拿到源码后感觉无从下手。
笔者在这里分享下自己的几个方法以供参考
- 使用Seay源码审计根据特征扫描出来的位置进行跟进审计
- 通读整个源码的流程层层跟进审计(这种需要耗费大量时间,但时间与收益是对等的,遗漏的东西不多)
- 黑盒+白盒共同测试,根据前台或后台页面位置的功能,有选择的审计某个功能所对应的源码。例如:sql注入一般是需要有输入的地方,那么可以找登录模块、搜索模块、文章模块等等
选择合适的方法可以为你接下来进行的工作提高效率。
0x3.1 前台登录一处注入
application\User\Controller\LoginController.class.php
LIne 116-140
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
| function dologin(){ if(!sp_check_verify_code()){ $this->error("验证码错误!"); } $users_model=M("Users"); $rules = array( array('username', 'require', '手机号/邮箱/用户名不能为空!', 1 ), array('password','require','密码不能为空!',1), ); if($users_model->validate($rules)->create()===false){ $this->error($users_model->getError()); } $username=$_POST['username']; if(preg_match('/^\d+$/', $username)){ $this->_do_mobile_login(); }else{ $this->_do_email_login(); } }
|
- 首先验证了验证码是否正确(默认是没有开验证码功能的)
- 验证输入的内容是否满足
$rules
的条件
- 132行直接接收了 $_POST传入的
username
- 判断是以那种方式,手机号或者用户名登录的
问题出在了
因为是直接接受的Post传过来的内容,在Thinkphp 3.2.3
where处存在缺陷如果没有经过I
函数接受数据则会导致SQL注入
这个缺陷因为执行流程篇幅过长,分析会在后面的文章进行详细分析。
_do_mobile_login
这里也存在,两个一样的类型。
0x3.2 后台广告编辑一处注入
application\Admin\Controller\AdController.class.php
Line 37-42
1 2 3 4 5 6 7
| function edit(){ $id=I("get.id"); $ad=$this->ad_model->where("ad_id=$id")->find(); $this->assign($ad); $this->display(); }
|
在where
出直接拼接了参数,I函数默认使用的是htmlspecialchars
并不会转义单引号所以这里存在了注入
0x3.3 后台友情连接处注入
application\Admin\Controller\LinkController.class.php
1 2 3 4 5 6 7 8
| function edit(){ $id=I("get.id"); $link=$this->link_model->where("link_id=$id")->find(); $this->assign($link); $this->assign("targets",$this->targets); $this->display(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function toggle(){ if(isset($_POST['ids']) && $_GET["display"]){ $ids = implode(",", $_POST['ids']); $data['link_status']=1; if ($this->link_model->where("link_id in ($ids)")->save($data)!==false) { $this->success("显示成功!"); } else { $this->error("显示失败!"); } } if(isset($_POST['ids']) && $_GET["hide"]){ $ids = implode(",", $_POST['ids']); $data['link_status']=0; if ($this->link_model->where("link_id in ($ids)")->save($data)!==false) { $this->success("隐藏成功!"); } else { $this->error("隐藏失败!"); } } }
|
这几处注入都是因为自己拼接了字符串 或者使用原生的POST、GET
获得数据没有任何过滤引起的注入
这种注入在此程序中有多处就不再一一列举
0x3.4 Getshell
没有getshell的审计,怎么能满足各位呢:)
application\Admin\Controller\RouteController.class.php
1 2 3 4 5 6 7
| function index(){ $routes=$this->route_model->order("listorder asc")->select(); sp_get_routes(true); $this->assign("routes",$routes); $this->display(); }
|
首先获取了数据库中route表内容的数据,跟进sp_get_routes
先从数据库中取出status
为1的路由规则,然后对取出来的路由数组进行htmlspecialchars_decode
解码
在Line 1225-1227
处
1 2 3 4 5 6 7 8 9
| F("routes",$cache_routes); $route_dir=SITE_PATH."/data/conf/"; if(!file_exists($route_dir)){ mkdir($route_dir); } $route_file=$route_dir."route.php"; file_put_contents($route_file, "<?php\treturn " . stripslashes(var_export($all_routes, true)) . ";");
|
这里判断了/data/conf/
文件夹是否存在不存在会创建,接着往下看可以看到使用file_put_contents
函数进行了文件操作。将从数据表获取的数据以键值的形式存进route.php
。这里要注意的是stripslashes
,如果没有这个函数的存在,数据中的'
会被var_export
全部转义。
因为多了一个stripslashes
去除转义符,那么getshell的思路就有了:
首先在后台添加路由到数据库内,不必担心添加数据时是否会被过滤.
添加后转到路由规则设置的首页
首页执行index()将数据库里的内容写入route.php
保存后访问route.php
看看是否成功
成功getshell
0x4 篇外
代码审计需要有足够的耐心和细心,此处的getshell处,笔者也是找了很久才发现。
多多阅读各位前辈公开的代码审计案例,以及乌云1000php,可以学习到一些不错的思路。
笔者已经搭建了乌云1000php,在这里贴上地址:http://php.evi1s.com/
当然此程序还有其他的漏洞,例如变量覆盖,xss等这里笔者就留给各位学习代码审计的道友去自行审计学习了
如果内容有错误处,请联系笔者,避免带偏其他人。