首发于奇安信攻防论坛:https://forum.butian.net/share/4251
NCTF遇到了一道pydash题目,似乎与SUCTF2025出的一道SU_blog都是这个知识点,遂想基于这道题分析一下链子。其实还可以结合idekctf那道题
我们首先来贴一下题目给的源码然后下载一下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 } " )
所以我们就要利用这个来进行污染某些参数从而读取到environ。
我们先来看看要污染什么值,首先从这个源码看不到可以读取environ的地方,所以我们应该去找一下import的库,可以看到最后面return bottle.template(path)
return了一个值,利用bottle来渲染模板,遂看其源码。
我们直接搜索到对应的函数,看到这一行
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进行渲染解析。
这里的name就是传入的bottle.template(path)
的值,这里假设我们path传的是environ
,然后通过abspath()
方法得到TEMPLATE_PATH的绝对路径。接着进入search方法(name=environ)
search方法中
最后把name拼接到lookup中的每个绝对路径中,然后读到文件后就返回这个fname,接着走到SimpleTemplate
的prepare
方法
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方法
走到execute方法
构建了env执行环境,然后在env中执行预编译的模板也就是这里的self.c
如果模板调用 rebase()
继承父模板就递归渲染父模版
这个时候就会去读取environ模板文件
然后在render中返回解析后的内容
至此整个链子就分析完了,最后整理一下链子逻辑
1
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函数
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中
所以我们要先污染这个RESTRICTED_KEYS
之后再去污染bottle的值即可。
接着
最后渲染
得到flag