[CTF] 布莱切利庄园
这道题考验web和密码学,进入题目

我们分别查看邮箱,其中,我们看处刑邮件知道有人试图查看指挥官的邮件被处刑,所以很明显,我们需要登录到指挥官的账户

特工消息显示:“今日密电格式:三转子位置固定,转子初始位随机化,插线板仅两对随机接线,其余参数原样。”
天气预报显示:
“WEATHERFORECASTWINDNORTHVISIBILITYGOOD”
也就是
“天气预报:风向北,能见度良好”
我们检查知道了有个叫做id的Cookie,内容为:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMDA3Iiwicm9sZSI6ImFnZW50In0.98272631e0a74f29bc8b9b0fe12dfc54
用两个点分割成了三个信息,前两个是base64加密,解码为:
{“alg”:“HS256”,“typ”:“JWT”}.{“name”:“007”,“role”:“agent”}
这是一个JWT None漏洞我们只需要把alg改成none,把name改成COMMANDER即可,最后分别编码,然后和第三个签名拼接:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=.eyJuYW1lIjoiQ09NTUFOREVSIiwicm9sZSI6ImFnZW50In0=.98272631e0a74f29bc8b9b0fe12dfc54
保存Cookie,我们成功进入到指挥官邮箱

我们发现,指挥官邮箱的两个邮件都是加密的,其中,一份是加密的flag,另一份却是天气预报,之前我们在普通邮箱里面也有一份天气预报,只不过这是加密的,所以我们需要通过明文和这个密文去解密,而结合特工的提示,这其实是二战时期著名的恩尼格玛机
如果恩尼格玛机不了解,大家可以在B站或者一些平台了解一下
提示有三个转子,转子位置固定,初始位置随机,两对插线板随机,其他的都没有调,所以我们需要效仿图灵的炸弹机去爆破了,虽然我们只有一条信息,但是我们有更先进的计算机,三个转子随机初始位置加上两对随机插线板,一共有八亿种组合方式,写脚本尝试穷举


