2018/05/10

SampleCMS 參數過濾元件說明

在 SampleCMS 系統中不管是前台或後台,從 GET (Request.QueryString) 與 POST (Request.Form) 傳送進系統的參數值皆會經過參數過濾機制的檢查,當系統發現參數值有疑似 XSS (Cross-Site Script) 或 SQL Injection 的指令關鍵字就會立刻終止開啟網頁的要求。

參數過濾機制使用參數過濾元件群來檢查各式各樣的規則,
以下說明參數過濾元件的各項功能以及使用方式,

首先,以前台網站為例,說明參數過濾元件的使用方式,
下列為前台網站 Global.asax 在處理使用者每次要求資源時,前置的 BeginRequest 事件處理程式碼,
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        // ...略...

        //檢查參數內容是否有效
        ParamFilterClient paramFilterClient = new ParamFilterClient();

        if (!paramFilterClient.IsParamValueValid(Context))
        {
            // ...略...

            Response.Redirect("~/ErrorPage.aspx#InvalidParameter");
        }

        // ...略...
    }

Line 6: 產生元件 ParamFilterClient ,ParamFilterClient 負責組織參數過濾規則以及檢查從 GET 與 POST 傳來的參數值,ParamFilterClient 在前台與後台各自一份,類別程式碼放在前台與後台的 App_Code 資料夾。

Line 8: 使用 ParamFilterClient 的方法 IsParamValueValid(Context) 開始檢查所有從 GET 與 POST 傳來的參數值。

Line 12: 萬一在檢查過程中發現任何不符合規定的內容,則 IsParamValueValid(Context) 會回傳 false,這裡在 false 的時候將使用者導向錯誤頁。

接下來繼續說明其他相關的元件與功能,
下圖為參數過濾元件關聯圖,
參數過濾元件關聯圖
除了 ParamFilterClient 之外,圖中的 ParamFilter, ConcreteParamFilter1, ConcreteParamFilter2 為制定參數過濾規則的類別,採用 Design Pattern 的職責鏈模式設計 (Chain-of-responsibility pattern)。

ParamFilter: 為參數過濾抽象類別,負責定義職責鏈模式的介面與方法 SetSuccessor(ParamFilter successor) 以及 HandleRequest(ParamInfo)。

ConcreteParamFilter1, ConcreteParamFilter2: 繼承 ParamFilter 實作參數過濾規則,由於這樣子的類別有 9 個,為了方便說明改畫兩個示意。
參數過濾規則類別實作方法 HandleRequest(ParamInfo) ,在方法中依照各自的規則檢查 ParamInfo 格式的參數資料,若在這項檢查沒發現問題就將參數資料交由下一個參數過濾規則類別繼續檢查,直到發現問題或者直到最後一個類別為止。

ParamFilterClient: 負責組織參數過濾規則以及將所有參數值丟給上述參數過濾規則元件檢查,下列以循序圖表示前台的 ParamFilterClient.IsParamValueValid() 檢查 GET 參數值時,(若參數值是正常的)所有經過的參數過濾規則類別。
Root_ParamFilterClient_IsQueryStringValueValid 功能循序圖
  1. 系統在 Global.asax 使用 ParamFilterClient.IsParamValueValid(Context)。
  2. ParamFilterClient 使用 IsQueryStringValueValid(string execFilePath, NameValueCollection queryString) ,在這個方法中串接需要的參數過濾規則,並且開始檢查所有 GET 參數值。
  3. ParamFilterClient 呼叫第一個參數過濾規則的方法 HandleRequest(paramInfo) 開始檢查參數值。SpecificPageParamFilter 用來檢查指定網頁與指定參數名稱的值,例如 WebResource.axd 的參數 t 內容只能是十六進制數字符號。
  4. 呼叫下一個物件的相同方法 HandleRequest(paramInfo) 將參數資料交由下一個規則檢查。NonStringParamFilter 檢查所有不屬於字串格式的內容(e.g., 數值、日期、Guid)。
  5. 換下一個物件 LimitedStringParamFilter 檢查特定參數名稱的字串值長度是否有超標。
  6. 換下一個物件 RegexParamFilter 以正規表達式檢查參數值。
  7. 換下一個物件 SQLInjectionFilterExt 檢查參數值是否有疑似 SQL Injection 的語法。
  8. 換下一個物件 HtmlDecodeParamValue 以 HtmlDecode() 解碼參數值。
  9. 換下一個物件 BlacklistKeywordFilter 在上一項解碼後,檢查參數值是否有出現黑名單的關鍵字。
  10. 換下一個物件 SQLInjectionFilterExt 在解碼後,再一次檢查參數值是否有疑似 SQL Injection 的語法。
