注:本文为“小米安全中心”原创,转载请联系“小米安全中心”
上期回顾: 技术分享|利用恶意软件检测服务向服务提供商植入恶意软件(二)
WEB攻击是十几年来黑客攻击的主流技术,国内的大厂们早已把WAF作为安全基础设施的标配,市面上也有很多安全厂商提供了WAF产品或云WAF服务。
对于没有自己安全团队,却又饱受sql注入、xss、cc等WEB攻击的中、小企业,对WAF的需求也是非常迫切的。
目前获取WAF的途径有以下几种:
对于收入不错的公司使用收费的产品或服务无可厚非,但是有些公司会因预算、数据私密性(云waf可以捕获所有流量的请求和响应的内容)等原因,不打算使用收费的产品或服务。
这种情况下只能使用免费的waf了,或者按业务需求自制一款适合自己的云WAF。
笔者会通过本文详细阐述如何用一周的时间自制一款简单易用的云WAF,以下为已经完成的云WAF的文档及github地址:
根据业务场景或需求的不同,WAF也有不同的架构,比如:
本文实现的云WAF采用了反向代理模式的架构
x-waf由x-waf本身以及web管理后台x-waf-admin组成,其中:
笔者呆过的2家公司都自主研发过云waf,架构一开始就设计成了适合大规模业务系统的,安装、部署、运维都比较复杂,不方便小企业快速部署,所以在参考了github中现有的开源的几款waf后,重新设计了一款轻量级的。
openresty默认不会执行lua脚本,需要在nginx.conf中进行配置,如下所示:
# 指定lua文件的查找路径
lua_package_path "/usr/local/openresty/nginx/conf/x-waf/?.lua;/usr/local/lib/lua/?.lua;;";
# 定义2个lua shared dict变量分别为limit和badGuys,分配的内存大小为100M
lua_shared_dict limit 100m;
lua_shared_dict badGuys 100m;
# 开启lua代码缓存功能
lua_code_cache on;
# 让nginx在init阶段执行init.lua文件中的lua代码
init_by_lua_file /usr/local/openresty/nginx/conf/x-waf/init.lua;
# 让nginx在每个http请求的access阶段执行access.lua文件中的lua代码
access_by_lua_file /usr/local/openresty/nginx/conf/x-waf/access.lua;
openresty在init阶段会根据配置文件指定的位置导入json格式的规则到全局的lua table中,不同的规则放在不同的table中,以加快正则匹配的速度
waf = require ( "waf" ) waf_rules = waf.load_rules()
waf.load_rules
会根据配置文件中指定的路径加载读取所有json格式的规则,并加载到不同的table中,然后封装一个get_rule的函数,方便在每个http进来时可以直接从lua table中获取对应类型的规则:
local _M = {
RULES = {}
}
function _M.load_rules()
_M.RULES = util.get_rules(config.config_rule_dir)
return _M.RULES
end
function _M.get_rule(rule_file_name)
ngx.log(ngx.DEBUG, rule_file_name)
return _M.RULES[rule_file_name]
end
util.get_rules
会将指定文件中的规则按规则名保存到lua table中供 waf.get_rule
函数在需要的时候获取规则:
function _M.get_rules(rules_path)
local rule_files = _M.get_rule_files(rules_path)
if rule_files == {} then
return nil
end
for rule_name, rule_file in pairs(rule_files) do
local t_rule = {}
local file_rule_name = io.open(rule_file)
local json_rules = file_rule_name:read("*a")
file_rule_name:close()
local table_rules = cjson.decode(json_rules)
if table_rules ~= nil then
for _, table_name in pairs(table_rules) do
table.insert(t_rule, table_name["RuleItem"])
end
end
_M.RULE_TABLE[rule_name] = t_rule
end
return(_M.RULE_TABLE)
end
每个请求进来时,waf会按ip白名单、ip黑名单、user_agent、是否cc攻击、url白名单、url黑名单、是否cc攻击、cookies、get和post参数的顺序进行过滤,如果匹配到其中任一种就会进行相应的处理(输出提示或跳转后),之后就不会继续判断是否为其他类型的攻击了。
function _M.check()
if _M.white_ip_check() then
elseif _M.black_ip_check() then
elseif _M.user_agent_attack_check() then
elseif _M.white_url_check() then
elseif _M.url_attack_check() then
elseif _M.cc_attack_check() then
elseif _M.cookie_attack_check() then
elseif _M.url_args_attack_check() then
elseif _M.post_attack_check() then
else
return
end
end
对每个请求的每种参数类型的判断都是先获取到参数内容,然后再循环与该类参数的正则规则进行匹配,如果匹配到则认为是攻击请求,以下为对post参数进行过滤的函数:
-- deny post
function _M.post_attack_check()
if config.config_post_check == "on" then
ngx.req.read_body()
local POST_RULES = _M.get_rule('post.rule')
for _, rule in pairs(POST_RULES) do
local POST_ARGS = ngx.req.get_post_args() or {}
for _, v in pairs(POST_ARGS) do
local post_data = ""
if type(v) == "table" then
post_data = table.concat(v, ", ")
else
post_data = v
end
if rule ~= "" and rulematch(post_data, rule, "jo") then
util.log_record('Deny_USER_POST_DATA', post_data, "-", rule)
if config.config_waf_enable == "on" then
util.waf_output()
return true
end
end
end
end
end
return false
end
waf的规则是以json格式的字符串,人工维护起来容量出错,另外云waf会有多台waf同时工作,如果人工做waf的后端主机的管理、规则同步与主机配置的同步等这些运维工作的话,非常容易出错或者疏漏,所以有必要提供一个自动化管理、同步配置的管理后台。
为了方便部署,x-waf-admin没有采用python、php等需要搭建运行环境或依赖第3方包的语言,而是用可以直接编译为可执行文件的go语言写的,具体的技术栈为go语言 + macron + xorm。
项目结构如下:
hartnett at hartnett-notebook in /data/code/golang/src/xsec-waf/x-waf-admin (master●)
$ tree -L 2
├── conf
│ └── app.ini
├── models
│ ├── models.go
│ ├── rules.go
│ ├── site.go
│ └── user.go
├── modules
│ └── util
├── public
│ ├── css
├── README.md
├── routers
│ ├── admin.go
│ ├── index.go
│ ├── rules.go
│ ├── site.go
│ └── user.go
├── server
├── server.go
├── setting
│ └── setting.go
└── templates
用户管理、后端站点管理与规则管理功能的实现大同小异,都是类似flask、martini、tornado、django等MTV WEB框架的应用,为了减少篇幅,本文只写 后端站点管理
功能如何实现,完整的代码请参见 github 。
先用xorm定义site的struct,然后再提供增、改、删、查看等方法,这些方法会被routers模块中的site文件调用:
// 因篇幅太长,省略部分代码,详细代码请查看github
// debuglevel: debug, info, notice, warn, error, crit, alert, emerg
// ssl: on, off
type Site struct {
Id int64
SiteName string `xorm:"unique"`
Port int
BackendAddr []string
Ssl string `xorm:"varchar(10) notnull default 'off'"`
DebugLevel string `xorm:"varchar(10) notnull default 'error'"`
LastChange time.Time `xorm:"updated"`
Version int `xorm:"version"` // 乐观锁
}
func ListSite() (sites []Site, err error) {
sites = make([]Site, 0)
err = Engine.Find(&sites)
log.Println(err, sites)
return sites, err
}
func NewSite(siteName string, Port int, BackendAddr []string, SSL string, DebugLevel string) (err error) {
if SSL == "" {
SSL = "off"
}
if DebugLevel == "" {
DebugLevel = "error"
}
_, err = Engine.Insert(&Site{SiteName: siteName, Port: Port, BackendAddr: BackendAddr, Ssl: SSL, DebugLevel: DebugLevel})
return err
}
首先import相应的包,然后分别编写以下处理器:
// 因篇幅太长,省略部分代码,详细代码请查看github
func NewSite (ctx *macaron.Context, sess session.Store, x csrf.CSRF) { if sess.Get( "uid" ) != "" { ctx.Data[ "csrf_token" ] = x.GetToken() ctx.HTML( 200 , "newSite" ) } else { ctx.Redirect( "/login/" ) } }
func DoNewSite (ctx *macaron.Context, sess session.Store) {
if sess.Get( "uid" ) != nil {
log .Println(sess.Get( "uid" )) siteName := ctx.Req.Form.Get( "sitename" ) port := ctx.Req.Form.Get( "port" ) Port, _ := strconv.Atoi(port) backaddr := ctx.Req.Form.Get( "backendaddr" ) backendaddr := strings.Split(backaddr, "/r/n" ) BackendAddr := make([] string , 0 )
for _, v := range backendaddr {
if v == "" {
continue } v = strings.TrimSpace(v) BackendAddr = append(BackendAddr, v) } ssl := ctx.Req.Form.Get( "ssl" ) debugLevel := ctx.Req.Form.Get( "debuglevel" )
log .Println(siteName, BackendAddr, ssl, debugLevel) models.NewSite(siteName, Port, BackendAddr, ssl, debugLevel) ctx.Redirect( "/admin/site/list/" ) } else{ ctx.Redirect(
"/login/"
) } }
大家一定注意到了,虽然用了mysql,但是没有要求在使用前手工去导入建表或插入初始化值的sql脚本,这是为神马呢?
因为我们使用了ORM,ORM会帮我们自动完成上面所说的操作,如下代码所示:
/ 因篇幅太长,省略部分代码,详细代码请查看github
func NewSite(ctx *macaron.Context, sess session.Store, x csrf.CSRF) { if sess.Get("uid") != "" {
ctx.Data["csrf_token"] = x.GetToken()
ctx.HTML(200, "newSite")
} else {
ctx.Redirect("/login/")
}
}
func DoNewSite(ctx *macaron.Context, sess session.Store) {
if sess.Get("uid") != nil {
log.Println(sess.Get("uid"))
siteName := ctx.Req.Form.Get("sitename")
port := ctx.Req.Form.Get("port")
Port, _ := strconv.Atoi(port)
backaddr := ctx.Req.Form.Get("backendaddr")
backendaddr := strings.Split(backaddr, "/r/n")
BackendAddr := make([]string, 0)
for _, v := range backendaddr {
if v == "" {
continue
}
v = strings.TrimSpace(v)
BackendAddr = append(BackendAddr, v)
}
ssl := ctx.Req.Form.Get("ssl")
debugLevel := ctx.Req.Form.Get("debuglevel")
log.Println(siteName, BackendAddr, ssl, debugLevel)
models.NewSite(siteName, Port, BackendAddr, ssl, debugLevel)
ctx.Redirect("/admin/site/list/")
} else {
ctx.Redirect("/login/")
}
}
当ORM、路由处理相关的代码写完后就可以在程序入口中配置路由了,将URL与路由处理的控制器对应起来,如下所示:
// 因篇幅太长,省略部分代码,详细代码请查看github
m .Group( "/admin" , func() {
m.Get(
"/index/"
, routers.Admin)
m .Group( "/site/" , func() {
m .Get( "" , routers.Admin)
m .Get( "/list/" , routers.Admin)
m .Get( "/new/" , routers.NewSite)
m .Post( "/new/" , csrf.Validate, routers.DoNewSite)
m .Get( "/edit/:id" , routers.EditSite)
m .Post( "/edit/:id" , csrf.Validate, routers.DoEditSite)
m .Get( "/del/:id" , routers.DelSite)
m .Get( "/sync/" , routers.SyncSite)
m .Get( "/sync/:id" , routers.SyncSiteById)
m .Get( "/json/" , routers.SiteJSON)
}) })
m .Group( "/api" , func() {
m .Get( "/site/sync/" , routers.SyncSiteApi)
m.Get(
"/rule/sync/"
, routers.SyncRuleApi) })
log .Printf( "xsec waf admin %s" , setting.AppVer)
log .Printf( "Run mode %s" , strings.Title(macaron.Env))
log .Printf( "Server is running on %s" , fmt.Sprintf( "0.0.0.0:%v" , setting.HTTPPort))
log .Println(http.ListenAndServe(fmt.Sprintf( "0.0.0.0:%v", setting.HTTPPort),
m
))
从前有座山,山里有个庙,庙里有个灰帽子小明同学,他有次通过一些不可描述的手段,得到了某个网站的反弹Shell,虽是root权限,但利用方法不稳定。
此时小明发现服务器的内网网卡上跑着一个有root权限的redis,但加了密码,只见小明虎躯一震,顿时有了思路:留个webshell,以后通过webshell来执行redis反弹shell的exp。
但当他看完nginx的配置后又菊花一紧,因为这个站点只跑了lua的web应用:
# 省略部分配置内容
http { include mime.types; default_type application/octet-stream; lua_package_path "/data0/www/a.b.com/?.lua/?.lua;/usr/local/lib/lua/?.lua;;"; lua_code_cache off; init_by_lua_file /data0/www/a.b.com/init.lua; sendfile on; keepalive_timeout 65; server { listen 80; server_name a.b.com; location /api/ { content_by_lua_file /data0/www/a.b.com/web.lua; } } }
请听题
▼▼▼
树上7只猴,地上1只猴,小明如何留一个lua的webshell,请大家贴一下思路及代码。
参与方式:点击下方“写留言”,写出你想到的思路及代码
参与时间:截止至8月21日18:00(要注意参与时间哦~)
【注: 本文为“小米安全中心”原创 作者: netxfly 安全脉搏整理发布】
【本文版权归安全脉搏所有,未经许可不得转载。文章仅代表作者看法,如有不同观点,欢迎添加安全脉搏微信号:SecPulse,进行交流。】