最近在学习一些WAF相关的内容,学习一些开源出来的 WAF 产品。搜索支持 Nginx 的 WAF 相关内容,开源实现并不多,Mod security, ironbee, Naxsi。前两个产品是支持多种 Web Server 的,这里 Naxsi 也就显示出独特之处了,它支持 Nginx,搜索时也会发现有不少文档和使用说明了。
那我们也先从代码最短的 Naxsi 开始吧,打开它的源代码你不得不惊叹作者代码写的真是丑啊!
网上也有些文章是这样描述 Naxsi 的:"Naxsi 是一款优秀的基于白名单的主动防御模块,bla..bla.."。代码缩进,格式混乱,命名怪异,不得不让博主怀疑其作者是中国名牌大学参加 ACM 的高阶选手。
这东西跑起来,默认不加各种"#define xxx"的话,是任何日志都不打的,加上混乱的代码,作者的手法应该是希望没人能忍着恶心看下去吧,但是!博主做到了!
我们还是先跑起来吧,网上瞎扯各种配置也没个完整的,且看这里:
worker_processes 1;
events {
}
error_log ./logs/error.log debug;
daemon off;
http {
root html;
MainRule "str:/*" "msg:mysql comment (/*)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1003;
MainRule "str:*/" "msg:mysql comment (*/)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1004;
MainRule "str:|" "msg:mysql keyword (|)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1005;
server {
listen 8080 default_server;
location / {
BasicRule "str:-" "msg:mysql keyword (-)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1006;
#LearningMode;
SecRulesEnabled;
#SecRulesDisabled;
DeniedUrl "/deny";
# include wl.conf;
CheckRule "$XSS >= 4" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$EVADE >= 8" BLOCK;
CheckRule "$UPLOAD >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$SQL >= 8" BLOCK;
echo ok;
}
location /deny {
echo deny;
}
}
}
>> curl -i "localhost:8080?|"
HTTP/1.1 200 OK
Server: Tengine/2.0.0
Date: Tue, 07 Jan 2014 15:32:07 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive
deny
>> curl -i "localhost:8080?-"
HTTP/1.1 200 OK
Server: Tengine/2.0.0
Date: Tue, 07 Jan 2014 16:30:21 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive
deny
规则是命中了"MainRule "str:|" "msg:mysql keyword (|)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1005;"这条
s:$SQL:8
:$SQL得分8,我们的CheckRule "$SQL >= 8" BLOCK
说大于等于8
就拒绝,跳转到location /deny {}
。
下面我们看看它的代码实现
对于配置的指令我们可以看下都有哪些:
ngx_http_naxsi_module:
MainRule
main_rule
BasicRule
basic_rule
DeniedUrl
denied_url
CheckRule
check_rule
LearningMode
learning_mode
SecRulesEnabled
rules_enabled
SecRulesDisabled
rules_disabled
大写小写各一种,它们的功能是相同的,想必只是用来满足不同用户的口味。当然这么做带来的一个非常恶心的事情就是它在每次指令解析的时候都要做类似的如下判断:
if (ngx_strcmp(value[0].data, TOP_CHECK_RULE_T) &&
ngx_strcmp(value[0].data, TOP_CHECK_RULE_N))
return (NGX_CONF_ERROR);
真是讨厌,当然恶心着恶心着后面就习惯了,以至于在我写这篇文章的时候已经觉得“咦,还不错呀”。
{ ngx_string(TOP_MAIN_BASIC_RULE_T),
NGX_HTTP_MAIN_CONF|NGX_CONF_1MORE,
ngx_http_dummy_read_main_conf,
NGX_HTTP_MAIN_CONF_OFFSET,
0,
NULL },
/* BasicRule (in main) - nginx style */
{ ngx_string(TOP_MAIN_BASIC_RULE_N),
NGX_HTTP_MAIN_CONF|NGX_CONF_1MORE,
ngx_http_dummy_read_main_conf,
NGX_HTTP_MAIN_CONF_OFFSET,
0,
NULL },
static char *
ngx_http_dummy_read_main_conf(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf)
{
...
ngx_http_dummy_main_conf_t *alcf = conf;
...
if (!alcf || !cf)
return (NGX_CONF_ERROR); /* alloc a new rule */
value = cf->args->elts;
...
if (ngx_http_dummy_cfg_parse_one_rule(cf/*, alcf*/, value, &rule,
cf->args->nelts) != NGX_CONF_OK) {
ngx_http_dummy_line_conf_error(cf, value);
return (NGX_CONF_ERROR);
}
...
main conf 变量命名 alcf,槽不吐不快。
函数进来第一句是 if (!alcf || !cf)
,可以看出作者是对 Nginx 真心不太熟。
ngx_http_dummy_cfg_parse_one_rule
用来解析 MainRule
指令的语法。
typedef struct
{
ngx_array_t *get_rules; /*ngx_http_rule_t*/
ngx_array_t *body_rules;
ngx_array_t *header_rules;
ngx_array_t *generic_rules;
ngx_array_t *locations; /*ngx_http_dummy_loc_conf_t*/
ngx_log_t *log;
} ngx_http_dummy_main_conf_t;
解析出的rule,会根据其所指名的匹配项BODY|URL|ARGS|$HEADERS_VAR
等来插入这些array中。
例如下面这段的做法:
if (rule.br->headers) {
if (alcf->header_rules == NULL) {
alcf->header_rules = ngx_array_create(cf->pool, 2,
sizeof(ngx_http_rule_t));
if (alcf->header_rules == NULL)
return NGX_CONF_ERROR;
}
rule_r = ngx_array_push(alcf->header_rules);
if (!rule_r) return (NGX_CONF_ERROR);
memcpy(rule_r, &rule, sizeof(ngx_http_rule_t));
}
这种写法也是让人十分捉急的写法,他完全可以在create_main_conf
时将这些array
初始化好,而不是在添加的时候才去判断需要初始化。这种写法在后面还有很多。作者的做法真是不能忍!
以及结构体的赋值:memcpy(rule_r, &rule;, sizeof(ngx_http_rule_t))
,这里也可以写成*rule_r = rule
,作者肯定没学好C语言,哼哼。
下面我们看下ngx_http_dummy_cfg_parse_one_rule
这个函数的实现:
void *
ngx_http_dummy_cfg_parse_one_rule(ngx_conf_t *cf,
ngx_str_t *value,
ngx_http_rule_t *current_rule,
ngx_int_t nb_elem)
{
int i, z;
void *ret;
int valid;
if (!value || !value[0].data)
return NGX_CONF_ERROR;
if (!ngx_strcmp(value[0].data, TOP_CHECK_RULE_T) ||
!ngx_strcmp(value[0].data, TOP_CHECK_RULE_N) ||
!ngx_strcmp(value[0].data, TOP_BASIC_RULE_T) ||
!ngx_strcmp(value[0].data, TOP_BASIC_RULE_N) ||
!ngx_strcmp(value[0].data, TOP_MAIN_BASIC_RULE_T) ||
!ngx_strcmp(value[0].data, TOP_MAIN_BASIC_RULE_N)) {
current_rule->type = BR;
current_rule->br = ngx_pcalloc(cf->pool, sizeof(ngx_http_basic_rule_t));
if (!current_rule->br)
return (NGX_CONF_ERROR);
} else {
return (NGX_CONF_ERROR);
}
...
吐槽在先:作者分明晓得都会有谁调用这个函数,以及在什么情况下调用,这里首先来的两个判断又显画蛇添足。
for(i = 1; i < nb_elem && value[i].len > 0; i++) {
valid = 0;
for (z = 0; rule_parser[z].pars; z++) {
if (!ngx_strncmp(value[i].data,
rule_parser[z].prefix,
strlen(rule_parser[z].prefix))) {
ret = rule_parser[z].pars(cf, &(value[i]),
current_rule);
if (ret != NGX_CONF_OK) {
return (ret);
}
valid = 1;
}
}
if (!valid)
return (NGX_CONF_ERROR);
}
static ngx_http_dummy_parser_t rule_parser[] = {
{ID_T, dummy_id},
{SCORE_T, dummy_score},
{MSG_T, dummy_msg},
{RX_T, dummy_rx},
{STR_T, dummy_str},
{MATCH_ZONE_T, dummy_zone},
{NEGATIVE_T, dummy_negative},
{WHITELIST_T, dummy_whitelist},
{NULL, NULL}
};
这里的rule_parser[]
用来解析指令中的各个标签id:
, s:
, msg:
, rx:
, str:
, mz
, wl:
。
MainRule "str:|" "msg:mysql keyword (|)" "mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie" "s:$SQL:8" id:1005;
void *
dummy_id(ngx_conf_t *r, ngx_str_t *tmp, ngx_http_rule_t *rule)
{
rule->rule_id = atoi((const char *) tmp->data+strlen(ID_T));
return (NGX_CONF_OK);
}
id:
很简单,给规则赋值一个id编号。
void *
dummy_score(ngx_conf_t *r, ngx_str_t *tmp, ngx_http_rule_t *rule)
{
...
while (*tmp_ptr) {
if (tmp_ptr[0] == '$') {
...
sc = ngx_array_push(rule->sscores);
sc->sc_score = atoi(tmp_end+1);
...
}
else if (tmp_ptr[0] == ',')
++tmp_ptr;
else if (!strcasecmp(tmp_ptr, "BLOCK")) {
rule->block = 1;
tmp_ptr += 5;
}
else if (!strcasecmp(tmp_ptr, "DROP")) {
rule->drop = 1;
tmp_ptr += 4;
}
else if (!strcasecmp(tmp_ptr, "ALLOW")) {
rule->allow = 1;
tmp_ptr += 5;
}
else if (!strcasecmp(tmp_ptr, "LOG")) {
rule->log = 1;
tmp_ptr += 3;
}
}
...
}
s:
相当于action,设置匹配这条规则之后的操作,可以计分,可以block
,drop
,allow
,log
。一条规则可以对多个项目计分,例如:$SQL
,$XSS
...
void *
dummy_msg(ngx_conf_t *r, ngx_str_t *tmp, ngx_http_rule_t *rule)
{
...
str = ngx_pcalloc(r->pool, sizeof(ngx_str_t));
str->data = tmp->data + strlen(STR_T);
str->len = tmp->len - strlen(STR_T);
rule->log_msg = str;
return (NGX_CONF_OK);
}
msg:
给这个规则设置一个描述,说明一下拦截策略什么的。
void *
dummy_rx(ngx_conf_t *r, ngx_str_t *tmp, ngx_http_rule_t *rule)
{
ha.data = tmp->data+strlen(RX_T);
ha.len = tmp->len-strlen(RX_T);
rgc = ngx_pcalloc(r->pool, sizeof(ngx_regex_compile_t));
rgc->options = PCRE_CASELESS|PCRE_MULTILINE;
rgc->pattern = ha;
rgc->err.len = 0;
rgc->err.data = NULL;
if (ngx_regex_compile(rgc) != NGX_OK) {
return (NGX_CONF_ERROR);
}
rule->br->rx = rgc;
return (NGX_CONF_OK);
}
rx:
设置规则的匹配方式,rx:
是创建一个正则表达式。
void *
dummy_str(ngx_conf_t *r, ngx_str_t *tmp, ngx_http_rule_t *rule)
{
str->data = tmp->data + strlen(STR_T);
str->len = tmp->len - strlen(STR_T);
for (i = 0; i < str->len; i++)
str->data[i] = tolower(str->data[i]);
rule->br->str = str;
return (NGX_CONF_OK);
}
str:
设置规则的匹配方式,str:
是创建一个字符串匹配
void *
dummy_zone(ngx_conf_t *r, ngx_str_t *tmp, ngx_http_rule_t *rule)
{
... bala.bala...
}
mz:
设置规则的作用区间:body
,url
,header
,args
,$headers_var:xxx
,$args_var:xxx
,$body_var:xxx
,相当于取哪些变量来参与规则的判断。
这里代码又臭又长就不贴了。我已经吐槽无力了。
static ngx_int_t
ngx_http_dummy_init(ngx_conf_t *cf)
{
...
h = ngx_array_push(&cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers);
*h = ngx_http_dummy_access_handler;
...
}
请求判断的处理是从rewrite_phase
开始的,作者是个不负责任的程序员,大言不惭的写着access_handler
。
static ngx_int_t ngx_http_dummy_access_handler(ngx_http_request_t *r)
{
...
ctx = ngx_http_get_module_ctx(r, ngx_http_naxsi_module);
if (ctx && ctx->over)
return (NGX_DECLINED);
...
ngx_http_dummy_data_parse(ctx, r);
...
}
在access_handler
里会进行一些被内部跳转的请求已经处理过的请求的判断。
最重要的还会调用ngx_http_dummy_data_parse
对规则进行判断。
void
ngx_http_dummy_data_parse(ngx_http_request_ctx_t *ctx,
ngx_http_request_t *r)
{
ngx_http_dummy_headers_parse(main_cf, cf, ctx, r);
ngx_http_dummy_uri_parse(main_cf, cf, ctx, r);
ngx_http_dummy_args_parse(main_cf, cf, ctx, r);
if ((r->method == NGX_HTTP_POST || r->method == NGX_HTTP_PUT) &&
(cf->body_rules || main_cf->body_rules) &&
r->request_body && ( (!ctx->block || ctx->learning) && !ctx->drop))
ngx_http_dummy_body_parse(ctx, r, cf, main_cf);
ngx_http_dummy_update_current_ctx_status(ctx, cf, r);
}
以ngx_http_dummy_headers_parse
为例:
void
ngx_http_dummy_headers_parse(ngx_http_dummy_main_conf_t *main_cf,
ngx_http_dummy_loc_conf_t *cf,
ngx_http_request_ctx_t *ctx, ngx_http_request_t *r)
{
part = &r->headers_in.headers.part;
h = part->elts;
for (i = 0; ( (!ctx->block || ctx->learning) && !ctx->block) ; i++) {
if (i >= part->nelts) {
if (part->next == NULL)
break;
part = part->next;
h = part->elts;
i = 0;
}
if (cf->header_rules)
ngx_http_basestr_ruleset_n(r->pool, &(h[i].key), &(h[i].value),
cf->header_rules, r, ctx, HEADERS);
if (main_cf->header_rules)
ngx_http_basestr_ruleset_n(r->pool, &(h[i].key), &(h[i].value),
main_cf->header_rules, r, ctx, HEADERS);
}
}
它会对每一个header
内容进行判断:
int
ngx_http_basestr_ruleset_n(ngx_pool_t *pool,
ngx_str_t *name,
ngx_str_t *value,
ngx_array_t *rules,
ngx_http_request_t *req,
ngx_http_request_ctx_t *ctx,
enum DUMMY_MATCH_ZONE zone)
{
...
r = rules->elts;
for (i = 0; i < rules->nelts && ( (!ctx->block || ctx->learning) && !ctx->drop ) ; i++) {
for (z = 0; z < r[i].br->custom_locations->nelts; z++) {
ret = ngx_http_process_basic_rule_buffer(value, &(r[i]), &nb_match);
if (ret == 1) {
ngx_http_apply_rulematch_v_n(&(r[i]), ctx, req, name, value, zone, nb_match, 0);
}
}
}
...
}
接着是对每个条规则去进行ngx_http_process_basic_rule_buffer
匹配判断:
int
ngx_http_process_basic_rule_buffer(ngx_str_t *str,
ngx_http_rule_t *rl,
ngx_int_t *nb_match)
{
if (rl->br->rx) {
tmp_idx = 0;
len = str->len;
while
(tmp_idx < len &&
(match = pcre_exec(rl->br->rx->regex->code, 0,
(const char *) str->data, str->len, tmp_idx, 0,
captures, 6)) >= 0)
{
for(i = 0; i < match; ++i)
*nb_match += 1;
tmp_idx = captures[1];
}
} else if (rl->br->str) {
match = 0;
tmp_idx = 0;
while (1) {
ret = (unsigned char *) strfaststr((unsigned char *)str->data+tmp_idx,
(unsigned int)str->len - tmp_idx,
(unsigned char *)rl->br->str->data,
(unsigned int)rl->br->str->len);
if (ret) {
match = 1;
*nb_match = *nb_match+1;
}
}
}
}
对有正则的规则进行正则判断,对有字符串匹配的进行子串查找。
匹配过后对符合规则的进行update
int
ngx_http_apply_rulematch_v_n(ngx_http_rule_t *r, ngx_http_request_ctx_t *ctx,
ngx_http_request_t *req, ngx_str_t *name,
ngx_str_t *value, enum DUMMY_MATCH_ZONE zone,
ngx_int_t nb_match, ngx_int_t target_name)
{
...
}
不想贴代码了,太烂,这里就是对请求计分,或者设置一些block
,alloc
的标记。
一个请求至此也就判断完毕了,总结一下:
对于配置的每条规则,以及参与匹配的每个参数都是使用的for{}
循环进行的判断,这里毫无高性能可言,对于配置了上百条规则的情况下,分分钟就被干死了。反而WAF成了最脆弱的一层,泥菩萨过河自身难保更别提保护后端应用了。
拦截之后是通过ngx_http_output_forbidden_page
来输出拦截页面的,或者也可以通过内部跳转到一个指定的location
内,使用nginx的配置来实现返回拦截页面。
这里不是我想说的最重要的,在这个函数里也会输出拦截日志,这里才是最奇葩的:ngx_http_append_log
在这个函数里,
ngx_str_t *ngx_http_append_log(ngx_http_request_t *r, ngx_array_t *ostr,
ngx_str_t *fragment, u_int *offset)
{
while ((seed = random() % 1000) == prev_seed);
sub = snprintf((char *)(fragment->data+*offset), MAX_SEED_LEN, "&seed;_start=%d", seed);
fragment->len = *offset+sub;
fragment = ngx_array_push(ostr);
if (!fragment)
return (NULL);
fragment->data = ngx_pcalloc(r->pool, MAX_LINE_SIZE+1);
if (!fragment->data)
return (NULL);
sub = snprintf((char *)fragment->data, MAX_SEED_LEN, "seed_end=%d", seed);
prev_seed = seed;
*offset = sub;
return (fragment);
}
闪亮的while(random() == xxx)
亮瞎了双眼。
Naxsi
是这么描述自己的:NAXSI is an open-source, high performance, low rules maintenance WAF for NGINX
。
最后博主只能呵呵
一笑,外加一个失意体前屈Orz
了。
EOF
Have a fun. :)