这是一篇放在草稿箱里已经长草的文章,清理的时候才想起来这篇由于没环境复现,后面一懒就忘记发了……
噢,喵喵也不记得有没有写完了,就将就发一下吧(摊手
引言
TCTF/0CTF 2021 Quals
比赛时间: 2021-07-03 10:00 ~ 2021-07-05 10:00
前不久有个 TCTF/0CTF 2021 线上预选赛,寻思着有点意思就来打了,题目好难啊。
只不过队里的小师傅们都在准备期末考试,么得办法只有几个老人随便玩了。
这篇就随便水水吧。
Misc
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:
Web
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 进决赛,太难受啦,喵呜呜呜!
总榜
(溜了溜了喵