Webhacking.kr 4xx

Challenge

The page collects nine KEY{<32-hex>} values and renders a flag image.

页面收集九个 KEY{<32-hex>} 值,并渲染一张 flag 图片。

1
http://webhacking.kr:10022/

页面标题是 Get 4XX for flag,表单字段为 key1 到 key9,每个字段的客户端 pattern 是 KEY\{([0-9]|[a-f]){32}\},页面引用 /img/flag.png。9 个 key 全部正确时表单返回 Your score : 9,并打印 FLAG is flag{<value>};提交顺序不影响计分(正序与逆序都是 9 分)。

Solution

key 由 HTTP 4xx 状态码发放:任何让 Apache 返回 4xx 的请求都会在响应体里给出该状态码对应的 key,格式是 4XX Error!<br> your key : <key-value>。同一状态码的 key 固定不变,所以凑齐 9 个不同的 4xx 状态码就能得到全部 9 个 key。

实测的状态码、触发请求与对应 key:

  • 400:发送包含 9 KB X-Big 请求头的请求,得到 KEY{10fac9b9f4112a1d9a650fec275bf164}。
  • 403:访问禁止列目录的 /img/,得到 KEY{cd79de80772c1873bcf63e41e3379c6f}。
  • 404:访问不存在的路径,得到 KEY{23c0b3a9ccc44b72f17a99eadb351d2f}。
  • 405:发送 TRACE /,得到 KEY{e5602408f2037c05bbbb0995fec6bc58}。
  • 408:发送请求行和 Host 后保持连接,不补完请求头,得到 KEY{e44fa3e1865a3839cbc0b658f1ae08cf}。
  • 412:请求静态文件 /img/flag.png 并设置 If-Match: "nope",得到 KEY{cd7609461c0dbe41a9137056fa4085e2}。
  • 414:发送约 30 KB 的超长请求路径,得到 KEY{d1617527ac2143863bc347c6123ed921}。
  • 416:设置 Range: bytes=999999999-,得到 KEY{622eced9aa42670c10ee74d29e58e5eb}。
  • 417:发送 POST / 并设置 Expect: foo,得到 KEY{ed2dd6cb38fe6a4a10e46d22d20047e6}。

Apache 负责生成这些 4xx 响应。405 需要使用不被允许的 method。412 依赖带 ETag 的静态文件;对 / 设置 If-Match 返回 200。417 要求 Expect 值不是 100-continue,后者会返回 100 Continue 并继续正常请求。408 需要原始 socket 在发送部分请求头后保持连接。Apache 2.4.29 对超长 header 返回 400,不产生 431。

收集脚本(原始 socket,因为 408 需要在发完部分请求头后等待):

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
#!/usr/bin/env python3
import re
import socket

HOST, PORT = "webhacking.kr", 10022
H = b"Host: webhacking.kr:10022\r\n"

REQUESTS = {
400: b"GET / HTTP/1.1\r\n" + H + b"X-Big: " + b"A" * 9000 + b"\r\n\r\n",
403: b"GET /img/ HTTP/1.1\r\n" + H + b"\r\n",
404: b"GET /definitely-not-here HTTP/1.1\r\n" + H + b"\r\n",
405: b"TRACE / HTTP/1.1\r\n" + H + b"\r\n",
408: b"GET / HTTP/1.1\r\n" + H, # 不补完请求头
412: b"GET /img/flag.png HTTP/1.1\r\n" + H + b'If-Match: "nope"\r\n\r\n',
414: b"GET /" + b"a" * 30000 + b" HTTP/1.1\r\n" + H + b"\r\n",
416: b"GET / HTTP/1.1\r\n" + H + b"Range: bytes=999999999-\r\n\r\n",
417: b"POST / HTTP/1.1\r\n" + H + b"Content-Length: 5\r\nExpect: foo\r\n\r\n",
}


def fetch(status: int, raw: bytes, wait: float = 4.0) -> str:
sock = socket.create_connection((HOST, PORT), timeout=wait + 8)
sock.sendall(raw)
sock.settimeout(wait)
data = b""
try:
while len(data) < 8000:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
except socket.timeout:
pass
sock.close()
body = data.decode("utf-8", "replace")
match = re.search(r"KEY\{[0-9a-f]{32}\}", body)
if not match:
raise RuntimeError(f"{status}: no key in response")
return match.group(0)


def main() -> None:
keys = {status: fetch(status, raw) for status, raw in REQUESTS.items()}
for status, key in sorted(keys.items()):
print(status, key)
query = "&".join(f"key{i}={key}" for i, key in enumerate(keys.values(), 1))
print(f"http://webhacking.kr:10022/?{query}")


if __name__ == "__main__":
main()

复现边界:入口、9 个状态码、9 个 key 值、Your score : 9 与 flag 回执均为现网实测;408 依赖服务端超时、412 依赖静态文件的 ETag,若实例的 Apache 配置变化需重新确认触发方式。

flag{iViZGYM7K5I}