基本信息
ThinkPHP是一个快速、兼容而且简单的轻量级国产PHP开发框架,遵循Apache 2开源协议发布,使用面向对象的开发结构和MVC模式,融合了Struts的思想和TagLib(标签库)、RoR的ORM映射和ActiveRecord模式。ThinkPHP可以支持windows/Unix/Linux等服务器环境,正式版需要PHP 5.0以上版本,支持MySql、PgSQL、Sqlite多种数据库以及PDO扩展。
此漏洞为tp框架中Lite精简模式中存在可能的漏洞,ThinkPHP 开启lite模式后,会加载Lite下的Dispacher.class.php文件去匹配URL并分发用户请求, 而在该文件中的一处使用了perg_replace函数的 /e参数,
此函数/e模式会将其的第二个参数当作php代码执行,而在此文件中一二参数都为可控,\1与\2分别代表着第一个参数中的两个括号的位置,例如当我们传访问的url为index.php?s=1/2/3/4/5/6时,在第一次进行匹配是\1即代表1,\2即代表2,在第二次匹配时\1即代表3,\2即代表4。
本地复现:
payload:
?s=/1/2/3/${eval($_GET[1])}&1=system(%22whoami%22);
要将我们的payload写到偶数位也就是\2处,原因是\2处为函数的第二个参数处且被双引号包裹,可以解析变量
记得安装mysql_pdo.so。不然连接数据库会报错的
tp3中的常用方法:
A快速实例化Action类库
B执行行为类
C配置参数存取方法
D快速实例化Model类库
F快速简单文本数据存取方法
L 语言参数存取方法
M快速高性能实例化模型
R快速远程调用Action类方法
S快速缓存存取方法
U URL动态生成和重定向方法
W 快速Widget输出方法
D函数实例化的是你当前项目的Lib/Model下面的模块。如果该模块不存在的话,直接返回实例化Model的对象(意义就与M()函数相同)。而M只返回,实例化Model的对象。它的$name参数作为数据库的表名来处理对数据库的操作。
I方法是ThinkPHP众多单字母函数中的新成员,其命名来自于英文Input(输入),主要用于更加方便和安全的获取系统输入变量,可以用于任何地方
D方法和M方法,这两个方法的区别在于M方法实例化模型无需用户为每个数据表定义模型类,如果D方法没有找到定义的模型类,则会自动调用M方法。通俗一点说:M实例化参数是数据库的表名。D实例化的是你自己在Model文件夹下面建立的模型文件
写个控制器:
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$Dao=M('users');
$result=$Dao->find(1);
if ($result){
echo"connect success";
}
var_dump($Dao->find(I('GET.id')));
}
}
访问下,我们输入的是id=1’,但是还是对数据库进行了正常查询,我们先简单跟下:
进I方法里面看一眼,这个获取我们get传进去的值:
为我们挑选一个过滤器,因为我们一开始对I方法,并没有传入filter,所以我们这里filter的值是null,所以派一个默认的filter给我们:
C函数我没跟进去看,直接看的idea的返回结果,这里给的filter是:htmlspecialchars
在下面进行过滤:
在往下走,走到后面,会回调think_filter这个函数进行过滤:
瞅一眼过滤了啥,这正则对我们当前payload的作用几乎为。。。:
回到过去,最后返回的还是1’ ,没过滤掉根本:
接着进了find方法,这里给options赋值,赋成了二维数组:
我这里重新换了个pyload:1’and%201=1%23,打个断点直接到这:
接着往下走,会有些对id参数是否是数组的判断,我们这里都进不去。就往下跟我们发现在这下面这个地方,我们的1’and%201=1%23变成了1:
对这个函数打个断点,进去瞅下发现有个parseType函数:
跟进去看看,这个函数是判断了我们数据库里的这个字段的类型,然后进行相应的处理,我这里用的是id参数,是int型,所以他直接用intval做了个强转,我们的1’and%201=1%23就是在这里变成了1:
我们进数据库把id类型改成string,然后在断点定位到这,跳过了这个parseType函数,接着走到了查询的地方,跟进下:
发现了在buildSelectSql处单引号被转意:
在跟进去这个解析相关的函数:
在跟下这个parsevalue:
第一个if通过了,这里执行了这个escapeString,跟进一下这个escapeString函数,可以看到就是在这里对我们的单引号进行了转意:
数字,字符都不行,只能尝试数组,看看在有些条件判断处能否进行一个绕过
payload1
我们传入的参数,是在_parseType()中被强转为int,如果我传?id[where]=1 and 1=1时根本不会进入这个if,更不用说进入强转的那个函数:
我们不传数组时的,到这的options中的where是个数组,所以会进到那个强转的函数:
再往下走,因为这回$where变成了字符串,所以在parseWhere这里直接拼接成了语句,依次返回执行:
payload:?name[0]=exp&name[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)
控制器:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index()
{
$User = D('Users');
$map = array('name' => $_GET['name']);
$user = $User->where($map)->find();
var_dump($user);
}
}
这里的写法可以类比一下where注入的那个控制器,where注入的传参方式是I(‘GET.id’),而这里是 $map = array(‘name’ => $_GET[‘name’])与where($map)结合,这个原因放到最后说,这可以打个断点,看看where函数主要做了什么:
主要在where函数里就做了个赋值操作,主要是赋值以后的find操作,我们跟进去看看,跟进以后的大多数过程都和之前比较类似,直到这里parseSql里的parseWhere方法:
跟进去parseWherer以后,主要是parseWhereItem方法,跟进去看看
首先会对$exp进行赋值,赋的值就是我们传入的 name[0]=exp
之后会进行一个判断,拼接这一步是比较重要的:
拼接以后会逐步递归返回,最后进行执行:
再说一下,这里为什么不能用I(‘GET.id’)进行传参,在我们调试where注入的时候,用I(‘GET.id’)进行传参时,会有一个think_filter进行过滤如下图,正则里写了"exp“:
控制器:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index()
{
$User = M("users");
$user['id'] = I('id');
$data['PWD'] = I('PWD');
$valu = $User->where($user)->save($data);
var_dump($valu);
}
}
payload : id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&PWD=
这里的id[0]=bind可以参考exp注入中的name[0]=exp,主要是为了进到那个if判断:
而我们这里对比exp注入,为什么要加一个id[1]=0呢,我们先输入id[0]=bind&id[1]=abc&PWD= 试试:
主要进入的是update方法,并且bind注入会产生绑定参数。就是假如我们输入的payload是id[0]=bind&id[1]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)&PWD=,那么最后没进行替换之前编译出的语句是:UPDATE users SET PWD =:0 WHERE id = :1 and updatexml(1,concat(0x7e,user(),0x7e),1),如果payload是id[0]=bind&id[1]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)&PWD=那么最后没进行替换之前编译出的语句是:UPDATE users SET PWD =:0 WHERE id = :0 and updatexml(1,concat(0x7e,user(),0x7e),1).
这是在这里完成的:
在接着用id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&PWD=这个payload进行bind注入调试,可以看到我们最终拼接的没有进行替换的语句是这样的:
UPDATE `users` SET `PWD`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)
但是我们最终执行的时候,在execute函数里的strtr函数是会对:0进行参数替换的:
上面红框圈出来的大概的意思是,对我们$this->bind中的数组的每个键的值先加上个单引号,然后再进行替换,可以看看strtr函数的作用:
这里的$this->bind是我们可以控制的,所以我们要我们的payload中构造一个有 “:0” => “xxxxx” 的这样的一个键值对,来达到对他的编译出的语句中的 :0进行替换的一个目的,我们要id[1]=0 and xxx就是为了要他先编译出的语句中含有:0不含有 :1、 :2、 :3 、:u 、:e 、:a这种其他字符,这就是我们的目的,在$this->bind中产生这样一个值的这步是在这里完成的,并且$val参数的值,就是我们PWD传入的值:
因为我这里的PWD输入的为空,所以经过bindParam后$this中是这样的结果:
最后在update方法中的execute进行先替换后执行:
执行: