在研發(fā)人員眼中,編碼開發(fā)的目的是實現(xiàn)相關(guān)功能邏輯可用,無明顯功能 bug。而實際上,在安全人員眼中,很多這樣看似沒有功能問題的代碼,卻可以利用來進行安全漏洞攻擊。雖然這在很多研發(fā)人員眼中是看似天方夜譚,但很不幸,通過以往的無數(shù)重大安全事件的驗證,這個事實客觀存在。
本文主要針對三類最有代表性、安全威脅等級最高的安全漏洞進行著重分析,從安全角度介紹看似合理的功能實現(xiàn)代碼是如何被 “攻破” 的。
一、SQL注入
所謂 SQL 注入,就是通過把 SQL 命令插入到 Web 表單提交或輸入域名或頁面請求的查詢字符串,最終達到欺騙服務器執(zhí)行惡意的 SQL 命令。具體來說,它是利用現(xiàn)有應用程序,將(惡意的)SQL 命令注入到后臺數(shù)據(jù)庫引擎執(zhí)行的能力,它可以通過在 Web 表單中輸入(惡意)SQL 語句得到一個存在安全漏洞的網(wǎng)站上的數(shù)據(jù)庫,而不是按照設(shè)計者意圖去執(zhí)行 SQL 語句。
由于用戶的輸入, 也是 SQL 語句的一部分, 所以攻擊者可以利用這部分可控制內(nèi)容, 注入自己定義的語句, 改變SQL語句執(zhí)行邏輯, 讓數(shù)據(jù)庫執(zhí)行任意自己需要的指令. 通過控制部分SQL語句, 攻擊者可以查詢數(shù)據(jù)庫中任何自己需要的數(shù)據(jù), 利用數(shù)據(jù)庫的讀寫文件等特性, 可以直接獲取數(shù)據(jù)庫服務器的系統(tǒng)權(quán)限。
舉例來說,以下代碼動態(tài)地構(gòu)造并執(zhí)行了一個 SQL 查詢,該查詢可以搜索與指定名稱相匹配的項。該查詢僅會顯示條目所有者與被授予權(quán)限的當前用戶一致的條目。
…
String userName = ctx.getAuthenticatedUserName();
String itemName=request.getParameter(“itemName”); String query = “SELECT * FROM items WHERE owner = ‘”+ userName + “‘ AND itemname = ‘” + itemName + “‘”;
ResultSet rs = stmt.execute(query);
…
這一代碼所執(zhí)行的查詢遵循如下方式:
SELECT * FROM items
WHERE owner =
AND itemname = ;
但是,由于這個查詢是動態(tài)構(gòu)造的,由一個不變的基查詢字符串和一個用戶輸入字符串連接而成,因此只有在 itemName 不包含單引號字符時,才會正確執(zhí)行這一查詢。如果一個用戶名為 wiley 的攻擊者為 itemName 表單字段輸入字符串 “name’ OR ‘a’=’a”,那么構(gòu)造的查詢就會變成:
SELECT * FROM items
WHERE owner = ‘wiley’
AND itemname = ‘name’ OR ‘a’=’a’;
附加條件 OR ‘a’=’a’ 會使 where 從句永遠評估為 true,因此該查詢在邏輯上將等同于一個更為簡化的查詢:
SELECT * FROM items;
這種查詢的簡化會使攻擊者繞過查詢只返回經(jīng)過驗證的用戶所擁有的條目的要求;而現(xiàn)在的查詢則會直接返回所有儲存在 items 表中的條目,不論它們的所有者是誰。
再更為極端一些,如果這個用戶名為 wiley 的攻擊者在itemName這個表單字段輸入字符串 “name’; DELETE FROM items; –”,那么構(gòu)造成的查詢語句將會變?yōu)閮蓚€:
SELECT * FROM items
WHERE owner = ‘wiley’
AND itemname = ‘name’;
DELETE FROM items;
–‘
第二個語句會造成什么結(jié)果,不言自明。
針對此類 SQL 注入攻擊,較為有效的方式是使用參數(shù)化查詢。
參數(shù)化查詢 (Parameterized Query 或 Parameterized Statement) 是指在設(shè)計與數(shù)據(jù)庫鏈接并訪問數(shù)據(jù)時,在需要填入數(shù)值或數(shù)據(jù)的地方,使用參數(shù) (Parameter) 來給值,這個方法目前已被視為最有效可預防SQL注入攻擊 (SQL Injection) 的攻擊手法的防御方式。
舉例來說,我們可以把上面存在SQL注入攻擊的例子改成如下代碼:
…
String userName = ctx.getAuthenticatedUserName();
String itemName =request.getParameter(“itemName”);
String query = “SELECT * FROM items WHERE itemname=? AND owner=?”;
PreparedStatement stmt =conn.prepareStatement(query);
stmt.setString(1, itemName);
stmt.setString(2, userName);
ResultSet results = stmt.execute();
…
通過這種參數(shù)綁定方式,無論攻擊者在 itemName、userName 表單字段填入任何內(nèi)容,這一代碼所執(zhí)行的查詢永遠遵循如下方式:
SELECT * FROM items
WHERE owner =
AND itemname =;
即通過參數(shù)化查詢的方式將后臺 SQL 查詢語句固化,防止攻擊者構(gòu)造惡意參數(shù)拼接 SQL 語句導致的 SQL 語句篡改實現(xiàn)的SQL注入攻擊問題。
二、跨站腳本 (XSS)
跨站腳本 (cross-site scripting,XSS) 是一種安全攻擊,其中,攻擊者在看上去來源可靠的鏈接中惡意嵌入譯碼。它允許惡意用戶將代碼注入到網(wǎng)頁上,由于動態(tài)網(wǎng)頁的 web 應用對用戶提交的請求中的參數(shù)未做充分的檢查過濾,允許攻擊者在提交的數(shù)據(jù)中加入 HTML、JS 代碼,未加編碼地輸出到第三方用戶的瀏覽器,并最終導致攻擊者構(gòu)造的惡意腳本在用戶瀏覽器中執(zhí)行。
跨站腳本攻擊危害十分嚴重,如可以竊取用戶 cookie,偽造用戶身份登錄、可控制用戶瀏覽器、結(jié)合瀏覽器及其插件漏洞,下載病毒木馬到瀏覽者的計算機、衍生 URL 跳轉(zhuǎn)漏洞、蠕蟲攻擊、釣魚欺騙等。
XSS 最為常見的兩類攻擊分別為反射型 XSS 和存儲型 XSS,接下來我們將分別介紹。
以下 JSP 代碼片段可從 HTTP 請求中讀取雇員 ID eid,并將其顯示給用戶。
<% String eid = request.getParameter(“eid”); %>
…
Employee ID: <%= eid %>
如果 eid 只包含標準的字母或數(shù)字文本,這個例子中的代碼就能正確運行。如果 eid 里有包含元字符或源代碼中的值,那么 Web 瀏覽器就會像顯示 HTTP 響應那樣執(zhí)行 eid 里的代碼。
起初,這個例子似乎是不會輕易遭受攻擊的。畢竟,有誰會輸入導致惡意代碼的 URL,并且還在自己的電腦上運行呢?真正的危險在于攻擊者會創(chuàng)建惡意的 URL,然后采用電子郵件或者社會工程的欺騙手段誘使受害者訪問此 URL 的鏈接。當受害者單擊這個鏈接時,他們不知不覺地通過易受攻擊的網(wǎng)絡(luò)應用程序,將惡意內(nèi)容帶到了自己的電腦中。這種對易受攻擊的 Web 應用程序進行盜取的機制通常被稱為反射型 XSS。
再來看看存儲型 XSS 的例子,以下 JSP 代碼片段可在數(shù)據(jù)庫中查詢具有給定 ID 的雇員,并輸出相應雇員姓名。
<%…
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(“select * from emp where id=”+eid);
if (rs != null) {
rs.next();
String name = rs.getString(“name”);
}
%>
Employee Name: <%= name %>
如同上一個例子,如果對 name 的值處理得當,該代碼就能正常地執(zhí)行各種功能;如若處理不當,就會對代碼的攻擊行為無能為力。同樣,這段代碼暴露出的危險較小,因為 name 的值是從數(shù)據(jù)庫中讀取的,而且顯然這些內(nèi)容是由應用程序管理的。然而,如果 name 的值是由用戶提供的數(shù)據(jù)產(chǎn)生,數(shù)據(jù)庫就會成為惡意內(nèi)容溝通的通道。如果不對數(shù)據(jù)庫中存儲的所有數(shù)據(jù)進行恰當?shù)妮斎腧炞C,那么攻擊者便能在用戶的 Web 瀏覽器中執(zhí)行惡意命令。這種類型的攻擊即成為存儲型 XSS,即攻擊者利用手段將惡意代碼存入數(shù)據(jù)庫,一旦該惡意代碼被從數(shù)據(jù)庫讀取,相關(guān)指令就會被執(zhí)行。該手法極其陰險狡猾,因為數(shù)據(jù)存儲導致的間接性使得辨別威脅的難度增大,而且還提高了一個攻擊影響多個用戶的可能性。
一般存儲型 XSS 攻擊會從訪問提供留言板、評論區(qū)等提供輸入字段的表單的網(wǎng)站開始。攻擊者會在這些留言板、評論區(qū)表單條目中嵌入 JavaScript,若后臺未經(jīng)驗證就將其存入數(shù)據(jù)庫,接下來所有訪問該留言板、評論區(qū)的用戶都會執(zhí)行這些惡意代碼。
針對 XSS 的防護,主要手段在于用戶輸入數(shù)據(jù)的驗證,包括:
1. 嚴格校驗用戶輸入的數(shù)據(jù),必須對所有輸入中的 script、iframe 等字樣進行嚴格的檢查和 html escape 轉(zhuǎn)義。這里的輸入不僅僅是用戶可以直接交互的輸入接口,也包括 HTTP 請求中的 cookie 中的變量,HTTP 請求頭部中的變量等。
2. 校驗數(shù)據(jù)類型,驗證其格式、長度、范圍和內(nèi)容。
3. 客戶端,服務端進行雙重校驗。
4. 對輸出的數(shù)據(jù)也要檢查,因為數(shù)據(jù)庫里的值有可能會在一個大網(wǎng)站的多處都有輸出,所以即使在輸入做了編碼等操作,在各處的輸出點時也要進行安全檢查。
三、任意命令執(zhí)行
任意命令執(zhí)行漏洞指的是 Web 應用程序未檢測用戶輸入的合法性,直接傳入程序中調(diào)用系統(tǒng)命令的函數(shù)中如: system(),eval(),exec(),從而可能會導致攻擊者通過構(gòu)造惡意參數(shù),在服務器上執(zhí)行任意命令。
攻擊者通過構(gòu)造惡意代碼,執(zhí)行任意命令可獲取服務器權(quán)限,導致服務器上的重要數(shù)據(jù),如:程序代碼、數(shù)據(jù)庫信息、文檔資料等泄露。
舉例來說,下面這段來自系統(tǒng)實用程序的代碼根據(jù)系統(tǒng)屬性 APPHOME 來決定其安裝目錄,然后根據(jù)指定目錄的相對路徑執(zhí)行一個初始化腳本。
…
String home = System.getProperty(“APPHOME”);
String cmd = home + INITCMD;
java.lang.Runtime.getRuntime().exec(cmd);
該代碼使得攻擊者可通過修改系統(tǒng)屬性 APPHOME 從而控制 INITCMD 的路徑指向,從而提高自己在應用程序中的權(quán)限,繼而隨心所欲地執(zhí)行命令。由于程序不會驗證從環(huán)境中讀取的值,所以如果攻擊者能夠控制系統(tǒng)屬性 APPHOME 的值,他們就能欺騙應用程序去運行惡意代碼從而取得系統(tǒng)控制權(quán)。
再看一個例子,下面的代碼來自一個管理 Web 應用程序,旨在使用戶能夠使用一個圍繞 rman 實用程序的批處理文件封裝器來啟動 Oracle 數(shù)據(jù)庫備份,然后運行一個 cleanup.bat 腳本來刪除一些臨時文件。腳本 rmanDB.bat 接受單個命令行參數(shù),該參數(shù)指定了要執(zhí)行的備份類型。由于訪問數(shù)據(jù)庫受限,所以應用程序執(zhí)行備份需要具有較高權(quán)限的用戶。
…
String btype = request.getParameter(“backuptype”);
String cmd = new String(“cmd.exe /K
\”c:\\util\\rmanDB.bat “+btype+”&&c:\\util\\cleanup.bat\””)
System.Runtime.getRuntime().exec(cmd);
…
這里的問題是:程序沒有對讀取自用戶的 backuptype 參數(shù)做任何驗證。通常情況下 Runtime.exec() 函數(shù)不會執(zhí)行多條命令,但在這種情況下,程序會首先運行 cmd.exe shell,從而可以通過調(diào)用一次 Runtime.exec() 來執(zhí)行多條命令。一旦調(diào)用了該 shell,它即會允許執(zhí)行用兩個與號分隔的多條命令。如果攻擊者傳遞了一個形式為 “&& del c:\\dbms\\*.*” 的字符串,那么應用程序?qū)㈦S程序指定的其他命令一起執(zhí)行此命令。由于該應用程序的特性,運行該應用程序需要具備與數(shù)據(jù)庫進行交互所需的權(quán)限,這就意味著攻擊者注入的任何命令都將通過這些權(quán)限得以運行。
為防止任意命令執(zhí)行漏洞,應當禁止用戶直接控制由程序執(zhí)行的命令。在用戶的輸入會影響命令執(zhí)行的情況下,應將用戶輸入限制為從預定的安全命令集合中進行選擇。如果輸入中出現(xiàn)了惡意的內(nèi)容,傳遞到命令執(zhí)行函數(shù)的值將默認從安全命令集合中選擇,或者程序?qū)⒕芙^執(zhí)行任何命令。
有時還可以執(zhí)行其他檢驗,以檢查這些來源是否已被惡意篡改。例如,如果一個配置文件為可寫,程序可能會拒絕運行。如果能夠預先得知有關(guān)要執(zhí)行的二進制代碼的信息,程序就會進行檢測,以檢驗這個二進制代碼的合法性。如果一個二進制代碼始終屬于某個特定的用戶,或者被指定了一組特定的訪問權(quán)限,這些屬性就會在執(zhí)行二進制代碼前通過程序進行檢驗。
四、總結(jié)&解決方案
上述三類安全漏洞,無一例外是在代碼功能正常的前提下進行的,可見功能可用不代表安全可靠。而為解決這些問題,更多的是需要在研發(fā)過程中各環(huán)節(jié)介入安全能力,實現(xiàn)對上述各類漏洞的上線前檢出以及修復,降低項目上線安全隱患。
企業(yè)應該將賦能服務貫穿需求分析、架構(gòu)設(shè)計、研發(fā)、測試回歸以及發(fā)布迭代全流程,通過賦能將專業(yè)安全能力賦予研發(fā)各環(huán)節(jié)人員,并在各環(huán)節(jié)提供不同工具(STAC、SAST、IAST、常態(tài)化安全運營)使賦能知識真實應用落地,最終以統(tǒng)一平臺展示、分析、回歸、閉環(huán)安全問題,并向安全部提供 SIEM,根據(jù)各流程頻現(xiàn)的漏洞類型、研發(fā)人員知識盲區(qū)等再次提供針對性培訓,最終針對性制定規(guī)章制度,實現(xiàn)制度精準逆推落地。
1、需求和架構(gòu)階段:基于業(yè)務場景的威脅建模 (STAC),以威脅建模賦能方式教會需求分析和架構(gòu)審計人員對項目內(nèi)場景潛在場景風險進行識別和剝離,通過威脅建模針對性提出安全方案,用于后續(xù)研發(fā)等環(huán)節(jié)的解決或規(guī)避。
2、軟件編碼階段:靜態(tài)應用安全測試 (SAST),通過與 git、svn 等代碼倉庫聯(lián)動,自動化拉取全量或增量代碼進行代碼安全檢查,以波谷時間檢測方式在上班時間前根據(jù)提交歷史以郵件形式同時相關(guān)責任人,降低對相關(guān)人員工作方式更改。
3、軟件測試階段:交互式安全測試 (IAST),IAST 通過代理、VPN 或者服務端 Agent 方式無感知獲取功能測試人員測試交互流量,基于模糊測試 (fuzz) 思想對流量進行攻擊代碼隨機插入和攻擊流量構(gòu)建,并自動化對被測程序進行安全測試,同時可準確確定漏洞所在的代碼文件、行數(shù)、函數(shù)及參數(shù)。
4、上線迭代階段:常態(tài)化安全運營,對項目上線后所在的服務器資產(chǎn)、中間件以及項目本身進行 7*24 小時周期性安全檢查,相當于有一個安全團隊或滲透測試工程師全天候管理線上資產(chǎn)、站點以及中間依賴的安全問題,有效確保安全健壯性。