CTF线下awd攻防赛中常用一个文件监控脚本来保护文件,但是就博主对于该脚本的审计分析
发现如下的问题:
1.记录文件的路径未修改导致log暴露
原文件备份文件夹:drops_JWI96TY7ZKNMQPDRUOSG0FLH41A3C5EXVB82/bak_EAR1IBM0JT9HZ75WU4Y3Q8KLPCX26NDFOGVS
(猥琐一点可以直接删除掉,不给对方自行恢复的机会)
文件监控记录文件:drops_JWI96TY7ZKNMQPDRUOSG0FLH41A3C5EXVB82/log_WMY4RVTLAJFB28960SC3KZX7EUP1IHOQN5GD/log.txt
(该文件记录了删除文件记录、恢复文件记录、上传文件记录等,可以用来偷其他战队的一些脚本)
上传文件删除后保存文件夹:drops_JWI96TY7ZKNMQPDRUOSG0FLH41A3C5EXVB82/webshell_WMY4RVTLAJFB28960SC3KZX7EUP1IHOQN5GD
(该脚本会把上传的文件直接删除,并且将上传文件以“原文件名+.txt”保存在该目录下)
被篡改文件备份保存夹:diff_UMTGPJO17F82K35Z0LEDA6QB9WH4IYRXVSCN/diff_UMTGPJO17F82K35Z0LEDA6QB9WH4IYRXVSCN
(被修改文件会被恢复,并且篡改后的文件会保存到该文件夹下,文件名为“原文件名+.txt”)
2.多次篡改同一文件可成功篡改
根据本人对于脚本代码的审计,发现作者在恢复被篡改文件时错误使用了move函数
即变成了将备份中的被篡改文件移动到原文件目录下,使得备份中的被篡改文件消失
当同一文件被篡改两次时,脚本便无法从备份中再找到被篡改文件的备份(第一次篡改恢复时备份已经移动到了原文件目录下)
问题代码定位在这里:
shutil.move(filekey, os.path.join(Special_path['difffile'], ntpath.basename(filekey) + '.txt'))
shutil.move(os.path.join(Special_path['bak'], ntpath.basename(filekey)), filekey)#这里直接使用了move将备份的文件移动至原目录下,导致备份中该文件消失,在第二次篡改时便无法再从备份中move相同的文件
3.删除文件后脚本无法恢复文件
这里在博主修复时发现了比较尴尬的一点,文件一旦被删除恢复后会被另一个监控上传文件的功能当作新上传的文件直接删除,形成了一个条件竞争,
在对代码进行分析后,发现该脚本是通过白名单来监控文件的,解决方案就是在恢复删除文件之后将该文件重新加入白名单
考虑到以上的问题,博主对脚本进行了一次修复优化,优化后的文件监控脚本源码如下:
1 # -*- coding: utf-8 -*-
2 import os
3 import re
4 import hashlib
5 import shutil
6 import ntpath
7 import time
8 import sys
9
10 # 设置系统字符集,防止写入log时出现错误
11 reload(sys)
12 sys.setdefaultencoding('utf-8')
13 CWD = os.getcwd()
14 FILE_MD5_DICT = {} # 文件MD5字典
15 ORIGIN_FILE_LIST = []
16
17 # 特殊文件路径字符串
18 Special_path_str = 'drops_B0503373BDA6E3C5CD4E5118C02ED13A' #drops_md5(icecoke1024)
19 bakstring = 'back_CA7CB46E9223293531C04586F3448350' #bak_md5(icecoke1)
20 logstring = 'log_8998F445923C88FF441813F0F320962C' #log_md5(icecoke2)
21 webshellstring = 'webshell_988A15AB87447653EFB4329A90FF45C5'#webshell_md5(icecoke3)
22 difffile = 'difference_3C95FA5FB01141398896EDAA8D667802' #diff_md5(icecoke4)
23
24 Special_string = 'drops_log' # 免死金牌
25 UNICODE_ENCODING = "utf-8"
26 INVALID_UNICODE_CHAR_FORMAT = r"\?%02x"
27
28 # 文件路径字典
29 spec_base_path = os.path.realpath(os.path.join(CWD, Special_path_str))
30 Special_path = {
31 'bak' : os.path.realpath(os.path.join(spec_base_path, bakstring)),
32 'log' : os.path.realpath(os.path.join(spec_base_path, logstring)),
33 'webshell' : os.path.realpath(os.path.join(spec_base_path, webshellstring)),
34 'difffile' : os.path.realpath(os.path.join(spec_base_path, difffile)),
35 }
36
37 def isListLike(value):
38 return isinstance(value, (list, tuple, set))
39
40 # 获取Unicode编码
41 def getUnicode(value, encoding=None, noneToNull=False):
42
43 if noneToNull and value is None:
44 return NULL
45
46 if isListLike(value):
47 value = list(getUnicode(_, encoding, noneToNull) for _ in value)
48 return value
49
50 if isinstance(value, unicode):
51 return value
52 elif isinstance(value, basestring):
53 while True:
54 try:
55 return unicode(value, encoding or UNICODE_ENCODING)
56 except UnicodeDecodeError, ex:
57 try:
58 return unicode(value, UNICODE_ENCODING)
59 except:
60 value = value[:ex.start] + "".join(INVALID_UNICODE_CHAR_FORMAT % ord(_) for _ in value[ex.start:ex.end]) + value[ex.end:]
61 else:
62 try:
63 return unicode(value)
64 except UnicodeDecodeError:
65 return unicode(str(value), errors="ignore")
66
67 # 目录创建
68 def mkdir_p(path):
69 import errno
70 try:
71 os.makedirs(path)
72 except OSError as exc:
73 if exc.errno == errno.EEXIST and os.path.isdir(path):
74 pass
75 else: raise
76
77 # 获取当前所有文件路径
78 def getfilelist(cwd):
79 filelist = []
80 for root,subdirs, files in os.walk(cwd):
81 for filepath in files:
82 originalfile = os.path.join(root, filepath)
83 if Special_path_str not in originalfile:
84 filelist.append(originalfile)
85 return filelist
86
87 # 计算机文件MD5值
88 def calcMD5(filepath):
89 try:
90 with open(filepath,'rb') as f:
91 md5obj = hashlib.md5()
92 md5obj.update(f.read())
93 hash = md5obj.hexdigest()
94 return hash
95 # 文件MD5消失即为文件被删除,恢复文件
96 except Exception, e:
97 print u'[*] 文件被删除 : ' + getUnicode(filepath)
98 shutil.copyfile(os.path.join(Special_path['bak'], ntpath.basename(filepath)), filepath)
99 for value in Special_path:
100 mkdir_p(Special_path[value])
101 ORIGIN_FILE_LIST = getfilelist(CWD)
102 FILE_MD5_DICT = getfilemd5dict(ORIGIN_FILE_LIST)
103 print u'[+] 被删除文件已恢复!'
104 try:
105 f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
106 f.write('deleted_file: ' + getUnicode(filepath) + ' 时间: ' + getUnicode(time.ctime()) + '\n')
107 f.close()
108 except Exception as e:
109 print u'[-] 记录失败 : 被删除文件: ' + getUnicode(filepath)
110 pass
111
112 # 获取所有文件MD5
113 def getfilemd5dict(filelist = []):
114 filemd5dict = {}
115 for ori_file in filelist:
116 if Special_path_str not in ori_file:
117 md5 = calcMD5(os.path.realpath(ori_file))
118 if md5:
119 filemd5dict[ori_file] = md5
120 return filemd5dict
121
122 # 备份所有文件
123 def backup_file(filelist=[]):
124 for filepath in filelist:
125 if Special_path_str not in filepath:
126 shutil.copy2(filepath, Special_path['bak'])
127
128 if __name__ == '__main__':
129 print u'---------持续监测文件中------------'
130 for value in Special_path:
131 mkdir_p(Special_path[value])
132 # 获取所有文件路径,并获取所有文件的MD5,同时备份所有文件
133 ORIGIN_FILE_LIST = getfilelist(CWD)
134 FILE_MD5_DICT = getfilemd5dict(ORIGIN_FILE_LIST)
135 backup_file(ORIGIN_FILE_LIST)
136 print u'[*] 所有文件已备份完毕!'
137 while True:
138 file_list = getfilelist(CWD)
139 # 移除新上传文件
140 diff_file_list = list(set(file_list) ^ set(ORIGIN_FILE_LIST))
141 if len(diff_file_list) != 0:
142 for filepath in diff_file_list:
143 try:
144 f = open(filepath, 'r').read()
145 except Exception, e:
146 break
147 if Special_string not in f:
148 try:
149 print u'[*] 查杀疑似WebShell上传文件: ' + getUnicode(filepath)
150 shutil.move(filepath, os.path.join(Special_path['webshell'], ntpath.basename(filepath) + '.txt'))
151 print u'[+] 新上传文件已删除!'
152 except Exception as e:
153 print u'[!] 移动文件失败, "%s" 疑似WebShell,请及时处理.'%getUnicode(filepath)
154 try:
155 f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
156 f.write('new_file: ' + getUnicode(filepath) + ' 时间: ' + str(time.ctime()) + '\n')
157 f.close()
158 except Exception as e:
159 print u'[-] 记录失败 : 上传文件: ' + getUnicode(e)
160
161 # 防止任意文件被修改,还原被修改文件
162 md5_dict = getfilemd5dict(ORIGIN_FILE_LIST)
163 for filekey in md5_dict:
164 if md5_dict[filekey] != FILE_MD5_DICT[filekey]:
165 try:
166 f = open(filekey, 'r').read()
167 except Exception, e:
168 break
169 if Special_string not in f:
170 try:
171 print u'[*] 该文件被修改 : ' + getUnicode(filekey)
172 shutil.move(filekey, os.path.join(Special_path['difffile'], ntpath.basename(filekey) + '.txt'))
173 shutil.copyfile(os.path.join(Special_path['bak'], ntpath.basename(filekey)), filekey)
174 print u'[+] 文件已复原!'
175 except Exception as e:
176 print u'[!] 移动文件失败, "%s" 疑似WebShell,请及时处理.'%getUnicode(filekey)
177 try:
178 f = open(os.path.join(Special_path['log'], 'log.txt'), 'a')
179 f.write('difference_file: ' + getUnicode(filekey) + ' 时间: ' + getUnicode(time.ctime()) + '\n')
180 f.close()
181 except Exception as e:
182 print u'[-] 记录失败 : 被修改文件: ' + getUnicode(filekey)
183 pass
184 time.sleep(2)