另外,下列為 ParamFilterClient.IsQueryStringValueValid() 組織參數過濾規則以及開始檢查的程式碼片段,
        //特殊頁面參數過濾
        SpecificPageParamFilter specificPageParam = new SpecificPageParamFilter();

        //非字串參數過濾
        NonStringParamFilter nonStringParam = new NonStringParamFilter();  
        // 指定參數名單
        nonStringParam.SetIntParamList(intParams);
        nonStringParam.SetGuidParamList(guidParams);

        //有限制長度的字串參數過濾
        LimitedStringParamFilter limitedStringParam = new LimitedStringParamFilter();  
        // 指定參數名稱與內容長度對照表
        limitedStringParam.SetParamValueLenLookup(paramValueLenLookup);

        //規則表達式黑名單過濾
        RegexParamFilter regexParam = new RegexParamFilter();
        regexParam.SetBlacklistPatterns(blacklistPatterns);

        //SQL Injection 過濾
        SQLInjectionFilterExt sqlInjection1 = new SQLInjectionFilterExt();

        //用 HtmlDecode 解碼參數內容
        HtmlDecodeParamValue htmlDecodeValue = new HtmlDecodeParamValue();

        //黑名單關鍵字過濾
        BlacklistKeywordFilter blacklistKw = new BlacklistKeywordFilter(); 
        // 指定黑名單
        blacklistKw.SetBlacklistKeywords(blacklistKeywords);

        //SQL Injection過濾
        SQLInjectionFilterExt sqlInjection2 = new SQLInjectionFilterExt();    

        //建立檢查順序
        ParamFilter chainOfResponsibility = specificPageParam;
        specificPageParam.SetSuccessor(nonStringParam);
        nonStringParam.SetSuccessor(limitedStringParam);
        limitedStringParam.SetSuccessor(regexParam);
        regexParam.SetSuccessor(sqlInjection1);
        sqlInjection1.SetSuccessor(htmlDecodeValue);
        htmlDecodeValue.SetSuccessor(blacklistKw);
        blacklistKw.SetSuccessor(sqlInjection2);

        // ...略...

        foreach (string key in queryString.Keys)
        {
            // ...略...

            //參數內容是否有效
            ParamFilter.ParamInfo paramInfo = new ParamFilter.ParamInfo()
            {
                Key = key,
                Value = queryString[key],
                ExecFilePath = execFilePath
            };

            if (!chainOfResponsibility.HandleRequest(paramInfo))
                return false;
        }


Line 1~31: 產生參數過濾規則物件。

Line 34~41: 串接參數過濾規則物件。

Line 45~59: 逐一檢查每一個 QueryString 參數值。

* 前台 ParamFilterClient 原始碼請參照 ParamFilterClient.cs
* 後台 ParamFilterClient 原始碼請參照 ParamFilterClient.cs


接著介紹每個參數過濾規則類別,

1. NonStringParamFilter - 非字串參數過濾
負責檢查類型限制為整數、日期、Guid 的參數。
整數類型的參數名稱名單透過方法 SetIntParamList(List<string> paramList) 設定。
日期類型的參數名稱名單透過方法 SetDateTimeParamList(List<string> paramList) 設定。
Guid 類型的參數名稱名單透過方法 SetGuidParamList(List<string> paramList) 設定。

