├── LICENSE ├── README.md └── smb1-util.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Red Canary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cb-response-smb1-utility 2 | A simple utility to check the status of and/or disable SMBv1 on Windows system via Cb Response's Live Response functionality. 3 | 4 | Assuming that you’re already set up wih [cbapi-python](https://github.com/carbonblack/cbapi-python), a survey is as simple as: 5 | 6 | ./smb1-util.py 7 | 8 | The output status will be one of: 9 | * smb1_enabled_default - The SMB1 subkey does not exist, meaning that SMBv1 is enabled by default. 10 | * smb1_enabled_explicit - The SMB1 subkey exists and is set to 1. This rarely occurs. 11 | * cblr_timeout - The system is online but we couldn't get a Live Response session. 12 | * error - An unhandled error during the Live Response routine. 13 | 14 | The above will only report the status of SMB1 on each available system. It will not make any changes. 15 | 16 | If you want to explicitly disable SMB1, run with: 17 | 18 | ./smb1-util.py —disable-smb1 19 | 20 | You can use --help for additional information. 21 | -------------------------------------------------------------------------------- /smb1-util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import Queue 5 | import sys 6 | import threading 7 | from time import sleep 8 | 9 | from cbapi.response import CbEnterpriseResponseAPI 10 | from cbapi.response.models import Sensor 11 | from cbapi.live_response_api import * 12 | from cbapi.errors import * 13 | 14 | # This is the key that we use to determine what the Automatic Update policy 15 | # looks like on a given system. For reference, see the following article: 16 | # 17 | # https://msdn.microsoft.com/en-us/library/dd939844(v=ws.10).aspx 18 | # 19 | # Section "Registry keys for Automatic Update configuration options" contains 20 | # the list of subkeys and possible values. 21 | check_key = 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters\\SMB1' 22 | 23 | 24 | def log_err(msg): 25 | """Format msg as an ERROR and print to stderr. 26 | """ 27 | msg = 'ERROR: {0}\n'.format(msg) 28 | sys.stderr.write(msg) 29 | 30 | 31 | def log_info(msg): 32 | """Format msg as INFO and print to stdout. 33 | """ 34 | msg = 'INFO: {0}\n'.format(msg) 35 | sys.stdout.write(msg) 36 | 37 | 38 | def hostname_from_fqdn(hostname): 39 | if '.' in hostname: 40 | hostname = hostname.split('.')[0] 41 | 42 | return hostname 43 | 44 | 45 | def hostnames_to_list(path): 46 | ret = set() 47 | with open(path, 'r') as fh_input: 48 | for entry in fh_input.readlines(): 49 | entry = entry.strip().lower() 50 | ret.add(hostname_from_fqdn(entry)) 51 | 52 | return list(ret) 53 | 54 | 55 | def set_smb1_disabled(lr, sensor): 56 | try: 57 | lr.set_registry_value(check_key, 0) 58 | except LiveResponseError, err: 59 | # We should not need to do this but the method to list registry keys 60 | # and values returns a server-side 500 error. 61 | if 'ERROR_FILE_NOT_FOUND' in str(err): 62 | lr.create_registry_key(check_key) 63 | finally: 64 | lr.set_registry_value(check_key, 0) 65 | 66 | 67 | def get_smb1_status(lr, sensor): 68 | try: 69 | sensor_name = sensor.computer_name.lower() 70 | 71 | resp = lr.get_registry_value(check_key)['value_data'] 72 | if resp == 0: 73 | output = 'smb1_disabled' 74 | elif resp == 1: 75 | output = 'smb1_enabled_explicit' 76 | except LiveResponseError, err: 77 | if 'ERROR_FILE_NOT_FOUND' in str(err): 78 | output = 'smb1_enabled_default' 79 | 80 | return output 81 | 82 | 83 | def process_sensor(cb, sensor, update=False, debug=False, ignore_hosts=None): 84 | """Do things specific to this sensor. For now: 85 | - Skip non-Windows endpoints 86 | - Output name of offline endpoints 87 | - Get SMB1 status of all that remain 88 | """ 89 | sensor_name = sensor.computer_name.lower() 90 | sensor_ignored = False 91 | 92 | if ignore_hosts is not None: 93 | if hostname_from_fqdn(sensor_name) in ignore_hosts: 94 | sensor_ignored = True 95 | 96 | if 'windows' in sensor.os_environment_display_string.lower(): 97 | if sensor_ignored == True: 98 | ret = '%s,%s' % (sensor_name, 'ignored') 99 | elif 'online' not in sensor.status.lower(): 100 | ret = '%s,%s' % (sensor_name, 'offline') 101 | else: 102 | try: 103 | if debug: log_info('{0} CBLR pending'.format(sensor_name)) 104 | lr = cb.live_response.request_session(sensor.id) 105 | if debug: log_info('{0} CBLR established (id:{1})'.format(sensor_name, str(lr.session_id))) 106 | smb1_status = get_smb1_status(lr, sensor) 107 | 108 | # If we're asked to update, only update if the key exists and 109 | # AU is disabled. If the key does not exist, we'll assume it's 110 | # because the system isn't domain joined or otherwise not 111 | # subject to policy, and we'll simply skip it and report 112 | # key_not_found. 113 | # 114 | # After updating, get the status again so that we're 115 | # accurately reporting the end state. 116 | if update == True and 'smb1_enabled' in smb1_status: 117 | if debug: log_info('{0} CBLR updating AU config'.format(sensor_name)) 118 | set_smb1_disabled(lr, sensor) 119 | smb1_status = get_smb1_status(lr, sensor) 120 | 121 | if debug: log_info('{0} CBLR closing (id:{1})'.format(sensor_name, str(lr.session_id))) 122 | lr.close() 123 | except TimeoutError: 124 | smb1_status = 'cblr_timeout' 125 | except Exception, err: 126 | log_err(err) 127 | smb1_status = 'error' 128 | 129 | ret = '%s,%s' % (sensor_name, smb1_status) 130 | 131 | sys.stdout.write(ret + '\n') 132 | 133 | return 134 | 135 | 136 | def process_sensors(cb, query_base=None, update=False, max_threads=None, 137 | debug=False, ignore_hosts=None): 138 | """Fetch all sensor objects associated with the cb server instance, and 139 | keep basic state as they are processed. 140 | """ 141 | 142 | if query_base is not None: 143 | query_result = cb.select(Sensor).where(query_base) 144 | else: 145 | query_result = cb.select(Sensor) 146 | query_result_len = len(query_result) 147 | 148 | q = Queue() 149 | 150 | # unique_sensors exists because we sometimes see the same sensor ID 151 | # returned multiple times in the paginated query results for 152 | # cb.select(Sensor). 153 | unique_sensors = set() 154 | 155 | for sensor in query_result: 156 | if sensor.id in unique_sensors: 157 | continue 158 | else: 159 | unique_sensors.add(sensor.id) 160 | q.put(sensor) 161 | 162 | threads = [] 163 | while not q.empty(): 164 | active_threads = threading.active_count() 165 | available_threads = max_threads - active_threads 166 | 167 | if available_threads > 0: 168 | for i in range(available_threads): 169 | sensor = q.get() 170 | t = threading.Thread(target=process_sensor, 171 | args=(cb, sensor, update, debug, ignore_hosts)) 172 | threads.append(t) 173 | t.start() 174 | 175 | if debug: log_info('Threads: {0}\tQ Size: {1}'.format(threading.active_count(), q.qsize())) 176 | 177 | if q.empty(): 178 | break 179 | else: 180 | if debug: log_info('No available threads. Waiting.') 181 | sleep(1) 182 | 183 | 184 | def main(): 185 | parser = argparse.ArgumentParser() 186 | parser.add_argument("--profile", type=str, action="store", 187 | help="The credentials.response profile to use.") 188 | parser.add_argument("--debug", action="store_true", 189 | help="Write additional logging info to stdout.") 190 | parser.add_argument("--max-threads", type=int, action="store", 191 | default=5, 192 | help="Maximum number of concurrent threads.") 193 | 194 | # Sensor query paramaters 195 | s = parser.add_mutually_exclusive_group(required=False) 196 | s.add_argument("--group-id", type=int, action="store", 197 | help="Target sensor group based on numeric ID.") 198 | s.add_argument("--hostname", type=str, action="store", 199 | help="Target sensor matching hostname.") 200 | s.add_argument("--ipaddr", type=str, action="store", 201 | help="Target sensor matching IP address (dotted quad).") 202 | 203 | # Options specific to this script 204 | parser.add_argument("--disable-smb1", action="store_true", 205 | help="If SMB1 is enabled, disable it.") 206 | parser.add_argument("--ignore-hosts", type=str, action="store", 207 | help="A file containing hostnames to ignore.") 208 | 209 | 210 | args = parser.parse_args() 211 | 212 | if args.profile: 213 | cb = CbEnterpriseResponseAPI(profile=args.profile) 214 | else: 215 | cb = CbEnterpriseResponseAPI() 216 | 217 | if args.ignore_hosts: 218 | ignore_hosts_list = hostnames_to_list(args.ignore_hosts) 219 | else: 220 | ignore_hosts_list = None 221 | 222 | query_base = None 223 | if args.group_id: 224 | query_base = 'groupid:%s' % args.group_id 225 | elif args.hostname: 226 | query_base = 'hostname:%s' % args.hostname 227 | elif args.ipaddr: 228 | query_base = 'ipaddr:%s' % args.ipaddr 229 | 230 | process_sensors(cb, query_base=query_base, 231 | update=args.disable_smb1, 232 | max_threads=args.max_threads, 233 | debug=args.debug, ignore_hosts=ignore_hosts_list) 234 | 235 | 236 | if __name__ == '__main__': 237 | 238 | sys.exit(main()) 239 | --------------------------------------------------------------------------------