抛砖引玉: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
2
3
4
1.对外不可见;埋点由网站开发或是安全人员写入,php中使用注释就可以轻易做到。
2.复杂性;可以是任意的字符串组合(也可以埋炸弹的),检测规则可也以各式各样。
3.不易猜测;每一次上传webshell可以看做一次埋点猜测,失败后,漏洞点就会暴露。
4.安全性,可以定期更换,操作难度不大。

0×03 如果被攻击者拿到网站源码怎么办?

我们就来简单说说埋点的四个特点:

埋点的复杂性,复杂度越高也就越难找到,这样即使是攻击者拿到php源码,也很难获取到它;特征就像密码一样,又比密码更复杂,同时,为了提高复杂性给攻击者更多的迷惑,也可以在埋点处埋入炸弹,这样就可以让攻击者主动暴露webshell。

不易猜测性:攻击者只有一次猜测机会——每一次成功的上传只要埋点错误就是一次免费的漏洞测试,这样网站安全人员就可以及时发现这个漏洞并补上。每个文件的埋点都可以有独立的特征值。不嫌麻烦的可以定期更换,提高安全性。

我们用方法一举个例子,比如:

我们服务器现在有三个php,分别是a.php,b.php,c.php

1
2
3
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脚本);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/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了。

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