完整演示代码请见本书GitHub上的13-2.py以及13-3.py。
WebShell具有很多访问特征,其中和有向图相关的为:
·入度出度均为0;
·入度出度均为1且自己指向自己。
完整处理流程如图13-8所示。
图13-8 HTTP日志数据处理流程
1.数据整理
我们在新安装的WordPress网站下面安装一个简单的后门1.php,如图13-9所示,内容为phpinfo。
图13-9 测试环境中放置的后门文件
Apache默认不记录refer字段,需要修改默认配置,开启HTTPD自定义日志格式,记录User-Agen以及Referer:
<IfModule logio_module> # You need to enable mod_logio.c to use %I and %O LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio </IfModule> CustomLog "logs/access_log" combined
针对1.php的访问日志为:
[root@instance-8lp4smgv logs]# cat access_log | grep 'wp-admin/1.php' 125.33.206.140 - - [26/Feb/2017:13:09:47 +0800] "GET /wordpress/wp-admin/1.php HTTP/1.1" 200 17 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36" 125.33.206.140 - - [26/Feb/2017:13:11:19 +0800] "GET /wordpress/wp-admin/1.php HTTP/1.1" 200 17 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36"
逐行处理日志,生成原始页面请求的URL和该页面refer指向URL的对应关系,聚合结果举例为:
- -> http://180.76.190.79/wordpress/wp-admin/1.php - -> http://180.76.190.79/wordpress/wp-admin/admin-ajax.php - -> http://180.76.190.79/wordpress/wp-admin/customize.php - -> http://180.76.190.79/wordpress/wp-admin/load-styles.php - -> http://180.76.190.79/wordpress/wp-admin/post-new.php - -> http://180.76.190.79/wordpress/wp-login.php http://180.76.190.79/wordpress/ -> http://180.76.190.79/wordpress/wp-admin/edit-comments.php http://180.76.190.79/wordpress/ -> http://180.76.190.79/wordpress/wp-admin/profile.php http://180.76.190.79/wordpress/ -> http://180.76.190.79/wordpress/wp-login.php http://180.76.190.79/wordpress/ -> http://180.76.190.79/wordpress/xmlrpc.php http://180.76.190.79/wordpress/wp-admin/ -> http://180.76.190.79/wordpress/wp
2.数据导入
连接数据库:
driver = GraphDatabase.driver("bolt://localhost:7687",auth=basic_auth("neo4j","maidou")) session = driver.session()
逐行读取,生成节点以及关联关系:
for line in file_object: matchObj = re.match( r'(\S+) -> (\S+)', line, re.M|re.I) if matchObj: path = matchObj.group(1); ref = matchObj.group(2); if path in nodes.keys(): path_node = nodes[path] else: path_node = "Page%d" % index nodes[path]=path_node sql = "create (%s:Page {url:\"%s\" , id:\"%d\",in:0,out:0})" %(path_node,path,index) index=index+1 session.run(sql)
把入度出度作为节点的属性,更新节点的出度入度属性:
if ref in nodes.keys(): ref_node = nodes[ref] else: ref_node = "Page%d" % index nodes[ref]=ref_node sql = "create (%s:Page {url:\"%s\",id:\"%d\",in:0,out:0})" %(ref_node,ref,index) index=index+1 session.run(sql) sql = "create (%s)-[:IN]->(%s)" %(path_node,ref_node) session.run(sql) sql = "match (n:Page {url:\"%s\"}) SET n.out=n.out+1" % path session.run(sql) sql = "match (n:Page {url:\"%s\"}) SET n.in=n.in+1" % ref session.run(sql)
3.查询结果
网页关联关系原始数据如图13-10所示。
图13-10 网页关联关系原始数据
网页关联关系可视化结果如图13-11所示。
图13-11 网页关联关系可视化图
查询入度为1出度均为0的节点或者查询入度出度均为1且指向自己的节点,由于把ref为空的情况也识别为"-"节点,所以入度为1出度均为0。查询满足条件的疑似WebShell链接,如图13-12所示。
图13-12 查询疑似WebShell的链接
在生产环境实际使用中,我们遇到的误报分为以下几种:
·主页,各种index页面;
·Phpmyadmin、Zabbix等运维管理后台;
·Hadoop、ELK等开源软件的控制台;
·API接口。
这些通过短期加白可以有效解决,比较麻烦的是扫描器对结果的影响,这部分需要通过扫描器指纹或者使用高大上的人机算法来去掉干扰。