|
| 1 | +--- |
| 2 | +ai_date: 2025-08-17 20:46:37 |
| 3 | +ai_summary: Exploited pickle protocol differences between Python and C implementations |
| 4 | + to bypass restrictions |
| 5 | +ai_tags: |
| 6 | +- pickle |
| 7 | +- protocol |
| 8 | +- exploit |
| 9 | +created: 2025-08-16T08:40 |
| 10 | +points: 100 |
| 11 | +solves: 105 |
| 12 | +title: Discrepancy |
| 13 | +updated: 2025-08-17T20:46 |
| 14 | +--- |
| 15 | + |
| 16 | +We are only allowed 8 bytes of input that has to somehow give different results in `pickle` `_pickle` and `pickletools`. |
| 17 | + |
| 18 | +I'm not gonna do this manually so let's brute force it. |
| 19 | + |
| 20 | +https://github.com/python/cpython/blob/3.13/Lib/pickletools.py#L1153 |
| 21 | + |
| 22 | +Just brute forced length 2 ones, then cherry picked op codes that worked and looked nice and did a longer brute force with a subset of opcodes. |
| 23 | + |
| 24 | +```python |
| 25 | +import io, itertools, time |
| 26 | +from io import BytesIO |
| 27 | +import pickle, _pickle, pickletools |
| 28 | +from contextlib import redirect_stdout |
| 29 | + |
| 30 | +def py_ok(data: bytes) -> bool: |
| 31 | + class SafePyUnpickler(pickle._Unpickler): |
| 32 | + def find_class(self, module_name, global_name): |
| 33 | + raise RuntimeError("blocked") |
| 34 | + try: |
| 35 | + SafePyUnpickler(BytesIO(data)).load() |
| 36 | + return True |
| 37 | + except Exception: |
| 38 | + return False |
| 39 | + |
| 40 | +def c_ok(data: bytes) -> bool: |
| 41 | + class SafeCUnpickler(_pickle.Unpickler): |
| 42 | + def find_class(self, module_name, global_name): |
| 43 | + raise RuntimeError("blocked") |
| 44 | + try: |
| 45 | + SafeCUnpickler(BytesIO(data)).load() |
| 46 | + return True |
| 47 | + except Exception: |
| 48 | + return False |
| 49 | + |
| 50 | +def dis_ok(data: bytes) -> bool: |
| 51 | + try: |
| 52 | + f = io.StringIO() |
| 53 | + with redirect_stdout(f): |
| 54 | + pickletools.dis(data) |
| 55 | + return True |
| 56 | + except Exception: |
| 57 | + return False |
| 58 | +answers = dict() |
| 59 | +alphabet = [0x88, 0x2e, 0x28, 0x90, 0x8f, 0x62, 0x61, 0x80, 0x95, 0x00, 0xff, 0x01, 0x7f] |
| 60 | + |
| 61 | +def test_bytes(b): |
| 62 | + s = bytes(b)[:8] |
| 63 | + py = py_ok(s); c = c_ok(s); d = dis_ok(s) |
| 64 | + return (py,c,d) |
| 65 | + |
| 66 | +start = time.time() |
| 67 | +checked = 0 |
| 68 | +for L in range(1, 9): |
| 69 | + print(f"Length {L}...") |
| 70 | + for tup in itertools.product(alphabet, repeat=L): |
| 71 | + checked += 1 |
| 72 | + candidate = bytes(tup)[:8] |
| 73 | + res = test_bytes(candidate) |
| 74 | + if answers.get(res) is None: |
| 75 | + answers[res] = candidate |
| 76 | + print(f"Found {res} -> {candidate.hex()}") |
| 77 | + if len(answers) == 2**3: |
| 78 | + print("Found all answers!") |
| 79 | + exit(0) |
| 80 | +``` |
| 81 | + |
| 82 | +``` |
| 83 | +Length 1... |
| 84 | +Found (False, False, False) -> 88 |
| 85 | +Length 2... |
| 86 | +Found (True, True, True) -> 882e |
| 87 | +Found (False, False, True) -> 282e |
| 88 | +Length 3... |
| 89 | +Found (True, True, False) -> 88882e |
| 90 | +Length 4... |
| 91 | +Found (False, True, True) -> 8828902e |
| 92 | +Found (True, False, True) -> 888f622e |
| 93 | +Length 5... |
| 94 | +Found (False, True, False) -> 888828902e |
| 95 | +Found (True, False, False) -> 88888f622e |
| 96 | +Found all answers! |
| 97 | +``` |
| 98 | + |
| 99 | +```python |
| 100 | +# ncat --ssl discrepancy.chals.sekai.team 1337 |
| 101 | +from pwn import * |
| 102 | + |
| 103 | +answers = {(False, False, False): b'\x88', (True, True, True): b'\x88.', (False, False, True): b'(.', (True, True, False): b'\x88\x88.', (False, True, True): b'\x88(\x90.', (True, False, True): b'\x88\x8fb.', (False, True, False): b'\x88\x88(\x90.', (True, False, False): b'\x88\x88\x8fb.'} |
| 104 | +conn = remote("discrepancy.chals.sekai.team", 1337, ssl=True) |
| 105 | +req = [(True,True,False), |
| 106 | + (False,True,True), |
| 107 | + (True,False,True), |
| 108 | + (False,False,True), |
| 109 | + (False,True,False)] |
| 110 | +for res in req: |
| 111 | + print(conn.recvline()) |
| 112 | + conn.sendline(answers[res].hex().encode()) |
| 113 | + |
| 114 | +print(conn.recvall().decode()) |
| 115 | + |
| 116 | +# SEKAI{p1ckleeeeeeeee_3a01fea10fb01a88c1cd554e7372f21ced43b497} |
| 117 | +``` |
| 118 | + |
| 119 | +```flag |
| 120 | +SEKAI{p1ckleeeeeeeee_3a01fea10fb01a88c1cd554e7372f21ced43b497} |
| 121 | +``` |
0 commit comments