Pydash原型链污染链子分析

首发于奇安信攻防论坛:https://forum.butian.net/share/4251

前言

NCTF遇到了一道pydash题目,似乎与SUCTF2025出的一道SU_blog都是这个知识点,遂想基于这道题分析一下链子。其实还可以结合idekctf那道题

NCTF-Pydash

我们首先来贴一下题目给的源码然后下载一下Pydash的源码

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
'''
Hints: Flag在环境变量中
'''


from typing import Optional


import pydash
import bottle



__forbidden_path__=['__annotations__', '__call__', '__class__', '__closure__',
               '__code__', '__defaults__', '__delattr__', '__dict__',
               '__dir__', '__doc__', '__eq__', '__format__',
               '__ge__', '__get__', '__getattribute__',
               '__gt__', '__hash__', '__init__', '__init_subclass__',
               '__kwdefaults__', '__le__', '__lt__', '__module__',
               '__name__', '__ne__', '__new__', '__qualname__',
               '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
               '__sizeof__', '__str__', '__subclasshook__', '__wrapped__',
               "Optional","render"
               ]
__forbidden_name__=[
    "bottle"
]
__forbidden_name__.extend(dir(globals()["__builtins__"]))

def setval(name:str, path:str, value:str)-> Optional[bool]:
    if name.find("__")>=0: return False
    for word in __forbidden_name__:
        if name==word:
            return False
    for word in __forbidden_path__:
        if path.find(word)>=0: return False
    obj=globals()[name]
    try:
        pydash.set_(obj,path,value)
    except:
        return False
    return True

@bottle.post('/setValue')
def set_value():
    name = bottle.request.query.get('name')
    path=bottle.request.json.get('path')
    if not isinstance(path,str):
        return "no"
    if len(name)>6 or len(path)>32:
        return "no"
    value=bottle.request.json.get('value')
    return "yes" if setval(name, path, value) else "no"

@bottle.get('/render')
def render_template():
    path=bottle.request.query.get('path')
    if len(path)>10:
        return "hacker"
    blacklist=["{","}",".","%","<",">","_"] 
    for c in path:
        if c in blacklist:
            return "hacker"
    return bottle.template(path)
bottle.run(host='0.0.0.0', port=8000)

主要看一下这里的set_

1
pydash.set_(obj,path,value)

我们来看一下pydash关于这一个函数是如何用的 /src/pydash/objects.py

 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
def set_(obj: T, path: PathT, value: t.Any) -> T:
    """
    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: Object to modify.
        path: Target path to set value to.
        value: Value to set.

    Returns:
        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.0
        Added :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 index
          substrings are used.
        - Remove alias ``deep_set``.
    """
    return set_with(obj, path, value)

也就是说我们往name中传入一个对象,path中传入其属性名,values传入更改的值就可以改掉其属性的值,例如这样子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pydash

class Apple:
    def __init__(self):
        self.name = "apple"
        self.sweet = 10

a = Apple()
print(f"修改前: {a.sweet}")

pydash.set_(a, "sweet", 100)

print(f"修改后: {a.sweet}") 

/img/Pydash原型链污染链子分析/1.png
所以我们就要利用这个来进行污染某些参数从而读取到environ。 我们先来看看要污染什么值,首先从这个源码看不到可以读取environ的地方,所以我们应该去找一下import的库,可以看到最后面return bottle.template(path)return了一个值,利用bottle来渲染模板,遂看其源码。
/img/Pydash原型链污染链子分析/2.png
我们直接搜索到对应的函数,看到这一行

1
2
adapter = kwargs.pop('template_adapter', SimpleTemplate)  
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)

可以看到默认的模板引擎是SimpleTemplate,lookup就是模板的搜索路径也就是TEMPLATE_PATH这个变量 默认的TEMPLATE_PATH的值为['./','./views/'],所以会去这里面去lookup 然后接着交给SimpleTemplate去解析

1
2
3
    TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)  
else:  
    TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)

跟进这个解析器,然后因为后面传入的是name是一个path而不是一个模板,所以会走到BaseTemplate这里先寻找并解析目录下的模板文件再扔给SimpleTemplate进行渲染解析。

/img/Pydash原型链污染链子分析/3.png
这里的name就是传入的bottle.template(path)的值,这里假设我们path传的是environ,然后通过abspath()方法得到TEMPLATE_PATH的绝对路径。接着进入search方法(name=environ)
/img/Pydash原型链污染链子分析/4.png
search方法中
/img/Pydash原型链污染链子分析/5.png
最后把name拼接到lookup中的每个绝对路径中,然后读到文件后就返回这个fname,接着走到SimpleTemplateprepare方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class SimpleTemplate(BaseTemplate):  
    def prepare(self,  
                escape_func=html_escape,  
                noescape=False,  
                syntax=None, **ka):  
        self.cache = {}  
        enc = self.encoding  
        self._str = lambda x: touni(x, enc)  
        self._escape = lambda x: escape_func(touni(x, enc))  
        self.syntax = syntax  
        if noescape:  
            self._str, self._escape = self._escape, self._str

这个方法的作用就是初始化模板的字符处理逻辑,支持 HTML 转义或直接输出原始 HTML,也就是解析这个传入的template变成html 然后回到template结尾

1
TEMPLATES[tplid].render(kwargs)

进入render方法

/img/Pydash原型链污染链子分析/6.png
走到execute方法
/img/Pydash原型链污染链子分析/7.png
构建了env执行环境,然后在env中执行预编译的模板也就是这里的self.c
/img/Pydash原型链污染链子分析/8.png
如果模板调用 rebase() 继承父模板就递归渲染父模版 这个时候就会去读取environ模板文件
/img/Pydash原型链污染链子分析/9.png
然后在render中返回解析后的内容
/img/Pydash原型链污染链子分析/10.png
至此整个链子就分析完了,最后整理一下链子逻辑

1
template:adapter()->class:BaseTemplate:search()->class:SimpleTemplate:prepare()->render()->exec()->stdout

所以很简单,我们只需要将模板文件改成linux的proc让他去读environ即可,利用set_方法更改掉TEMPLATE_

/img/Pydash原型链污染链子分析/11.png
其默认的值为././views/,我们需要读取其environ,就可以利用../../../proc/self/environ来读取,只需要我们将TEMPLATE_PATH改为../../../proc/self/然后把environ放入到其中进行渲染即可得到环境变量。 接下来去看setval函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def setval(name:str, path:str, value:str)-> Optional[bool]:  
    if name.find("__")>=0: return False  
    for word in __forbidden_name__:  
        if name==word:  
            return False  
    for word in __forbidden_path__:  
        if path.find(word)>=0: return False  
    obj=globals()[name]  
    try:  
        pydash.set_(obj,path,value)  
    except:  
        return False  
    return True

所以结合黑名单,playload应该是这样子的

1
setval.__globals__.bottle.TEMPLATE_PATH=['../../../../../proc/self/']

但是pydash是不允许去修改__globals__属性的,去看⼀下代码 在helpers.py中

/img/Pydash原型链污染链子分析/12.png
所以我们要先污染这个RESTRICTED_KEYS之后再去污染bottle的值即可。
/img/Pydash原型链污染链子分析/13.png
接着
/img/Pydash原型链污染链子分析/14.png
最后渲染
/img/Pydash原型链污染链子分析/15.png
得到flag