├── CVE-2020-5902_bigip_ioc_checker.py └── README.md /CVE-2020-5902_bigip_ioc_checker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """This script is intended to search the BIG-IP for potential 3 | Indicators of Compromise (IoCs) relating to CVE-2020-5902""" 4 | 5 | # Copyright 2019 F5 Networks, Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # Author: 19 | # Sr ENE Bean Wu, 20 | # Sp Support Engineer Kidd Wang, 21 | # Sr Security Engineer David Wang, 22 | # Sr Security Engineer Aaron Brailsford 23 | 24 | # Version: CVE-2020-5902_bigip_ioc_checker.py v1.5 25 | 26 | import sys 27 | import os 28 | import re 29 | import gzip 30 | import getopt 31 | import base64 32 | 33 | try: 34 | from hashlib import md5 35 | except ImportError: 36 | from md5 import md5 37 | 38 | try: 39 | all 40 | except NameError: 41 | def all(iterable): 42 | for element in iterable: 43 | if not element: 44 | return False 45 | return True 46 | try: 47 | any 48 | except NameError: 49 | def any(s): 50 | for v in s: 51 | if v: 52 | return True 53 | return False 54 | 55 | 56 | class Util(object): 57 | """Small utility class to make printing to screen easier, handles 58 | selective colourization of messages (for output to a terminal vs. 59 | redirected output to a file) and header lines""" 60 | def __init__(self, disable_color_print): 61 | self.output = "" 62 | self.disable_color_print = disable_color_print 63 | 64 | def exec_cmd(self, cmd): 65 | # need to compatible with python 2.4 no finally 66 | try: 67 | self.output = os.popen(cmd).read() 68 | return self.output 69 | except Exception: 70 | t, e = sys.exc_info()[:2] 71 | print("CMD Error: %s %s" % (e, cmd)) 72 | return self.output 73 | 74 | def print_msg(self, message, key_words=None): 75 | """Prints a colourized message out to stdout, used to highlight important 76 | messages""" 77 | if not self.disable_color_print: 78 | color_begin = '\033[1;31m' 79 | color_end = '\033[0m' 80 | if key_words: 81 | message = message.replace(key_words, color_begin + key_words + color_end) 82 | print(message) 83 | 84 | @staticmethod 85 | def head_line(message): 86 | """Prints the 'headline' message, i.e. used to indicate sections of checks""" 87 | n = int((80 - len(message)) / 2) 88 | title = r"%s %s %s" % ('=' * n, message, '=' * n) 89 | return title 90 | 91 | 92 | class Checker(object): 93 | """This is essentially the main program logic; we take input of which checks to skip 94 | and which to run, then run through them one-at-a-time to check the system for 95 | the defined potential IoCs""" 96 | def __init__(self, checks_to_skip): 97 | self.checks_to_skip = checks_to_skip 98 | self.blacklist = ['.php', '.jsp', 'fsockopen', 'bash -i', 'nc -e', 'use Socket', 99 | 'socket.socket', 'curl ', 'wget ', 'base64', 'nmap ', 'passthru', 100 | 'shell_exec', 'phpinfo', 'base64_decode', 'fopen', 'fclose', 101 | 'readfile', 'php_uname', 'eval'] 102 | self.fixed_version = {'16': 0, '15': 104, 103 | '14': 126, '13': 134, '12': 152, '11': 652} 104 | self.bug_id = '895525' 105 | self.sys_eicheck_hash = ['68f669cc7e164ad70b87a1f08b6094f5', 106 | '77280f2ba2ae018c63ccb2fac65bc32e', 107 | '9a7b875ae839d4690d27e18ad02ac3fb'] 108 | self.upgrade_time = '' 109 | self.is_recent_upgrade = False 110 | self.backdoor_flag = False 111 | self.patched_flag = False 112 | self.attacked_flag = False 113 | self.base64_commnd_count = 0 114 | self.attack_msg_header = '!! IoC pattern' 115 | self.result = { 116 | 'version': '', 117 | 'hf': '', 118 | } 119 | self.tmp_normal_file = [ 120 | '/tmp/._f5_Debug_Control.mmap', 121 | '/tmp/hosts_script', 122 | '/tmp/pci_ethmap', 123 | '/tmp/new-license', 124 | '/tmp/pci_order_ethmap', 125 | '/tmp/bwc-tmm.mp.bwcdist', 126 | '/tmp/old-license', 127 | '/tmp/sshd_login.sh', 128 | '/tmp/.rnd', 129 | '/tmp/BigDB.dat.orig', 130 | '/tmp/localdbmgr_tmp.out', 131 | '/tmp/dhclient.sh', 132 | '/tmp/.s.PGSQL.5432.lock', 133 | '/tmp/httpd_reconfig.pl', 134 | '/tmp/mgmt_dhclient.sh', 135 | '/tmp/systemauth.pl.null' 136 | ] 137 | 138 | def __get_version(self): 139 | # read system os version, build and enghf ID 140 | util = Util(self.checks_to_skip['disable_color_print']) 141 | version_output = util.exec_cmd('tmsh show sys version') 142 | version = '0' 143 | hf = '0' 144 | if version_output: 145 | for line in version_output.split('\n'): 146 | if ' Version' in line: 147 | version = line.strip().split(' ')[-1] 148 | elif ' Build' in line: 149 | hf = line.strip().split(' ')[-1] 150 | self.result['version'] = version 151 | self.result['hf'] = hf 152 | return version_output 153 | 154 | def __check_version(self, version_output): 155 | # check if current version fixed CVE-2020-5902 156 | if self.result['version']: 157 | main = self.result['version'].split('.')[0] 158 | miner = ''.join(self.result['version'].split('.')[1:]) 159 | if len(miner) == 2: 160 | miner += '0' 161 | if int(miner) < self.fixed_version[main] and self.bug_id not in version_output: 162 | return False 163 | return True 164 | return False 165 | 166 | def __check_version_info(self): 167 | util = Util(self.checks_to_skip['disable_color_print']) 168 | version_output = self.__get_version() 169 | is_patched = self.__check_version(version_output) 170 | self.patched_flag = is_patched 171 | util.print_msg( 172 | '[+] Version %s Build %s CVE-2020-5902 Fixed: %s' 173 | % (self.result['version'], self.result['hf'], is_patched) 174 | ) 175 | if is_patched: 176 | util.print_msg( 177 | '[+] Running non-vulnerable version, ' 178 | 'double check if any attack happened before upgrading', 179 | 'Running non-vulnerable version,' 180 | ' double check if any attack happened before upgrading') 181 | 182 | def __home_check(self): 183 | util = Util(self.checks_to_skip['disable_color_print']) 184 | content = util.exec_cmd("ls -al /home/ 2>/dev/null |grep -e systems -e bigipuser ") 185 | if content: 186 | title = util.head_line(' /home/ checking ') 187 | message = "%s malicious user created in /home/:\n%s" \ 188 | % (self.attack_msg_header, content) 189 | util.print_msg("\n[+] %s" % title) 190 | util.print_msg(message, content) 191 | 192 | def __check_each_audit(self, log_file): 193 | util = Util(self.checks_to_skip['disable_color_print']) 194 | # Get log file content 195 | if log_file.endswith('.gz'): 196 | f = gzip.open(log_file, 'rb') 197 | content = f.read() 198 | f.close() 199 | else: 200 | f = open(log_file, 'rb') 201 | content = f.read() 202 | f.close() 203 | result = {} 204 | 205 | admin_auditlog_pattern = re.compile(r'list\s+/?\s*auth\s+user\s+admin') 206 | for line in content.split('\n'): 207 | regex_result = admin_auditlog_pattern.search(str(line), re.I) 208 | if regex_result and 'user=admin' in line: 209 | result['list_admin_auditlog'] = [log_file] 210 | self.attacked_flag = True 211 | break 212 | 213 | # attacker can only change tmsh modify, create, list and delete to alias bash! 214 | alias_pattern = re.compile(r'create\s+/?\s*cli\s+alias') 215 | cmd_bash_pattern = re.compile(r'command\s+bash') 216 | cli_alias_bash_pattern = re.compile(r'cli_shared_alias_command\s+"bash"') 217 | for line in content.split('\n'): 218 | line = line.lower() 219 | is_curd = 'list ' in line or 'delete ' in line or 'modify ' in line or 'create ' in line 220 | is_cli_alias = alias_pattern.search(line, re.I) 221 | is_cmd_bash = cmd_bash_pattern.search(line, re.I) 222 | is_cli_alias_bash = cli_alias_bash_pattern.search(line, re.I) 223 | if (is_cli_alias and is_curd and is_cmd_bash) or is_cli_alias_bash: 224 | self.attacked_flag = True 225 | result['create_alias_auditlog'] = [log_file] 226 | break 227 | 228 | # check if run util bash command contains blacklisted word. 229 | cmd_check_pattern = re.compile(r"run\s+/?\s*util\s+bash.*") 230 | if not self.checks_to_skip['bigiq_cmd_check']: 231 | for line in content.split('\n'): 232 | if 'base64 ' in line: 233 | self.base64_commnd_count += 1 234 | cmd = cmd_check_pattern.search(str(line), re.I) 235 | if cmd and any(word in line for word in self.blacklist): 236 | self.attacked_flag = True 237 | if 'run_bash_command' not in result.keys(): 238 | result['run_bash_command'] = [cmd.group(0)] 239 | if not cmd.group(0) in result['run_bash_command']: 240 | result['run_bash_command'].append(cmd.group(0)) 241 | else: 242 | try: 243 | bigiq_blacklist = [] 244 | for word in self.blacklist: 245 | if word != 'base64': 246 | bigiq_blacklist.append(word) 247 | util_bash_pattern = re.compile(r'run\s+/?\s*util\s+bash.*') 248 | base64_pattern = re.compile(r'run\s+/?\s*util\s+bash.*base64\s+-{1,2}d') 249 | for line in content.split('\n'): 250 | regex_bash = util_bash_pattern.search(str(line), re.I) 251 | # highlight_part = 'malicious bash command found' 252 | if regex_bash and any(word in line for word in bigiq_blacklist): 253 | self.attacked_flag = True 254 | if 'run_bash_command' not in result.keys(): 255 | result['run_bash_command'] = [regex_bash.group(0)] 256 | if not regex_bash.group(0) in result['run_bash_command']: 257 | result['run_bash_command'].append(regex_bash.group(0)) 258 | elif regex_bash and 'base64' in line: 259 | regex_base64 = base64_pattern.search(line, re.I) 260 | match = regex_base64.group(0) 261 | tmp = match.replace('|', '').replace('"', '').replace("'", '') 262 | tmp = re.sub(r'\s+', ' ', tmp) 263 | base64_str = tmp.split(' ')[-3] 264 | data = base64.b64decode(base64_str) 265 | if any(word in data for word in self.blacklist): 266 | self.attacked_flag = True 267 | if 'run_bash_command' not in result.keys(): 268 | result['run_bash_command'] = [match] 269 | if not match in result['run_bash_command']: 270 | result['run_bash_command'].append(match) 271 | except Exception: 272 | t, e = sys.exc_info()[:2] 273 | util.print_msg("Base64 Error: %s %s" % (e, match)) 274 | 275 | # check if user systems was created in audit log 276 | regex_bigip = re.search(r'auth\s+user\s+systems', content, re.I) 277 | regex_mcpd = re.search(r'user_role_partition_user\s+"systems"', content, re.I) 278 | if regex_bigip or regex_mcpd: 279 | self.attacked_flag = True 280 | result['create_user_systems_auditlog'] = { 281 | 'systems': [] 282 | } 283 | result['create_user_systems_auditlog']['systems'].append(log_file) 284 | 285 | # check if user bigipuserX was created in audit log 286 | regex_bigip = re.findall(r'auth\s+user\s+(bigipuser\w+)', content, re.I) 287 | regex_mcpd = re.findall(r'user_role_partition_user\s+"(bigipuser\w+)', content, re.I) 288 | if regex_bigip and regex_mcpd: 289 | regex_bigip = list(set(regex_bigip)) 290 | regex_mcpd = list(set(regex_mcpd)) 291 | if len(regex_bigip) > len(regex_mcpd): 292 | regex_result = regex_bigip 293 | else: 294 | regex_result = regex_mcpd 295 | elif regex_bigip: 296 | regex_result = list(set(regex_bigip)) 297 | elif regex_mcpd: 298 | regex_result = list(set(regex_mcpd)) 299 | else: 300 | regex_result = None 301 | 302 | if regex_result: 303 | self.attacked_flag = True 304 | if 'create_user_systems_auditlog' not in result.keys(): 305 | result['create_user_systems_auditlog'] = {} 306 | for user in regex_result: 307 | if not user in result['create_user_systems_auditlog'].keys(): 308 | result['create_user_systems_auditlog'][user] = [log_file] 309 | else: 310 | result['create_user_systems_auditlog'][user].append(log_file) 311 | 312 | # check if command "run /util bash /tmp/" was run in tmsh 313 | regex_result = re.search(r'run\s+/?\s*util\s+bash\s+/tmp', content, re.I) 314 | if regex_result: 315 | self.attacked_flag = True 316 | result['run_bash_auditlog'] = [log_file] 317 | 318 | if 'user "%tmui"' in content: 319 | self.attacked_flag = True 320 | result['user_tmui'] = [log_file] 321 | return result 322 | 323 | def __audit_check(self): 324 | # find all of audit logs 325 | util = Util(self.checks_to_skip['disable_color_print']) 326 | cmd_output = util.exec_cmd( 327 | 'find /var/log/ -name "audit*" -type f 2>/dev/null') 328 | audit_logs = cmd_output.strip().split('\n') 329 | check_result = {} 330 | if audit_logs: 331 | for audit_log in audit_logs: 332 | if 'audit' in audit_log: 333 | single_result = self.__check_each_audit(audit_log) 334 | 335 | if not check_result: 336 | check_result = single_result 337 | else: 338 | for single_key, single_value in single_result.items(): 339 | if single_key not in check_result.keys(): 340 | check_result[single_key] = single_value 341 | else: 342 | if isinstance(single_value, dict): 343 | for sec_key, sec_value in single_value.items(): 344 | if sec_key not in check_result[single_key].keys(): 345 | check_result[single_key][sec_key] = sec_value 346 | else: 347 | check_result[single_key][sec_key].extend(sec_value) 348 | else: 349 | check_result[single_key].extend(single_value) 350 | title = util.head_line('log and config checking') 351 | util.print_msg("\n[+] %s" % title) 352 | if 'list_admin_auditlog' in check_result.keys(): 353 | message = r'%s "list auth user admin" found in files %s' % \ 354 | (self.attack_msg_header, str(check_result['list_admin_auditlog'])) 355 | util.print_msg(message, 'list auth user admin') 356 | if 'create_user_systems_auditlog' in check_result.keys(): 357 | for name in check_result['create_user_systems_auditlog'].keys(): 358 | log_files = str(check_result['create_user_systems_auditlog'][name]) 359 | message = r'%s user "%s" created in files %s' % \ 360 | (self.attack_msg_header, name, log_files) 361 | util.print_msg(message, name) 362 | if 'create_alias_auditlog' in check_result.keys(): 363 | log_files = str(check_result['create_alias_auditlog']) 364 | message = r'%s cli alias found in files %s' % (self.attack_msg_header, log_files) 365 | util.print_msg(message, 'cli alias') 366 | if 'run_bash_auditlog' in check_result.keys(): 367 | log_files = str(check_result['run_bash_auditlog']) 368 | message = r'%s "run /util bash /tmp" found in files %s' % \ 369 | (self.attack_msg_header, log_files) 370 | util.print_msg(message, "run /util bash /tmp") 371 | if 'user_tmui' in check_result.keys(): 372 | log_files = str(check_result['user_tmui']) 373 | message = r'%s user %%tmui error found in files %s' % \ 374 | (self.attack_msg_header, log_files) 375 | util.print_msg(message, 'user %tmui') 376 | if 'run_bash_command' in check_result.keys(): 377 | commands = str(check_result['run_bash_command']) 378 | message = r'%s malicious bash command found: %s' % (self.attack_msg_header, commands) 379 | util.print_msg(message, 'malicious bash command found') 380 | if self.base64_commnd_count > 10: 381 | msg = "!! %s duplicated bash commands are detected as malicious"\ 382 | " because of the base64 encoding script! Those could be false positives"\ 383 | " on a BIG-IP system managed by BIG-IQ. You can use option -q"\ 384 | " to ignore some legitimate base64 encoding scripts sent from BIG-IQ."\ 385 | %self.base64_commnd_count 386 | util.print_msg(msg, msg) 387 | 388 | def __journal_check(self): 389 | # checking if journal log contains '..;', '/hsqldb;' and '/hsqldb%0a' 390 | journal_check_result = {'hsqldb': False, 'semicolon': False} 391 | if os.path.exists('/bin/journalctl'): 392 | util = Util(self.checks_to_skip['disable_color_print']) 393 | content = util.exec_cmd( 394 | "journalctl /bin/logger | grep -e ';' -e 'hsqldb'") 395 | for line in content.split('\n'): 396 | is_hsqldb = 'hsqldb;' in line or "hsqldb\\n" in line 397 | is_not_404 = ' 404 ' not in line 398 | is_ssl_acc = '[ssl_acc]' in line 399 | is_semicolon = '..;' in line 400 | if not journal_check_result['hsqldb']: 401 | if is_hsqldb and is_not_404 and is_ssl_acc: 402 | self.attacked_flag = True 403 | message = r"%s request to /hsqldb found in journal log, Run command " \ 404 | r"'/bin/journalctl /bin/logger|grep hsqldb' " \ 405 | % self.attack_msg_header 406 | if self.patched_flag: 407 | message = r"""%s -- Could be false positive if the system has been patched, 408 | need to check log time and patching time! 409 | """ % message 410 | util.print_msg(message, '/hsqldb') 411 | journal_check_result['hsqldb'] = True 412 | if not journal_check_result['semicolon']: 413 | if is_semicolon and is_not_404 and is_ssl_acc: 414 | self.attacked_flag = True 415 | message = r"""%s '..;' found in journal log, Run command '/bin/journalctl /bin/logger|grep "\\.\\.;"' 416 | """ % self.attack_msg_header 417 | if self.patched_flag: 418 | message = r"""%s -- Could be false positive if the system has been patched, 419 | need to check log time and patching time! 420 | """ % message 421 | util.print_msg(message, r"'..;'") 422 | journal_check_result['semicolon'] = True 423 | 424 | def __systems_check(self): 425 | if os.path.exists('/config/bigip_user.conf'): 426 | f = open('/config/bigip_user.conf') 427 | content = f.read() 428 | f.close() 429 | 430 | util = Util(self.checks_to_skip['disable_color_print']) 431 | # check if user systems has been created in /config/bigip_user.conf 432 | regex_result = re.search(r"user\s+systems", content, re.I) 433 | if regex_result: 434 | self.attacked_flag = True 435 | message = '%s user "systems" created in bigip_user.conf, need to confirm with operator' % self.attack_msg_header 436 | util.print_msg(message, r"systems") 437 | 438 | # check if user bigipuserX has been created in /config/bigip_user.conf 439 | regex_result = re.findall(r"user\s+(bigipuser\w+)", content, re.I) 440 | if regex_result: 441 | self.attacked_flag = True 442 | for name in regex_result: 443 | message = '%s user "%s" created in bigip_user.conf, need to confirm with operator' % (self.attack_msg_header, name) 444 | util.print_msg(message, name) 445 | 446 | def __alias_check(self): 447 | # attacker can only change tmsh modify, create, list and delete to alias bash! 448 | util = Util(self.checks_to_skip['disable_color_print']) 449 | content = util.exec_cmd("awk '/^cli.alias/,/^}/' /config/bigip_*.conf 2>/dev/null") 450 | is_tmsh_curd = re.search(r"list|modify|create|delete\s+{", content, re.I) 451 | is_cmd_bash = re.search(r"command\s+bash", content, re.I) 452 | if is_tmsh_curd and is_cmd_bash: 453 | self.attacked_flag = True 454 | content = content.replace('\n', ' ') 455 | message = "%s cli alias found in config file as below:\n %s" \ 456 | % (self.attack_msg_header, content) 457 | util.print_msg(message, content) 458 | 459 | def __catalina_check(self): 460 | # When fileSave.jsp and fileRead.jsp could not read or 461 | # save file due to permission or file not found error, Catalina will log it. 462 | util = Util(self.checks_to_skip['disable_color_print']) 463 | output = util.exec_cmd( 464 | "zgrep -e 'workspace.fileSave_jsp] in context with path' -e " 465 | "'workspace.fileRead_jsp] in context with path' -A 1 /var/log/tomcat/catalina.out* " 466 | "2>/dev/null|grep FileNotFoundException " 467 | ) 468 | catalina_check_result = {} 469 | if len(output) > 5: 470 | for line in output.split('\n'): 471 | if 'FileNotFoundException' in line and '/' in ''.join(line.split(':')[1:]): 472 | self.attacked_flag = True 473 | file = line.split(' ')[1] 474 | log = line.split(' ')[0].split(':')[0] 475 | if not log in catalina_check_result: 476 | catalina_check_result[log] = [] 477 | if not file in catalina_check_result[log]: 478 | catalina_check_result[log].append(file) 479 | if catalina_check_result: 480 | for log in catalina_check_result: 481 | message = "%s %s access denied in file %s" % (self.attack_msg_header, catalina_check_result[log], log) 482 | util.print_msg(message, str(catalina_check_result[log])) 483 | 484 | def __bigipstartup_check(self): 485 | # check if /config/startup contains blacklisted words 486 | bigipstartup_check_result = [] 487 | util = Util(self.checks_to_skip['disable_color_print']) 488 | if os.path.exists('/config/startup'): 489 | f = open('/config/startup') 490 | content = f.read() 491 | f.close() 492 | for line in content.split('\n'): 493 | if not line.startswith('#') and \ 494 | any(i in line for i in self.blacklist) and \ 495 | line.strip() not in bigipstartup_check_result: 496 | self.attacked_flag = True 497 | bigipstartup_check_result.append(line.strip()) 498 | highlight_part = line.strip() 499 | message = r"%s Possible backdoor %s in file /config/startup" \ 500 | % (self.attack_msg_header, highlight_part) 501 | util.print_msg(message, highlight_part) 502 | 503 | def __get_liveinstalltime(self): 504 | util = Util(self.checks_to_skip['disable_color_print']) 505 | if os.path.exists('/var/log/liveinstall.log'): 506 | cmd_output = util.exec_cmd( 507 | r"grep 'install start at' " 508 | r"/var/log/liveinstall.log|grep -Po '\d+/\d+/\d+'") 509 | self.upgradetime = cmd_output.strip() 510 | if '/' in self.upgradetime: 511 | year = self.upgradetime.split('/')[0] 512 | month = self.upgradetime.split('/')[1] 513 | date = self.upgradetime.split('/')[2] 514 | if year == '2020': 515 | if month == '06' and int(date) > 29: 516 | self.is_recent_upgrade = True 517 | elif int(month) > 6: 518 | self.is_recent_upgrade = True 519 | else: 520 | self.is_recent_upgrade = False 521 | else: 522 | self.is_recent_upgrade = False 523 | 524 | def __webshell_check(self): 525 | # check Files created after 2020 Jun 29 in /usr/local/www/. Could be webshell 526 | util = Util(self.checks_to_skip['disable_color_print']) 527 | util.exec_cmd('/bin/touch -t 202006290100 /tmp/kbtime') 528 | web_shell_self_result = util.exec_cmd( 529 | 'find /usr/local/www/ -type f -newer /tmp/kbtime -ls|grep -v /usr/local/www/xui/framework/scripts/variables.js') 530 | if web_shell_self_result: 531 | self.backdoor_flag = True 532 | title = util.head_line(' webshell checking ') 533 | util.print_msg('\n[+]' + title) 534 | util.print_msg( 535 | '!! Files created in /usr/local/www/ after 2020 Jun 29, ' 536 | 'need to check if those are webshell or information leakage') 537 | print(web_shell_self_result) 538 | if self.is_recent_upgrade: 539 | msg = "!! System OS installed on %s, false positive possible." % self.upgradetime 540 | highlight_part = "System OS installed on %s, " \ 541 | "false positive possible" % self.upgradetime 542 | util.print_msg(msg, highlight_part) 543 | if len(web_shell_self_result.split('\n')) > 20: 544 | msg = "!! Too many files detected, You can use option -w to skip webshell check if those are false positive" 545 | util.print_msg(msg, msg) 546 | 547 | def __autostart_check(self): 548 | # check Files created after 2020 Jun 29 in /etc/. Could be autostart script 549 | util = Util(self.checks_to_skip['disable_color_print']) 550 | util.exec_cmd('/bin/touch -t 202006290100 /tmp/kbtime') 551 | auto_self_result = util.exec_cmd( 552 | "(find /etc/ -type f -newer /tmp/kbtime -ls; " 553 | "find /var/spool/cron -type f -newer /tmp/kbtime -ls) " 554 | "|grep -e 'init.d' -e 'modules' -e cron -e 'rc.local'" 555 | "|grep -v /etc/selinux/targeted") 556 | if auto_self_result: 557 | self.backdoor_flag = True 558 | title = util.head_line(' auto start script checking ') 559 | util.print_msg('\n[+]' + title) 560 | util.print_msg( 561 | '!! Files created in /etc/ after 2020 Jun 29, need ' 562 | 'to check if those are malicious daemon startup script') 563 | if self.is_recent_upgrade: 564 | self.attacked_flag = True 565 | msg = "!! System OS installed on %s, false positive possible" % self.upgradetime 566 | highlight_part = "System OS installed on %s, " \ 567 | "false positive possible" % self.upgradetime 568 | util.print_msg(msg, highlight_part) 569 | util.print_msg(auto_self_result) 570 | 571 | def __tmp_check(self): 572 | util = Util(self.checks_to_skip['disable_color_print']) 573 | # check files content contains blacklist words 574 | auto_self_result = util.exec_cmd( 575 | "find /tmp/ -type f -ls|grep -v 'sess_' |grep -v hsperfdata") 576 | lines = [] 577 | for line in auto_self_result.split('\n'): 578 | if not line.split(' ')[-1].strip() in self.tmp_normal_file: 579 | lines.append(line) 580 | auto_self_result = '\n'.join(lines) 581 | malicious_files = [] 582 | if auto_self_result: 583 | for i in auto_self_result.split('\n'): 584 | i = re.sub(r'\s+', ' ', i) 585 | file = i.strip().split(' ')[-1] 586 | if '/tmp/' in file: 587 | f_name = open(file) 588 | content = f_name.read() 589 | f_name.close() 590 | # files contain blacklisted words could be malicious 591 | if any(kword in content for kword in self.blacklist): 592 | malicious_files.append(file) 593 | # highlight_part = file 594 | # check Files created after 2020 Jun 29 in /tmp. Could be malicious script 595 | util.exec_cmd('/bin/touch -t 202006290100 /tmp/kbtime') 596 | auto_self_result = util.exec_cmd( 597 | "find /tmp/ -type f -newer /tmp/kbtime -ls|grep -v 'sess_' |grep -v hsperfdata") 598 | lines = [] 599 | for line in auto_self_result.split('\n'): 600 | if not line.split(' ')[-1].strip() in self.tmp_normal_file: 601 | lines.append(line) 602 | auto_self_result = '\n'.join(lines) 603 | if auto_self_result or malicious_files: 604 | self.backdoor_flag = True 605 | title = util.head_line(' /tmp/ checking ') 606 | util.print_msg('\n[+]' + title) 607 | for filename in malicious_files: 608 | message = '!! File %s could be a malicous script' % filename 609 | highlight_part = filename 610 | util.print_msg(message, highlight_part) 611 | util.print_msg( 612 | '!! Files created in /tmp/ after 2020 Jun 29, ' 613 | 'need to check if those are malicious scripts') 614 | if self.is_recent_upgrade: 615 | message = "!! System OS installed on %s, false positive possible" % self.upgradetime 616 | highlight_part = "System OS installed on %s, " \ 617 | "false positive possible" % self.upgradetime 618 | util.print_msg(message, highlight_part) 619 | util.print_msg(auto_self_result) 620 | 621 | def __sys_eicheck(self): 622 | util = Util(self.checks_to_skip['disable_color_print']) 623 | if self.result['version']: 624 | version_number = ''.join(self.result['version'].split('.')[:2]) 625 | if int(version_number) >= 131 and not os.path.exists("/usr/libexec/sys-eicheck.py"): 626 | highlight_part = 'Critical: /usr/libexec/sys-eicheck.py is missing' 627 | message = r"!! %s" % highlight_part 628 | util.print_msg(message, highlight_part) 629 | if os.path.exists("/usr/libexec/sys-eicheck.py"): 630 | a = open('/usr/libexec/sys-eicheck.py') 631 | b = a.read() 632 | a.close() 633 | py_sys_eicheck_hash = md5(b).hexdigest() 634 | bash_sys_eicheck_hash = util.exec_cmd( 635 | 'md5sum /usr/libexec/sys-eicheck.py').strip().split(' ')[0] 636 | if py_sys_eicheck_hash == bash_sys_eicheck_hash and \ 637 | py_sys_eicheck_hash in self.sys_eicheck_hash: 638 | util.print_msg('[+] sys-eicheck.py hash check: PASS') 639 | util.print_msg( 640 | '[+] BIG-IP Integrity check in progress. This may take several minutes.\n[+] ' 641 | 'You can use -i or --skip_sys-eicheck argument to skip it.\n') 642 | output = util.exec_cmd("/usr/libexec/sys-eicheck.py") 643 | if 'Integrity Test Result: [ PASS ]' in output: 644 | util.print_msg('[+] BIG-IP Integrity check result: PASS') 645 | else: 646 | util.print_msg( 647 | '[+] BIG-IP Integrity check result: FAIL', 'FAIL') 648 | util.print_msg(output, 'FAIL') 649 | else: 650 | util.print_msg('[+] sys-eicheck.py hash check: FAIL, ' 651 | 'sys-eicheck.py may have been altered.', 652 | 'sys-eicheck.py hash check: FAIL, ' 653 | 'sys-eicheck.py may have been altered.') 654 | 655 | def checker(self): 656 | """Main logic block for this class; calls each IoC check in turn""" 657 | util = Util(self.checks_to_skip['disable_color_print']) 658 | util.print_msg( 659 | 'CVE-2020-5902 Indicators of Compromise checker. False positive reports ' 660 | 'are possible and all results should be manually verified.') 661 | 662 | self.__check_version_info() 663 | 664 | self.__get_liveinstalltime() 665 | 666 | self.__home_check() 667 | 668 | if not self.checks_to_skip['sys_eicheck']: 669 | self.__sys_eicheck() 670 | 671 | if not self.checks_to_skip['audit_check']: 672 | self.__audit_check() 673 | 674 | if not self.checks_to_skip['journal_check']: 675 | self.__journal_check() 676 | 677 | if not self.checks_to_skip['systems_check']: 678 | self.__systems_check() 679 | 680 | if not self.checks_to_skip['alias_check']: 681 | self.__alias_check() 682 | 683 | if not self.checks_to_skip['catalina_check']: 684 | self.__catalina_check() 685 | 686 | if not self.checks_to_skip['bigipstartup_check']: 687 | self.__bigipstartup_check() 688 | 689 | if not self.checks_to_skip['autostart_check']: 690 | self.__autostart_check() 691 | 692 | if not self.checks_to_skip['tmp_check']: 693 | self.__tmp_check() 694 | 695 | if not self.checks_to_skip['webshell_check']: 696 | self.__webshell_check() 697 | 698 | if not self.attacked_flag and not self.backdoor_flag: 699 | util.print_msg('No indications of compromise known to this script found.') 700 | 701 | 702 | if __name__ == '__main__': 703 | checks_to_skip = { 704 | 'sys_eicheck': False, 705 | 'audit_check': False, 706 | 'journal_check': False, 707 | 'systems_check': False, 708 | 'alias_check': False, 709 | 'catalina_check': False, 710 | 'bigipstartup_check': False, 711 | 'webshell_check': False, 712 | 'autostart_check': False, 713 | 'tmp_check': False, 714 | 'bigiq_cmd_check': False, 715 | 'disable_color_print': False 716 | } 717 | try: 718 | opts, args = getopt.getopt( 719 | sys.argv[1:], 'hiajylcbwutqp', 720 | ['help', 'skip_sys-eicheck', 'skip_audit_check', 'skip_journal_check', 721 | 'skip_systems_check', 'skip_alias_check', 'skip_catalina_check', 722 | 'skip_catalina_check', 'skip_bigipstartup_check', 723 | 'skip_webshell_check', 'skip_autostart_check', 'skip_tmp_check', 'bigiq_cmd_check', 'disable_color_print']) 724 | except getopt.GetoptError: 725 | t, e = sys.exc_info()[:2] 726 | print(e) 727 | sys.exit() 728 | 729 | for opt_name, opt_value in opts: 730 | if opt_name in ('-h', '--help'): 731 | print("USAGE: python " + sys.argv[0]) 732 | print("\nYou can use the following options <-iajylcbwut> to skip some checks:\n") 733 | print("Option: -i or --skip_sys-eicheck\n[+] Skip using the sys_eicheck utility " 734 | "to scan the BIG-IP system for any unexpected changes to the system software.\n" 735 | "[+] Please refer to https://support.f5.com/csp/article/K00029945" 736 | "for more details about the sys-eicheck utility.\n" 737 | "[+] The sys-eicheck utility may take several minutes to finish.\n") 738 | print("Option: -a or --skip_audit_check " 739 | "[+] Skip scanning the audit log for malicious activities.") 740 | print("Option: -j or --skip_journal_check " 741 | "[+] Skip scanning the journal log for malicious activities.") 742 | print("Option: -y or --skip_systems_check " 743 | "[+] Skip scanning /config/bigip_user.conf to look for malicious users.") 744 | print("Option: -l or --skip_alias_check " 745 | "[+] Skip scanning /config/bigip_*.conf for malicious alias definition.") 746 | print("Option: -c or --skip_catalina_check " 747 | "[+] Skip scanning the tomcat catalina.out log for malicious activities.") 748 | print("Option: -b or --skip_bigipstartup_check " 749 | "[+] Skip checking if /config/startup contains blacklisted words.") 750 | print("Option: -w or --skip_webshell_check " 751 | "[+] Skip checking Files created after 2020 Jun 29 " 752 | "in the /usr/local/www/ to look for possible webshell files.") 753 | print("Option: -u or --skip_autostart_check " 754 | "[+] Skip checking Files created after 2020 Jun 29 " 755 | "in /etc/ to look for the possible autostart script.") 756 | print("Option: -t or --skip_tmp_check " 757 | "[+] Skip checking Files created after 2020 Jun 29 in /tmp") 758 | print("Option: -p or --disable_color_print " 759 | "[+] Disable color print, better for saving result") 760 | print("Option: -q or --bigiq_cmd_check " 761 | "[+] Run BIG-IQ compatible malicious command check\n") 762 | print("Option: -h or --help [+] Print usage") 763 | 764 | sys.exit() 765 | if opt_name in ('-i', '--skip_sys-eicheck'): 766 | checks_to_skip['sys_eicheck'] = True 767 | print(">>>> Skip sys-eicheck check") 768 | if opt_name in ('-a', '--skip_audit_check'): 769 | checks_to_skip['audit_check'] = True 770 | print(">>>> Skip audit log check") 771 | if opt_name in ('-j', '--skip_journal_check'): 772 | checks_to_skip['journal_check'] = True 773 | print(">>>> Skip journal log check") 774 | if opt_name in ('-y', '--skip_systems_check'): 775 | checks_to_skip['systems_check'] = True 776 | print(">>>> Skip systems user check") 777 | if opt_name in ('-l', '--skip_alias_check'): 778 | checks_to_skip['alias_check'] = True 779 | print(">>>> Skip alias command check") 780 | if opt_name in ('-c', '--skip_catalina_check'): 781 | checks_to_skip['catalina_check'] = True 782 | print(">>>> Skip tomcat catalina log check") 783 | if opt_name in ('-b', '--skip_bigipstartup_check'): 784 | checks_to_skip['bigipstartup_check'] = True 785 | print(">>>> Skip bigip startup check") 786 | if opt_name in ('-w', '--skip_webshell_check'): 787 | checks_to_skip['webshell_check'] = True 788 | print(">>>> Skip webshell check") 789 | if opt_name in ('-u', '--skip_autostart_check'): 790 | checks_to_skip['autostart_check'] = True 791 | print(">>>> Skip autostart check") 792 | if opt_name in ('-t', '--skip_tmp_check'): 793 | checks_to_skip['tmp_check'] = True 794 | print(">>>> Skip tmp directory check") 795 | if opt_name in ('-p', '--disable_color_print'): 796 | print(">>>> Disable color print") 797 | checks_to_skip['disable_color_print'] = True 798 | if opt_name in ('-q', '--bigiq_cmd_check'): 799 | checks_to_skip['bigiq_cmd_check'] = True 800 | print(">>>> Run BIG-IQ compatible malicious command check") 801 | checker_obj = Checker(checks_to_skip) 802 | checker_obj.checker() 803 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2020-5902 IoC Detection Tool 2 | 3 | This script is intended to be executed locally on an F5 BIG-IP in Advanced Shell (bash) by a user with root privileges; it is not intended to be run in any other setting. Note: Appliance Mode does not allow access to Advanced Shell, and therefore this tool cannot be run on such systems. 4 | 5 | The script examines the BIG-IP for the Indicators of Compromise associated with CVE-2020-5902 which were known to F5 Networks at the time of authoring. The script collates these IoCs and presents a report as an overview you can use to inform your determination of the best path forward. If this tool uncovers any IoCs, you should manually examine and confirm them, then follow your own documented procedures for handling suspected compromised systems. F5 specific guidance may be found in [K11438344: Considerations and guidance when you suspect a security compromise on a BIG-IP system](https://support.f5.com/csp/article/K11438344). 6 | 7 | Note: If you have any uncertainty or doubt as to the integrity of any system, the cautious approach is to consider the system compromised and to follow your internal procedures for handling a compromised system. Note also that any information contained on a compromised system should itself be considered compromised. This includes, but is not limited to, passwords, private keys, digital certificates, configurations, logs, etc. 8 | 9 | ## Limitations and considerations 10 | 11 | It must be noted that IoCs may no longer be present either due to age (for example, by log rotation schedules) or removal by a sufficiently skilled adversary. As exploitation of CVE-2020-5902 potentially results in remote code execution as the root user, a skilled attacker would be free to sanitize the system of traces after exploitation. 12 | 13 | Note also that an attacker could poison any binary on the system, _including_ python. Reasonable steps have been taken to avoid the possibility of this; however, you must determine your level of trust in the output of this tool on any system suspected to be compromised. 14 | 15 | On BIG-IP versions that include [sys-eicheck](https://support.f5.com/csp/article/K00029945) (13.1.0 onward), this tool offers the administrator the opportunity to check the running system against the original installation RPMs to look for any file modifications in an attempt to ensure the integrity of the system commands this tool relies upon. Note that sys-eicheck can be run on all platforms regardless of FIPS capability. 16 | 17 | On earlier versions the tool does not currently include this functionality and will run without checking the originally installed system binaries for tampering. 18 | 19 | ## Verifying the authenticity of this script 20 | 21 | The sha384 sum of this version of CVE-2020-5902_bigip_ioc_checker.py is: ce674235744b54de3331c19a402f911c4390dbbf297643ec3eed165bda49a951f48c1be9188372de3d92dd9f28595cbb 22 | 23 | You should only download these files from the [F5 DevCentral GitHub repository](https://github.com/f5devcentral) 24 | 25 | ## CVE-2020-5902 information 26 | 27 | Note that the authoritative source of information on CVE-2020-5902 is always the F5 Security Advisory, [K52145254: TMUI RCE vulnerability CVE-2020-5902](https://support.f5.com/csp/article/K52145254) 28 | 29 | ## Running the script 30 | 31 | Simply download the file `CVE-2020-5902_bigip_ioc_checker.py` to the target BIG-IP and run it using the python installation already present on BIG-IP. 32 | 33 | Example: 34 | ``` 35 | [root@hostname:Active:Standalone] tmp # python CVE-2020-5902_bigip_ioc_checker.py -h 36 | USAGE: python CVE-2020-5902_bigip_ioc_checker.py 37 | 38 | You can use the following options <-iajylcbwut> to skip some checks: 39 | 40 | Option: -i or --skip_sys-eicheck 41 | [+] Skip using the sys_eicheck utility to scan the BIG-IP system for any unexpected changes to the system software. 42 | [+] Please refer to https://support.f5.com/csp/article/K00029945for more details about the sys-eicheck utility. 43 | [+] The sys-eicheck utility may take several minutes to finish. 44 | 45 | Option: -a or --skip_audit_check [+] Skip scanning the audit log for malicious activities. 46 | Option: -j or --skip_journal_check [+] Skip scanning the journal log for malicious activities. 47 | Option: -y or --skip_systems_check [+] Skip scanning /config/bigip_user.conf to look for malicious users. 48 | Option: -l or --skip_alias_check [+] Skip scanning /config/bigip_*.conf for malicious alias definition. 49 | Option: -c or --skip_catalina_check [+] Skip scanning the tomcat catalina.out log for malicious activities. 50 | Option: -b or --skip_bigipstartup_check [+] Skip checking if /config/startup contains blacklisted words. 51 | Option: -w or --skip_webshell_check [+] Skip checking Files created after 2020 Jun 29 in the /usr/local/www/ to look for possible webshell files. 52 | Option: -u or --skip_autostart_check [+] Skip checking Files created after 2020 Jun 29 in /etc/ to look for the possible autostart script. 53 | Option: -t or --skip_tmp_check [+] Skip checking Files created after 2020 Jun 29 in /tmp 54 | Option: -p or --disable_color_print [+] Disable color print, better for saving result 55 | Option: -q or --bigiq_cmd_check [+] Run BIG-IQ compatible malicious command check 56 | 57 | Option: -h or --help [+] Print usage 58 | ``` 59 | ## Known Issues 60 | 61 | If a BIG-IP is managed by BIG-IQ or another automation/management system, it may receive bash commands from the BIG-IQ or another automation/management system. Some of the bash commands have the base64 encoding script embedded. Those bash commands logged in the BIG-IP audit logs could lead the IoC Detection Tool to report false positives notification: "!! IoC pattern malicious bash command found". To reduce the falses positives, you can use -q option to make the IoC Detection Tool ignore the legitimate base64 encoded script in the audit logs. 62 | 63 | Upgrading the system may update the timestamps on files in /usr/local/www/ which may result in a false positive, for example, if the fixed software was recently installed. 64 | 65 | If any results are returned for any of the file creation date checks, in /usr/local/www/, /etc/, and /tmp/, the files should be examined to determine their legitimacy. 66 | 67 | It is possible to still find Indication of Compromise from the journalctl logs after an upgrade to a fixed version or after applying the workaround. Please review the time stamp of the logs should they be present and verify they happened before the upgrade or applying the workaround. 68 | 69 | ### Sample Output 70 | 71 | ``` 72 | # python CVE-2020-5902_bigip_ioc_checker.py -iqa 73 | >>>> Skip sys-eicheck check 74 | >>>> Run BIG-IQ compatible malicious command check 75 | >>>> Skip audit log check 76 | CVE-2020-5902 Indicators of Compromise checker. False positive reports are possible and all results should be manually verified. 77 | [+] Version 13.1.0 Build 0.0.1868 CVE-2020-5902 Fixed: False 78 | !! IoC pattern ['/etc/fakefile', '/etc/passwd', '/etc/fakefile1'] access denied in file /var/log/tomcat/catalina.out 79 | !! IoC pattern ['/config/bigip_base.conf'] access denied in file /var/log/tomcat/catalina.out.1 80 | !! IoC pattern Possible backdoor echo " /usr/local/www/xui/common/css/webshell.php in file /config/startup 81 | !! IoC pattern Possible backdoor echo "Runtime.getRuntime().exec("cmd.exe /C " + cmd);" > /usr/local/www/xui/common/css/webshell.jsp in file /config/startup 82 | 83 | [+]========================== auto start script checking ========================== 84 | !! Files created in /etc/ after 2020 Jun 29, need to check if those are malicious daemon startup script 85 | 95291 1 -rw-r--r-- 1 root root 0 Jul 20 14:49 /etc/rc.d/init.d/autostartbackdoor 86 | 104847 4 -rw------- 1 root root 199 Jul 13 10:46 /var/spool/cron/root 87 | 88 | 89 | [+]================================ /tmp/ checking ================================ 90 | !! File /tmp/backdoor_curl could be a malicous script 91 | !! File /tmp/CVE-2020-5902_bigip_ioc_checker.py could be a malicous script 92 | !! Files created in /tmp/ after 2020 Jun 29, need to check if those are malicious scripts 93 | 18491 40 -rw-r--r-- 1 root root 38066 Jul 20 14:57 /tmp/CVE-2020-5902_bigip_ioc_checker.py 94 | 18497 1 -rw-r--r-- 1 root root 0 Jul 20 14:49 /tmp/backdoor_new 95 | 96 | 97 | [+]============================== webshell checking ============================== 98 | !! Files created in /usr/local/www/ after 2020 Jun 29, need to check if those are webshell or information leakage 99 | 243368 4 -rw-r--r-- 1 root root 33 Jul 20 14:49 /usr/local/www/xui/common/css/webshell.php 100 | 243367 4 -rw-r--r-- 1 root root 33 Jul 17 08:30 /usr/local/www/xui/common/css/css.php 101 | 243369 4 -rw-r--r-- 1 root root 46 Jul 20 14:49 /usr/local/www/xui/common/css/webshell.jsp 102 | ``` 103 | 104 | 105 | ###### EULA, Warranty and licenses 106 | 107 | F5 provides the CVE-2020-5902 IoC Detection Tool to help its customers analyze their F5 devices outside of iHealth for certain indicators of compromise related to CVE-2020-5902. Please note, however, that: 108 | 109 | 1. The CVE-2020-5902 IoC Detection Tool is not comprehensive, nor is it intended to be: it does not identify all possible indicators of compromise, but only a select group that F5 has found to be generally reliable based on its internal analyses of compromised F5 devices. 110 | 111 | 2. Not all compromised F5 devices show the same indicators and attackers may be able to remove traces of their work. It is not possible to prove that any device has not been compromised; if there is any uncertainty, additional analysis may be required and/or you may want to consult with your security team. 112 | 113 | 3. To avoid undue interruption to a user’s business operations, the CVE-2020-5902 IoC Detection Tool should not be operated during peak traffic hours and should instead generally be used during users’ regular maintenance windows. 114 | 115 | 4. If indicators of compromise are identified, F5 recommends that users follow their documented internal incident response procedures. F5 has provided general considerations and guidance for when a security compromise on a BIG-IP system is suspected in [K11438344: Considerations and guidance when you suspect a security compromise on a BIG-IP system](https://support.f5.com/csp/article/K11438344). Additionally, users can contact F5 directly for additional support via [the Customer Support Portal](https://websupport.f5.com/) or [other standard channels](https://www.f5.com/company/contact/regional-offices#product-support). 116 | 117 | The CVE-2020-5902 IoC Detection Tool is made available for F5 users’ convenience and is provided on an “as is” basis under the terms of the Apache License. You use the CVE-2020-5902 IoC Detection Tool at your own risk. 118 | 119 | Copyright © 2020 F5 Networks, Inc. 120 | 121 | Licensed under the Apache License, Version 2.0 (the “License”); you may not use the CVE-2020-5902 IoC Detection Tool except in compliance with the License. You may obtain a copy of the License at: 122 | 123 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 124 | 125 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 126 | --------------------------------------------------------------------------------