import sys
import time
import itertools
import argparse
from multiprocessing import Process, Value, Queue, Event, cpu_count
def to_int(c): return ord(c) - ord('A')
def to_char(i): return chr(i + ord('A'))
def mod26(a): return (a % 26 + 26) % 26
ROTORS_MAP = {
0: "EKMFLGDQVZNTOWYHXUSPAIBRCJ",
1: "AJDKSIRUXBLHWTMCQGZNPYFVOE",
2: "BDFHJLCPRTXVZNYEIWGAKMUSQO"
}
NOTCHES_MAP = {0: 'Q', 1: 'E', 2: 'V'}
REFLECTOR_B = "YRUHQSLDPXNGOKMIEBFZCWVJAT"
FIXED_ROTOR_INDICES = [0, 1, 2]
FIXED_RINGS = [0, 0, 0]
class EnigmaMachine:
def __init__(self, rotor_indices, start_positions, ring_settings, plugboard_pairs=None):
self.rotor_indices = rotor_indices
self.initial_positions = list(start_positions)
self.rings = list(ring_settings)
self.plugboard = list(range(26))
if plugboard_pairs:
pairs_list = plugboard_pairs.upper().strip().split(" ")
for pair in pairs_list:
if len(pair) == 2:
a = to_int(pair[0]);
b = to_int(pair[1])
self.plugboard[a] = b
self.plugboard[b] = a
def map_forward(self, rotor_map_idx, charIdx, current_positions):
rotorIdx = self.rotor_indices[rotor_map_idx]
P = current_positions[rotor_map_idx];
R = self.rings[rotor_map_idx]
inputIndex = mod26(charIdx + P - R)
mappedIndex = to_int(ROTORS_MAP[rotorIdx][inputIndex])
return mod26(mappedIndex - P + R)
def map_backward(self, rotor_map_idx, charIdx, current_positions):
rotorIdx = self.rotor_indices[rotor_map_idx]
P = current_positions[rotor_map_idx];
R = self.rings[rotor_map_idx]
inputIndex = mod26(charIdx + P - R)
charToFind = to_char(inputIndex)
mappedIndex = ROTORS_MAP[rotorIdx].index(charToFind)
return mod26(mappedIndex - P + R)
def step(self, current_positions):
# 这里的逻辑:先判定Notch,再移动
new_positions = list(current_positions)
middle_rotor_idx = self.rotor_indices[1]
right_rotor_idx = self.rotor_indices[2]
isMiddleOnNotch = new_positions[1] == to_int(NOTCHES_MAP[middle_rotor_idx])
isRightOnNotch = new_positions[2] == to_int(NOTCHES_MAP[right_rotor_idx])
if isMiddleOnNotch:
new_positions[0] = mod26(new_positions[0] + 1)
new_positions[1] = mod26(new_positions[1] + 1)
elif isRightOnNotch:
new_positions[1] = mod26(new_positions[1] + 1)
new_positions[2] = mod26(new_positions[2] + 1)
return new_positions
def encrypt(self, input_text, use_plugboard=True):
result = []
input_clean = "".join(c for c in input_text.upper() if 'A' <= c <= 'Z')
current_positions = list(self.initial_positions)
for c in input_clean:
current_positions = self.step(current_positions)
signal = to_int(c)
if use_plugboard: signal = self.plugboard[signal]
signal = self.map_forward(2, signal, current_positions)
signal = self.map_forward(1, signal, current_positions)
signal = self.map_forward(0, signal, current_positions)
signal = to_int(REFLECTOR_B[signal])
signal = self.map_backward(0, signal, current_positions)
signal = self.map_backward(1, signal, current_positions)
signal = self.map_backward(2, signal, current_positions)
if use_plugboard: signal = self.plugboard[signal]
result.append(to_char(signal))
return "".join(result)
def generate_all_plugboard_pairs_2():
idx = list(range(26))
for connected in itertools.combinations(idx, 4):
i1, i2, i3, i4 = connected
pairings = [((i1, i2), (i3, i4)), ((i1, i3), (i2, i4)), ((i1, i4), (i2, i3))]
for pairing in pairings:
yield " ".join([f"{to_char(p[0])}{to_char(p[1])}" for p in pairing])
def generate_all_plugboard_pairs_1():
idx = list(range(26))
for connected in itertools.combinations(idx, 2):
yield f"{to_char(connected[0])}{to_char(connected[1])}"
def idx_to_positions(idx):
a = idx // (26 * 26)
rem = idx % (26 * 26)
b = rem // 26
c = rem % 26
return (a, b, c)
def worker_process(start_idx, end_idx, target_ciphertext, plaintext, progress_value: Value, done_event: Event,
out_queue: Queue, prefix_len=6):
plug1 = list(generate_all_plugboard_pairs_1())
plug2 = list(generate_all_plugboard_pairs_2())
target_prefix = target_ciphertext[:prefix_len]
plain_prefix = plaintext[:prefix_len]
try:
for idx in range(start_idx, end_idx):
if done_event.is_set():
break
pos = idx_to_positions(idx)
rotor_positions = [pos[0], pos[1], pos[2]]
# --- 0 对 ---
m = EnigmaMachine(FIXED_ROTOR_INDICES, rotor_positions, FIXED_RINGS, "")
if m.encrypt(plain_prefix) == target_prefix:
if m.encrypt(plaintext) == target_ciphertext:
next_pos = m.step(rotor_positions)
out_queue.put({
'raw_pos': rotor_positions,
'next_pos': next_pos,
'plugs': "无",
})
done_event.set()
break
# --- 1 对 ---
found = False
for pb in plug1:
m = EnigmaMachine(FIXED_ROTOR_INDICES, rotor_positions, FIXED_RINGS, pb)
if m.encrypt(plain_prefix) == target_prefix:
if m.encrypt(plaintext) == target_ciphertext:
next_pos = m.step(rotor_positions)
out_queue.put({
'raw_pos': rotor_positions,
'next_pos': next_pos,
'plugs': pb,
})
done_event.set()
found = True
break
if found: break
# --- 2 对 ---
for pb in plug2:
m = EnigmaMachine(FIXED_ROTOR_INDICES, rotor_positions, FIXED_RINGS, pb)
if m.encrypt(plain_prefix) != target_prefix:
continue
if m.encrypt(plaintext) == target_ciphertext:
next_pos = m.step(rotor_positions)
out_queue.put({
'raw_pos': rotor_positions,
'next_pos': next_pos,
'plugs': pb,
})
done_event.set()
found = True
break
if found: break
with progress_value.get_lock():
progress_value.value += 1
except Exception as e:
out_queue.put({'error': repr(e)})
done_event.set()
def proc_runner(chunks, target, plain, prog, done_evt, out_q, prefix_len):
for s, e in chunks:
if done_evt.is_set(): break
worker_process(s, e, target, plain, prog, done_evt, out_q, prefix_len)
def run_mp(target_ciphertext, plaintext, workers=None, chunk=128, prefix_len=6):
if workers is None:
workers = max(1, (cpu_count() or 1) - 1)
total_positions = 26 ** 3
print("=" * 60)
print(f"开始暴力破解 (找到即停)...")
print(f"目标密文: {target_ciphertext}")
print(f"对应明文: {plaintext}")
print(f"Workers: {workers}, Prefix Check: {prefix_len}")
print("=" * 60)
progress = Value('I', 0)
done_event = Event()
out_q = Queue()
indices = [(i, min(i + chunk, total_positions)) for i in range(0, total_positions, chunk)]
chunks_per_proc = [[] for _ in range(workers)]
for i, chunk_range in enumerate(indices):
chunks_per_proc[i % workers].append(chunk_range)
procs = []
for i in range(workers):
p = Process(target=proc_runner,
args=(chunks_per_proc[i], target_ciphertext, plaintext, progress, done_event, out_q, prefix_len))
p.start()
procs.append(p)
start_time = time.time()
last_print = 0
try:
while any(p.is_alive() for p in procs):
# 检查结果
while not out_q.empty():
item = out_q.get()
if 'error' in item:
print(f"\n[ERROR] {item['error']}")
done_event.set()
else:
raw = item['raw_pos']
nxt = item['next_pos']
# 显示:把 0-based 转成 1-based 显示
start_nums = f"{raw[0] + 1},{raw[1] + 1},{raw[2] + 1}"
start_chars = f"{to_char(raw[0])}{to_char(raw[1])}{to_char(raw[2])}"
print("\n" + "#" * 60)
print(f"【!!! 找到正确解 !!!】")
print(f"初始位置 (穷举): {start_nums:<10} [{start_chars}]")
print(f"插线板 (Plugs): {item['plugs']}")
print("#" * 60 + "\n")
done_event.set()
# 详细进度条
now = time.time()
if now - last_print >= 0.5:
with progress.get_lock():
v = progress.value
pct = v / total_positions * 100
elapsed = now - start_time
speed = v / elapsed if elapsed > 0 else 0.0
sys.stdout.write(
f"\r进度: {v}/{total_positions} ({pct:.2f}%) | 耗时: {elapsed:.1f}s | 速度: {speed:.1f} pos/s")
sys.stdout.flush()
last_print = now
if done_event.is_set():
time.sleep(0.1)
break
else:
time.sleep(0.05)
finally:
for p in procs:
if p.is_alive():
p.terminate()
for p in procs:
p.join()
elapsed = time.time() - start_time
with progress.get_lock():
final_v = progress.value
print(f"\n\n完成: {final_v}/{total_positions} | 总耗时: {elapsed:.1f}s")
print("-" * 60)
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('ciphertext')
p.add_argument('plaintext')
p.add_argument('--workers', type=int, default=None)
p.add_argument('--chunk', type=int, default=128)
p.add_argument('--prefix', type=int, default=6)
args = p.parse_args()
target = args.ciphertext.upper().replace(" ", "")
plain = args.plaintext.upper().replace(" ", "")
run_mp(target, plain, workers=args.workers, chunk=args.chunk, prefix_len=args.prefix)
使用方式:python boom.py 密文 明文

我这里没有做很好的优化,因为之前写的剪枝会把正确答案剪没,所以肯定是有优化空间的,现在我们得到了初始位置在XIM,插线板GO UV
找一个在线的恩尼格玛机加解密网站:
The Enigma machine: Encrypt and decrypt online - cryptii
我们调好位置,然后把flag的密文扔进去

我们得到flag:computerisagreatinvention
(计算机是一个伟大的发明)
包裹平台所需的格式后提交
注意,flag是固定的,但是加密所调整的是随机的