本期推荐 不潮不用花钱 林俊杰 孤独患者 陈奕迅
前置知识 先简单的了解一下bottle框架
Bottle 是一个非常轻量级的 Python Web 框架,适合用于构建简单的 Web 应用和 RESTful API。Bottle 的最大特点之一是它的单文件设计,意味着你只需一个文件 bottle.py 即可使用整个框架,而不需要安装其他依赖
简单的起一个bottle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from bottle import route, run@rout('/' ) def sb (): return "今天是七夕节,你在做什么呢" @route('/dsb' ) def hello (): return "Hello, World!" run(host='localhost' , port=8080 )
基本上和flask一样,
1 2 3 4 5 6 import bottle@bottle.route('/index' ) def index (): name = bottle.request.query.get('name' , 'TG1u' ) return f'{name} '
像这种请求我们在之前也见过了,所以不必多言
关于bottle的cookie机制在xyctf的那篇文章里我们也分析过了一遍了,也包括了如何去构造一个可以执行pickle反序列化的cookie
注意点:关于bottle的cookie机制 我在搜索的时候看到过这样一篇文章,关于在windows和Linux上的复现的生成的cookie的问题
原文链接 这位作者是通过起bottle服务的方式来得到cookie(但是我感觉在这题似乎是多此一举)在复现时的一个要注意的地方:
在复现的时候发现linux起的服务会一直error,但是windows就不会。为了搞清楚问题所在,把main.py的try去掉使之报错,会报“No moudle named “nt”,众所周知nt是只有在windows中有的py库,那就很神奇了,bottle也没有调用,main也没有调用,怎么回事呢?
其实是因为得到恶意cookie需要起服务来拿cookie,而我是在win上起的,导致生成的cookie和linux上起服务是不一样的(大概python对于两个系统有做差分)。只要在linux上起cookie服务就能解决这个问题。或者考虑直接生成cookie而非利用服务来间接拿到cookie,前提是知道cookie生成的原理。
关于SSTI 1 2 3 4 5 总体上来说是和Flask差不多的 bottle默认的模板语法只有{{}} 但是我们自己都知道,还有很多种,比如<%%>,还有% 当{{}}被过滤时我们就可以用下列方法
推荐可以完整阅读一遍这篇博客
SSTI的斜体字绕过 lmtx正版讲解
原理 何为斜体字 所谓的斜体字,实际上是指Decomposition后为同一个字符的字符集 比如在https://www.compart.com/ 里我们搜索一个a,就会蹦跶出一堆a
这些长得不一样的a统称为正版a的斜体字符集
斜体字为何可以被输入 直接去看bottle.template
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 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)
简单看一下处理的部分
1 adapter = kwargs.pop('template_adapter' , SimpleTemplate)
这里默认了使用SimpleTemplate,也就是说如果没有自己通过kwargs传入自己要用的模板的话,就是使用SimpleTemplate
然后跟进去看 关于传入内容的处理主要就是
1 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
没看出有什么问题,接下来看到render,这个是在SimpleTemplate这个类中定义的成员方法
1 2 3 4 5 6 7 8 9 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)
检查了一下update,似乎没有什么问题,那么久去看看self.execute
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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' ): subtpl, rargs = env.pop('_rebase' ) rargs['base' ] = '' .join(_stdout) del _stdout[:] return self ._include(env, subtpl, **rargs) return env
同样是被定义在SimpleTemplate这个类中的成员方法
看到一个exec,大家都知道exec是一个很敏感的函数,除此之外这里还有self.co,这里再跟进去看一下
1 2 def co (self ): return compile (self .code, self .filename or '<string>' , 'exec' )
有一个self.code还有self.filename 这里也是直接compile了一下,把这些内容都编译成可执行程序,然后给到上一步的exec进行执行
1 2 3 4 5 6 7 8 9 10 11 12 13 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
self.filename是写在basetemplate里的,这里应该是不起什么作用 看看self.code
1 2 3 4 5 6 7 8 9 10 11 12 13 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
看到了encoding的方法为utf8 还有个touni(source)
1 2 3 4 def touni (s, enc='utf8' , err='strict' ): if isinstance (s, bytes ): return s.decode(enc, err) return unicode("" if s is None else s)
return一个unicode 进到unicode里去看
这里的unicode指的就是str,而此处将str->touni->self.code->self.co->exec实现被执行的任务
似乎是对我们整个过程影响不大
接下来接着看self.code
1 code = parser.translate()
注意到在try语句之后来了个translate(),跟进去看一下translate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def translate (self ): if self .offset: raise RuntimeError('Parser is a one time instance.' ) while True : m = self .re_split.search(self .source, pos=self .offset) if m: text = self .source[self .offset:m.start()] self .text_buffer.append(text) self .offset = m.end() if m.group(1 ): line, sep, _ = self .source[self .offset:].partition('\n' ) self .text_buffer.append(self .source[m.start():m.start(1 )] + m.group(2 ) + line + sep) self .offset += len (line + sep) continue self .flush_text() self .offset += self .read_code(self .source[self .offset:], multiline=bool (m.group(4 ))) else : break self .text_buffer.append(self .source[self .offset:]) self .flush_text() return '' .join(self .code_buffer)
可疑可疑真可疑,上面的丢给AI没什么动静,AI建议我重点关注一手flush_text和text_budder
看一手拉蒙特徐的博客,似乎是在flush中出现了问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def flush_text (self ): text = '' .join(self .text_buffer) del self .text_buffer[:] if not text: return parts, pos, nl = [], 0 , '\\\n' + ' ' * self .indent for m in self .re_inl.finditer(text): prefix, pos = text[pos:m.start()], m.end() if prefix: parts.append(nl.join(map (repr , prefix.splitlines(True )))) if prefix.endswith('\n' ): parts[-1 ] += nl parts.append(self .process_inline(m.group(1 ).strip())) if pos < len (text): prefix = text[pos:] lines = prefix.splitlines(True ) if lines[-1 ].endswith('\\\\\n' ): lines[-1 ] = lines[-1 ][:-3 ] elif lines[-1 ].endswith('\\\\\r\n' ): lines[-1 ] = lines[-1 ][:-4 ] parts.append(nl.join(map (repr , lines))) code = '_printlist((%s,))' % ', ' .join(parts) self .lineno += code.count('\n' ) + 1 self .write_code(code)
1 parts.append(self .process_inline(m.group(1 ).strip()))
叽里咕噜加什么呢,看看腿
去看看process_inline
1 2 3 4 @staticmethod def process_inline (chunk ): if chunk[0 ] == '!' : return '_str(%s)' % chunk[1 :] return '_escape(%s)' % chunk
看不懂了,接下来的解释摘自拉蒙特徐
终于,出现了与转码有关的_escape函数。我们对照刚才回顾的exec执行的全局空间。我们看到:’_escape’: self._escape,。我们去找SimpleTemplate类的self._escape看看。还记得每一次进入SimpleTemplate都有一次初始化吗,就是prepare函数这些,我们来看:
1 2 3 4 5 6 7 8 9 10 11 12 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
可以看到初始化了self._escape = lambda x: escape_func(touni(x, enc))
touni()是老熟人了,看escape_func()。
1 escape_func=html_escape,
看定义在全局空间的html_escape()
1 2 3 4 def html_escape (string ): """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ return string.replace('&' , '&' ).replace('<' , '<' ).replace('>' , '>' )\ .replace('"' , '"' ).replace("'" , ''' )
就是一个防止XSS的HTML编码函数
引用结束
重新回到flush_text code那行会把我们的东西给变成另外一种更规范的形式,比如像sb会变成
1 code=_printlist(('sb' , _escape(sb),))
跟进_printlist,就会发现是在env.update里面的内容
1 2 3 4 5 6 7 8 9 10 11 12 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__ })
所以这里的意思是_printlist只是一个在exec执行的全局空间里的打印函数 那并没有什么软用
1 至此我们得出结论:我们的输入,不论在不在{{}}里,经过唯一的编码检查就是对source的touni(),但是由于全局变量中的unicode在python3下是全体str ,这就导致了我们可以输入斜体字符
关于如何传入,对于get传参只有a和o两个字符url编码后去掉%C2后传参可以起到等效于正常ascii字符的效果(不要问为什么要去掉%C2。自己打个断点去测试,是可以发现会自动补全的)
拉蒙特徐的get传参的exp(膜拜)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import redef replace_unquoted (text ): pattern = r'(\'.*?\'|\".*?\")|([oa])' def replacement (match ): if match .group(1 ): return match .group(1 ) else : char = match .group(2 ) replacements = { 'o' : '%ba' , 'a' : '%aa' , } return replacements.get(char, char) result = re.sub(pattern, replacement, text) return result input_text = '' output_text = replace_unquoted(input_text) print ("处理后的字符串:" , output_text)
关于upload一个文件进行渲染打斜体字绕过ssti 这个在不久的将来就会遇到这样子的题目,出生出题者ban了除了flag四个字母以外的所有字母以及一堆标点符号,到那个时候,就可以做到这样子的题目了
斜体字生成站 感谢拉蒙特徐https://exotictext.com/zh-cn/italic/