DASCTF2024 Sanic’s revenge(复现)
下载附件,得到不完整的源码,
from sanic import Sanic
import os
from sanic.response import text, html
import sys
import random
import pydash
# pydash==5.1.2
# 这里的源码好像被admin删掉了一些,听他说里面藏有大秘密
class 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 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')
就是 CISCN 的 sanic 改的,同样是通过原型链污染来查看文件名称。但是这里把 parts 禁掉了也就是无法继续用 CISCN 的污染链了,
不过在 gxngxngxn 师傅的博客最后还发现可以通过把 file_or_directory
污染来实现文件读取。但是不会显示文件名称。先开启列目录功能 payload:
{"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": "/"}
效果如下
但是不知道 flag 文件名,没法直接读 flag。那么先读一手环境变量,文件名称“/proc/self/environ”,有时候也可以读取“/proc/1/environ”试试,发现还真有 flag。
一交竟然是个假的,天塌了。看来还是得老实的一步一步做呀,看见源码说删除了一些源码,那么当务之急是要先寻找源码,访问"/app/app. py",显示文件不存在,猜测源码文件应该是改了名字或者换了位置,然后本来想试着自己去找找新链子发现确实是不怎么好找,摆了。
赛后,直接访问"/proc/1/cmdline" (这个可以查看进程为 1 的系统命令参数)
可以看到就是执行文件 start. sh 嘛,该文件在根目录,直接访问得到内容
知道了源码文件名称,下载查看
from sanic import Sanic
import os
from sanic.response import text, html
import sys
import random
import pydash
# pydash==5.1.2
#源码好像被admin删掉了一些,听他说里面藏有大秘密
class 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/", "D:/yinwenmingtwo/PythonCode/测试/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')
先看 hint 是什么
知道了 flag 在/app 下,不过还是需要知道 flag 的文件名称才行。
在之前国赛中,我们跟踪到 DirectoryHandler
类后,只注意了其初始化部分
当时知道 directory
是个对象无法被污染,然后跟进其来源也就是 Path 类中
可以看到通过 _from_parts
处理来获得的对象,
前面调试发现最终目录是由 parts 控制的,但由于 parts 是个 tuble ,其不能被污染。这里可以看到其赋值给_parts 了,是个 list 型,所以最后污染_parts 来实现了根目录文件读取。
因为这里 parts 字符串被过滤了嘛,而且 value 不能是 list 型,所以得另外找污染的了。我先是继续跟进了下 _parse_args
函数,想着看看 parts 是怎么赋的值,但是发现是通过遍历 args
,而 args
也是个 tuble 型。所以只能向后找,不过向后又确实没什么好找的了,刚刚看到直接就成对象了。
回到 gxngxngxn 师傅思路,还是在 DirectoryHandler
类中,在其 handle 方法中发现在开启 directory_view
功能后还会对 directory 对象进行处理
return self._index(
self.directory / current, path, request.app.debug
)
跟进看看 _index
函数
没什么就是对路径的一些处理。调试一波
继续看传入的参数
return self._index(
self.directory / current, path, request.app.debug
)
这里有个拼接。directory 我们知道是路径,这个 current 是什么呢。
current = path.strip("/")[len(self.base) :].strip("/")
经 GDP 一解释大概就是把 path 去掉 / 后,提取出 base 内容,最后剩下的就是 current
。调试发现访问 url/static/
current
的值是空,那么我们如果把其构造为 ..
是不是就可以实现目录穿越了呢。其值是由 path 和 base 控制,需要看这两个是否可控
path 是访问的 url 路径,base 我之前试过可直接进行污染。
手动把 current
改为"…"试试
发现成功实现目录穿越。那么现在关键就是怎么污染呢,能使 current
为 ..
,访问 /static/gao…/(需要一个存在的目录)
发现 current
为 gao..
,看了 gxngxngxn 师傅分析,把 base 污染为 static/gao 即可获得 ..
那么具体这道题该怎么用呢,我们要看的是 /app 目的文件。但是 app 目录下有什么目录呢,那不就是/static(后面反应过来和这个没关,只要是存在子目录就行)。想到可以利用污染 file_or_directory
为 /app,访问 /static/static../
,实现穿越(这里穿越还是穿越的/static/,也就是到了 /app 目录下,因为 parts 没变嘛,污染 file_or_directory
只是为了访问其子目录 (上面说了必须要要子目录才行))。
发现不行,看了 gxngxngxn 师傅说这在 windows 里面可以,但是在 linux 中就访问 /static/static../
就必须要有 static..
目录才行,所以出题人在这里给了 tmp 目录,那么答案也显而易见了。
把 file_or_directory
设为/tmp,在路由 adminLook
查看/tmp 目录。
可以看到是有 7a4Eku..
目录的,先进行污染
开启列目录功能
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": true}
污染 file_or_directory
为/tmp
{"key":"__class__.__init__.__globals__.app.router.name_index.__mp_main__.static.handler.keywords.file_or_directory","value": "/tmp"}
污染 base
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/7a4Eku"}
最后访问路由/static/7a4Eku…/就会使得 current 为"…"实现/static 的目录穿越到/app 下。
最后访问/app/45W698WqtsgQT1_flag 得到 flag。