為了要減少花費在參數名稱比對的時間,名單中的參數名稱需要以小寫填入。例如前台整數類型的參數名單如下,
    List<string> intParams = new List<string>(new string[] { 
        "l", "lang", "p", "id", "w", 
        "h", "saveas", "stretch", "flexable"
    });

本項規則測試方式為測試參數值是否能正確轉型為指定類型,可以就表示通過並回傳 true,反之回傳 false。

進入此測試規則,若參數名稱符合上述名單的其中一種,則不需再交由下一項規則測試。

2. LimitedStringParamFilter - 有限制長度的字串參數過濾
負責檢查已知有限制長度的參數值,為了要減少花費在參數名稱比對的時間,名單中的參數名稱需要以小寫填入。以前台設定值為例,
        Dictionary<string, int> paramValueLenLookup = new Dictionary<string, int>();

        paramValueLenLookup.Add("alias", 50);
        paramValueLenLookup.Add("kw", 100);
        paramValueLenLookup.Add("q", 100);
        paramValueLenLookup.Add("preview", 256);
        paramValueLenLookup.Add("servicename", 50);
        paramValueLenLookup.Add("term", 100);

例如 Line 3 的 alias 用來存放網址別名,而對應在資料庫裡的欄位類型為 varchar(50),代表任何在這參數中填入超過 50 個字的情況都是不正常的。

本項規則測試方式為測試參數值長度是否在指定的限制內,若超過表示有問題並回傳 false,若在限制內或者不在指定名單中皆繼續交由下一項規則測試。

另外,後續測試主要是檢查 XSS 與 SQL Injection 相關的關鍵字,因此較短的字串值可省略檢查,目前只要在 5 個字以內的都直接回傳 true 表示參數值沒有問題。

3. BlacklistKeywordFilter - 黑名單關鍵字過濾
負責比對參數值中是否有出現指定的黑名單關鍵字。

關鍵字黑名單透過方法 SetBlacklistKeywords(string[] keywords) 設定。
為了要達成白名單的比對功能,名單中的字串需要以小寫填入。
另外,由於比對範圍包括 sql 指令,而 sql 指令的間隔又允許不定長度的空白(例: waitfor delay 與 waitfor     delay 皆可執行),因此名單中的字串也需要拿掉空白。
以前台黑名單為例,
    string[] blacklistKeywords = new string[] {
        "javascript:", "vbscript:", "mocha:", 
        "livescript:", "<script", "alert(", 
        "../../etc/passwd", "../../windows/win.ini", "xp_cmdshell", 
        "acustart", "acuend", "prompt(", 
        "<metahttp-equiv", "waitfordelay", "sleep(", 
        "window.location", "dow.loca", "substring",
        "db_name", "sysprocesses", "db_", 
        "${", "#{", "t(", 
        "msgbox(", "'():;", "onmouse", 
        "onresize", "\"style=", "ssion("
    };

本項規則測試方式為測試參數值是否包括黑名單中的任一關鍵字,若有表示有問題並回傳 false。若沒有發現任何關鍵字則繼續交由下一項規則測試。

關於黑名單關鍵字中的白名單機制,以後台為例,
我希望後台的網頁 Article-Config.aspx 裡面兩個存放 Html 編輯器修改結果的 post 參數 ctl00$cphContent$txtCkeContextZhTw 以及 ctl00$cphContent$txtCkeContextEn 能忽略上述第五個關鍵字 "<script" 的檢查。以下為白名單的建立方式,
        Dictionary<string, NameValueCollection> whitelistOfBlacklistKeywords = new Dictionary<string, NameValueCollection>();
  
        StringBuilder sbParas = null;
        NameValueCollection keyWordAndParamLookup = new NameValueCollection();

        //Article-Config.aspx
        sbParas = new StringBuilder();

        // ,變數名,變數名, (,ParamName,ParamName,)
        sbParas.Append(",")
            .Append("ctl00$cphContent$txtCkeContextZhTw").Append(",")
            .Append("ctl00$cphContent$txtCkeContextEn").Append(",");

        keyWordAndParamLookup = new NameValueCollection();

        // 關鍵字 (Keyword)
        // needs lowercase
        keyWordAndParamLookup.Add("<script", sbParas.ToString());

        // 頁面名 (execFilePath)
        // needs lowercase
        whitelistOfBlacklistKeywords.Add("article-config.aspx", keyWordAndParamLookup);

