转载

抛砖引玉:Webshell埋点检测法

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了。

安全是一个整体,方法不是万能的,也许你会有比本文更好的方法,欢迎一起交流,

*本文原创作者:mosnail,属FreeBuf原创奖励计划文章,未经许可禁止转载。

正文到此结束
Loading...