官方參考:
前置知識
什么是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;
}
}
檢測邏輯
- 只允許基本的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 的技術。
同樣,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);
}
}