Hed9eh0g

前进的路上总是孤独的

2019 Hackergame部分Writeup记录

本文共计有7929个字

签到题

设置一个按钮,内容为:点击此处获取flag,然而直接点击无效,f12改按钮属性:

《2019 Hackergame部分Writeup记录》

把disabled属性去除即可。

白与夜

下载图片后用stegsolve打开即可看到flag。

信息安全2077

抓包看到请求内容:

POST /flag.txt HTTP/1.1
Host: x.x.x.x:2077
Connection: keep-alive
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
Sec-Fetch-Mode: cors
Origin: http://x.x.x.x:2077
If-Unmodified-Since: Mon, 22 Oct 2019 03:59:59 GMT
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36
Accept: */*
Sec-Fetch-Site: same-origin
Referer: http://x.x.x.x:2077/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

发现其中的If-Unmodified-Since存在时间戳,再看看响应内容:

HTTP/1.0 412 PRECONDITION FAILED
Content-Type: text/plain; charset=utf-8
Last-Modified: Fri, 01 Oct 2077 00:00:00 GMT
Content-Length: 0
Server: Werkzeug/0.15.5 Python/3.7.4
Date: Mon, 22 Oct 2019 04:00:00 GMT

其中 Last-Modified 这一 Header 指示了另一个时间戳(北京时间 2077 年 10 月 1 日早 8 点),结合题目的提示可以推测:只需要把 If-Unmodified-Since 设置在 Last-Modified 之后就可以解决问题了。

解法一:把电脑时钟调到目标时间之后

改完电脑时钟后重新打开网页就可以了。当然这题如果选用 HTTPS 而非 HTTP,这样做可能就不太合适了。

解法二:构造一个设置为目标时间的请求头

直接在浏览器的控制台下构造一个 Fetch 请求:

(await fetch("/flag.txt", {headers: {"If-Unmodified-Since": "Fri, 01 Oct 2077 00:00:00 GMT"}, method: "POST"})).text()

宇宙终极问题

要求解决三个数学问题来分别得到flag,收获了一些解题网站

42

问满足x³+y³+z³=42的整数x、y、z,最近挺火的一个话题,直接网上找一波。

《2019 Hackergame部分Writeup记录》

Everything

求满足a³ + b³ + c³ + d³ = i² + j² + k² + l² = random_prime(2^256) * random_prime(2^256)的整数a、b、c、d或i、j、k、l,其中等号右边是随机生成的两个素数相乘。

在线网上解一波:https://www.alpertron.com.ar/FCUBES.HTM

Last Question

An integer greater than one can be written as a sum of two squares if and only if its prime decomposition contains no prime congruent to 3 modulo 4 raised to an odd power.

数学太难了,这道咕了

网页读取器

题目描述:

《2019 Hackergame部分Writeup记录》

这道题主要的点就是 URL 的 parser 和 requester 的不一致性导致的意料之外的 SSRF 问题。

关于 URI(统一资源标志符)

URL(统一资源定位符)是 URI 最常见的一种形式,而 URL 就是我们常说的「网址」。

URI 的语法大概长成下面这个样子:

scheme:[//authority]path[?query][#fragment]

其中用 [] 围住的是可选的,各部分分别为:

  • 协议 (scheme):访问资源使用的协议,比如说 http, https 之类的。
  • 来源 (authority):来源中包含了主机名,和可选的用户信息和端口号,比如 www.ustc.edu.cn, admin:admin@www.example.com:2333(使用用户名为 admin,密码为 admin,访问 www.example.com 的 2333 端口获取资源)。
  • 来源 (authority):来源中包含了主机名,和可选的用户信息和端口号,比如 www.ustc.edu.cn, admin:admin@www.example.com:2333(使用用户名为 admin,密码为 admin,访问 www.example.com 的 2333 端口获取资源)。
  • 路径 (path):比如说 /cgi-bin, /a/b/c/d/e/f/g 等,有等级 (hierarchical) 关系。
  • 查询 (query):没有等级 (non-hierarchical) 关系的数据。一般来说,查询中的参数会被网站的后端获取到,然后进行对应的处理,比如 ?q=keyword,?a=1&b=2。
  • 片段 (fragment):指向一个更低级别的资源,例如 #Examples,浏览器访问时会滚动到 id 为 Examples 的标签。如果写过单页面应用 (SPA) 的同学可能会知道,一些框架处理路由时使用 hash 模式,这里的 hash 就是片段开头的 #。

再看看小 T 自己写的脚本:

def check_hostname(url):
    for i in whitelist_scheme:
        if url.startswith(i):
            url = url[len(i):]  # strip scheme
            url = url[url.find("@") + 1:]  # strip userinfo
            if not url.find("/") == -1:
                url = url[:url.find("/")]  # strip parts after authority
            if not url.find(":") == -1:
                url = url[:url.find(":")]  # strip port
            if url not in whitelist_hostname:
                return (False, "hostname {} not in whitelist".format(url))
            return (True, "ok")
    return (False, "scheme not in whitelist, only {} allowed".format(whitelist_scheme))

看起来似乎没有什么问题:把协议丢掉,来源后面的东西丢掉,用户信息和端口号也丢掉,剩下来的不就是主机名吗?对大部分的 URL,这没有太大的问题。

但如果不按常规,想办法让 check_hostname 解释出主机名为 example.com 的同时,让 requests.get() 实际访问我们想要的地址,那就成功了。

预期解就使用到了 fragment(当然用 ? 也是可以的)。现有的 requester 都会直接忽略掉 # 后面的东西,毕竟这对请求网站内容是没有意义的。但这里的代码没有对 # 进行任何处理,而且会粗暴地忽略掉 @ 前面的所有内容。

也就是说,可以构造出类似于下面的payload即可得到flag:

http://web1/flag#@example.com

达拉崩吧大冒险

挺有趣的一道题,算是发现了前端的新玩法。

《2019 Hackergame部分Writeup记录》游戏大致界面如下,可以知道只要把boss打死即可拿到flag,但是打boss需要把attack的值大大提升,而提升attack值需要通过money来买童子鸡吃。很明显按照正常流程是难以打attack是8位数的boss的。

f12键对游戏全程进行调试,可以发现整个过程都是用v值进行传递数值的:

《2019 Hackergame部分Writeup记录》

按照网页端经验,前两个选项应该发送 0。尝试发送非 0 或 1 的值,服务器提示“不知道你在干些什么,但是看起来很像搞渗透,反正失去了同步”,由此可见固定选项的输入都有范围校验,于是把目标放在了利用“料理大市场”上。

大整数溢出

也就是说在料理大市场购买童子鸡的数量也是通过这个v来传递的,所以考虑对v下手,尝试发送v= -1,发现购买成功,攻击力为负值,接下来尝试是否可以造成攻击力数值向下溢出。尝试购买 -9223372036854775808 (64 位整型的最小值)只鸡,发现攻击力也被设置到该值,推断已达到下界。再购买 -1 只鸡,成功造成向下溢出。

《2019 Hackergame部分Writeup记录》

这会儿attack值爆表了,打boss就很轻松了。

Happy LUG

《2019 Hackergame部分Writeup记录》

题目提示:看起来是一个域名,但里面怎么会有 😂 这个 emoji?而且,虽然浏览器访问不了,但这个域名确实是存在着的。

涉及到了两个新知识:

Punycode

对域名中存在非ASCII码与非Unicode码的字符进行编码的规则被称为Punycode,

很明显emoji表情无法直接被解析,应该利用的Punycode,Google一波得知 😂对应为:xn--g28h

DNS解析资源

题目说这个域名无法通过浏览器访问,也就是这个域名没有指向任何 IP 地址。实际上一个域名除了可以指向一个或多个 IP 地址(A 或 AAAA 记录)之外还可以包含其他信息,例如指向另一个域名(CNAME 记录),指示接收邮件的服务器(MX 记录),或者提供任意字符串(TXT 记录)。最后一个就是这道题的第二个知识点,域名 xn--g28h.hack.ustclug.org. 有一个 TXT 记录,其中就是 flag。查询 DNS 记录有很多种方式,例如网上搜一个查询服务或者使用 nslookup 等命令行工具,总之查出这个 TXT 记录就对了。

《2019 Hackergame部分Writeup记录》

正则验证器

《2019 Hackergame部分Writeup记录》

正则表达式在各种软件开发中是很常用的工具,但是稍微不注意的话就会导致代码出现 DoS 的漏洞,即精心挑选的正则表达式(或者待匹配字符串)可以让匹配引擎长时间运行,占用大量 CPU 资源。

找到一个能卡住匹配引擎的表达式并不难,例如 Wikipedia 上的第一个示例就可以直接拿来用:

Regex: (a*)*$
String: aaaaaaaaaaaaaaaaaaaaaaab

上面这个正则由于失败时回溯的存在,每增加一个 a 就会使匹配时间翻一倍,出题时我测算了几次,i7-8850H (4.30 GHz) 可以在 1 秒内对 25 个 a 运行(并失败),所以实际字符串限制我就写成了 24 个字符。

三教奇妙夜

《2019 Hackergame部分Writeup记录》

题目所给的是一个时间十分长的白屏录像,根据提示可以知道flag是在这段录像其中的一帧且这一帧为黑底白字,理论上如果能够坚持把这段录像看完是肯定能够得到flag的,但是这样太累了。

FFmpeg解法

这是一个能够处理视频文件的工具,根据FFmpeg官方文档,找到一个名为blackframe的filter,既然只有flag那一帧是黑底的,那么就可以利用该指令过滤掉其他没有用的帧,输入命令ffmpeg -i output.mp4 -vf blackframe why_output.mp4,输出大致如下。

[Parsed_blackframe_0 @ 0x55d873a3f580] frame:1499 pblack:98 pts:767488 t:59.960000 type:I last_keyframe:1499
(下略)

从blackframe的输出中能发现黑帧的时间:59.960000。

于是再ffplay -ss 58这样看看这帧到底有什么flag就好啦。

OpenCV 解法

OpenCV 是一个计算机视觉库。我们这里使用 OpenCV 的 Python 接口。其实只需要知道这么几个 API 就可以了:

  • file = cv2.VideoCapture(filename): 打开一个视频文件。
  • file.read() -> ret, frame: 读取这个视频文件的一帧,返回状态与对应的帧。
  • cv2.absdiff(frame1, frame2): 比较两个帧,返回一个 Array。
  • array.sum(): 求 Array 的和。

此外为了显示图片,可以使用 matplotlib,from matplotlib import pyplot as plt 然后 plt.imshow(frame) 和 plt.show() 就行。完整的代码如下:

import cv2
from matplotlib import pyplot as plt

file = cv2.VideoCapture("output-1.mp4")
ret, preframe = file.read()

while True:
  ret, frame = file.read()
  if ret == 0:
    break
  diff = cv2.absdiff(preframe, frame).sum()
  if diff > 10000:
    print("diff: {}".format(diff))
    plt.imshow(frame)
    plt.show()
  preframe = frame

flag 图片将全部被提取到。

天书残篇

了解到一种加密文本的新知识

Whitespace

Whitespace 是一个十分神奇的语言。它只视空格 (space)、制表符 (tabs) 和换行 (new lines) 为语法的一部分,它的直译器忽略所有非空白字元。

使用这个 Whitespace 反汇编器即可获取原内容。

无限猴子定理

《2019 Hackergame部分Writeup记录》

find_flag.py的脚本如下:

from time import sleep
from signal import signal, SIGINT

def random_iter(value):
    while True:
        yield value
        value *= 0x7603
        value += 0x980B
        value %= 0xFFFB

def print_flags():
    a = open('might_be_flag.txt').read()
    i = iter(random_iter(0x0000))
    l = [next(i)]
    while True:
        l = [l[-1]] + [next(i) for _ in range(0x0011)]
        s = (l[0], ''.join([a[n] for n in l]))
        print('(0x%04X) => flag{%s}' % s)
        sleep(0.05)

if __name__ == '__main__':
    signal(SIGINT, lambda a, b: exit())
    print_flags()

通过对 find_flag.py 的解读可以注意到,每次“flag”的选取都是在一个无限长的序列中选取一个长为 18 的窗口截取的结果,而这个窗口会不断向前滑动 17 个单位:

=>EH650ARRBN6wFygy3EELPa1VXQdAYxlD1FxVd+7waO7T0B5pAs2n+TkwdI21/wzJ5YiIdw...
=>EH650ARRBN6wFygy3EELPa1VXQdAYxlD1FxVd+7waO7T0B5pAs2n+TkwdI21/wzJ5YiIdw...
=>EH650ARRBN6wFygy3EELPa1VXQdAYxlD1FxVd+7waO7T0B5pAs2n+TkwdI21/wzJ5YiIdw...
=>EH650ARRBN6wFygy3EELPa1VXQdAYxlD1FxVd+7waO7T0B5pAs2n+TkwdI21/wzJ5YiIdw...

我们很快就会产生对于这个序列是如何生成的的兴趣。经过研究,我们可以注意到这一序列是一个基于线性同余法的随机数生成器生成相应的数,并一一映射到某个字符串的下标上形成的。

以下是这个随机数生成器的递推公式:

next(x) = (x * 30211 + 38923) % 65531

既然是线性同余法,我们自然会意识到这个随机数生成器是存在周期的,我们可以截获这个周期以内所有可能的“flag”。我们放任这个程序生成(记得把 sleep 那句去掉),直到输出出现周期。

00001: (0x0000) => flag{EH650ARRBN6wFygy3E}
00002: (0x316B) => flag{EELPa1VXQdAYxlD1Fx}
00003: (0xB110) => flag{xVd+7waO7T0B5pAs2n}
00004: (0x7762) => flag{n+TkwdI21/wzJ5YiId}



65511: (0xB025) => flag{nHaQ/zePfOoxuXRQNy}
65512: (0x5C0B) => flag{yAaUnoXFpNWPp9j9/E}
65513: (0x0000) => flag{EH650ARRBN6wFygy3E}
65514: (0x316B) => flag{EELPa1VXQdAYxlD1Fx}

我们会注意到周期 65512 小于随机数生成器理论上的最大周期 65531。此时我们应该能够立刻意识到:这不是一个好的随机数生成器(一个好的随机数生成器应能遍历最大周期内所有的值)。我们自然想搞清楚排在周期之外的 19 个随机数身上发生了什么。

获取剩余 19 个随机数对应的内容

运行脚本,记得和源文件放在同一目录下运行:

import find_flag

l = 65531
s = set(range(l))
i = iter(find_flag.random_iter(0))
a = open('might_be_flag.txt').read()

for n in range(l):
    s.discard(next(i))

for n in s:
    i = iter(find_flag.random_iter(n))
    l = [next(i)] + [next(i) for _ in range(17)]
    print('(%s) => flag{%s}' % ('0x%04X' % l[0], ''.join([a[n] for n in l])))

以下是输出:

(0x0C2A) => flag{+8ad+LC+Generat0r+}
(0x19A3) => flag{at0r+A+8ad+LC+Gene}
(0x271C) => flag{+Generat0r+A+8ad+L}
(0x3495) => flag{8ad+LC+Generat0r+A}
(0x420E) => flag{t0r+A+8ad+LC+Gener}
(0x4F87) => flag{Generat0r+A+8ad+LC}
(0x5D00) => flag{ad+LC+Generat0r+A+}
(0x6A79) => flag{0r+A+8ad+LC+Genera}
(0x77F2) => flag{enerat0r+A+8ad+LC+}
(0x856B) => flag{d+LC+Generat0r+A+8}
(0x92E4) => flag{r+A+8ad+LC+Generat}
(0xA05D) => flag{nerat0r+A+8ad+LC+G}
(0xADD6) => flag{+LC+Generat0r+A+8a}
(0xBB4F) => flag{+A+8ad+LC+Generat0}
(0xC8C8) => flag{erat0r+A+8ad+LC+Ge}
(0xD641) => flag{LC+Generat0r+A+8ad}
(0xE3BA) => flag{A+8ad+LC+Generat0r}
(0xF133) => flag{rat0r+A+8ad+LC+Gen}
(0xFEAC) => flag{C+Generat0r+A+8ad+}

事实上这十九个输出看起来都只是一个序列的重排。通过规则“flag 代表的第一个英文单词既不会是名词,也不会是形容词”,我们很容易排出 0xE3BA 代表的便是正确 flag。

参考文章

2019 hackgame官方writeup

点赞

发表评论

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