1. 程式人生 > >Python安全 - 從SSRF到命令執行慘案

Python安全 - 從SSRF到命令執行慘案

失敗 返回對象 我不 fancybox 邏輯 hacker tcp alt tar

前兩天遇到的一個問題,起源是在某個數據包裏看到url=這個關鍵字,當時第一想到會不會有SSRF漏洞。

以前烏雲上有很多從SSRF打到內網並執行命令的案例,比如有通過SSRF+S2-016漏洞漫遊內網的案例,十分經典。不過當時拿到這個目標,我只是想確認一下他是不是SSRF漏洞,沒想到後面找到了很多有趣的東西。截圖不多(有的是後面補得),大家湊合看吧。

0x01 判斷SSRF漏洞

目標example.com,根據其中csrf_token的樣式,我猜測其為flask開發(當然也可能是一個我不太熟悉的框架使用了和flaskwtf相似的代碼):

技術分享圖片

開著代理瀏覽了一遍整個網站的功能,功能點不多,比較小眾的一個分享型站點。偶然間在數據包裏看到url=

,看了一下發現是一個本地化外部圖片這麽一個功能。這種功能很容易出現兩種漏洞:

  1. SSRF漏洞
  2. XSS漏洞

SSRF漏洞就不用多說了,在拉取外部資源的時候沒有檢查URL,導致可以向內網發送請求;XSS漏洞容易被忽略,拉取到目標後儲存的時候沒有過濾特殊字符,就可能導致XSS漏洞。

簡單fuzz一下,依次訪問http://127.0.0.1:80/http://127.0.0.1:80/404404404not_foundhttp://127.0.0.1:12321/

技術分享圖片

技術分享圖片

技術分享圖片

依次返回了error和兩個500,這三個結果分別代表什麽?

因為平時做Python開發比較多,這種500的情況也見的比較多,通常是因為代碼沒有捕捉異常導致返回500。感覺第二個可能是HTTP請求404導致拋出異常,而第三個可能是TCP連接被拒絕(12321端口未開放)導致拋出異常。

雖然我還沒理清目標的代碼邏輯,但我能肯定這其中存在SSRF漏洞。

0x02 雞肋redis服務?

經過簡單的測試,我發現目標站點下載外部資源後,會檢查資源類型是否是圖片,不是圖片則返回error。這樣就很尷尬了,這是一個沒有回顯的SSRF漏洞。

這時候我突然想到,既然是判斷圖片,會不會是用imagemagick組件來判斷的?然後我將imagetragick的POC保存到外網的某個poc.gif裏,然後讓其訪問:

技術分享圖片

直接把內容返回了,沒出現error,也沒500,但命令也沒執行成功。

當時沒想清楚目標究竟是怎麽判斷圖片的,後來拿到shell以後看了源碼才知道:目標是判斷返回包的content-type,如果不是圖片就直接返回error,我想的太復雜了。

imagemagick這條路死了,我就沒再研究這塊邏輯。因為我不知道目標內網IP段,所以準備先探測一下127.0.0.1的端口,我列了一些常用端口,用Burp跑了一下:

技術分享圖片

看到6379是200的時候,我著實激動了一下,眾所周知,在滲透中遇到redis是一件很愉快的事情。

不過我很快發現,GET請求我沒法控制Redis的命令。

科普一下,Redis的協議是簡單的文本流,比如我可以向6379端口發送如下TCP流:

SET x 1
SET y 2

每行代表一個命令,上述數據包執行了兩條set命令。但現在尷尬的是,普通GET請求的數據包如下:

GET / HTTP/1.1
Host: example.com
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close

我控制不了任意一行的起始部分,也就沒法自定義redis命令了,非常雞肋……

0x03 CVE-2016-5699 化腐朽為神奇

真的雞肋麽?

