0×00 背景:
接到任务要对webshell做防护,然后就搜罗了各种webshell的检测防护文章,方法很多,但是实践效果却还是不太完美;无意中看到一篇文章,使用文件校验的方式进行主动防御,对php文件(本文指的是可以被解析执行的文件,当然可以把解析路径里面的所有文件做检查)和配置文件进行MD5后保存,隔一段时间对网站的这些文件进行MD5并与保存的结果进行对比,出现不一样的告警,当然新增加的文件肯定是会触发告警的;顿时觉得前面的道路明亮了许多,但是频繁的网站更新会让这种方法有些不完善的地方,怎样才能使它更完美呢?让他成为一种简单而低误报率的检测方式;于是就有了下文;
下面我来说说我的改进方法:
0×01 另辟蹊径:
我们把MD5校验改为自己可以识别的标识符检测–埋点检测;并加入到正常php源码中。
这里强调正常php文件,是因为如果网站之前已经被上传webshell,并且被作为正常php文件处理了,那我就呵呵了。然后使用检测脚本循环对所有php文件进行检测;假如我把检测脚本执行间隔设为1分钟,上传webshell的最大存活时间就为1分钟。检测脚本就不写了,我相信任何一个能看到这篇文章的人都会写出来(python,peal,就是用C写也不会超过100行),脚本性能和实施这里就不讨论了。
0×02 发散思维:
如果有人说他能在触发检测脚本之前使用webshell去尝试拿到埋点并加入webshell,那请继续往下看。
另外一种思路:大家知道像php(还有asp,jsp等)这样的解释性语言,当客户发送HTTP请求一个php文件时,服务器会去解读这个php文件,解读标签中的内容,而并不会像其他文件一样将文件传过去。服务器需要一个解释器去解释这个脚本,解释器需要用真正的编程语言去做,比如C语言,服务器解读之后就会去执行php要求的行为;这样问题就来了,我们可以在进行解读时由解释器对文件的埋点进行检测,检测通过的进行执行,没有通过的告警;这样初始的webshell就不会被执行,通过webshell拿埋点这条路就不通了,这样是不是更好?这次完美了一些。这种方法我没有具体实践过,只是提供一种思路。
前面两点其实用一句话总结:写入埋点,检查它。
埋点有以下特点:
1.对外不可见;埋点由网站开发或是安全人员写入,php中使用注释就可以轻易做到。 2.复杂性;可以是任意的字符串组合(也可以埋炸弹的),检测规则可也以各式各样。 3.不易猜测;每一次上传webshell可以看做一次埋点猜测,失败后,漏洞点就会暴露。 4.安全性,可以定期更换,操作难度不大。
0×03 如果被攻击者拿到网站源码怎么办?
我们就来简单说说埋点的四个特点:
埋点的复杂性,复杂度越高也就越难找到,这样即使是攻击者拿到php源码,也很难获取到它;特征就像密码一样,又比密码更复杂,同时,为了提高复杂性给攻击者更多的迷惑,也可以在埋点处埋入炸弹,这样就可以让攻击者主动暴露webshell。
不易猜测性:攻击者只有一次猜测机会——每一次成功的上传只要埋点错误就是一次免费的漏洞测试,这样网站安全人员就可以及时发现这个漏洞并补上。每个文件的埋点都可以有独立的特征值。不嫌麻烦的可以定期更换,提高安全性。
我们用方法一举个例子,比如:
我们服务器现在有三个php,分别是a.php,b.php,c.php
1.我们给他们都加入三个字符串。“aaaaaa”,“bbbbbb”,“cccccc。 2.设定埋点检测规则为:检测a.php埋点位置的字符串是否是“aaaaaa”,b.php埋点位置的字符串是否是“bbbbbb”和c.php埋点位置的字符串是否是“cccccc”,当然埋点位置和检测的字符串都可以任意调整,只要能达到扰乱和迷惑的效果;规定后面新增的php文件埋点字符串“dddddd”。 3.假设我们现在要更新网站的代码,加入新的php文件,我们就给它加入埋点字符串“dddddd”。
埋点和检测方法都是变量,不好猜测了吧,好了,一个简单的埋点和检测规则设定完了。
我们再假设存在一种情况,攻击者在正常php文件中写入webshell并且没有打乱埋点和正常php功能,或者是直接通过其他途径拿到埋点并写入webshell(能拿到埋点和检测规则的攻击者,我想也就没必要上传webshell了,这种场景不会太多,埋点检测应该是可以应付日常的webshell检测了),这样单一的埋点检测方式就有可能会失效,那我们就引入文件校验作为辅助功能。这次完美了吗?
附上python版本的文件校验脚本(思路参考某位大神的perl脚本);
#!/usr/bin/env python # -*- coding: utf-8 -*-s import hashlib import sys import os import re import time import smtplib from email.mime.text import MIMEText except_dir = [‘/var/www/xxx’] #例外目录,填写绝对路径 contents = [] def send_mail(content): fp = open(‘runlog.txt’,’a’) if content not in contents: contents.append(content) fp.write(content) to_list=["xxx@qq.com"] mail_host="smtp.163.com" mail_user="" mail_pass="" mail_postfix="163.com" me=mail_user+"<"+mail_user+"@"+mail_postfix+">" msg = MIMEText(content) msg[‘Subject’] =‘warning’ msg[‘From’] = me msg[‘To’] = ";".join(to_list) try: s = smtplib.SMTP() s.connect(mail_host) s.login(mail_user,mail_pass) s.sendmail(me, to_list, msg.as_string()) s.close() print ’send email ok’ return True except Exception, e: print str(e) return False def md5Checksum(filePath): fh = open(filePath, ’rb’) m = hashlib.md5() while True: data = fh.read(8192) if not data: break m.update(data) fh.close() return m.hexdigest() def load_filelist(f): f1=open(f,’r’) f_list=[] while 1: line=f1.readline() if not line: break f_list.append(line) dic={} for str1 in f_list: item1,item2= str1.split(‘:’) dic[item1]=item2 f1.close() return dic def save_config(configpath,webdir): f1=open(‘config’,’w’) f1.writelines(‘configpath:’+configpath+’/r/n’) f1.writelines(‘webdir:’+webdir+’/r/n’) f1.close() def find(): lists=[] lists=findchange() for str1 in lists: print str1 def findchange(): relist=[] dic1={} dic1= load_filelist(‘save_hash’) dic2={} dic2=load_filelist(‘config’) weblist=[] weblist=load_all_path(dic2[‘webdir’].replace(‘/r/n’,’’)) weblist.append(str(dic2[‘configpath’].replace(‘/r/n’,’’))) for webpage in weblist: if str(dic1.get(webpage))==‘None’: relist.append(webpage+’ is new file/r/n’) elif str(dic1.get(webpage)).replace(‘/r/n’,’’)!=md5Checksum(webpage): relist.append(webpage+’ has been changed/r/n’) return relist def load_all_path(rootDir): str1=[] list_dirs = os.walk(rootDir) pattern = re.compile(r’.php’,re.IGNORECASE) for root, dirs, files in list_dirs: for f in files: str_php = str(os.path.splitext(f)[1]) match = pattern.match(str_php) if match or str(os.path.splitext(f)[0])==‘.htaccess’: filepath = os.path.join(root, f) if except_dir !=[]: for dir in except_dir: if dir in filepath: pass else: #print filepath str1.append(filepath) else: #print filepath str1.append(filepath) return str1 def save(config,webpath): save_config(config,webpath) confighash=md5Checksum(config) weblist=[] weblist=load_all_path(webpath) #print weblist f1=open(‘save_hash’,’w’) f1.writelines(config+’:’+confighash+"/r/n") for str1 in weblist: print str1 f1.writelines(str1+’:’+md5Checksum(str1)+"/r/n") f1.close() def listen(config,webpath): save(config,webpath) while 1: lists=[] lists=findchange() if(len(lists)!=0): str2=‘‘ for str1 in lists: str2=str1.replace(‘/r/n’,’’)+’/n’ send_mail(str2) time.sleep(60) if __name__ == ’__main__’: banner=‘‘‘usage: find.py -save config webpath find.py -find nohup python find.py -listen config webpath $ Example: python find.py -save /etc/apache2/apache2.conf /var/www python find.py -find nohup python find.py -listen /etc/apache2/apache2.conf /var/www & ’’’ if (len(sys.argv)<2): print banner elif (len(sys.argv)==4 and sys.argv[1]==‘-save’): save(sys.argv[2],sys.argv[3]) elif (len(sys.argv)==2 and sys.argv[1]==‘-find’): find() elif (sys.argv[1]==‘-listen’): listen(sys.argv[2],sys.argv[3]) else : print banner
0×04 写在最后
这两种方法适用于解释性语言,文章以php为例进行说明;本人觉得第二种方法对付webshell最彻底,初始webshell不能执行,是不是就没招了?当然第一种方法使用恰当也能做到,而且实现起来更容易。文章旨在说明一种webshell的防御方法,具体实践中还需要根据自己的环境做调整,或者配合其他的检测方法使用。妈妈再也不担心我被getshell了。
安全是一个整体,方法不是万能的,也许你会有比本文更好的方法,欢迎一起交流,