本帖最后由 HK.JH 于 2022-2-12 22:49 编辑
Libinjection
简介
Libinjection是一个轻量级的C语言编写的sql注入攻击检测库,libinjection将输入的数据依据它定义好的特征码进行转换, 之后就会得到SQL注入识别特征,或者说指纹, 然后通过二分查找算法,在特征库中进行匹配,匹配到则报SQL注入漏洞。
原理
其通过对用户输入进行词法分析生成指纹规则,然后通过二分查找算法,在特征库中进行匹配,匹配到则报SQL注入漏洞。
核心思想
转换
libinjection通过将输入预处理,将sqli攻击解析为sql原始查询串。
就是将输入转换为令牌流。(lib自己定义了一套字符对于的令牌)
ibinjection做了一个假设,它假设黑客只能通过三种方法来进行sql攻击
直接注入sql语句
在单引号内注入sql
在双引号内注入sql
Libinjection处理流程
Libinjection总的框架
libinjection首先是初始化issqlii变量,
接着设置数据结构并初始化变量state,
libinjection_sqli_init()函数将初始化SQL检测所需的结构体,
之后通过libinjection_is_sqli()函数进行具体分析,
如果存在issqli,则将SQL注入识别特征复制进fingerprint变量并返回,
如果不存在则将fingerprint变量设置为空并返回
1.判断用户输入的字符串长度是否合法,为零则返回FALSE。(即没有发现SQL注入)
2.执行SQL注入识别函数(libinjection_sqli_fingerprint,无引号,标准SQL语法),获取字符串识别特征码
3.执行结构体内的查询函数(这里为libinjection_sqli_lookup_word,二分查找算法),对比第二步获取的识别特征是否与SQL注入识别特征匹配。
4.如果第三步发现SQL注入特征匹配结果为真,则返回true,并将SQL注入识别特征匹配到的fingerprint写入结构体,
5.如果检测失败,则调用reparse_as_mysql()函数判断是否存在“(dash-dash-[notwhite]) 注释”或“’#’ 运算符号”,如果存在则再次执行SQL识别函数(libinjection_sqli_fingerprint,无引号,MYSQL语法),
6. 同时执行结构体的分析函数(libinjection_sqli_lookup_word,二分查找算法)进行特征匹配检测,如果结果为真,则返回true,并将SQL注入识别特征匹配到的fingerprint写入结构体。
7.如果前面的判断没有返回结果,将扫描参数(即用户输入的字符串)查找是否存在单
引号。如果为真接着重复上述检测步骤。
8.如果前面的判断依旧没有返回结果,将扫描参数(即用户输入的字符串)查找是否存
在双引号。如果为真则接着执行SQL注入识别函数
(libinjection_sqli_fingerprint,双引号,MYSQL语法)
同时执行结构体的分析函数(libinjection_sqli_lookup_word,二分查找算法)进行特征匹配检测结果为真,则返回true,并将SQL注入识别特征匹配到的fingerprint写入结构体。
9. 如果前面三种判断均无结果则默认该参数(用户输入的字符串)不存在SQL注入。
libinjection_is_sqli() 处理
函数的作用:
libinjection_sqli_init()函数将初始化SQL检测所需的libinjection_sqli_state结构体,这个结构体在后面十分重要。
libinjection_is_sqli()函数主要功能函数,判断是否为sql注入,返回bool结果
libinjection_sqli_lookup_word()函数从特征库查找,与生成指纹是否匹配
libinjection_sqli_fingerprint()函数生成SQL语句指纹
模式
它内部分为六种模式
1. 无符号 标准SQL 模式
2. 无符号 MySQL模式
3. 单引号 标准SQL 模式
4. 单引号 MySQL 模式
5. 双引号 MySQL 模式
6. 双引号 标准模式
源码分析之一些重要的结构体说明
libinjection_sqli_state
libinjection_sqli_state 这个结构体主要输入sql字符串及令牌化后的所有涉及的信息
struct libinjection_sqli_state {
const char *s; /* 输入待检测的sql字符串 如 "1' OR 1=1" */
size_t slen; /* 输入待检测的sql字符串的长度 */
/* 输入字符串令牌化(如 s&1)后 去特征库查找的函数指针 在init时候赋值的 这个后面讨论
lookup =
typedef char (*ptr_lookup_fn)(struct libinjection_sqli_state*,
int lookuptype, const char* word, size_t len);
*/
ptr_lookup_fn lookup;
void* userdata; /* 貌似没啥用 */
/* flags 是每次检查的标志 它的结构体:sqli_flags
包括:没有引号 , 单引号 , 双引号 ,标准sql , mysql */
int flags;
/* 字符串在令牌化过程中的指针位置 如 "1' OR 1=1"
每次往后移动一位去检测 pos 从0 自加*/
size_t pos;
/* 记录输入sql字符串对应的每个令牌结构 最大令牌是5个 这个8 为了字节对齐 */
struct libinjection_sqli_token tokenvec[8];
/* 用于代码中临时指向 tokenvec 每一个令牌结构 用于判断处理 */
struct libinjection_sqli_token *current;
/* 存储输入sql字符串 令牌化后的 令牌 如 数字在令牌化后都变成 1(看 sqli_token_types ) ,
同上 最大令牌5个+一个结束符 NULL */
char fingerprint[8];
/* debug情况下记录 是否为sqli 的理由 即是在代码哪一行决定的 */
int reason;
/* mysql注释方式为 --加空格 模式
MYSQL和标准SQL的区别(注释)在上个博客中已经说明 */
int stats_comment_ddw;
int stats_comment_ddx; /* mysql中 --后面没有空格和-一个破折号 的非注释情况处理 */
int stats_comment_c; /* c语言模式注释 /x . x/ 未涉及 */
/* 处理# 其中,ANSI模式下#是个操作符, mysql模式下 #是个注释 后面跟注释 */
int stats_comment_hash;
int stats_folds; /* 用来统计sql输入字符串令牌化后的个数 令牌化一个就+1 */
/* 用于统计一共令牌化了几个字符串 如 "1' OR 1=1"
令牌化后就是 s&1 那么 stats_tokens = 3 */
int stats_tokens;
};
libinjection_sqli_token
libinjection_sqli_token 记录令牌的信息
struct libinjection_sqli_token {
size_t pos; /* 记录sql输入字符串被令牌化的位置 如果转换了 pos+1 */
size_t len; /* 记录sql输入字符串被令牌化长度 */
int count; /* 记录@符号开头的数量 */
/*
记录每个字符令牌化后的类型 类型在 sqli_token_types 中
如
TYPE_NUMBER = (int)'1' /*所有数字会被识别为1
TYPE_STRING = (int)'s' /*单引号和双引号
……等等
*/
char type;
/*
记录字符串开始字符是什么
如 CHAR_TICK '`'
CHAR_SINGLE '\''
或者其他具体字符等
记录字符串结束字符是什么
如 CHAR_NULL '\0'
或者其他具体字符等
*/
char str_open;
char str_close;
/* 记录输入字符串的每个字符的令牌 一般结尾+结束符\0 */
char val[32];
};
sqli_flags
sqli_flags 其中的标志位记录 libinjection_sqli_fingerprint 进行令牌/指纹化过程中的参数类型
分四种情况:
1. FLAG_QUOTE_NONE + FLAG_SQL_ANSI 无引号,标准SQL语法
2. FLAG_QUOTE_NONE + FLAG_SQL_MYSQL 无引号,MYSQL语法
3. FLAG_QUOTE_SINGLE + FLAG_SQL_MYSQL 单引号,MYSQL语法
4. FLAG_QUOTE_DOUBLE + FLAG_SQL_MYSQL 单引号,MYSQL语法
enum sqli_flags {
FLAG_NONE = 0
, FLAG_QUOTE_NONE = 1 /* 1 << 0 */
, FLAG_QUOTE_SINGLE = 2 /* 1 << 1 */
, FLAG_QUOTE_DOUBLE = 4 /* 1 << 2 */
, FLAG_SQL_ANSI = 8 /* 1 << 3 */
, FLAG_SQL_MYSQL = 16 /* 1 << 4 */
};
lookup_type
LOOKUP_WORD
/* 查找字符串对应的令牌类型
返回 sql_keywords 中字符对应的类型 对应的 sqli_token_types 表中的类型 */
LOOKUP_TYPE /* 未涉及 */
LOOKUP_OPERATOR
/* 查找字符串的操作符对应的令牌类型
返回 sql_keywords 中字符对应的类型 对应的 sqli_token_types 表中的类型 */
LOOKUP_FINGERPRINT
/* 输入sql字符串最终的令牌/指纹 进行特征库查找 */
enum lookup_type {
LOOKUP_WORD = 1
, LOOKUP_TYPE = 2
, LOOKUP_OPERATOR = 3
, LOOKUP_FINGERPRINT = 4
};
char_parse_map
char_parse_map 列出了 ASCII表每个字符对于的解析函数
例如, ASCII前32个字符都通过 parse_white 函数处理,其不处理+1 往后进行
例如, ASCII 48到57 是数字 0-9 通过 parse_number 函数处理
其中 129-255 这写字符通过 parse_word 函数处理
这里为什么是255 ?没整明白 不应该只有128吗?
typedef struct libinjection_sqli_state sfilter;
typedef size_t (*pt2Function)(sfilter *sf);
static const pt2Function char_parse_map[] = {
&parse_white, /* 0 */
&parse_white, /* 1 */
&parse_white, /* 2 */
……
&parse_word, /* 253 */
&parse_word, /* 254 */
&parse_word, /* 255 */
};
sql_keywords
sql_keywords 列出了 9352 种特征对应的指纹/令牌字典表
由于篇幅,下表省略了许多。
static const keyword_t sql_keywords[] = {
{"!!", 'o'},
{"0&(1)O", 'F'},
……
{"|=", 'o'},
{"||", '&'},
{"~*", 'o'},
};
static const size_t sql_keywords_sz = 9352;
整体框架解读
libinjection 做了一个假设,它假设黑客只能通过三种方法来进行sql攻击
直接注入sql语句
在单引号内注入sql
在双引号内注入sql
1.1 直接注入sql语句
libinjection_sqli_fingerprint,无引号,标准SQL语法
1.1.1分析
1.判断用户输入的字符串长度是否合法,为零则返回FALSE。(即没有发现SQL注入)
执行SQL注入识别函数(libinjection_sqli_fingerprint,无引号,标准SQL语法),获取字符串识别特征码
2.1 执行结构体内的查询函数(这里为libinjection_sqli_lookup_word,二分查找算法),对比第二步获取的识别特征是否与SQL注入识别特征匹配。
如果第三步发现SQL注入特征匹配结果为真,则返回true,并将SQL注入识别特征匹配到的fingerprint写入结构体,
2.2 如果检测失败,则调用reparse_as_mysql()函数判断是否存在“(dash-dash-[notwhite]) 注释”或“’#’ 运算符号”,如果存在则再次执行SQL识别函数(libinjection_sqli_fingerprint,无引号,MYSQL语法),
1.1.2 代码解读
/* 入口 参数:输入字符串,长度 输出指纹结果 */
libinjection_sqli(input, slen, fingerprint)
/*
初始化 libinjection_sqli_state
主要是赋值指纹/令牌查找回调函数 libinjection_sqli_lookup_word
*/
libinjection_sqli_init
/* 判断输入sql语句是不是 sql注入 */
libinjection_is_sqli
/*
执行SQL注入识别函数 libinjection_sqli_fingerprint,
无引号,标准SQL语法
获取字符串识别特征码/令牌
*/
libinjection_sqli_fingerprint(sql_state,
FLAG_QUOTE_NONE | FLAG_SQL_ANSI);
/*
执行结构体内的查询函数 libinjection_sqli_lookup_word
对比获取的识别特征是否与SQL注入识别特征匹配。
*/
sql_state->lookup
/*
如果发现SQL注入特征匹配结果为真,则返回true,
并将SQL注入识别特征匹配到的fingerprint写入结构体
*/
return TRUE;
/*
如果检测失败,则调用 reparse_as_mysql
函数判断是否存在“(dash-dash-[notwhite]) -- 注释”
或“’#’ 运算符号”
*/
reparse_as_mysql
/*
如果存在则再次执行SQL识别函数 libinjection_sqli_fingerprint
无引号,MYSQL语法
*/
libinjection_sqli_fingerprint(sql_state,
FLAG_QUOTE_NONE | FLAG_SQL_MYSQL)
sql_state->lookup
/* 如果发现,则返回true */
return TRUE;
1.2 在单引号内注入sql
libinjection_sqli_fingerprint,单引号,标准SQL语法
1.2.1 分析
如果前面的判断没有返回结果,将扫描参数(即用户输入的字符串)查找是否存在单引号。如果为真接着重复上述检测步骤。
1.2.2 代码解读
/* 如果前面的判断没有返回结果,将扫描参数(即用户输入的字符串)查找是否存在单引号。*/
memchr(s, CHAR_SINGLE, slen)
/*
如果存在则次执行SQL识别函数 libinjection_sqli_fingerprint,
单引号,标准SQL语法
*/
libinjection_sqli_fingerprint(sql_state, FLAG_QUOTE_SINGLE | FLAG_SQL_ANSI);
sql_state->lookup
/* 如果发现,则返回true */
return TRUE;
/*
如果检测失败,则调用 reparse_as_mysql 函数判断
是否存在“(dash-dash-[notwhite]) -- 注释”或“’#’ 运算符号”
*/
reparse_as_mysql
/*
如果存在则再次执行SQL识别函数 libinjection_sqli_fingerprint,
单引号,MYSQL语法
*/
libinjection_sqli_fingerprint(sql_state, FLAG_QUOTE_NONE | FLAG_SQL_MYSQL)
sql_state->lookup
/* 如果发现,则返回true */
return TRUE;
1.3 在双引号内注入sql
libinjection_sqli_fingerprint,双引号,标准SQL语法
1.3.1 分析
如果前面的判断依旧没有返回结果,将扫描参数(即用户输入的字符串)查找是否存在双引号。如果为真则接着执行SQL注入识别函数
(libinjection_sqli_fingerprint,双引号,MYSQL语法)
同时执行结构体的分析函数(libinjection_sqli_lookup_word,二分查找算法)进行特征匹配检测结果为真,则返回true,并将SQL注入识别特征匹配到的fingerprint写入结构体
1.3.2 代码解读
/* 如果前面的判断没有返回结果,将扫描参数(即用户输入的字符串)查找是否存在双引号。*/
memchr(s, CHAR_DOUBLE, slen)
/*
如果存在则次执行SQL识别函数 libinjection_sqli_fingerprint,
双引号,标准SQL语法
*/
libinjection_sqli_fingerprint(sql_state, FLAG_QUOTE_DOUBLE | FLAG_SQL_ANSI);
sql_state->lookup
/* 如果发现,则返回true */
return TRUE;
libinjection对特征码的定义
sqli_token_types 是规定的字符串对应的令牌/指纹的转换表
其中,TYPE_XXX 类型在 sql_keywords (就叫它一个特征库表吧)中给出
这个 sql_keywords 表一共列出了 9352 种特征情况,并给出每种情况的特征结果,结果就是 sqli_token_types key值
typedef enum {
TYPE_NONE = 0
, TYPE_KEYWORD = (int)'k' // 关键字 多 如 SELECT ALTER IN FROM
, TYPE_UNION = (int)'U' // 就几个 如 EXCEPT/返回两个结果集的差 UNION/并集 INTERSECT/交集
, TYPE_GROUP = (int)'B' // 分组 GROUP BY/进行分组 ORDER BY/对结果集进行排序 LIMIT/限制查询结果返回的数量
, TYPE_EXPRESSION = (int)'E' // 表达式 如 CASE/判断 SELECT/选择 UPDATE/修改
, TYPE_SQLTYPE = (int)'t' // sql类型 很多 具体 TYPE_SQLTYPE 参见下面解释
, TYPE_FUNCTION = (int)'f' // sql函数 很多 具体 TYPE_FUNCTION 参见下面解释
, TYPE_BAREWORD = (int)'n' // 裸词 如 BY DO DATABASE LOCK/LOCK IN FOR OUT WITH
, TYPE_NUMBER = (int)'1' // Number 类型 FLOAT Double True/False也是的
, TYPE_VARIABLE = (int)'v' // sql 变量 如 CURRENT DATE/PATH/TIME 等 及 NULL UNKNOWN
, TYPE_STRING = (int)'s' // 字符串 一般以 ' 和 '' 开头
, TYPE_OPERATOR = (int)'o' // 操作符 如 += >> 及 常用的BETWEEN IS NOT IS NULL等等
, TYPE_LOGIC_OPERATOR = (int)'&' // 逻辑操作符 暂时未涉及
, TYPE_COMMENT = (int)'c' // 注释处理 --空格 # /* */
, TYPE_COLLATE = (int)'A' // 排序 就一个 COLLATE collate在sql中是用来定义排序规则的
, TYPE_LEFTPARENS = (int)'(' // 左括号
, TYPE_RIGHTPARENS = (int)')' /* not used? */ // 右括号
, TYPE_LEFTBRACE = (int)'{' // 左大括号
, TYPE_RIGHTBRACE = (int)'}' // 右大括号
, TYPE_DOT = (int)'.' // 点好 或者叫做 顿号
, TYPE_COMMA = (int)',' // 逗号
, TYPE_COLON = (int)':' // 冒号
, TYPE_SEMICOLON = (int)';' // 分号
, TYPE_TSQL = (int)'T' /* TSQL start */ // Transact-SQL 是SQL的增强版常见 BEGIN GO GOTO CALL等
, TYPE_UNKNOWN = (int)'?' // 未知符号
, TYPE_EVIL = (int)'X' /* unparsable, abort */ // 不能解释的符号
, TYPE_FINGERPRINT = (int)'F' /* not really a token */ // 指纹符号
, TYPE_BACKSLASH = (int)'\\' // 反斜杠 \
} sqli_token_types;
其中,常见数据类型如下所示:
TYPE_SQLTYPE 常见的有:整形,单精度,双精度,可变长度字符,固定长度字符,长型,日期等等。
Binary ·Varbinary ·Char·Varchar·Nchar·Nvarchar·Datetime ·Text ·Image ·Ntext
·Smalldatetime ·Decimal ·Numeric·Float·Real ·Int ·Smallint ·Tinyint
·Money ·Smallmoney ·Bit ·Cursor ·Sysname ·Timestamp ·Uniqueidentifier
TYPE_FUNCTION SQL 拥有很多可用于计数和计算的内建函数
函数的基本类型是:
Aggregate 合计函数/Aggregate functions AVG/平均值 COUNT/行数 MAX MIN SUM FIRST LAST
Scalar 函数 UCASE,LCASE/转大小写 LEN/长度 MOD/取余
libinjection实例分析
libinjection将输入的数据依据上述的定义进行转换,之后就会得到SQL注入识别特征,或者说指纹,然后通过二分查找算法,在特征库中进行匹配,匹配到则报SQL注入漏洞。
先下载源代码https://github.com/client9/libinjection
进入src目录执行gcc -Wall -Wextra examples.c libinjection_sqli.c进行编译
输入SQL注入的检测语句
-1' and 1=1 union/* foo */select load_file('/etc/passwd')--
你输入这个字符串,解析按空格分隔开,这里比较重要的点是没有闭合 ’或“也按字符串处理 -1' 转换为特征码s(string);
第二个字串 and 关键字 特征码为 &;
1 = 1这里比较特殊它按数字来转了(具体可以跟代码来深入研究) 特征码为 1;
union 关键字联合查询 特征码为 U;
select 关键字特征码为 E;
所以合起来就是s&1UE;
‘ and 1=1
libinjection会将其转换为s&1,其中单引号依据定义被转换为s,and被转换为&,数字被转换为1
' UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL--
libinjection会将其转换为sUEvc,其中单引号依据定义被转换为s,UNION ALL被转换为U,SELECT被转换为E,NULL被转换为v,后面相同的NULL合并为一个v,–注释符被转换为c
libinjection在转换完后,通过二分查找算法对内置的8000多个特征进行匹配,匹配到则将SQL注入识别特征复制进fingerprint变量并返回。
通过上述两个例子就可以知道libinjection对数据的转换逻辑,其中针对一些特殊情况会有特殊处理
特殊处理
有时候匹配的字符串会超过五个这时候就会进行特殊处理
当匹配的数量超过五个,并且滿足特定條件的时候按照特定顺序取五个指纹进入指纹库使用二分法匹配
Libinjection库的优点
语义分析库libinjection相比传统正则匹配识别SQL注入的好处在于速度快以及低误报,低漏报
速度快体现在该库全程比较耗性能的就一二分查找算法
库本身没有内存申请,使用的内存大小十分稳定
单线程
SQL注入语义好就好在,要想满足他的匹配规则,一般来说必须满足三个特征以上,比如s&1或者sUEvc,而每个特征要么是特殊字符,要么是SQL语句的保留字。
Libinjection库的缺点
其实还有一种注入方法是在注释内注入sql攻击,但是 libinjection不支持这种攻击检测。
为什么# “ad1n’– %a%0aunion select 1,database(),user() — ” 这么一个简单的可以绕过呢。
首先他是吧 admi’ 先进入无符号的标准SQL 然后发现有一个’ 后面就转到 单引号的标准SQL
单引号标准SQL 首先获取的admn’ 然后break
继续到了下一层碰到了一个 – 那么走到parse_dash函数中
static size_t parse_dash(struct libinjection_sqli_state * sf)
{
const char *cs = sf->s;
const size_t slen = sf->slen;
size_t pos = sf->pos;
if (pos + 2 < slen && cs[pos + 1] == '-' && char_is_white(cs[pos+2]) ) {
return parse_eol_comment(sf);
} else if (pos +2 == slen && cs[pos + 1] == '-') {
return parse_eol_comment(sf);
} else if (pos + 1 < slen && cs[pos + 1] == '-' && (sf->flags & FLAG_SQL_ANSI)) {
sf->stats_comment_ddx += 1;
return parse_eol_comment(sf);
} else {
st_assign_char(sf->current, TYPE_OPERATOR, pos, 1, '-');
return pos + 1;
}
}
然后当前的pos 一定是符合第一个判断条件的。继续跟踪parse_eol_comment 函数
static size_t parse_eol_comment(struct libinjection_sqli_state * sf)
{
const char *cs = sf->s;
const size_t slen = sf->slen;
size_t pos = sf->pos;
const char *endpos =(const char *) memchr((const void *) (cs + pos), '\n', slen - pos);
if (endpos == NULL) {
st_assign(sf->current, TYPE_COMMENT, pos, slen - pos, cs + pos);
return slen;
} else {
st_assign(sf->current, TYPE_COMMENT, pos, (size_t)(endpos - cs) - pos, cs + pos);
return (size_t)((endpos - cs) + 1);
}
}
这里只是判断了是否是有\n 这个。然后就直接进入到st_assign 函数中。继续跟踪st_assign 函数
static void st_assign(stoken_t * st, const char stype,size_t pos, size_t len, const char* value)
{
const size_t MSIZE = LIBINJECTION_SQLI_TOKEN_SIZE;
size_t last = len < MSIZE ? len : (MSIZE - 1);
st->type = (char) stype;
st->pos = pos;
st->len = last;
memcpy(st->val, value, last);
st->val[last] = CHAR_NULL;
}
当前函数也是只是赋值了一下val然后就可以做任何操作。 最后就是 admin’ 转换成了S — 因为没有查询到直接是C最后得到的匹配规则为SC 。此匹配规则不在数据库中。完成了绕过
总结
SQL注入语义分析库libinjection相比传统正则匹配识别SQL注入的好处在于速度快以及低误报,低漏报 。
速度快体现在该库全程比较耗性能的就一二分查找算法,相对于正则对性能的消耗来说可以忽略不计,这点从火焰图上可以很明显的看出来,所以无论特征数是800,8000还是80000,对处理速度来说都不会有太大影响,而正则匹配规则达到千条以上就能明显感觉到性能的变化。
其核心思想一为转换,Libinjection通过将输入预处理,将sqli攻击解析为sql原始查询串,将输入转换为一套自己能够识别的语言;
二为预演,Libinjection预设SQL注入发生在3中情况内(直接注入sql语句、在单引号内注入sql、在双引号内注入sql)
要想满足他的匹配规则,必须满足三个特征以上,比如s&1或者sUEvc,而每个特征要么是特殊字符,要么是SQL语句的保留字,所以正常情况下用户输入,很少会出现这种误报这种情况。
* libinjection 的思路是将输入转换为语法特征,与之前构造好的语法特征库进行比对,目前特征库有8K+条,在覆盖率方面有优势,同时匹配时用二分法查找,较之字符串的特征匹配性能也不会随着特征库的变大而有较大下降;
特征库中的8K指纹有可能是一些行业内的范例
能在不知晓网站结构的情况下,仅通过分析用户的输入就可以检测出可能的SQL注入行为并进行预警
|