去年,Python的urllib庫曾出過一個頭註入的漏洞,CVE-2016-5699( http://blog.neargle.com/SecNewsBak/drops/Python%20urllib%20HTTP%E5%A4%B4%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E.html )

因為在CTF裏有過類似的思路,所以我基本第一時間就想到了。我在外網服務器用nc開了個端口,在目標web頁面傳入http://[vps-ip]%0d%0aX-injected:%20header:12345/foo,發現果然註入成功了:

技術分享圖片

有點小激動,因為之前只是聽說過這個漏洞,沒有真實案例的依托,這次真遇到了。如果我們能註入HTTP頭,也就能控制發送往Redis的數據包的某一行,這樣就能執行任意Redis命令了。

有點怕影響目標站,我先在本地搭了個類似的環境。

攻擊Redis有幾個思路,核心就在於寫文件。在本地測試,發現了幾個巨坑:

  1. CONFIG SET dir /tmp,傳遞斜線/的時候必須進行二次編碼(%252f),否則urllib2會拋出URLError: <urlopen error no host given>的異常。
  2. url過長會導致拋出UnicodeError: label empty or too long的異常,所以我需要依次傳入CONFIG SETSAVE等幾個命令。

最後,我依次發送http://127.0.0.1%0d%0aCONFIG%20SET%20dir%20%252ftmp%0d%0a:6379/foohttp://127.0.0.1%0d%0aCONFIG%20SET%20dbfilename%20evil%0d%0a:6379/foohttp://127.0.0.1%0d%0aSET%20foo%20bar%0d%0aSAVE%0d%0a:6379/foo,最後成功在本地寫入/tmp/evil文件。

技術分享圖片

不過目標環境就有點蛋疼了,一是完全沒有回顯,我無法知道我是否寫入成功;二是失敗原因我無法預測,有可能是redis有密碼,或redis是普通權限,或config set命令被禁用等等。

感覺又是一個比較蛋疼和雞肋的情境。

0x04 Python反序列化逆襲

果然在線上環境嘗試寫入cron文件,都沒成功反彈回shell。

在這個地方卡了很久,思路一直在考慮“是否真的成功寫入文件”這個問題,如果“成功寫入了文件”,為什麽沒有反彈到shell;如果沒有成功寫入文件,是不是沒有權限,是否可以寫入python的webshell?總結起來有幾個思路:

  1. 寫入ssh key進行getshell。但掃端口發現似乎並沒有開放22,推測是更換了ssh端口並進行的IP限制,或者直接沒有運行sshd。
  2. 寫入cron嘗試反彈shell,但沒成功。也許是redis沒權限,也許是因為目標是ubuntu或debian,這兩個系統對於cron文件格式限制會比較嚴,很難用redis反彈shell。
  3. 寫入python的webshell,但可能也會遇到文件格式要求過嚴導致python運行失敗,而且通常寫入python腳本需要重啟服務器才能奏效
  4. 寫入jinja2模板文件,並通過模板引擎支持的語法執行命令。

總結起來,第4個方法最靠譜,因為模板文件對格式要求不嚴,只要我需要執行的語句放在類似{{ }}的標簽中即可。但經過測試,還是有幾個問題:一是web路徑(關鍵是存放模板文件的路徑)和模板名稱都需要猜,這個太難;二是redis如果是從源進行安裝,一般是redis用戶運行,一般無法寫入web目錄。

吃個夜宵再回來想想,我覺得首先得解決“是否真的成功寫入文件”這個問題。後面fuzz了一下,測試了一堆目錄,發現成功在/var/www/html/static下寫入了文件,並通過http://example.com/static/xxxfile直接可以訪問!

下載剛寫入的文件,其實這個文件即為redis的導出文件。我將之導入到自己本地的redis環境中,又是一個驚喜:

技術分享圖片

看到這個樣式的數據,我就知道這一定是反序列化數據,而且是Python2.7的反序列化數據。

這裏科普一下,Python2.7和3.5默認使用的序列化格式有所區別,一般帶有括號和換行的序列化數據是2.7使用的,而包含\x00的一般是3.5使用的。

後續利用就和 https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html 這篇文章一個套路。目標站使用redis存儲session數據,且session數據使用序列化保存,我們可以通過反序列化來執行任意命令。

使用python2.7構造一個執行反彈shell腳本的序列化數據,並通過SSRF漏洞設置成session:hacker的值,然後訪問目標站點的時候設置Cookie session=hacker

不過有一點需要註意,就是SSRF時URL太長的話會拋出錯誤(之前本地測試的時候說過),所以需要曲線救國,使用redis的append命令,將數據一段一段寫入,類似於這樣:

技術分享圖片

另外還有個坑,寫入的時候,特殊字符(如換行)需要轉義:http://127.0.0.1%0d%0aAPPEND%20session:hacker%20"(S‘id‘\np1\ntp2\nRp3\n."%0d%0aSAVE%0d%0a:6379/,而且只有值被引號包裹時轉義符才能轉義,否則轉義符又會被轉義……這個把我坑了好久,差點就以為功虧一簣了。

最後感覺,挖漏洞思路還是得跳,之前一直在考慮怎麽通過redis寫文件來進行getshell,卻沒想到通過讀取redis的備份文件,找到了突破口。

成功反彈shell:

技術分享圖片

總結

這一次案例,出現漏洞的根本原因有幾個:

  1. Web層面出現SSRF漏洞
  2. Python版本過低,存在CVE-2016-5699頭註入漏洞
  3. Redis版本過低,新版Redis寫入的文件權限一般是660,可以極大程度上避免寫文件造成的漏洞

拿到shell以後我看了下源碼,其邏輯是這樣:獲取用戶傳入的url參數,直接發送HTTP請求並拿到返回對象,判斷返回對象的Content-Type是否包含image,如果包含則離線數據並顯示出來,否則返回error。

這就導致HTTP請求一旦出現錯誤,服務器就會拋出500,而返回error是手工判斷的結果,所以狀態碼還是200。

另外還有個感想,我怕把目標環境搞壞了,整個過程中多次用到了本地環境進行測試,而所有本地環境都是用docker啟動的 ,非常方便。

之後我應該會模擬一下這個目標,做一個vulhub環境給大家,有時間再說吧……

Python安全 - 從SSRF到命令執行慘案