基础知识
0x00:为什么要使用框架
使用框架相当于别人已经帮助完成一些基础工作,开发者只需要集中精力在系统的业务逻辑设计上即可。而且相较于原生代码开发更稳定、安全、易扩展。
0x01:TP3.2.3程序目录结构
入口文件(应用对外提供的接口)
核心框架目录
模块集合(
Common
模块优先于其他模块执行)缓存目录(
./Application/Runtime
)公共资源目录(
./Public
)
更详细的可以看官方手册上描述的:
@http://document.thinkphp.cn/manual_3_2.html#directory_structure
0x02:MVC分层架构
控制器(
Controller
):负责用户请求的调度和处理业务逻辑模型(
Model
):负责业务数据的处理和与数据库的交互视图(
View
):提供了展示数据的各种方式
0x03:模板渲染&入口绑定
在TP3.2.3模板中先建立一个Admin文件夹,内容及文件夹名称Copy前端文件夹Home即可
如果想要更改默认首页内容,在Application\Admin\Controller\IndexController.class.php
(控制器)进行修改
在开发时一般后台所使用的Public文件夹是独立开的,因此这里后台也就需要更改一下默认的Public文件夹,需要先在应用入口文件中定义应用目录
然后在Admin\Conf\config.php
处进行配置
最后将JS/CSS/HTML
(视图)文件进行引入,修改路径即可成功访问
除此之外,还是有很多知识是没有接触到的,上面控制器中代码有M,D,U,I这些方法,具体功能参考大师傅的文章:
@https://www.cnblogs.com/kenshinobiy/p/9165662.html
代码则位于Thinkphp/Common/functions.php
例如I
方法主要用于更加方便和安全的获取系统输入变量,用法如下:
I('get.id'); <==> $_GET['id']
I('id'); <==> I('param.id')
param 自动判断请求类型获取GET、POST或者PUT参数
其他函数的用法看手册即可
0x04:TP3路由格式:
这里通过一个简单的例子来了解常用的四种路由模式:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index($name)
{
echo "hello ".$name;
}
}
普通模式:
http://localhost/index.php?m=Home&c=Index&f=index&name=Sn0w
http://网址/index.php?m=模块名称&c=控制器&a=方法
兼容模式:
http://localhost/index.php?s=Home/Index/index/name/Sn0w
http://网址/index.php?s=/模块/控制器/方法
REWRITE模式:
http://localhost/Home/Index/index/name/Sn0w/
http://网址/模块/控制器/操作方法
PATHINFO模式:
http://localhost/index.php/Home/Index/index/name/Sn0w/
http://网址/index.php/模块/控制器/操作方法
最终呈现的效果都一样:
TP3.2.3 SQL注入
环境搭建:
先去官网上下载一份TP3.2.3源码,再创建一个数据库thinkphp
和数据表users
添加好数据后,将数据库配置设置好
访问一下Application
目录下便会自动生成目录结构,接下来将控制器给配置好
Application/Home/Controller/IndexController.class.php
#更改内容为:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function shy()
{
$data = M('users')->find(I('GET.id'));
dump($data);
}
}
测试一下效果:
下断点,至于如何设置PHPstorm
调试的可以参考如下文章:
@https://blog.csdn.net/Xxy605/article/details/120973447
接下来就输入正常的SQL测试语句?id=1' or 1=1%23
,看一下传入的参数所走的流程:
M
方法先实例化一个数据库操作对象,F8跳过此方法到I
方法中,不影响传入参数的代码就不再叙述,默认的filter是htmlspecialchars()
先经过htmlspecialchars()
的处理,此函数默认是不转义单引号的,之后再回调think_filter
函数进行过滤
跟踪一下这个函数,是黑名单过滤,看都过滤了哪些字符
最终参数输出为
I
方法结束后,便会进入到find
方法中去,这个方法作用是查询数据,F7跟着代码走一下,到分析表达式这块,这里调用了_parseOptions()
这个函数
此时id还是之前传入的1' or 1=1#
,跟进这个函数看做了什么处理,如下图进入字段类型验证这段代码
又有一个_parseType
函数对我们传入的参数(即$options['where']
)进行处理,此时id依旧是我们传入的,跟进这个函数继续看
这里的代码把我们传入的id进行了强制类型转换,使用了intval
这个函数,到这一步我们传入的参数就变成了1,然后再返回给_parseOptions()
这个函数,再进行查询便不会出现SQL注入
梳理一下:
id=1' or 1=1# -> M() -> find() -> _parseOptions() -> _parseType()
传入的参数便是在_parseType()
这个函数处被处理了,但具体是怎么处理的还是没搞清楚,因为前面创建数据库id
列使用的是int
类型,所以到intval
这个函数就会直接被处理,所以将id
列改为varchar
,F7继续跟进
进入buildSelectSql
这个函数前,传入的参数还是正常的,跟进这个函数,进入到parseSql
函数,作用是对原初的SQL语句进行替换,构造的id
在where
那里,跟进一下parseWhere()
跟进到此,又发现调用了两个函数,此时传入的还是之前的
跟进parseWhereItem
这个函数,发现到这一步又调用了parseValue
进行跟进看一下这个函数对传入的参数进行了什么处理
/**
* value分析
* @access protected
* @param mixed $value
* @return string
*/
protected function parseValue($value) {
if(is_string($value)) {
$value = strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\''.$this->escapeString($value).'\'';
}elseif(isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp'){
$value = $this->escapeString($value[1]);
}elseif(is_array($value)) {
$value = array_map(array($this, 'parseValue'),$value);
}elseif(is_bool($value)){
$value = $value ? '1' : '0';
}elseif(is_null($value)){
$value = 'null';
}
return $value;
}
那这就很明显了,第一个if条件判断成功,进入escapeString
函数,使用addslashes()
转义传入的单引号,后面就无需再跟进了就是拼接SQL语句
但代码中还有很多判断数组的代码,因此就要考虑一下id
传数组,再跟进代码看一下是否存在SQL注入,最后用一张图来总结便是:
Thinkphp3.2.3 where注入
Payload:
thinkphp_3.2.3/index.php/home/index/shy?id[where]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23
先放出Payload,下面跟着代码看是怎么出现的这个where
报错注入,这次传入的参数为数组?id[where]= 1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23
,I
方法在最后的代码会对数组参数的每个成员使用think_filter
函数
刚才跟着正常的SQL测试语句知道,知道think_filter
函数是一个黑名单,过滤了一些特殊字符,但明显过滤的不是很全,updatexml、extractvalue
这些报错函数都未过滤
继续跟进便进入了find
函数到_parseOptions
这个函数中,在进入此函数前$options
是
刚才正常传入的SQL测试语句只有进入到_parseType()
后才会被intval
函数给强制转换,但这里这个if判断中的is_array($options['where'])
没有满足,此时的where的值不是数组
和上面正常传入的SQL测试语句对比一下,便会发现不同,最后通过此函数处理后传入的payload依旧没有变
F7继续跟进
到select
函数后,发现buildSelectSql
的作用是拼接SQL语句
跟进这个函数
第一个if
语句就是计算limit
,这里不是重点,发现后面又调用了parseSql
这个函数
构造的参数是在where
,跟进看一下
此时的$whereStr
是字符串,所以就没有经过中间的代码,直接返回此语句
F7跟进就会发现最终拼接的语句便还是我们传入的,并没有被过滤掉,便导致了thinkphp3.2.3 where注入
官方修补方法:
@https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04
v3.2.4
将$options
和$this->options
进行了区分,从而传入的参数无法污染到$this->options
,也就无法控制sql语句了
Thinkphp3.2.3 exp注入
payload:
?username[0]=exp&username[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)
把环境重新更改一下,使用全局数组进行传参,这里之所以不用I
函数来获取参数,是因为I
函数会回调think_filter()
函数
function think_filter(&$value)
{
// TODO 其他安全过滤
// 过滤查询特殊字符
if (preg_match('/^(NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
}
过滤了EXP字符串,并会在后面拼接一个空格,这个点会影响exp注入,到后面便能了解了,打上断点,先进入where
函数中
只有最后的代码对传入的参数发挥了作用,其作用便是把数组array('username' => $_GET['username'])
传给$this->options['where']
,继续跟进到find
函数,一直到parseWhere()
函数,观察到这一步和上面的where注入有哪些区别
传入的参数会进入到parseWhereItem()
函数中,而where注入的是当作字符串直接跳过了这一段代码,先判断该变量是否为数组,再判断索引为0的值是否为字符串,到下面的代码还要验证该索引值是否等于exp
关键点在于下面的代码:
正常来说传入的参数是字符串即$val=test
,但这里传入了数组,$exp
便是$val[0]
又满足了elseif
语句(跟到这里便也能理解为什么要用超全局数组,而不用I函数了,如果使用I函数不满足条件,便会异常抛出,从而影响注入),把where
条件直接用点拼接,这时传入
username[0]=exp&username[1]==1 and test
便会造成SQL注入,最终拼接出来的语句便是
select * from users where `username` $val[1] limit 1
thinkphp3.2.3 bind注入
简述:由于框架实现安全数据库过程中在
update
更新数据的过程中存在SQL语句的拼接,并且当传入数组未过滤时导致出现了SQL注入
将环境更改一下
payload:
?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1
传入参数,跟进一下代码,看一下为什么这样的payload
能造成报错注入,跟前面几条链一样,参数会先赋值给$this->options['where']
再跟进到save
方法中去,调用_parseOptions
方法
到这一步$val
为数组,不属于标量,所以不经过_parseType
方法验证类型
从该方法中出来后$this->options
赋值给$options
,接下来便调用$this->db->update
方法
在这里出现一段SQL语句和参数绑定,跟进parseSet
这个方法查看
再跟进到$this->bindParam($name,$val);
因为$this->bind
为空,所以$name
为0,而$val
为1
经过此方法的处理,SQL语句便存在一个:
阻断了注入,最终绑定参数为:
产生的SQL语句为:
UPDATE `users` SET `password`=:0
再调用parseWhereItem
方法拼接where
部分处理后的语句,当$exp
为bind时,$whereStr
部分可控
两段SQL语句再进行合并,最终形成的SQL语句
UPDATE `users` SET `password`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)
最后跟进execute
方法,该方法中有对绑定参数的处理:
if(!empty($this->bind)){
$that = $this;
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
}
这两行代码便是起到替换的作用,代码先是创建一个闭包,调用array_map
,对$this->bind
这个数组中的每个参数都调用这个闭包,对$this->bind
进行处理,未处理前为array(":0"=>"1");
,处理后为:array(":0"=>"'1'");
,最后再通过strtr
函数处理$this->queryStr
语句,得到的结果如下:
:0
替换为外部传进来的字符串,所以将传入参数等于0,这样就拼接了一个:0
,然后会通过strtr()
被替换为1,SQL语句便可以正常执行,这也就是为什么payload中id[1]=0
官方修复:
过滤bind
即可
TP3.2.3 RCE漏洞
业务代码中如果模板赋值方法assign的第一个参数可控,则可导致模板文件路径变量被覆盖为携带攻击代码的文件路径,造成任意文件包含,执行任意代码
程序会进入模板渲染方法中,需要先创建对应的模板文件(View
),模板文件位置为:\Application\Home\View\Index\index.html
,这里的模板渲染方法除了display
,也可以为fetch、show
但使用fetch
会有一些区别,如上图其程序逻辑会使用到ob_start()
打开缓冲区,所以PHP代码的数据块和echo()
输出都会进入缓冲区而不会立刻输出,如果想要fetch
方法对应的攻击代码输出的话,需要在攻击代码末尾带上exit()
或die()
Debug开启和关闭是有一些区别的,具体如下:
Log记录目录:
若开启debug模式日志会到:\Application\Runtime\Logs\Home\
下
若未开启debug模式日志会到:\Application\Runtime\Logs\Common\
下
这里以Debug关闭为例(define('APP_DEBUG',false)
)
构造请求包:
构造攻击请求:
index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/22_08_03.log
若Debug
开启,正确的请求日志也会被记录到日志中,只是日志路径不 一样了而已
下面跟着代码来走一遍,看看此payload如何触发文件包含导致RCE
先传入参数会先进入assign
函数中,再赋值给$this→tVar
之后会进入到display
方法中,display
方法开始解析并获取模板文件内容,此时模板文件路径和内容为空
再进入到fetch
方法中,此时传入的参数为空,程序会根据配置去获取默认模板文件的位置
当TMPL_ENGINE_TYPE
配置为php
时,会采用PHP原生模板,默认的为Think
,便进入到else分支中,获取$this→tVar
变量值赋值给$params
,之后再进入到Hook::listen
方法中
进入exec
方法中,处理后调用Behavior\ParseTemplateBehavior
类中的run方法处理$params
这个带有日志文件路径的值
程序进入run方法中,一系列判断后,进入else分支,调用Think\Template类中的fetch方法对变量$_data(带有日志文件路径的变量值)进行处理
进入Think\Template类中的fetch方法,获取缓存文件路径后,再进入Storage的load方法中
$_filename
为之前获取的缓存文件路径,$var则为之前带有_filename=日志文件路径的数组,$vars不为空则使用extract方法的EXTR_OVERWRITE
默认描述对变量值进行覆盖,之后include该日志文件路径,造成文件包含,最终导致包含文件造成RCE
Thinkphp3.2.3 日志泄露
前提:开启了Debug
THINKPHP3.2 结构:Application/Runtime/Logs/Home/年份_月份_日期.log
参考博客:
@https://y4er.com/post/thinkphp3-vuln/
@https://blog.csdn.net/rfrder/article/details/114024426
@https://www.cnblogs.com/zpchcbd/p/12552185.html
@https://paper.seebug.org/573/
@https://mp.weixin.qq.com/s/_4IZe-aZ_3O2PmdQrVbpdQ
@https://blog.csdn.net/Mruos/article/details/109802121
@https://blog.csdn.net/qq_33382313/article/details/51702787
@https://www.cnblogs.com/lingzhisec/p/15728886.html