白名單透過 SetWhitelistOfBlacklistKeywords(Dictionary<string, NameValueCollection> whitelistOfBlacklistKeywords) 設定。
參數 whitelistOfBlacklistKeywords 內容結構為 ( 頁面名[需小寫] -> 關鍵字 -> ,變數名,變數名, )。
上述程式碼建立的內容結構為 ( "article-config.aspx" -> "<script" -> ",ctl00$cphContent$txtCkeContextZhTw,ctl00$cphContent$txtCkeContextEn," )。

4. UrlDecodeParamValue - 用 UrlDecode 解碼參數內容
負責將參數值使用 HttpUtility.UrlDecode() 解碼,將解碼後的參數值繼續交由下一項規則測試。

5. HtmlDecodeParamValue - 用 HtmlDecode 解碼參數內容
負責將參數值使用 HttpUtility.HtmlDecode() 解碼,將解碼後的參數值繼續交由下一項規則測試。

另外,依照 XSS (2009 年) 測試記錄 內文描述的第 6 點和第 7 點,這裡在解碼前需要先把 HttpUtility.HtmlDecode() 不認識的編碼格式轉換為認識的格式。
例如:
&#x6a 轉換為 &#x6a;
&#106 轉換為 &#106;
\x6a 轉換為 &#x6a;
\75 轉換為 &#x75;

再依照內文描述的第 9 點,將解碼後參數值之中的 tab 和換行符號拿掉再交由下一項規則測試。

6. ForAcunetixPostParamFilter - 針對 Acunetix 送來的 Post 參數過濾
負責針對弱點掃描軟體 Acunetix Web Vulnerability Scanner 的特殊規則做測試。
它會把參數名稱之中的 $ 符號改用 %24 傳送,例如  ctl00$contentplaceholder1$uclogin$txtAccount 被改為 ctl00%24contentplaceholder1%24uclogin%24txtAccount 傳至系統,若系統未加以阻擋則會被弱掃軟體列為有風險。

本項規則測試方式為測試參數名稱是否包含 %24 ,若有則回傳 false 表示有問題,若沒有則繼續交由下一項規則測試。

7. SpecificPageParamFilter - 特殊頁面參數過濾
負責檢查指定網頁與指定參數名稱的值。
本項規則測試方式目前為檢查網頁 ScriptResource.axd 與 WebResource.axd 的 t、d 兩項參數。
參數 t 內容必須全為十六進制數字符號。
參數 d 內容必須符合正規表達式 [A-Za-z0-9+/=_\-]+   //比Base-64多了_-

若未符合規則就回傳 false 表示有問題,若符合規則就繼續交由下一項規則測試。

8. RegexParamFilter - 規則表達式黑名單過濾
負責比對參數值內容是否有符合指定的正規表達式。
正規表達式黑名單透過方法 SetBlacklistPatterns(string[] patterns) 設定。
以前台為例,目前使用的正規表達式如下,
string[] blacklistPatterns = new string[] { 
        "<[a-zA-Z0-9]+ [a-zA-Z0-9'\"]+=[a-zA-Z0-9'\"]+>", // e.g., <% contenteditable onresize=HVUx(9663)> <%div style=width:expression(S0K4(9408))>
        "[a-zA-Z0-9]+<[a-zA-Z0-9]+<"    // e.g., yfwribon<8FoJTt<  e<7k8rzz<
    }

