

CTF | 2021 TCTF/0CTF Quals WriteUp
source link: https://miaotony.xyz/2021/07/15/CTF_2021TCTF-0CTF/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这是一篇放在草稿箱里已经长草的文章,清理的时候才想起来这篇由于没环境复现,后面一懒就忘记发了……
噢,喵喵也不记得有没有写完了,就将就发一下吧(摊手

TCTF/0CTF 2021 Quals
比赛时间: 2021-07-03 10:00 ~ 2021-07-05 10:00
前不久有个 TCTF/0CTF 2021 线上预选赛,寻思着有点意思就来打了,题目好难啊。
只不过队里的小师傅们都在准备期末考试,么得办法只有几个老人随便玩了。
这篇就随便水水吧。
welcome
Welcome to 0CTF/TCTF 2021! Join Discord for flag
flag{welcome_to_0ctf/tctf_2021_have_fun}
GutHib
What happens on GutHib, stays on GutHib.
根据 commit 历史可以看到有敏感信息被移除了。

发现用了 BFG Repo-Cleaner 来清理 commit 历史里留存的敏感信息。

其官方的文档:https://rtyley.github.io/bfg-repo-cleaner/
使用说明(Usage)

对比可以发现没有执行 gc 垃圾清理操作,理论上本地的 object 里还存有原始的数据。
然而直接 git clone 下来的 object pack 中并没有冗余信息,可能传输过程中进行了精简吧……
不过利用 GitHub API 记录的 events,由于 API 是读的是 GitHub 的 event 数据库,而不是 git repository,本地最后 push 上来的并没有影响服务器上的。而 GitHub 上的也没进行 gc 清理,这就能够恢复了。
https://api.github.com/repos/awesome-ctf/TCTF2021-Guthib/events?page=4
首先从 event 里拿到之前错误的 commit hash,然后用 commit hash 再去找对应的提交和文件。

flag{ZJaNicLjnDytwqosX8ebwiMdLGcMBL}
挺有意思的 233.
See also:
使用 Roberto Tyley 的 BFG Repo-Cleaner 移除 git 库中的二进制文件
在 git 中,所有文件和文件夹的数据都只存储一次,并且每个都会有一个独一无二的 id——git-id。如果大量的 commit 都没有修改某个文件,那么这个文件只会被保存一次。如果这个文件有两个版本,并且需要在两个版本间来回切换,这个文件只会存储两次,每个版本各一次。
BFG 只对 git 库中的每个对象清理一次,然后记住它的 git-id,以后遇到它,不把它计算在内就行了。
BFG Repo-Cleaner - 从 Git 历史中真正删除文件
其实刚开始考虑 BFG 默认认为当前最新的 commit 已经删除了敏感信息,于是其进行的修改只涉及历史 commit,最新 commit 不做更改。后来发现其实当前最新的 commit 也已经删掉敏感信息了……
(然后把 git 底层咋存储的给摸了一遍 233
Survey

flag{im_curious_have_u_viewed_source_before_filling_out_the_survey}
噢是这个意思啊。

Singer
Sing a song~
special format: flag{[a-z]*}
https://attachment.ctf.0ops.sjtu.cn/singer_0cc09b073ebbbffcc5720d081f42feea
给的文件是这个。
A6-D#6
G#6
G6
G6
G#6
A6-D#6
C6-G5
F#5
F#5
C6-G5
A6-F#6,D#6
A6,F#6,D#6
A6,F#6-D#6
A6,D#6
A6-D#6
A6,D#6
F#7-C7
E7-D7
F7,C#7
F#7,C7
E6,A#5
E6-A#5
E6,A#5
A6-D#6
A6-G6
F#6-E6
A6-D#6
C#7-G6
C#7,G6
C#7,A#6,G6
C#7,A#6-G6
寻思着应该是音符。

想了半天没搞懂啥意思。。
后来考虑是不是音游的谱子啊,查了查发现也不是吧。
但后来又看到某音游——

会不会是这个按键连成的啥字符啊。
然而比赛的时候就没画出来,草!
赛后发现有个 Online Sequencer,是一个在线制作音乐的工具,支持模拟多种乐器,可以导出 ogg/mp3/midi,还能分享 sequence。
然后按照文件里的分块,每个分块是一个字母,-
就是两个键之间连起来,,
就是多个键同时按下。
于是就可以得到这样的图形。

于是 flag 就是 flag{musiking}
(脑洞真的大……
TCTF Share
It is a misc quest absolutely. https://bt.hzh.moe/
Flag format: TCTF{.*}

是个 hugo 建的博客,两篇博文。
上面那篇 Wonder Egg Priority OP

两个磁力链接分别是
https://bt.hzh.moe/files/Wonder-Egg-Priority-Safe.torrent
magnet:?xt=urn:btih:CIY7BQVJYC7GXF3AF4OA6SLZTZZAQDBS&dn=%E3%83%AF%E3%83%B3%E3%83%80%E3%83%BC%E3%82%A8%E3%83%83%E3%82%B0%E3%83%BB%E3%83%97%E3%83%A9%E3%82%A4%E3%82%AA%E3%83%AA%E3%83%86%E3%82%A3%20(Wonder%20Egg%20Priority)%20OP.7z
看了下有个 txt 文件,链接就是个动漫。https://www.youtube.com/watch?v=5yN9w_yzH2w
下面那篇 Tencent CTF

https://bt.hzh.moe/files/TCTF.torrent
magnet:?xt=urn:btih:AQEQB7YBOGIMOQARSRXSHGULZUI25EY3&dn=Secret%20of%20TCTF%202021.7z
很明显就是这个了,然而上面那个下载下来是全空的,下面那个下载不下来。
参考磁力链接的结构,btih 是指 BitTorrent Info Hash。


参考大师傅的 wp,AQEQB7YBOGIMOQARSRXSHGULZUI25EY3
即被 Base32 编码过的 BTIH 散列结果,把它转换为 hex 编码的。

%04%09%00%ff%01%71%90%c7%40%11%94%6f%23%9a%8b%cd%11%ae%93%1b
再参考第一个 torrent 文件,可以得到 httpseeds

HTTP-BASED SEEDING SPECIFICATION
PROTOCOL:
The client calls the URL given, in the following format:
<url>?info_hash=[hash]&piece=[piece]{&ranges=[start]-[end]{,[start]-[end]}...}
Examples:
http://www.whatever.com/seed.php?info_hash=%9C%D9i%8A%F5Uu%1A%91%86%AE%06lW%EA%21W%235%E0&piece=3 http://www.whatever.com/seed.php?info_hash=%9C%D9i%8A%F5Uu%1A%91%86%AE%06lW%EA%21W%235%E0&piece=8&ranges=49152-131071,180224-262143
于是可以构造出地址为
直接浏览器里打开不行,需要改 UA 为 BT 客户端……
写个脚本分 piece 下载一下。超过范围会返回 400,判断一下退出。
import requests
fp = open('1.7z', 'wb')
header = {'user-agent': 'uTorrent'}
i = 0
while True:
print(i)
url = f'https://bt.hzh.moe/seeding?info_hash=%04%09%00%FF%01%71%90%C7%40%11%94%6F%23%9A%8B%CD%11%AE%93%1B&piece={i}'
r = requests.get(url, headers=header)
fp.write(r.content)
if r.status_code == 400:
break
i += 1
print('Download OK!')
最后 piece 是 0-46,下载下来解压得到一个 TCTF.vhdx
。
挂载得到一个 hint.bat

最后提示了文件名进行了修改。
于是可以去查看 NTFS 硬盘历史记录。想起上次强网杯那道 EzTime 题目了……
这里试了试用 AXIOM 分析硬盘 $LogFile
,发现巨大多修改文件名的记录。
找了半天确实能找到 flag 了……

TCTF{B7UmV4x8JRHNKBCWxyevXt97tnQJtHjrZ}
或者也可以导出硬盘日志,然后写个脚本整合一下。
首先用 DiskGenius 把显示元文件(Metafile)勾上,然后才能看到 $LogFile 这些信息,然后导出来。

用 LogFileParser 来解析硬盘日志,可以得到 csv 数据文件。


根据这个 LogFile_lfUsnJrnl.csv
可以得知文件的修改历史。

写个脚本,找一下更改为新名字且文件名长度为1的文件名,另外再根据 MFTReference
分别统计并输出。
with open('LogFile_lfUsnJrnl.csv', 'r', encoding='utf-8') as f:
s = f.read()
l = s.split('\n')
# 404.html|304|2021-07-02 20:12:48.5212936|FILE_CREATE+DATA_EXTEND+CLOSE|40|1|39|1|archive|2|0|0x00000000|0
info = {}
data = ''
for x in l:
d = x.split('|')
# if d[3] == 'RENAME_NEW_NAME+CLOSE':
if 'RENAME_NEW_NAME+CLOSE' in d:
f = d[0]
print(f)
filename = f.split('.')[0]
if len(filename) == 1:
# extension = f.split('.')[1]
MFTReference = d[4]
if MFTReference not in info:
info[MFTReference] = ''
info[MFTReference] += filename
print(info)
for i in sorted(info, key=lambda x: x[0]):
print(i, info[i])
# 43 wpa4b1AQVPOibxTBwz7Qn2xXcFy{Jbnca7pDyIMAR7i8Sseg5sRpPIBIW68opBiNB
# 49 t}IuwI9Chdl0O1P8hYzwku8MTfKub4li49wzetpEbWHIHfhl3WGc7gh0c0WzpMClH
# 46 iPt86USAMpWJz5fDI47Vzu6BdlqxCxX0n6viKvNoZ7AzEAEHOpny0ZfV6tuT4QZm
# 47 j}vUG4Hr}DVGRFKZcY6op5BGLT}LqLrudLZqHOrh9Hcx8LVpOIeobvkrEyaTPzpV
# 44 phYaPUyusxk5}0PJQ19HsH3jHIonhFQVBxyzvtTUL7}W61xv7t3tUBhWOKiVio}tM
# 40 a3bGkqTNUhMOUAp2DMsAlA3}5jTxtPamvcuRJugpnD{pr}Wp4FDdr2HqUqYVyQ26
# 50 5TW57LSdRtbqa404yFJd2ujXJ{lPRPUwtfGtywtJBP5eGUF{mvkZE7}wSg5deoHL}i
# 59 NCJUO0B}Olw1b6CgZTFsncv2}pYfTJ323WJHeE03g2IZaCcyhgLn3WS2BGKr7zglR6
# 54 m0ngAg1g16KUDp8Zl0K7nbwLrLhMoasREHClz2aWJY6edmGlHrOdqXFgeKUQBEko
# 53 Q2DnB4b{K{2FxleNfZ5ZDaA6tl{GIUbMFN3kLOs1p5kJgu9TzYBxGDTmYa}iet2}P
# 51 Dyxy3OTAJdsovteo3{Z1MirxF9BmJ1ZAfov7tGKi1CpiDmNF7o3IJEvhOmV9RjLDP
# 57 WOW0Is1That2Flag3TCTF{B7UmV4x8JRHNKBCWxyevXt97tnQJtHjrZ}4God5Job
# 64 Nh1W7gq{Ea7XLGNCnFpKlo3j0sXxUklmrqFWtWt04Dc80XDg90JqFgZzLiWeoJbzx
# 65 ngnv2GZ6UD9aJnfnuFBIx1Mo084lND4}Oom7x{rbT7mTHl6OtNM9CrOjZGD4Lce
# 62 L61ulkKDvC7Kh}FYXwslD0jp{auQstMN5S2hIXmSMkdZtYTVmltslyLPCTOdF1ysG
# 61 yrOyO9XJK13Pbnr2Msym{NzdA5mbyhKL5cgbO2moJC90f92B7Q0lGIucN2XAXj}MQ
最后也可以得到 flag。
WOW0Is1That2Flag3TCTF{B7UmV4x8JRHNKBCWxyevXt97tnQJtHjrZ}4God5Job
Crypto
checkin
Welcome to 0CTF/TCTF 2021!
111.186.59.11:16256
Nothing fancy, just do the math.
虽然是大数求解,但直接拿 gmpy2 来算就完事了。
from gmpy2 import mpz
from pwn import *
import re
context.log_level = 'debug'
context.timeout = 10
sh = remote('111.186.59.11', 16256)
sh.recvuntil("Show me your computation:\n")
x = sh.recvuntil(" = ?").decode()
print('====> x:', x)
d = re.findall(r'(\d+)\^\((\d+)\^(\d+)\) mod (\d+) = \?', x)[0]
print('=====> info:', d)
a, b, c, p = [int(a) for a in d]
result = pow(mpz(a), pow(mpz(b), mpz(c)), mpz(p))
print('=========> result:', result)
sh.sendlineafter("Your answer: ", str(result))
sh.recvall()
# Here is your flag: flag{h0w_m4ny_squar3s_can_u_d0_in_10_sec0nds?}
zer0lfsr-
Much easier than zer0lfsr!
nc 111.186.59.28 31337
NOTICE: the download link is updated. use the link below
#!/usr/bin/env python3
import random
import signal
import socketserver
import string
from hashlib import sha256
from os import urandom
from secret import flag
def _prod(L):
p = 1
for x in L:
p *= x
return p
def _sum(L):
s = 0
for x in L:
s ^= x
return s
def n2l(x, l):
return list(map(int, '{{0:0{}b}}'.format(l).format(x)))
class Generator1:
def __init__(self, key: list):
assert len(key) == 64
self.NFSR = key[: 48]
self.LFSR = key[48: ]
self.TAP = [0, 1, 12, 15]
self.TAP2 = [[2], [5], [9], [15], [22], [26], [39], [26, 30], [5, 9], [15, 22, 26], [15, 22, 39], [9, 22, 26, 39]]
self.h_IN = [2, 4, 7, 15, 27]
self.h_OUT = [[1], [3], [0, 3], [0, 1, 2], [0, 2, 3], [0, 2, 4], [0, 1, 2, 4]]
def g(self):
x = self.NFSR
return _sum(_prod(x[i] for i in j) for j in self.TAP2)
def h(self):
x = [self.LFSR[i] for i in self.h_IN[:-1]] + [self.NFSR[self.h_IN[-1]]]
return _sum(_prod(x[i] for i in j) for j in self.h_OUT)
def f(self):
return _sum([self.NFSR[0], self.h()])
def clock(self):
o = self.f()
self.NFSR = self.NFSR[1: ] + [self.LFSR[0] ^ self.g()]
self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
return o
class Generator2:
def __init__(self, key):
assert len(key) == 64
self.NFSR = key[: 16]
self.LFSR = key[16: ]
self.TAP = [0, 35]
self.f_IN = [0, 10, 20, 30, 40, 47]
self.f_OUT = [[0, 1, 2, 3], [0, 1, 2, 4, 5], [0, 1, 2, 5], [0, 1, 2], [0, 1, 3, 4, 5], [0, 1, 3, 5], [0, 1, 3], [0, 1, 4], [0, 1, 5], [0, 2, 3, 4, 5], [
0, 2, 3], [0, 3, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4], [1, 2, 3, 5], [1, 2], [1, 3, 5], [1, 3], [1, 4], [1], [2, 4, 5], [2, 4], [2], [3, 4], [4, 5], [4], [5]]
self.TAP2 = [[0, 3, 7], [1, 11, 13, 15], [2, 9]]
self.h_IN = [0, 2, 4, 6, 8, 13, 14]
self.h_OUT = [[0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6], [1, 3, 4]]
def f(self):
x = [self.LFSR[i] for i in self.f_IN]
return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
def h(self):
x = [self.NFSR[i] for i in self.h_IN]
return _sum(_prod(x[i] for i in j) for j in self.h_OUT)
def g(self):
x = self.NFSR
return _sum(_prod(x[i] for i in j) for j in self.TAP2)
def clock(self):
self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
self.NFSR = self.NFSR[1: ] + [self.LFSR[1] ^ self.g()]
return self.f() ^ self.h()
class Generator3:
def __init__(self, key: list):
assert len(key) == 64
self.LFSR = key
self.TAP = [0, 55]
self.f_IN = [0, 8, 16, 24, 32, 40, 63]
self.f_OUT = [[1], [6], [0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6]]
def f(self):
x = [self.LFSR[i] for i in self.f_IN]
return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
def clock(self):
self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
return self.f()
class zer0lfsr:
def __init__(self, msk: int, t: int):
if t == 1:
self.g = Generator1(n2l(msk, 64))
elif t == 2:
self.g = Generator2(n2l(msk, 64))
else:
self.g = Generator3(n2l(msk, 64))
self.t = t
def next(self):
for i in range(self.t):
o = self.g.clock()
return o
class Task(socketserver.BaseRequestHandler):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def proof_of_work(self):
random.seed(urandom(8))
proof = ''.join([random.choice(string.ascii_letters + string.digits + '!#$%&*-?') for _ in range(20)])
digest = sha256(proof.encode()).hexdigest()
self.dosend('sha256(XXXX + {}) == {}'.format(proof[4: ], digest))
self.dosend('Give me XXXX:')
x = self.request.recv(10)
x = (x.strip()).decode('utf-8')
if len(x) != 4 or sha256((x + proof[4: ]).encode()).hexdigest() != digest:
return False
return True
def dosend(self, msg):
try:
self.request.sendall(msg.encode('latin-1') + b'\n')
except:
pass
def timeout_handler(self, signum, frame):
raise TimeoutError
def handle(self):
try:
signal.signal(signal.SIGALRM, self.timeout_handler)
signal.alarm(30)
if not self.proof_of_work():
self.dosend('You must pass the PoW!')
return
signal.alarm(50)
available = [1, 2, 3]
for _ in range(2):
self.dosend('which one: ')
idx = int(self.request.recv(10).strip())
assert idx in available
available.remove(idx)
msk = random.getrandbits(64)
lfsr = zer0lfsr(msk, idx)
for i in range(5):
keystream = ''
for j in range(1000):
b = 0
for k in range(8):
b = (b << 1) + lfsr.next()
keystream += chr(b)
self.dosend('start:::' + keystream + ':::end')
hint = sha256(str(msk).encode()).hexdigest()
self.dosend('hint: ' + hint)
self.dosend('k: ')
guess = int(self.request.recv(100).strip())
if guess != msk:
self.dosend('Wrong ;(')
self.request.close()
else:
self.dosend('Good :)')
self.dosend(flag)
except TimeoutError:
self.dosend('Timeout!')
self.request.close()
except:
self.dosend('Wtf?')
self.request.close()
class ThreadedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass
if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 31337
server = ThreadedServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()
是辣个 线性反馈移位寄存器(LFSR),但又不是咱之前见过的那种。
这个貌似可以用 Fast Correlation Attacks(project)。
2019 年的 TCTF 就出过这样一题,0CTF/TCTF 2019 Quals - zer0lfsr.
他那题是静态的文件数据,没有和服务器的交互,写起来比较方便,求解也可以直接拿 Z3 solver 来实现了。
这题的话 Generator1 和 Generator3 比较简单,然而直接拿 Z3 发现整不通,会提示 unsupported format string passed to BitVecRef.__format__
,也就是说 BitVec 不支持 format 操作。

比赛的时候看了一晚上这题不知道咋整,赛后看了人家大佬的 wp 才发现只需要重写一下 n2l
函数就完事了。
把这个改成位操作,用位与、移位操作来对每个 BitVec 进行操作,最后形成一个 BitVec 的 list。
另外在接收数据的处理上,只需要给前 200 位这样的结果建立方程组就能求解了,没必要全部加上。
(8000个方程太多了也没必要)
Exp 如下。
"""MiaoTony"""
from pwn import *
from itertools import product
from string import ascii_letters, digits
from hashlib import sha256
from z3 import *
def _prod(L):
p = 1
for x in L:
p *= x
return p
def _sum(L):
s = 0
for x in L:
s ^= x
return s
def n2l_(x, l):
return list(map(int, '{{0:0{}b}}'.format(l).format(x)))
def n2l(x, l): # need to rewrite n2l with bit computation
ans = []
for i in range(l):
ans.append(x & 1)
x = x >> 1
return ans[::-1]
class Generator1:
def __init__(self, key: list):
# assert len(key) == 64
self.NFSR = key[: 48]
self.LFSR = key[48:]
self.TAP = [0, 1, 12, 15]
self.TAP2 = [[2], [5], [9], [15], [22], [26], [39], [26, 30], [
5, 9], [15, 22, 26], [15, 22, 39], [9, 22, 26, 39]]
self.h_IN = [2, 4, 7, 15, 27]
self.h_OUT = [[1], [3], [0, 3], [0, 1, 2],
[0, 2, 3], [0, 2, 4], [0, 1, 2, 4]]
def g(self):
x = self.NFSR
return _sum(_prod(x[i] for i in j) for j in self.TAP2)
def h(self):
x = [self.LFSR[i] for i in self.h_IN[:-1]] + [self.NFSR[self.h_IN[-1]]]
return _sum(_prod(x[i] for i in j) for j in self.h_OUT)
def f(self):
return _sum([self.NFSR[0], self.h()])
def clock(self):
o = self.f()
self.NFSR = self.NFSR[1:] + [self.LFSR[0] ^ self.g()]
self.LFSR = self.LFSR[1:] + [_sum(self.LFSR[i] for i in self.TAP)]
return o
class Generator2:
def __init__(self, key):
# assert len(key) == 64
self.NFSR = key[: 16]
self.LFSR = key[16:]
self.TAP = [0, 35]
self.f_IN = [0, 10, 20, 30, 40, 47]
self.f_OUT = [[0, 1, 2, 3], [0, 1, 2, 4, 5], [0, 1, 2, 5], [0, 1, 2], [0, 1, 3, 4, 5], [0, 1, 3, 5], [0, 1, 3], [0, 1, 4], [0, 1, 5], [0, 2, 3, 4, 5], [
0, 2, 3], [0, 3, 5], [1, 2, 3, 4, 5], [1, 2, 3, 4], [1, 2, 3, 5], [1, 2], [1, 3, 5], [1, 3], [1, 4], [1], [2, 4, 5], [2, 4], [2], [3, 4], [4, 5], [4], [5]]
self.TAP2 = [[0, 3, 7], [1, 11, 13, 15], [2, 9]]
self.h_IN = [0, 2, 4, 6, 8, 13, 14]
self.h_OUT = [[0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6], [1, 3, 4]]
def f(self):
x = [self.LFSR[i] for i in self.f_IN]
return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
def h(self):
x = [self.NFSR[i] for i in self.h_IN]
return _sum(_prod(x[i] for i in j) for j in self.h_OUT)
def g(self):
x = self.NFSR
return _sum(_prod(x[i] for i in j) for j in self.TAP2)
def clock(self):
self.LFSR = self.LFSR[1:] + [_sum(self.LFSR[i] for i in self.TAP)]
self.NFSR = self.NFSR[1:] + [self.LFSR[1] ^ self.g()]
return self.f() ^ self.h()
class Generator3:
def __init__(self, key: list):
# assert len(key) == 64
self.LFSR = key
self.TAP = [0, 55]
self.f_IN = [0, 8, 16, 24, 32, 40, 63]
self.f_OUT = [[1], [6], [0, 1, 2, 3, 4, 5], [0, 1, 2, 4, 6]]
def f(self):
x = [self.LFSR[i] for i in self.f_IN]
return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
def clock(self):
self.LFSR = self.LFSR[1:] + [_sum(self.LFSR[i] for i in self.TAP)]
return self.f()
class zer0lfsr:
def __init__(self, msk: int, t: int):
if t == 1:
self.g = Generator1(n2l(msk, 64))
elif t == 2:
self.g = Generator2(n2l(msk, 64))
else:
self.g = Generator3(n2l(msk, 64))
self.t = t
def next(self):
for i in range(self.t):
o = self.g.clock()
return o
context.log_level = 'debug'
context.timeout = 10
r = remote('111.186.59.28', 31337)
rec = r.recvline().strip().decode()
suffix = rec.split("+ ")[1].split(")")[0]
digest = rec.split("== ")[1]
log.info(f"suffix: {suffix}\ndigest: {digest}")
for comb in product(ascii_letters + digits + '!#$%&*-?', repeat=4):
prefix = ''.join(comb)
if sha256((prefix+suffix).encode()).hexdigest() == digest:
print(prefix)
break
else:
log.info("PoW failed")
r.sendlineafter(b"Give me XXXX:", prefix.encode())
for choice in [1, 3]:
r.sendlineafter(b'which one: ', str(choice))
d = ''
for i in range(5):
x = r.recvuntil(":::end\n").strip().lstrip(
b'start:::').rstrip(b':::end').decode('latin-1')
d += x
# print("=====> d:", d)
r.recvuntil("hint: ")
hint = r.recvuntil("\n").strip().decode()
print("=====> hint:", hint)
r.recvuntil('k: \n')
s = Solver()
msk1 = BitVec('msk1', 64)
lfsr = zer0lfsr(msk1, choice)
# for i in range(5):
# keystream = ''
# for j in range(1000):
# b = 0
# for k in range(8):
# b = (b << 1) + lfsr.next()
# keystream += chr(b)
digits = []
for x in d:
x = ord(x)
for i in range(8):
digit = 1 if (x & 0b10000000) > 0 else 0
x <<= 1
digits.append(digit)
# print(digit, x)
# s.add(digit == lfsr.next())
print('[+] Receive OK!')
for i in range(200):
s.add(digits[i] == lfsr.next())
s.check()
print(s.model())
msk = s.model()[msk1]
assert sha256(str(msk).encode()).hexdigest() == hint
r.sendline(str(msk))
r.interactive()

flag{we_have_tried_our_best_to_prevent_the_use_of_z3}
好家伙,原来是你们故意的啊,坏坏!
另外,发现一个有意思的地方。
用 latin-1 进行编码不会改变字符的长度,而用 utf-8 编码就可能会使得长度增大。

这情况在之前也遇到过,于是处理的时候就要特别小心,正好发现 2019 TCTF zer0lfsr 一题也有这个情况。
原因是进行 utf-8 编码,当 chr
操作的数字 <= 127 时可以正常用 1byte 表示,而 python2 大于 127 会报错,python3 会使用 2bytes 进行表示。
# 在区间[128, 192)内,数字的表示形式会加上xc2前缀:
>>> chr(130).encode()
b'\xc2\x80'
# 在区间[192, 255)内,数字的表示形式会加上xc3前缀,同时数字本身减去64:
>>> chr(200).encode()
b'\xc3\x88'
解码脚本:
最简单的就是直接以 bytes 都进来,直接 decode
即可。
# python3环境下
with open('keystream','rb') as f:
data = f.read()
data = data.decode()
# python2环境下
import codecs
with codecs.open('keystream', 'rb', 'utf8') as f:
data = f.read()
复杂一点就手动根据前缀来解码(参考网上的)
#!/usr/bin/python3
b = b''
with open('keystream','rb') as f:
data = f.read()
i = 0
while i < len(data):
if data[i] == 194:
b += bytes([data[i+1]])
i += 1
elif data[i] == 195:
b += bytes([data[i+1] + 64])
i += 1
else:
b += bytes([data[i]])
i += 1
Extensive Reading:
1linephp
http://111.186.59.2:50080
http://111.186.59.2:50081
http://111.186.59.2:50082
The three servers are the same, you can choose any one. server will be reset every 10 minutes.
<?php
($_=@$_GET['yxxx'].'.php') && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__) && include('phpinfo.html');
这题是 HITCON2018 One Line PHP Challenge 的升级版。
那题的源码是
($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);
这题其实就文件名加了个 .php
。
hitcon2018 One Line PHP Challenge
hitcon 2018 One Line PHP Challenge
利用 PHP upload_progress 进行文件包含来上传文件并执行。
先看一看 session 相关的配置。

session.auto_start = Off
,代码如果没有 session_start()
也是没有开启 session 的,但是如果在多部分的 POST 数据里加入 PHP_SESSION_UPLOAD_PROGRESS
字段就可以开启 session 了。
session.upload_progress.cleanup = On
,文件上传后,session 文件内容会被立即清空,于是要进行条件竞争,传一个大文件来拖时间,在清空之前包含该文件。
到这里就是之前那题就可以解了。
而这题的话,利用 zip://
协议可以在包含的时候包含压缩包里的文件,这样就能在拼接 .php
的时候读取到需要的 php 文件了。
然而还需要一个 zip 的 trick。
由于 PHP 在进行 zip 文件读取的时候是从结尾的部分读偏移量,然后从开头+偏移量的位置开始读取,于是就构造一个 zip,跳过 upload_progress_
来读取就完事了。
赛后发现有另一种方案,就是先在压缩包里放一个文件,然后通过命令行给压缩包附加上所需的 php 木马文件,也就是说,这样 php🐎 所存储的位置不会被破坏掉,能通过 php zip 协议正常解析。然后打过去就完事了。
下面是队友的 Writeup:
很明显是 hitcon 那个 oneline php ,复习一下 wp ,然后发现多了一个 php 后缀,对于这个点尝试了 phar/zip LFI 都不太行,最后看 zip 格式的时候,发现 zip 是从后往前读的,类似指针的构造,所以我们构造一个 zip 文件,然后修改一下,刚好upload_progress_
是 16 字节,只需要把 zip 那几个段的偏移加 16 字节,挪一下就可以了。
然后用 burp 一边用zip:///tmp/sess_b#index
包含这个 zip 文件,一边用 php upload progress 刷新 session ,写马进去就可以了。

worldcup
这题是队友写的 wp,后面环境关了喵喵复现不了了(
在修改昵称处用/*
尝试多行注释的时候,发现得到的内容都被删除了,猜想有可能是服务端渲染,又因为是 golang ,搜了一下发现有: https://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html
尝试先用 {{.}}
试试看,发现在主页成功输出
const msg = [
{
name: "map[_nonce:ttJVZSLyh5Us2Gx1gLzALQ code:fadc1d lv:1 msg:\u0022\u0027*\/`s name:zedd1]",
}
]
const ParseMSG = (d) => {
if (d['name'] != "map[_nonce:ttJVZSLyh5Us2Gx1gLzALQ code:fadc1d lv:1 msg:\u0022\u0027*\/`s name:zedd1]")
return `${d['name']}(quota ${d['quota'] != -1 ? d['used'] + "/" + d['quota'] : 'unlimited'}): ${d['msg']}`;
else
return `Your quota: 1, Nickname: {"_nonce":"ttJVZSLyh5Us2Gx1gLzALQ","code":"fadc1d","lv":1,"msg":"\"'*/`s","name":"zedd1"}`;
};
for (let i = 0; i < 5; ++i)
$("#msg").append('<div class="form-group row"><p>' + ParseMSG(msg[i]) + '</p></div>');
$("#newmsg").text('(quota 1/1): \u0022\u0027*\/`s');
发现在return
语句中可以尝试使用反引号闭合,所以我们只需要改一下 msg 的内容就好了。
{"msg":"`+alert()+`"}
就能弹窗了,所以接下来就是 xss 了,直接window.location
跳就行了。
拿到 level1 cookie 之后,访问 level2 ,但是好像可以直接 X ,不知道设置的意义在哪
$("#newmsg").text(`"`+alert()+`"`);
拿到两个 cookie
level1=NoQWeCy70QekDB5b; level2=Autx5F53FmmSFayM
访问 http://111.186.58.249:19260/casino?bet=1
用{{$}}
可以看到变量名,然后看了一下 text 模板文档,一开始用
{{if%20gt%20.o0ps_u_Do1nt_no_t1%20.o0ps_u_Do1nt_no_t2}}{{printf%201}}{{else}}{{printf%200}}{{end}}
前端赢麻了但是后端不加钱,然后陷入脑洞当中。
后来想到可能是先加载的模版,再更新钱,于是想个办法把错的时候搞崩就行了。
0{{if%20gt%20.o0ps_u_Do1nt_no_t1%20.o0ps_u_Do1nt_no_t2}}{{call%201}}{{end}}
然后就可以稳赢了。
这比赛题目太难了啊!!!
喵喵好菜啊,喵呜呜呜!!!
哭了,队友大多在准备期末考试,都没什么师傅来一起玩。。
最后只打到了 RisingStar Scoreboard 第13,然而前 12 进决赛,太难受啦,喵呜呜呜!


(溜了溜了喵
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK