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)