第一個式子用來抓取 <% contenteditable onresize=HVUx(9663)> 以及 <%div style=width:expression(S0K4(9408))> 這樣的格式。
第二個式子用來抓取 yfwribon<8FoJTt< 以及 e<7k8rzz< 這樣的格式。
上述這兩種格式是 2017 年版本的 Acunetix Web Vulnerability Scanner 用來測試 XSS 漏洞的格式,而參數過濾規則 BlacklistKeywordFilter 無法只憑單純的關鍵字來抓取,因此增加本項規則彌補不足。

白名單透過 SetWhitelistOfBlacklistPatterns(Dictionary<string, NameValueCollection> whitelistOfBlacklistPatterns) 設定。
參數 whitelistOfBlacklistPatterns 內容結構為 ( 頁面名[需小寫] -> 規則 -> ,變數名,變數名, )。
寫法可參考類似的「3. BlacklistKeywordFilter」的白名單範例程式碼。

本項規則測試方式為測試參數值是否有符合指定的正規表達式,若有表示有問題並回傳 false。若沒有則繼續交由下一項規則測試。

9. SQLInjectionFilter - SQL Injection 過濾、
    SQLInjectionFilterExt - SQL Injection 過濾加強版
負責比對出可能的 SQL Injection 指令。

SQLInjectionFilter 負責抓出像 or xxx=xxx 這樣的格式 (例:or 5 * 2 /3 = 3)
以及 xxx' and 31337-31337='0 這樣的格式,雖然 SQL Injection 都是透過 or + 相等式 來達成忽略原本 where 判斷的效果,但是因為 Acunetix 弱掃軟體會改用 and + 相等式 來測試,所以 or and 都要抓。

當 SQLInjectionFilter 抓到符合格式的文字時,再透過方法 IsSQLInjectionExpr(string expr) 判斷 or, and 之後的相等式是否有效。由於我還不知道怎麼在 .net 裡面測試這樣的指令,所以 SQLInjectionFilter.IsSQLInjectionExpr(string expr) 只會回傳 true 代表有問題。

而 SQLInjectionFilterExt 就是強化 IsSQLInjectionExpr(string expr) 的版本,
它會將 or, and 之後的相等式丟到資料庫的下列 Stored Procedure 做測試。
-- =============================================
-- Author:      <lozen_lin>
-- Create date: <2018/01/17>
-- Description: <測試運算式是否成立,能否被用來做 SQL Injection>
/*
exec dbo.spIsSQLInjectionExpr '31337-31337="0'
exec dbo.spIsSQLInjectionExpr ' 5 * 2 /3  = 3'
exec dbo.spIsSQLInjectionExpr ' 5 * 2 /3  = ''3'
exec dbo.spIsSQLInjectionExpr ' ''5'' * 2 /3  = 3'
*/
-- =============================================
create procedure dbo.spIsSQLInjectionExpr
@Expr varchar(8000)
as
begin
 --雙引號都改為單引號
 set @Expr = replace(@Expr, '"', '''')
 
 --測試運算式
 begin try
  --有單引號的,如: or 31337-31337='0
  exec('if '+@Expr+''' select cast(1 as bit) else select cast(0 as bit)')
  print 'path1'
  return
 end try
 begin catch
 end catch

 begin try
  --沒單引號的,如: and 31337-31337=0
  exec('if '+@Expr+' select cast(1 as bit) else select cast(0 as bit)')
  print 'path2'
  return
 end try
 begin catch
 end catch

 print 'path3'
 select cast(0 as bit)
end

本項規則測試若 IsSQLInjectionExpr(string expr) 為 true 表示有問題並回傳 false。否則繼續交由下一項規則測試。

* 所有參數過濾規則類別原始碼請參照 ParamFilter.cs
   SQLInjectionFilterExt 原始碼請參照 SQLInjectionFilterExt.cs




沒有留言:

張貼留言