NCTF遇到了一道pydash题目,似乎与SUCTF2025出的一道SU_blog都是这个知识点,遂想基于这道题分析一下链子。其实还可以结合idekctf那道题
我们首先来贴一下题目给的源码然后下载一下Pydash的源码
'''
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_
pydash.set_(obj,path,value)
我们来看一下pydash关于这一个函数是如何用的
/src/pydash/objects.py
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传入更改的值就可以改掉其属性的值,例如这样子
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}")
所以我们就要利用这个来进行污染某些参数从而读取到environ。
我们先来看看要污染什么值,首先从这个源码看不到可以读取environ的地方,所以我们应该去找一下import的库,可以看到最后面return bottle.template(path)
return了一个值,利用bottle来渲染模板,遂看其源码。
我们直接搜索到对应的函数,看到这一行
adapter = kwargs.pop('template_adapter', SimpleTemplate)
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
可以看到默认的模板引擎是SimpleTemplate,lookup就是模板的搜索路径也就是TEMPLATE_PATH
这个变量
默认的TEMPLATE_PATH的值为['./','./views/']
,所以会去这里面去lookup
然后接着交给SimpleTemplate
去解析
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
else:
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
跟进这个解析器,然后因为后面传入的是name是一个path而不是一个模板,所以会走到BaseTemplate这里先寻找并解析目录下的模板文件再扔给SimpleTemplate进行渲染解析。
这里的name就是传入的bottle.template(path)
的值,这里假设我们path传的是environ
,然后通过abspath()
方法得到TEMPLATE_PATH的绝对路径。接着进入search方法(name=environ)
search方法中
最后把name拼接到lookup中的每个绝对路径中,然后读到文件后就返回这个fname,接着走到SimpleTemplate
的prepare
方法
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结尾
TEMPLATES[tplid].render(kwargs)
进入render方法
走到execute方法
构建了env执行环境,然后在env中执行预编译的模板也就是这里的self.c
如果模板调用 rebase()
继承父模板就递归渲染父模版
这个时候就会去读取environ模板文件
然后在render中返回解析后的内容
至此整个链子就分析完了,最后整理一下链子逻辑
template:adapter()->class:BaseTemplate:search()->class:SimpleTemplate:prepare()->render()->exec()->stdout
所以很简单,我们只需要将模板文件改成linux的proc让他去读environ即可,利用set_
方法更改掉TEMPLATE_
其默认的值为./
和./views/
,我们需要读取其environ,就可以利用../../../proc/self/environ
来读取,只需要我们将TEMPLATE_PATH改为../../../proc/self/
然后把environ放入到其中进行渲染即可得到环境变量。
接下来去看setval函数
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应该是这样子的
setval.__globals__.bottle.TEMPLATE_PATH=['../../../../../proc/self/']
但是pydash是不允许去修改globals属性的,去看⼀下代码
在helpers.py中
所以我们要先污染这个RESTRICTED_KEYS
之后再去污染bottle的值即可。
接着
最后渲染
得到flag
9 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!