Druid SQL注入防御模塊技術淺析


官方參考:

https://www.bookstack.cn/read/Druid/ffdd9118e6208531.md

前置知識

什么是Druid?

Druid是一個高效的數據查詢系統,主要解決的是對於大量的基於時序的數據進行聚合查詢。數據可以實時攝入,進入到Druid后立即可查,同時數據是幾乎是不可變。通常是基於時序的事實事件,事實發生后進入Druid,外部系統就可以對該事實進行查詢。

源碼架構

這里解釋一下druid從解析到判斷sql語句的注入攻擊性的代碼檢測流程:

圖片

1.詞法解析(lexer類)

druid\src\main\java\com\alibaba\druid\sql\parser\sqlparser.java

druid\src\main\java\com\alibaba\druid\sql\parser\lexer.java

druid\src\main\java\com\alibaba\druid\sql\parser\token.java

druid\src\main\java\com\alibaba\druid\sql\parser\token.java
public enum token
{
    select("select"),
    delete("delete"),
    insert("insert"),
    update("update"),
    from("from"),
    having("having"),
    where("where"),
    order("order"),
......

這一步負責把整個sql字符串進行"詞法解析(注意和語法解析區分)",即把一個完整的sql語句進行切分,拆分成一個個單獨的sql token,即解析成"詞法樹"。

設置是否忽略注釋,之后是通過大量的if判斷對sql的token進行識別。

2.語法分析

語法解析器:在sql parser詞法解析的基礎上,對詞法樹(tokens)中的token節點進行語義識別(sql語義),將其解析成一個符合sql語法的規范化結構語法樹。druid\src\main\java\com\alibaba\druid\sql\parser\sqlstatementparser.java

for (;;)
  {
if (max != -1)
  {
if (statementlist.size() >= max)
     {
return;
}
}
if (lexer.token() == token.eof)
  {
return;
}
if (lexer.token() == (token.semi))
  {
lexer.nexttoken();
continue;
}
if (lexer.token() == token.select)
  {
statementlist.add(parseselect());
continue;
}
... ...

因為SQL是結構化語言,所以通過遞歸遍歷,以及又是一大段if判斷,一層層解析出一個個子節點作為“特征單元”添加到語法樹中,最后生成一個statementlist列表。
比如輸入:

select name,pwd from admin where id=1 and 1=1;

最后生成的statementlist,每個list元素被打上不同的標簽做不同的檢測:

select name, pwd
from admin
where id = 1
and 1 = 1
lasttoken: eof

最終字符串被解析為一個有不同的“特征單元”組成的多層次語法樹。

3.注入檢測

程序會自動根據當前sqlstatement節點的節點類型,判斷數據庫類型,再去調用相應的類。后文所說的sql注入檢測的規則,就是在這些類中抽象出的對象里體現出來的。

如果我們的規則匹配成功,即在用戶輸入的sql語句中檢測到了注入攻擊的行為,則調用addviolation()添加檢測結果信息,報錯的同時寫入日志。

private static void addviolation(wallvisitor visitor, int errorcode, string message, sqlobject x) 
{
        visitor.addviolation(new illegalsqlobjectviolation(errorcode, message, visitor.tosql(x)));
}

檢測思路

1.檢測規則

上文的語義分析部分已經解釋了SQL語句的解析方法,下面就是Druid根據解析出來的“特征單元”和語法樹,編寫不同的檢測邏輯,針對不同的數據庫進行的不同注入方式進行的針對性防護。

例子如下:

druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java

  • 此處為針對char()+char()...+char()的繞過檢測

  • 此條規則說明:只要sql內容中包含超過四條char或者chr,則告警

if (groupList.size() >= 4) {
    int chrCount = 0;
    for (int i = 0; i < groupList.size(); ++i) {
        SQLExpr item = groupList.get(i);
        if (item instanceof SQLMethodInvokeExpr) {
            SQLMethodInvokeExpr methodExpr = (SQLMethodInvokeExpr) item;
            String methodName = methodExpr.getMethodName().toLowerCase();
            //判斷調用了char()方法
            if ("chr".equals(methodName) || "char".equals(methodName)) {
                if (methodExpr.getArguments().get(0) instanceof SQLLiteralExpr) {
                    chrCount++;
                }
                /*
                * 此處為針對char()+char()...+char()的繞過檢測
                * 此條規則說明:只要sql內容中包含超過四條char或者chr,則告警
                */
            }
                /*
                *對char函數內參數長度超過5的內容加白
                */
        } else if (item instanceof SQLCharExpr) {
            if (((SQLCharExpr) item).getText().length() > 5) {
                chrCount = 0;
                continue;
            }
        }
                
        if (chrCount >= 4) {
            addViolation(visitor, ErrorCode.EVIL_CONCAT, "evil concat", x);
            break;
        }
    }

檢測邏輯

  1. 只允許基本的crud命令(增刪改查)

druid\src\main\java\com\alibaba\druid\wall\WallConfig.java

noneBaseStatementAllow參數絕對了是否允許非基本語句的其他語句,缺省關閉,通過這個選項就能夠屏蔽諸如CREATE、DROP、ALERT等可能存在嚴重危害的DDL語言。

圖片

默認值為false,是最嚴格的過濾格式,基本不可行,現在正常的企業業務幾乎不存在完全屏蔽crud之外所有命令還能正常運行的,開啟之后會嚴重損害SQL的靈活性。

2.禁止訪問系統級表

出於權限控制的需要,Druid對於系統表的操作進行了詳細的限制,給予用戶充分的自定義空間。舉例:

select * from information_schema.columns;

該操作不存在注入點,只是對系統表進行簡單查詢,所以是被允許的。
但是如果是:

select id  from admin  where id = 1  and 5 = 6  union  select concat(id, name, score) from (select column_name from information_schema.columns  where table_name = class1)

因為SQL在子語句中使用了union進行了concat拼接,拼接之后連接了系統表進行查詢。Druid在sql parser解析后,判斷information_schema在層次中的位置,如果它的父節點為SQL表達式(select等)、左節點為"from",就會滿足子句拼接的條件,從而被認為具有攻擊性。
判斷拼接的Druid代碼位於druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java

圖片

代碼中的owner參數由配置文件確定,可以自行修改,以mysql為例,位於druid\src\main\resources\META-INF\druid\wall\mysql\deny-schema.txt圖片

3.禁止訪問系統變量

Druid同樣也是通過配置策略的方式限制用戶對於系統敏感變量的訪問,代碼與系統表的限制類似,正常的針對version、basedir的查詢不會報錯,但是:

select * from database where id='1' and len(@@version)>0 and '1'='1'

上文的語句中使用邏輯表達式,嘗試探測版本信息。因為@@version的內容在where或having之后,所以會被禁止。判斷代碼Druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java
Druid使用黑名單限制了對敏感的系統變量的訪問,具體內容直接被寫在配置文件Druid\src\main\resources\META-INF\druid\wall\mysql\deny-variant.txt中:

圖片

4.禁止訪問系統函數

和系統敏感的表、變量一樣,Druid冶金用了諸如sleep等危險的系統函數的使用,最新的Druid在mysql中摒棄了黑名單的做法,采用白名單的方式限制函數的使用,其他數據庫仍舊使用黑名單。

而且在判斷使用危險系統函數的時候,和上文一樣,Druid會判斷敏感函數在sql語句中出現的位置:

select load_file('\\etc\\passwd');

不會被禁止,原因也是一樣,不存在注入點。

select * from  ((select sleep(0))a);   

會被禁止,因為顯而易見的sleep函數出現在了可能存在注入點的位置(from的子節點)。
Druid\src\main\resources\META-INF\druid\wall\mysql\permit-function.txt

圖片

Druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java

5.禁止出現注釋

通常的業務SQl語句不會帶有注釋,而在SQL注入中類似的行為卻很常見,Druid默認模式下,會在SQL parser解析之前,先消除語句中的單行和多行注釋內容。

諸如'//or//'1'='2等常見繞waf手段都是利用了SQL的快注釋符。

刪除注釋,並重新拼接為“合規”的sql語句的代碼,位於

Druid\src\main\java\com\alibaba\druid\sql\parser\lexer.java

public final void nexttoken()
{
  ... ...
/*
  解析'#'注釋符
  判斷'#'解析出的節點是'單行注釋'、或'多行注釋'
*/
case '#':
    scanSharp();
    if ((token == Token.LINE_COMMENT || token == Token.MULTI_LINE_COMMENT) && skipComment) {
        bufPos = 0;
        continue;
    }
    return;

以“#”為例,首先判斷#號的注釋符,然后判斷如果是單行或者多行注釋。
這是一種對業務低傷害的防護方式,因為業務人員如果是正常使用sql的注釋功能,刪除之后正常進入解析器,不會對語句正常執行造成任何影響,而如果是惡意的SQL注入行為,則會報錯告警。

6.禁止同時執行多條SQL語句

Druid默認每次只允許執行一條SQL,一次執行多條會被認為疑似是惡意SQL注入語句。

7.禁止永真條件

利用永真條件判斷是否存在注入點是sql注入攻擊最常用的手段。Druid對where、order by和group by節點之后的兩個及以上永真條件進行過濾。

因為單純的永真語句普遍存在於業務代碼中,比如

$sql = "select info from admin where ID = $id";

其中$id為可控輸入,如果輸入為1,在數據庫層就會變成永真條件。因此Druid目前的規則允許語句子句之后最多只存在一個永真邏輯表達式。

 where id =-1 or 1=1;

之類的都會被攔截。

private static Object getValue_and(WallVisitor visitor, List<SQLExpr> groupList) {
    int dalConst = 0;
    Boolean allTrue = Boolean.TRUE;
    for (int i = groupList.size() - 1; i >= 0; --i) {
        SQLExpr item = groupList.get(i);
        Object result = getValue(visitor, item);
        Boolean booleanVal = SQLEvalVisitorUtils.castToBoolean(result);
        if (Boolean.TRUE == booleanVal) {
            final WallConditionContext wallContext = WallVisitorUtils.getWallConditionContext();
            if (wallContext != null && !isFirst(item)) {
                wallContext.setPartAlwayTrue(true);
            }
            dalConst++;
        } else if (Boolean.FALSE == booleanVal) {
            final WallConditionContext wallContext = WallVisitorUtils.getWallConditionContext();
            if (wallContext != null && !isFirst(item)) {
                wallContext.setPartAlwayFalse(true);
            }
            allTrue = Boolean.FALSE;
            dalConst++;
        } else {
            if (allTrue != Boolean.FALSE) {
                allTrue = null;
            }
            dalConst = 0;
        }
        if (dalConst == 2 && visitor != null && !visitor.getConfig().isConditionDoubleConstAllow()) {
            addViolation(visitor, ErrorCode.DOUBLE_CONST_CONDITION, "double const condition", item);
        }
    }

8.禁止 getshell

into outfile是常用的利用注入點進行文件寫入,從而getshell 的技術。

https://blog.csdn.net/qq_44159028/article/details/116274542

同樣,druid的攔截是智能的,它只對真正的注入進行攔截,而正常的語句,例如:

記錄每個用戶的登錄ip,寫入文件中:

select "127.0.0.1" into outfile 'c:\index.php';   -- 允許

而攻擊者常用的攻擊語句(寫入編碼后的一句話)

select id from messages where id=?id=3 union select 1,0x3c3f706870206576616c28245f524551554553545b315d293b3f3e,3 into outfile 'C:\\Users\\Administrator.WIN2012\\Desktop\\phpStudy\\WWW\\outfile.php' --+

這個語句會被攔截下來

9.SQL盲注防御

盲注手法千千萬萬,也是防御模塊最復雜的一部分,這里舉幾個例子來對防御方式進行說明:

0xa 盲注

  • order by
select * from cnp_news where id='23' order by if((len(@@version)>0),1,0);

利用盲注思想來進行注入,獲取敏感信息

  • group by

select * from cnp_news where id='23' group by (select @@version);

利用數據庫的錯誤信息報錯來進行注入,獲取敏感信息

  • having
select * from users where id=1 having 1=(nullif(ascii((substring(user,1,1))),0));

利用數據庫的錯誤信息進行列名的盲注、
druid\src\main\java\com\alibaba\druid\wall\spi\wallvisitorutils.java

/*
    having
    如果having條件出現了永真,則認為正處於被攻擊狀態。例如:
    select f1, count(*) from t group by f1 having 1 = 1
*/
if (boolean.true == getconditionvalue(visitor, x, visitor.getconfig().isselecthavingalwaytruecheck()))
{
    if (!issimpleconstexpr(x))
    {
    addviolation(visitor, errorcode.alway_true, "having alway true condition not allow", x);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM