我们发布了一份仅总结要点、便于阅读的摘要文章。推荐想要先了解概况,或者时间不充裕的人阅读。
2017 年 12 月 1 日,Struts2 的安全更新已发布。 发布前,邮件列表中就有消息称这与 Jackson(Java 中流行的 JSON 库)的漏洞有关,笔者所在公司的内部系统和工具中也使用了 Jackson,因此笔者也一直关注具体内容。
实际发布的内容修复了以下两个安全问题。只有 S2-055 受到 Jackson 组件 jackson-databind 漏洞的影响。
REST 插件此前已内置了使用 JSON-lib 的 handler 和使用 Jackson 的 handler,似乎用户可以自行选择。 S2-054 将默认 handler 切换为 Jackson,S2-055 进一步将旧版 Jackson 更新至最新版,这似乎是本次修复的全貌。
那么,CVE-2017-7525 到底是一个什么样的漏洞?笔者平时在 Java 中处理 JSON 时也使用 Jackson,因此在 12 月 2、3 日的周末调查了这个问题,本文便是其成果。
用于验证示例代码的笔者环境:
Adam Caudill 先生的博客中公开了 CVE-2017-7525 的说明。
用笔者自己的话大致总结一下:jackson-databind 提供了将 JSON 映射到 Java 对象的功能(ObjectMapper 类)。
这里通过调用 ObjectMapper.enableDefaultTyping(),可以根据 JSON 中自行嵌入的类名进行映射。
“可以指定输入 JSON 中的类名”这一点,可能已经有人感到不安,而 CVE-2017-7525 正是这种不安被言中了。
在进入漏洞说明之前,先解释一下为什么当初要实现这样的功能。
请参考以下示例代码了解 jackson-databind 反序列化的基本使用方法。(本文中的 Jackson 示例代码使用了 Groovy。使用 @Grab 可以方便地切换 jackson-databind 的版本。)
在上述示例代码中,直接可以将 "animal" 键映射到 Animal 类。 那么,在以下情况下又会如何呢?```java class Zoo { Animal animal; }
abstract class Animal { String name; protected Animal() { } }
class Dog extends Animal { double barkVolume; Dog() { } }
class Cat extends Animal { boolean likesCream; int lives; Cat() { } }
在这种配置下,"animal"键的内容会出现指向Dog类和指向Cat类两种情况。因此,需要额外的信息来确定具体使用哪个类进行映射。
为了解决这个问题,jackson-databind内置了可以将映射类名嵌入JSON的自有处理机制。
例如如下方式,将"animal"键的内容改成数组,并在第一个元素中指定类名。```
{"animal":["Dog",{"name":"dog1","barkVolume":1.2}]}
これにより ObjectMapper.readValue() は "animal" キーの中身が Dog クラスだと認識してマッピングを行います。
もちろん、そのままでは "animal" キーの中身がもともと配列だったのか、jackson-databind独自のクラス名情報が含まれたものなのか、判別できません。
これを切り替えるのが ObjectMapper.enableDefaultTyping() メソッドになります。
他にも @JsonTypeInfo アノテーションをクラスに定義する方法もあります。詳細は以下のJacksonドキュメントを参照してください。
実際に ObjectMapper.enableDefaultTyping() メソッドを使ったサンプルコードを次に示します。
以上見てきたように、クラス名に続いてそのプロパティをJSONで与えることで、ある程度制限はあるものの、任意のクラスを任意のプロパティで生成することが可能となります。 これを悪用したのがCVE-2017-7525の脆弱性で、きっかけとなったのは恐らく次のレポートと思われます。
JacksonなどJavaでよく使われているserialize/deserializeライブラリについて、クラス名などの操作で任意コード実行につながる危険性がレポートされており、実際にどのようなクラスが危険か具体的なクラス名がリストアップされています。
これを受けてのものか分かりませんが、日付的には上記リポジトリの1st commitの直後に、jackson-databind で以下のIssueが立てられ、対応が始まりました。
実際にこの脆弱性を突くようなJSONデータとJavaコードはどのようなものでしょうか? このIssueで対応された jackson-databind 2.8.9 のテストコードにヒントがあります :
このテストコードを元に、動作確認できるよう調整したサンプルコードを次に示します。
@Grab で 2.8.9 を指定して実行すると your jackson version IS SAFE to CVE-2017-7525 と表示されます。これは 2.8.9 の修正によりインスタンス化されるクラス名指定でblacklist検査が追加されたことによります。
ここで @Grab に 2.8.8 を指定して実行すると、以下のように出力されます。```
your jackson version MAY NOT BE SAFE to CVE-2017-7525
com.fasterxml.jackson.databind.JsonMappingException: N/A
at [Source:
{
"id" : 124,
"obj" : [
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
{
"transletBytecodes" : [ "AAIAZQ==" ],
"transletName" : "a.b",
"outputProperties" : { }
}
]
}
; line: 9, column: 28] (through reference chain: Bean1599["obj"]->com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["outputProperties"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:277)
(...)
at org.codehaus.groovy.tools.GroovyStarter.main(GroovyStarter.java:128)
Caused by: java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:399)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.fasterxml.jackson.databind.deser.impl.SetterlessProperty.deserializeAndSet(SetterlessProperty.java:116)
... 30 more
null
在输出结果中,有一行显示 `at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)`。
在这个示例代码中,虽然抛出了NullPointerException,但从方法名来看,可以推测出可能发生了某种含有副作用的过程。
要构造出能够成功执行代码的JSON,看来还需要进一步调查。
本文在这里暂且只介绍到这,但如果其他网站发布了进一步调查的文章,我们也会在此处进行补充。
那么,关键的黑名单是在哪里实现的呢?就是以下这个类。
* https://github.com/FasterXML/jackson-databind/blob/jackson-databind-2.8.9/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java#L51
实际上,在2.8.9版本时,这个黑名单似乎存在遗漏。该问题就是CVE-2017-15095。
### 改善黑名单的CVE-2017-15095应对
关于黑名单遗漏的改善,首先在 https://github.com/FasterXML/jackson-databind/issues/1680 中添加了 `s.add("com.sun.rowset.JdbcRowSetImpl");`。
在随后发布了2.9.0之后,又在 https://github.com/FasterXML/jackson-databind/issues/1737 中添加了以下黑名单检查。```java
// [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");
s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean");
s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
こうして 2.8.10 / 2.9.1 がリリースされ、CVE-2017-15095 への対応完了となっています。
2.8.10 でのblack list動作をチェックするテストコード:
このテストコードを元に、動作確認できるよう調整したサンプルコードを次に示します。
対応が完了したとされている 2.8.10 を @Grab で指定して動かしてみると、your jackson version IS SAFE to CVE-2017-15095 と表示されます。
続いて、black list改善前の 2.8.9 を @Grab で指定して動かすと、以下が出力されます。```
your jackson version MAY NOT BE SAFE to CVE-2017-15095
com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of java.util.logging.FileHandler, problem: \tmp\foobar.txt.lck
at [Source:
{
"v" : [
"java.util.logging.FileHandler",
"/tmp/foobar.txt"
]
}
; line: 5, column: 5] (through reference chain: PolyWrapper["v"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:277)
(...)
Caused by: java.nio.file.NoSuchFileException: \tmp\foobar.txt.lck
(...)
at java.util.logging.FileHandler.openFiles(FileHandler.java:459)
at java.util.logging.FileHandler.(FileHandler.java:292)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call1(AnnotatedConstructor.java:129)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:318)
... 31 more
null
对应前的版本中,`java.util.logging.FileHandler` 类名绕过了黑名单检查,通过实例化可以确认它实际尝试打开文件。
对应后的版本中,它被黑名单捕获,并抛出了 `JsonMappingException` 异常。
此外,2.8.10 版本的黑名单如下。以 `[databind#1737]` 注释开头的是针对 CVE-2017-15095 新增的黑名单。
* https://github.com/FasterXML/jackson-databind/blob/jackson-databind-2.8.10/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java#L52
### 关于受漏洞影响的条件、攻击实现难度以及应用侧的对策
总结以上结果,jackson-databind 漏洞受影响需要满足以下条件:
1. 使用 jackson-databind 2.8.9 / 2.9.0 或更低版本
2. 对从不可信来源获取的 JSON 执行了以下任一操作:
* 调用 `ObjectMapper.enableDefaultTyping()` 后进行反序列化。
* 未调用 `ObjectMapper.enableDefaultTyping()`,但通过类声明中的 `@JsonTypeInfo` 注解启用映射后进行反序列化。
* 即使应用代码未使用,框架也可能根据 `Accept` 请求头或 URL 扩展名自动进行反序列化。
3. classpath 中包含可能被 Java 序列化/反序列化漏洞利用的类("Gadget")。
4. 映射目标 Java 类的成员字段使用了可接受 Gadget 类的类型(如 Object 类型)。
* 如果字段类型是与 Gadget 类不兼容的应用特定 Bean 类,则可以在生成实际实例前通过类型检查错误进行拦截。(应用特定 Bean 类本身存在反序列化漏洞的情况除外)
是否真正受影响,似乎高度取决于 `ObjectMapper` 的使用方法/配置状态、`@JsonTypeInfo` 注解的组合方式,以及映射目标类的成员字段等应用侧代码。
此外,如果 JSON 中的键名在反序列化目标 Java 类中不存在,Jackson 会直接忽略。
因此,要成功攻击,需要针对每个应用的 JSON 进行定制,很难制作出适用于多个应用的攻击代码。
关于条件 4,通常的写法中,很少会特意将 Java 类的字段设为 Object 类型。
能够实现任意代码执行的类,大多数情况下也与应用构建的 Bean 类不兼容。
综上所述,我们认为发生大规模攻击或实际利用此漏洞造成损害(即攻击成功)的可能性较低。
关于应用侧的对策,条件 3 中 JDK 自带的类也可能被利用,因此实际上无法应对。
因此,基本对策是将 jackson-databind 升级到最新版本。
如果无法升级到最新版本,则需要移除 `ObjectMapper.enableDefaultTyping()` 调用或 `@JsonTypeInfo` 注解,并修改设计使其不依赖这些功能。例如,可以考虑创建自定义序列化器。
不过,如果已经开始用作远程可调用的 API,则无法轻易更改 JSON 格式。
关于 `@JsonTypeInfo` 注解,根据设置可以限制子类,使其只接受程序员预期的类名。
详细信息请参阅以下文档:
* JacksonPolymorphicDeserialization
* https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
#### 关于黑名单对策的利弊及自定义反序列化器的创建
jackson-databind 2.8.10 / 2.9.1 通过黑名单对策应对了 CVE-2017-7525 和 CVE-2017-15095。
然而,正如 Struts2 的 OGNL 相关问题所表明的,黑名单对策并非万全。
(笔者个人认为,对于只使用扫描工具的所谓的 "脚本小子" 级别,这在一定程度上是有效的对策。)
因此,笔者考虑,真正根本性的对策应该是禁用或不使用 `ObjectMapper.enableDefaultTyping()` 等将类信息嵌入 JSON 的功能。
那么,`ObjectMapper.enableDefaultTyping()` 原本试图解决的问题该如何解决呢?
对于这一点,笔者也没有能够称之为正确答案的替代方案。
问题的根源在于:“当要映射的 Java 类不明确时,能够在不依赖不可信 JSON 的情况下进行映射。”
笔者认为,实现这一点的可能方法之一是创建自定义反序列化器。
自定义反序列化器可以在程序代码侧接收 JSON 并自行控制生成的对象。
例如,当接收到 `{"animal":{"name":"dog1","barkVolume":1.2}}` 时,可以判断 “因为有 `barkVolume` 键,所以将其实例化为 Dog 类”;
当接收到 `{"animal":{"name":"cat1","likesCream":true,"lives":10}}` 时,可以判断 “因为有 `likesCream` 键和 `lives` 键,所以将其实例化为 Cat 类”。
这样就不需要在 JSON 中特意嵌入类名。
就个人而言,在 JSON 中嵌入类名的格式可能会影响与其他语言/库的互操作性(如果其他语言或库有类似的扩展功能,请告知)。
如果要与其他系统互操作,与其让对方特意按照 Jackson 的方便嵌入类名,不如在 Jackson 侧创建自定义反序列化器来应对更为合理。
我认为还有其他一些解决方案,如果读者有 “还有这样的解决方案” 的想法,欢迎提出意见。
(一个极端的例子:不映射到特定类,而是将所有内容反序列化为 `Map<String, Object>` 或 `List<String, Object>` 格式。)
我找到了一些关于创建自定义反序列化器的参考文章,虽然是英文文章,但附上了链接:
* Jackson: create a custom JSON deserializer with StdDeserializer and JsonToken classes | Dede Blog
* http://www.davismol.net/2015/05/20/jackson-create-a-custom-json-deserializer-with-stddeserializer-and-jsontoken-classes/
* Getting Started with Deserialization in Jackson | Baeldung
* http://www.baeldung.com/jackson-deserialization
* Custom JSON Deserialization with Jackson - DZone Integration
* https://dzone.com/articles/custom-json-deserialization-with-jackson
* Building a Custom Jackson Deserializer - The Boy Wonders
* http://www.robinhowlett.com/blog/2015/01/01/building-a-custom-jackson-deserializer/
jackson-databind 的 JavaDoc(2.8 系列, 2.9 系列):
* https://fasterxml.github.io/jackson-databind/javadoc/2.8/
* https://fasterxml.github.io/jackson-databind/javadoc/2.9/
笔者也制作了自定义反序列化器的示例代码,希望能提供一些提示:
* [custom-deserializer-demo.groovy](https://github.com/SecureSkyTechnology/study-struts2-s2-054_055-jackson-cve-2017-7525_cve-2017-15095/blob/master/custom-deserializer-demo.groovy)
## 关于 S2-055
到此为止,我们讨论了 Jackson 本身的漏洞。那么,它实际上如何影响 Struts2 的 REST plugin 呢?我们使用 Struts2 附带的 struts2-rest-showcase 进行了验证。
Struts REST plugin 的使用方法参考了以下内容:
* http://struts.apache.org/plugins/rest/
### Struts2 REST plugin 未调用 ObjectMapper.enableDefaultTyping()
顺便一提,CVE-2017-7525 的实际漏洞条件包括:
* 对从不可信来源获取的 JSON 执行了以下任一操作:
* 调用 `ObjectMapper.enableDefaultTyping()` 后进行反序列化。
* 未调用 `ObjectMapper.enableDefaultTyping()`,但通过类声明中的 `@JsonTypeInfo` 注解启用映射后进行反序列化。
我们检查了 Struts2 REST plugin 中是否存在符合这些条件的代码,结果确认都不包含。
实际上,在对应前的 2.5.14 版本中,无论是 `ObjectMapper.enableDefaultTyping()` 还是 `@JsonTypeInfo`,在整个 Struts2 源码树中都没有使用。
* https://github.com/apache/struts/tree/STRUTS_2_5_14
在 Struts2 的整个源码树中,使用 Jackson ObjectMapper 的只有 org.apache.struts2.rest.handler.JacksonLibHandler 类。检查源代码后确认,在 2.5.14 版本中确实没有使用 `ObjectMapper.enableDefaultTyping()`。
* https://github.com/apache/struts/blob/STRUTS_2_5_14/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonLibHandler.java
* 该 Java 文件在 2.5.14.1 中内容未变。
因此,我们认为即使针对 CVE-2017-7525,REST plugin 本身在 2.5.14 版本中也没有问题。
漏洞发生的情况是,应用侧在映射 JSON 的类的字段上设置了 `@JsonTypeInfo`。
因此,在使用 struts2-rest-showcase 的下方验证中,我们在应用侧添加的映射目标字段上设置了 `@JsonTypeInfo` 进行了验证。
顺便一提,Struts2 中似乎还有一个 JSON plugin。
* http://struts.apache.org/plugins/json/
* 查看 JSON plugin 的源代码,pom.xml 中没有将其他 JSON 库作为依赖。
* 似乎是自己实现了 JSON 处理。
* https://github.com/apache/struts/tree/STRUTS_2_5_14/plugins/json
* 因此,jackson-databind 的漏洞应该不影响 JSON plugin。
### 使 struts2-rest-showcase 支持 Jackson
struts2-rest-showcase 是使用 REST plugin 实现 Order 类 CRUD 的示例。我们在此基础上添加了 Jackson 漏洞示例代码中使用的 Zoo / Animal / Cat / Dog 类,以及用于通过 JSON 处理其 CRUD 的 ZooController 等。
整体源代码请参见以下链接:(本文使用 JDK8 进行构建和运行确认)
* [rest-showcase](https://github.com/SecureSkyTechnology/study-struts2-s2-054_055-jackson-cve-2017-7525_cve-2017-15095/tree/master/rest-showcase)
主要修改点:
* 将 jetty-maven-plugin 的监听端口改为 18088。(`mvn jetty:run`)
* 将 jackson-core、jackson-databind 添加到依赖中。
* 添加了 Zoo、Animal(抽象)、Dog、Cat 类。添加了服务层 ZooService 类。
* 添加了具备最低限度 CRUD 功能的 ZooController。(省略了视图 JSP)
* 在 struts.xml 中将 JSON 处理器改为 JacksonLibHandler。
* 嵌入了 maven-wrapper,只要安装了 JDK,就可以直接通过 mvnw / mvnw.bat 进行构建和运行。
构建和运行:
1. 克隆仓库后,cd 到 rest-showcase 目录,执行 `mvnw jetty:run`。(首次运行时需要下载 maven,可能需要几分钟或甚至十分钟以上,请注意)
2. 访问 http://localhost:18088/struts2-rest-showcase/,如果显示 Order 列表则成功。
3. 按 Ctrl-C 可终止运行。
4. 修改 Java 文件后,按 Ctrl-C 终止,然后再次执行 `mvnw jetty:run`。
使用 curl 命令进行确认:(假设通过 localhost:8080 作为本地 HTTP 代理)```
一覧取得:
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo"
ID指定:
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo/1"
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo/2"
削除:
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo/2" -X DELETE
仓库中 Zoo.java 的 animal 字段如下所示,@JsonTypeInfo 被注释掉了,并且是抽象类 Animal 类型。```java
//@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)
public Animal animal;
//public Object animal;
现在,我们尝试使用POST方法发送JSON请求,并调用`ZooController.create()`方法。```
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":{"name":"dog2","barkVolume":2.3}}'
すると以下のエラーメッセージを含む例外が発生しました。Animalクラスはabstractなため、インスタンスを生成できていません。``` Can not construct instance of org.demo.rest.example.Animal: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
そこで `animal` フィールドの `@JsonTypeInfo` のコメントアウトを外して有効化します。Webアプリケーションは Ctrl-C で中止し、もう一度 `mvnw jetty:run` を実行します。```java
// 次のimportを忘れずに追加
import com.fasterxml.jackson.annotation.JsonTypeInfo;
//...
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)
public Animal animal;
//public Object animal;
这样就能在JSON中嵌入类名了。接下来用下面的curl命令,尝试发送一个嵌入了类名的JSON。``` curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":["org.demo.rest.example.Dog",{"name":"dog2","barkVolume":2.3}]}'
→ `HTTP/1.1 201 Created` 会被返回。通过获取列表,可以确认确实已经添加。
### Struts2 REST plugin 2.5.14 中确认 CVE-2017-7525
由于仓库的 pom.xml 中 `<parent>` 的 struts artifact 指定了版本 2.5.14,因此该版本存在 CVE-2017-7525 漏洞。
为了确认这一点,请执行以下 curl 命令。类名及其内容参考了 cve-2017-7525-check.groovy,因此如果返回与 jackson-databind 2.8.8 相同的结果,则可以认为存在漏洞。```
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":["com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",{"transletBytecodes":["AAIAZQ=="],"transletName":"a.b","outputProperties":{}}]}'
→ 以下错误消息包含的异常已被抛出。``` java.lang.IllegalArgumentException: Class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl not subtype of [simple type, class org.demo.rest.example.Animal]
`animal` 字段的类型是 Animal 类,而 Animal 类并非 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 类的子类型,因此似乎触发了 IllegalArgumentException。
那么,将 Zoo.java 中的 `animal` 字段修改为 Object 类型,再通过 `mvnw jetty:run` 重新运行一次看看。```java
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY)
//public Animal animal;
public Object animal;
这样,再次执行刚才的curl命令时,发生了包含以下堆栈跟踪的异常。``` Caused by: java.lang.NullPointerException at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401) ~[?:1.8.0_92]
这与使用 cve-2017-7525-check.groovy 验证时相同,是存在漏洞时的异常。
综上所述,在 Struts2 REST plugin 2.5.14 中,确认存在 jackson-databind 的漏洞 CVE-2017-7525。
此外,我们还了解到除了 `@JsonTypeInfo` 外,还需要使用 Object 类型。
以下是笔者的个人见解:在创建 REST API 的数据类时,特意将字段类型指定为 Object 类或与 Java 反序列化漏洞中可用作 Gadget 的类兼容的类,我认为并不常见。因此,感觉实际攻击成功可能比较困难。
### 确认 Struts2 REST plugin 2.5.14.1 中的修复
那么,我们来确认一下版本 2.5.14.1 是否修复了该漏洞。
将 pom.xml 中 `<parent>` artifact 的版本修改为 2.5.14.1,使用 `mvnw jetty:run` 重新启动,然后执行与之前相同的 curl 命令。```
curl -v -x localhost:8080 -H "Accept: application/json" "http://localhost:18088/struts2-rest-showcase/zoo" -X POST -H "Content-Type: application/json" -d '{"id":"3","animal":["com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",{"transletBytecodes":["AAIAZQ=="],"transletName":"a.b","outputProperties":{}}]}'
→ 发生了包含以下错误消息的异常。```
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl: Illegal type (com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) to deserialize: prevented for security reasons
这是与用 cve-2017-7525-check.groovy 验证时相同的、漏洞修复后的异常。
使用Eclipse等支持Maven的IDE打开,解决依赖关系后查看jackson-databind的版本,应该可以确认确实是2.9.2。如果没有IDE,可以通过 `mvnw help:effective-pom` 输出最终的pom,在其中搜索jackson-databind,即可确认正在使用version 2.9.2。
关于CVE-2017-15095在此省略,但通过以上内容可以确认2.5.14.1已经应对了jackson-databind的漏洞。
### S2-055的PoC
※ 2017-12-08 追记
关于S2-055的调查文章和PoC报告已发布。
* S2-055漏洞环境搭建与分析 | 绿盟科技博客
* http://blog.nsfocus.net/s2-055/
借助Google翻译阅读后,发现关于发生条件和攻击实现程度似乎是相同的见解。
实际上还展示了修正rest-showcase后,启动计算器的HTTP通信的PoC。
只借用了JSON部分,首先尝试用Jackson单独测试,代码如下。
* [cve-2017-7525-poc.groovy](https://github.com/SecureSkyTechnology/study-struts2-s2-054_055-jackson-cve-2017-7525_cve-2017-15095/blob/master/cve-2017-7525-poc.groovy)
这里指定使用2.8.8版本运行,在笔者环境中输出如下。```
your jackson version MAY NOT BE SAFE to CVE-2017-7525
(...)
Caused by: java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$1.run(TemplatesImpl.java:401)
(...)
cve-2017-7525-check.groovy 时类似,run() 方法运行并发生了 NullPointerException。但是,计算器并未启动。
推测而言,xalan 的 TemplatesImpl 是 Java 内部的类,因此有可能 Java 方面进行了某种修正。或者,实际攻击成功可能需要更苛刻的条件。
PoC 文章中并未明确验证所使用的 Java 版本,因此未能达到启动计算器的地步。如果出现更多信息,我们想再次进行验证。
至此,我们以 S2-055 为开端,主要介绍了 Jackson 的漏洞 CVE-2017-7525 和 CVE-2017-15095。那么,另一方的 S2-054 是什么情况?这里总结一下粗略调查的结果。
结论来说,在本文章撰写时(2017-12-03),尚未找到具体信息。也没有找到可以检查是否存在漏洞的 PoC 等。
在 S2-054 的信息公开页面中说明,REST 插件使用了旧的 JSON-lib 库,可以通过被篡改的 JSON 进行 DoS 攻击。
REST 插件使用的是过时的 JSON-lib 库,该库存在漏洞,允许通过精心构造的 JSON payload 的恶意请求执行 DoS 攻击。
作为对此问题的应对发布的 2.5.14.1 页面中,链接了对应的 JIRA 工单 WW-4892。
我确认了 WW-4892,但哪里都没有提到 DoS 或 JSON-lib 的漏洞。
即使阅读了 "Description",也只写着因为 JSON-lib 过于老旧且未维护,所以将默认 handler 改为 Jackson。
GitHub 方面的 pull request 如下,但这里也没有提到具体的 JSON-lib 问题。
于是我们决定查看 JSON-lib 方面。官方网站如下。
另外,截至2017年,似乎由 GitHub 管理。
哪边是最新的?截至本文撰写时,GitHub 方面没有发布。于是我们查看 Maven Central 仓库的注册情况。 搜索 "json-lib" 会命中几个 groupId。
哪个 groupId 是正确的?我们实际确认 Struts2 2.5.14.1 的 REST 插件的 pom.xml。
查看 groupId = net.sf.json-lib, artifactId = json-lib 的发布版本,2010年12月的版本 2.4 是最后的发布。
查看 sourceforge 方面的页面,同样,2012年12月的版本 2.4 是最后的发布。
实际上,GitHub 方面虽然没有打发布标签,但追踪 commit 日志可以看到有一个 2010年12月发布版本 2.4 的 commit。此外,此后虽然 pull request 合并在进行,但没有发布动作。
查看 GitHub 方面的 Issue,包括已关闭的,没有看到可能导致 DoS 的标题。
查看 sourceforge 方面的工单,终于遇到了内存泄漏问题的工单。似乎都还没有被修复。
终于查到了似乎仍有内存泄漏问题,但由于作者的能力和时间原因,未能进行进一步的调查。 如果您有实际使用这种模式的 JSON 发生内存泄漏或导致 DoS 的具体信息,请赐教,将不胜感激。
由于使用 Jackson 的 OSS 很多,因此也存在受此漏洞影响的其他库和框架。 例如,Pivotal 产品群中的 Spring Security 受到影响,并于 2017年6月公开了信息。
除此之外,例如 Spring Framework 本体又如何呢?至少在 https://pivotal.io/security/ 上没有公开来自 Jackson 的更新信息。
即使如此也不能掉以轻心。原本 Spring Framework 中的 Jackson 设计就非常容易定制,可以生成应用程序特有的 ObjectMapper。应用程序是否定制了 Jackson 的配置/功能?是否使用了 @JsonTypeInfo?等等,建议最好确认一下。
我按时间顺序整理了 jackson-databind 的 GitHub Issue/发布信息以及 RedHat 的 bugzilla 等参考链接。 如果有误,请不吝向作者指正或联系。
此时的黑名单列表如下:```java 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");
在此,同月内作为黑名单对策的附加修正,发布了 2.9.0。
* https://github.com/FasterXML/jackson-databind/issues/1680
新增了以下内容:```java
s.add("com.sun.rowset.JdbcRowSetImpl");
此外,在同一个月中,以下Issue被打开。
→黑名单中添加了以下内容,并被纳入 2.8.10 / 2.9.1 版本中。```java // [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"); s.add("org.springframework.beans.factory.config.PropertyPathFactoryBean"); s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource"); s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
### 2017-08
* 2.8.10 发布,对应 #1680、#1737。
* 此外,8月又开放了以下 Issue,并围绕 CVE-2017-7525 进行了全面的应对讨论。
* https://github.com/FasterXML/jackson-databind/issues/1723
* Adam Caudill 的博客中公开了针对 CVE-2017-7525 的利用代码解说。
* https://adamcaudill.com/2017/10/04/exploiting-jackson-rce-cve-2017-7525/
### 2017-09
* 2.9.1 发布,对应 #1737。
### 2017-10
在以下 bugzilla 中,指出 CVE-2017-7525 修复时点的 2.8.9 / 2.9.0 应对不充分,并开始将最新版 2.8.10 / 2.9.1 的黑名单应用于新 CVE-2017-15095 的工作。
* https://bugzilla.redhat.com/show_bug.cgi?id=1506612
### 2017-11
* RedHat 公开了 CVE-2017-15095 的信息。
* https://access.redhat.com/security/cve/cve-2017-15095
* 在 jackson-databind 的 Issue 中,也通过以下链接进行了关于 CVE-2017-15095 应对情况的问答。
* https://github.com/FasterXML/jackson-databind/issues/1847
* 回复称已在 2.8.10 / 2.9.1 中进行了应对。
### 2017-12
* 云端 WAF Scutum 的开发者在博客中公开了解说文章。
* https://www.scutum.jp/information/waf_tech_blog/2017/12/waf-blog-052.html
* 该文章指出,在 2.9.3 的黑名单中 Spring 的类存在不完善之处,虽不会立刻引发紧急问题,但可能还需要更新。
* 此外,文章还讨论了该漏洞的责任究竟在库本身还是应用端的问题。
## 关于未来 Java 序列化/反序列化相关漏洞的趋势
从去年到今年,感觉 Java 序列化/反序列化相关的漏洞信息有所增加。
例如,包含 Spring 在内的 Pivotal 产品漏洞信息在 https://pivotal.io/security/ 上公开,实际上进入 2017 年后,除了 CVE-2017-4995 之外,还公开了以下漏洞信息。
* https://pivotal.io/security/cve-2017-8045
* Spring AMQP 中 `org.springframework.amqp.core.Message` 的反序列化问题导致的 RCE
* https://pivotal.io/security/cve-2017-8046
* Spring Data REST 中 PATCH 方法对 JSON 处理不当导致的 RCE
由于这些是容易导致 RCE 的漏洞,攻击者和漏洞研究者都开始关注 Java 的序列化/反序列化。
预计在未来几年内,与序列化/反序列化处理相关的漏洞报告还会继续出现。
尽管如此,包括 Jackson 在内,在通过远程 API 进行数据交换已成为常态的今天,完全不使用序列化/反序列化、或者从头自己实现,在大多数现场是不现实的。
笔者认为,重点在于构建一种能够尽快更新库的开发体制和文化,以便在漏洞公开后迅速响应。
对于所依赖的中间件、库、框架的漏洞,今后该如何应对?笔者在心理层面有所感触,特此写下以下感想。
## 感想
当看到 S2-054、055 公开,且其原因与 Jackson 漏洞有关的信息时,笔者受到了相当大的冲击。
因为在几天前,同事问笔者“有没有推荐的 Java JSON 解析库?”,笔者还得意地回答说:“Jackson 在 OSS 中广泛使用、经验丰富,上网一搜就能找到很多文章和问答,所以推荐它。”
笔者本人也在公司内部工具开发中使用 Jackson,深感其便利。
然而,当时笔者并未掌握 CVE-2017-7525,只是因为 Jackson 被 OSS 广泛使用且用户众多而疏忽大意。
后来另一位同事发现了 Adam Caudill 的博客,告诉了笔者 CVE-2017-7525 的存在。而就在得意推荐 Jackson 的几天后,Struts2 因 Jackson 漏洞而发布了更新,而且 Jackson 漏洞本身在几个月前就已经得到了修复。作为身处安全行业的技术人员,如果被指责没有及时收集自己所用库的漏洞信息,也是无法辩解的(实际上只能承认确实如此)。
因此,这阵子笔者的心理状态非常糟糕(血压、脉搏升高,手抖,虽不悲伤却忍不住流泪,心悸不止等)。为了改善这种情况,笔者下定决心首先要弄清楚 CVE-2017-7525 到底是什么、Jackson 究竟是什么状况,于是花费周末时间调查并撰写了本文。
回顾这一点,笔者深切感受到,专心开发时确实很难关注依赖库的更新信息。
在不断涌来的开发任务中,对所有使用库的功能及安全性等品质方面进行彻底调查,现实中是不可能的。
另一方面,开发所需的功能越来越多,全部自己实现也是不现实的。
需要在某些地方“信任”所使用的库,以提高开发效率。
然而,在日常开发工作中,追踪所有使用工具和库的更新信息,并留意是否包含安全问题修正,同样非常困难。
当然,笔者知道近年来有服务可以通过注册使用的工具和库来推送更新信息,以解决这一课题。
此次因自身心理状况恶化,笔者感受到的是,自身对于“没有做到的事情”的自责感和罪恶感非常强烈。
“身为安全工程师,却连自己使用的库的漏洞信息都不知道……”,笔者给自己贴上了这样的负面标签。
即使是与安全行业无关的普通开发者,可能也有很多人在心中抱有后悔和不安,例如:
“如果当时没有推荐 Struts2……”或者“必须更认真地进行库/框架的生命周期管理(= 对现在做不到的自己/状况感到不安)”。
如果将这些作为“课题”正面去“解决”,那么就需要逐一收集漏洞信息、逐项检查计划使用的库和框架的源代码和功能、仔细调查是否可称为事实标准,并在投产之后严格进行生命周期管理。
但是,这种“深思熟虑、谨小慎微”的做法,在如今多样化的开发现场真的可能吗?
笔者撰写本文后思考的是:将“没有做/没有做到/没有注意到”视为原因或罪魁祸首,并将其消除(即“使之能做到”)作为“课题解决”的唯一方式,这种时代或许已经结束了。
在这种文化下,除非是完美的人,否则开发者将永远面对着自己的“没做到、没做、没注意到”。
既然没有完美的人,若非心理极其强大的人,恐怕难以承受。
在软件安全问题中,实际造成损害的犯罪者是攻击者。使状况变得消极的,是那些利用漏洞的攻击者。
绝大多数开发者,基本上都是出于善意、认真致力于开发的。在这一点上,已经处于积极状态。
“没有进行库/框架的生命周期管理”、“没有收集和监控所使用库的漏洞信息”,仅仅是没有做而已,既不是积极也不是消极的状态。
然而,正是因为攻击者的存在,“没做的事”变成了“没做到/没注意到”的消极面,这是否太过悲哀?
不可否认,“深思熟虑、谨小慎微”的价值观受到现代日本社会和企业文化的影响。
实际上,因编写包含 SQL 注入等漏洞的代码而导致遭受攻击损害的案例层出不穷,也出现了在法庭上追究开发公司责任的案例。
也有因业务疏忽导致重大事故的情况。
但是,如果将所有问题都归结为“开发公司/开发者”的问题,那么整个 IT 开发就会萎缩。
真正恶的是利用漏洞的攻击者。
如此想来,“没做/没做到/没注意到”的开发者和开发公司,与其说是加害者,不如说是受害者。
既然如此,笔者强烈认为,不应该指责受害者“你没做/没做到/没注意到所以你不好”,而应该向他们伸出温暖的手,以“这样会更安全,我们可以一起改善”为目标携手共进,同时在各自的专业领域互相切磋、互相促进。
这样,开发公司/开发者也能安心地应对漏洞,并且此后也能基于安心更积极地开展开发。
开发现场在安心、积极、主动的开发氛围下,最终会带来更多创新成果,让日本社会更加富足。
笔者个人的强烈愿望是,未来以下想法能够普及:
* 现场开发者
* “使用了含有漏洞的库”本身并非消极。
* 使情况消极的是利用漏洞的攻击者,以及将之评价为消极的文化本身。
* 每天认真工作、致力开发本身已经足够积极。
* 漏洞应对不是将消极归零的工作,而是将每日开发成果改进为更安全形态的积极工作。
* 管理开发者的人员(经理、领导)、经营层
* 不要将“没做/没做到/没注意到”评价为消极。这样的价值观应当逐渐淡出。
* 停止把“没做/没做到/没注意到”的成员当作犯人对待。他们(以及我们全体)都是利用漏洞的攻击者的受害者,从这个角度看立场相同。
* 停止试图通过“完美对策”来解决问题。
虽然这涉及到开发者、经理、领导乃至经营层“改变看待事物的方式”,但显然这非常困难。
究竟该如何改变呢?
笔者自身能力不足,无法给出正确答案,但笔者认为其中有一个启示。
那就是:或许应该放弃寻找正确答案本身。
任何软件开发都有某种目的。为了以安全、可靠的方式实现该目的而寻找“正确答案”,在如今软件世界如此复杂的情况下,恐怕已经不可能了。
笔者认为,那里存在的或许是一种以“变化”为前提的开发范式:众多参与者各自以自己方式进行试错、获取反馈、相互交换信息,并在此基础上自由分支和变化。
在系统开发中,一次创建的软件资产会持续使用数年甚至数十年。
但是,Web 开发等连接到互联网的软件,会暴露在数月到数年间不断变化的环境中。
5 年前开发时使用的库或框架,可能已经无法继续使用。
这样一来,将最初创建的库/框架视为“正确答案”并“固定”版本的开发范式,弊端会越来越多。
因为外部世界不断变化,所用的库/框架出现漏洞已是常态。
如果固定版本,将无法跟上这种变化,结果在漏洞应对上付出巨大的精神和物理成本。
因此,笔者强烈感到,转向以“变化和能够适应变化”为前提的开发范式,从长远来看会带来更好的成果。
以上内容较长,感谢阅读。
----
作者:坂本 昌彦(隶属于研究开发部,从事公司内部 Web 应用诊断工具开发等工作)
* 邮箱:[email protected]
* Twitter:https://twitter.com/msakamoto_sf
* Facebook:https://www.facebook.com/masahiko.sakamoto.75
* GitHub:https://github.com/msakamoto-sf
关于本文的意见或咨询请联系坂本。