diff --git a/README.md b/README.md index 21e133c..dcce6f4 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,17 @@ There is a one-time use `deploy.sh` deploy script included which will do these s Just run this once and you can then use the resources for the `--role` and `--s3-zip` arguments: ``` -./deploy.sh - +❯ ./deploy.sh [*] Creating S3 bucket -"/b424c464-6bff-11eb-89b9-faffc212389a-lambda-port-scanner" +"/lambdaportscanner" [*] Uploading code ZIP files -upload: ./lambda-port-scanner--ports.zip to s3://b424c464-6bff-11eb-89b9-faffc212389a-lambda-port-scanner/lambda-port-scanner--ports.zip -upload: ./lambda-port-scanner--nmap.zip to s3://b424c464-6bff-11eb-89b9-faffc212389a-lambda-port-scanner/lambda-port-scanner--nmap.zip +upload: ./workercode-portscan.zip to s3://lambdaportscanner/workercode-portscan.zip +upload: ./workercode-nmap.zip to s3://lambdaportscanner/workercode-nmap.zip [*] Creating Lambda IAM Execution Role -Use these for controller arguments: ---role "arn:aws:iam:::role/service-role/lambda-port-scanner" ---s3-zip b424c464-6bff-11eb-89b9-faffc212389a-lambda-port-scanner/lambda-port-scanner--ports.zip +** Use these for controller arguments: ** +--role "arn:aws:iam:::role/service-role/lambdaportscanner" +For portscan command: --s3-zip lambdaportscanner/workercode-portscan.zip +For nmap command: --s3-zip lambdaportscanner/workercode-nmap.zip ``` ## Installation @@ -59,35 +59,56 @@ pip3 install boto3 python3 lambscanController.py --help ``` ## Usage +There are 3 subcommands available: `portscan`, `nmap`, and `clean`. + +``` +❯ pipenv run python lambscanController.py -h +usage: lambscanController.py [-h] ... + +positional arguments: + + portscan Portscan subcommand. Get port up/down status. scan is randomized across target ports and IPs + nmap Nmap subcommand. Each Lambda worker will conduct Nmap on its target + clean Clean subcommand. Do not scan. Delete all Lambda functions matching ^scanworker_. Use if something goes wrong. + +optional arguments: + -h, --help show this help message and exit ``` -usage: lambscanController.py [-h] [--ports [PORTS]] [--role [ROLE]] [--target [TARGET]] [--target-file [TARGET_FILE]] [--workers WORKER_MAX] [--threads THREAD_MAX] [--clean] [--region REGION] - [--outfile OUTFILE] [--open] [-v] -Create workers in Lambda to conduct distributed port scans on selected targets +## Usage: portscan command + +``` +❯ pipenv run python lambscanController.py portscan -h +usage: lambscanController.py portscan [-h] --ports [PORTS] --role [ROLE] [--target [TARGET]] [--target-file [TARGET_FILE]] --region + [REGION] [--workers WORKER_MAX] [--threads THREAD_MAX] [--outfile OUTFILE] [--open] + [--s3-zip [S3ZIP]] [-v] optional arguments: -h, --help show this help message and exit + --workers WORKER_MAX Number of Lambda workers to create. Default: 1 + --threads THREAD_MAX Max number of threads for port scanning. Default: 1 + --outfile OUTFILE Specify the output file to store timestamped scan results + --open Only show open ports + --s3-zip [S3ZIP] Provide a S3 bucket and key path to the code ZIP file and the controller will use that to create each + function worker instead of uploading each manually + -v, --verbose increase console output verbosity + +required named arguments: --ports [PORTS] Ports to scan. Example:80,81,1000-2000 --role [ROLE] AWS IAM role ARN for lambda functions to use --target [TARGET] IP address or CIDR range --target-file [TARGET_FILE] File with one IP address or CIDR per line - --workers WORKER_MAX Number of Lambda workers to create - --threads THREAD_MAX Max number of threads for port scanning - --clean Do not scan. Delete all Lambda functions matching ^scanworker_. Use if something goes wrong - --region REGION Specify the target region to create and run the lambscan workers (e.g, us-east-1) - --outfile OUTFILE Specify the output file to store timestamped scan results - --open Only show open ports - --s3-zip [S3ZIP] Provide a S3 bucket and key path to the code ZIP file and the controller will use that to create each function worker instead of uploading each manually - -v, --verbose increase console output verbosity + --region [REGION] Specify the target region to create and run the lambscan workers (e.g, us-east-1) ``` -### Examples: +#### Portscan command examples: Simple example with verbosity (`-v`) enabled to see the details of what is happening. This scan provisions 2 Lambda workers to scan 4 ports on the target `scanme.nmap.org `. From the output, we can see that those 2 workers were assigned the IPs `3.233.217.229` and `3.233.217.229` from the AWS IP pool, and the controller rotated through them to complete the 4 port scans: ``` -$ pipenv run python lambscanController.py \ - --role arn:aws:iam::: \ +❯ pipenv run python lambscanController.py \ + portscan \ + --role arn:aws:iam::<>:<> \ --target scanme.nmap.org \ --threads 2 \ --workers 2 \ @@ -100,70 +121,63 @@ Resolved scanme.nmap.org to 45.33.32.156 [*] Creating 2 Lambda workers in AWS... Creating worker: scanworker_0 Creating worker: scanworker_1 -Success creating: scanworker_0 Success creating: scanworker_1 +Success creating: scanworker_0 Verifying worker count... Workers: 2 [*] Scanning ports... Using 2 threads -Using worker scanworker_0 for target: 45.33.32.156:80 -Using worker scanworker_1 for target: 45.33.32.156:21 -Using worker scanworker_0 for target: 45.33.32.156:443 -3.233.217.229 --> 45.33.32.156 80/tcp Open -Using worker scanworker_1 for target: 45.33.32.156:22 -3.238.3.192 --> 45.33.32.156 21/tcp Closed/Filtered -3.233.217.229 --> 45.33.32.156 443/tcp Closed/Filtered -3.238.3.192 --> 45.33.32.156 22/tcp Open +Using scanworker_0 for target: 45.33.32.156:443 +Using scanworker_1 for target: 45.33.32.156:21 +Using scanworker_0 for target: 45.33.32.156:80 +3.237.37.244 --> 45.33.32.156 443/tcp Closed/Filtered +Using scanworker_1 for target: 45.33.32.156:22 +34.231.122.36 --> 45.33.32.156 21/tcp Closed/Filtered +3.237.37.244 --> 45.33.32.156 80/tcp Open +34.231.122.36 --> 45.33.32.156 22/tcp Open All threads completed [*] Deleting Lambda workers from AWS... Lambda Functions to delete: 2 -Deleting scanworker_0 Deleting scanworker_1 -('Success Deleting', 'scanworker_0') +Deleting scanworker_0 ('Success Deleting', 'scanworker_1') +('Success Deleting', 'scanworker_0') Lambda Functions now: 0 [*] Done! ``` -Scan targeted ports on a target subnet. (This rotates each port scan through 20 Lambda workers provisioned in the `us-east-1` region, which will provide 20 unique source IPs to rotate through): +Scan targeted ports on a target subnet and only show open ports. (This rotates each port scan through 20 Lambda workers provisioned in the `us-east-1` region, which will provide 20 unique source IPs to rotate through): ``` -$ pipenv run python lambscanController.py \ - --role arn:aws:iam::: \ +❯ pipenv run python lambscanController.py \ + portscan \ + --role arn:aws:iam::<>:<> \ --target 52.94.76.0/24 \ --threads 20 \ --workers 20 \ --ports 21,22,80,443,8080 \ - --region us-east-1 + --region us-east-1 \ + --open [*] Creating 20 Lambda workers in AWS... [*] Scanning ports... -54.237.193.133 --> 52.94.76.149 22/tcp Closed/Filtered -3.88.15.82 --> 52.94.76.38 8080/tcp Closed/Filtered -3.88.185.34 --> 52.94.76.184 80/tcp Closed/Filtered -3.83.78.214 --> 52.94.76.137 22/tcp Closed/Filtered -3.235.94.80 --> 52.94.76.61 443/tcp Closed/Filtered -18.205.96.112 --> 52.94.76.151 22/tcp Closed/Filtered -...... -18.205.96.112 --> 52.94.76.23 8080/tcp Closed/Filtered -3.235.94.80 --> 52.94.76.55 443/tcp Closed/Filtered -18.207.120.219 --> 52.94.76.35 21/tcp Closed/Filtered -3.83.78.214 --> 52.94.76.252 443/tcp Closed/Filtered -34.201.108.107 --> 52.94.76.191 443/tcp Closed/Filtered +54.237.193.133 --> 52.94.76.149 22/tcp Open +3.83.78.214 --> 52.94.76.252 443/tcp Open All threads completed [*] Deleting Lambda workers from AWS... [*] Done! ``` -Scan ports 1-1024 on all targets in the `targets.txt` file, using 800 Lambda function workers in the `us-west-1` region. Lambda functions will be created using the code uploaded to `s3://b424c464-6bff-11eb-89b9-faffc212389a-lambda-port-scanner/lambda-port-scanner--ports.zip`. All requests (both open and closed) will show on the console and will also be logged with a timestamp to `scan.log`: +Scan ports 1-1024 on all targets in the `targets.txt` file, using 800 Lambda function workers in the `us-west-1` region. Lambda functions will be created using the code uploaded to `s3://lambdaportscanner/workercode-portscan.zip` using the deploy script. All requests (both open and closed) will show on the console and will also be logged with a timestamp to `scan.log`: ``` -$ pipenv run python lambscanController.py \ - --role arn:aws:iam::: \ - --s3-zip b424c464-6bff-11eb-89b9-faffc212389a-lambda-port-scanner/lambdaportscanner-workercode.zip +❯ pipenv run python lambscanController.py \ + portscan \ + --role arn:aws:iam::<>:<> \ + --s3-zip lambdaportscanner/workercode-portscan.zip --target-file targets.txt \ --threads 200 \ - --workers 1000 \ + --workers 800 \ --ports 1-1024 \ --region us-west-1 \ --outfile scan.log @@ -172,10 +186,97 @@ $ pipenv run python lambscanController.py \ ...... ``` +## Usage: nmap command + +``` +❯ pipenv run python lambscanController.py nmap -h +usage: lambscanController.py nmap [-h] [--target [TARGET]] [--target-file [TARGET_FILE]] --region [REGION] --role [ROLE] + [--workers WORKER_MAX] [--threads THREAD_MAX] [--s3-zip [S3ZIP]] [--outfile OUTFILE] [-v] + nmapargs + +positional arguments: + nmapargs Arguments to pass to nmap. Note: do NOT include any output flags. Add -- before all nmap arguments to parse + correctly. Example: lambscanController.py nmap --target [] --role [] --region [] --s3-zip [] -- "-Pn --top- + ports 100" + +optional arguments: + -h, --help show this help message and exit + --workers WORKER_MAX Number of Lambda workers to create. Default: 1 + --threads THREAD_MAX Max number of threads for port scanning. Default: 1 + --s3-zip [S3ZIP] Provide a S3 bucket and key path to the code ZIP file and the controller will use that to create each + function worker instead of uploading each manually + --outfile OUTFILE Specify the output file to store timestamped scan results + -v, --verbose increase console output verbosity + +required named arguments: + --target [TARGET] IP address or CIDR range + --target-file [TARGET_FILE] + File with one IP address or CIDR per line + --region [REGION] Specify the target region to create and run the lambscan workers (e.g, us-east-1) + --role [ROLE] AWS IAM role ARN for lambda functions to use +``` + +**Note**: the nmap command automatically grabs the `.nmap`, `.gnmap`, and `.xml` output files for each scan from the Lambda worker and writes it to the current directory. + +#### Nmap command examples: + +Example nmap against the same targets in the targets.txt file. We pass the Nmap arguments `-Pn -vv --reason --top-ports 50`. + +**IMPORTANT**: ONLY TCP CONNECT Nmap scans are allowed, see "Caveats" section below for details. + +``` +❯ pipenv run python lambscanController.py \ + nmap \ + --role "arn:aws:iam::372924254905:role/service-role/lambda-port-scanner" + --s3-zip lambdaportscanner/workercode-nmap.zip + --target-file targets.txt + --threads 10 + --workers 10 + --region us-east-1 + -- "-Pn -vv --reason --top-ports 50" + +[*] Creating 10 Lambda workers in AWS... +[*] Executing Nmap scans... +52.91.251.144 --> 45.33.32.156 +3.238.204.3 --> 122.51.154.13 +3.91.89.108 --> 38.153.23.30 +3.237.190.89 --> 106.4.192.53 +3.236.105.223 --> 82.17.26.186 +34.239.128.242 --> 121.115.226.105 +34.232.63.54 --> 141.36.60.117 +All threads completed +[*] Deleting LambScan functions from AWS... +``` + +And then we can see the raw nmap output returned from the Lambda worker for the scan of the `45.33.32.156` target at `45.33.32.156.nmap` + +``` +❯ cat 45.33.32.156.nmap +# Nmap 7.60 scan initiated Fri Feb 12 18:53:10 2021 as: ./nmap -Pn -vv --reason --top-ports 50 -oA /tmp/45.33.32.156 45.33.32.156 +Nmap scan report for scanme.nmap.org (45.33.32.156) +Host is up, received user-set (0.075s latency). +Scanned at 2021-02-12 18:53:10 UTC for 1s + +PORT STATE SERVICE REASON +21/tcp closed ftp conn-refused +22/tcp open ssh syn-ack +23/tcp closed telnet conn-refused +25/tcp filtered smtp no-response +26/tcp closed rsftp conn-refused +53/tcp closed domain conn-refused +80/tcp open http syn-ack +81/tcp closed hosts2-ns conn-refused +110/tcp closed pop3 conn-refused +...... +``` + +## Usage: clean command + Crash, exception, or other issues? Trigger a manual cleanup of any leftover worker functions that may still exist in a region: -`pipenv run python lambscanController.py --clean --region us-east-1` +`pipenv run python lambscanController.py clean --region us-east-1` ## Caveats: +- **IMPORTANT:** The `nmap` subcommand **CANNOT** run with root privileges on the Lambda workers. This means you will get errors when attempting flags such as SYN-scan `-sS`, UDP `-pU`, fragmentation `-f`, and others. - You may encounter stability issues when using more than ~800 function workers, such as rate limiting issues (`TooManyRequestsException`), concurrency issues, or Boto3 session issues. If this occurs, reduce the worker count or play with the settings of the `self.botoConfig` Boto3 configuration of the Scanner class. ## Credits: diff --git a/deploy.sh b/deploy.sh index 92c1fe6..edebd0e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,13 +1,11 @@ #!/bin/bash -PORTS_LAMBDA_WORKER_ZIP='lambdaportscanner-workercode.zip' -NMAP_LAMBDA_WORKER_ZIP='lambdaportscanner-workercode-nmap.zip' -BUCKET_SUFFIX='lambda-port-scanner' -ROLE_NAME='lambda-port-scanner' +PORTS_LAMBDA_WORKER_ZIP='workercode-portscan.zip' +NMAP_LAMBDA_WORKER_ZIP='workercode-nmap.zip' +BUCKET_NAME='lambdaportscanner' +ROLE_NAME='lambdaportscanner' function create_bucket() { - GUID=$(python -c 'import uuid; print(uuid.uuid1())') - BUCKET_NAME=$GUID-$BUCKET_SUFFIX aws s3api create-bucket --bucket $BUCKET_NAME | jq '.Location' } @@ -29,9 +27,10 @@ function main() { upload_code_zips echo "[*] Creating Lambda IAM Execution Role" create_role - echo "Use these for controller arguments:" + echo "** Use these for controller arguments: **" echo "--role $ROLE_ARN" - echo "--s3-zip $BUCKET_NAME/$PORTS_LAMBDA_WORKER_ZIP" + echo "For portscan command: --s3-zip $BUCKET_NAME/$PORTS_LAMBDA_WORKER_ZIP" + echo "For nmap command: --s3-zip $BUCKET_NAME/$NMAP_LAMBDA_WORKER_ZIP" } main diff --git a/lambscanController.py b/lambscanController.py index 38ba329..16340b5 100644 --- a/lambscanController.py +++ b/lambscanController.py @@ -12,18 +12,20 @@ logger = logging.getLogger(__file__) logger.setLevel(logging.DEBUG) -ZIPFILE = "lambdaportscanner-workercode.zip" +ZIPFILE = "workercode-portscan.zip" # local ZIP if not using S3zip FN_NAME = "scanworker" class Scanner: def __init__( self, - role_arn="", + role_arn=None, + handler_name=None, + region=None, s3zip=None, worker_name=FN_NAME, worker_max=1, thread_max=1, - region=None): + nmapargs=None): self.botoConfig = Config( region_name=region, @@ -32,25 +34,26 @@ def __init__( retries={"total_max_attempts": 10} ) + self.handler_name = handler_name + self.nmapargs = nmapargs self.role_arn = role_arn self.region = region + + # Populate codeParams with the code ZIP source (S3 or local) if s3zip: - self.s3bucket = s3zip.split('/')[0] - self.s3key = "/".join(s3zip.split('/')[1:]) - logger.debug(f"Using code from S3. Bucket = {self.s3bucket}, Key = {self.s3key}") + s3bucket = s3zip.split('/')[0] + s3key = "/".join(s3zip.split('/')[1:]) + self.codeParams = {'S3Bucket': s3bucket, 'S3Key': s3key} + logger.debug(f"Using code from S3. Bucket = {s3bucket}, Key = {s3key}") else: - self.s3bucket = None - self.s3key = None + self.codeParams = {'ZipFile': open(ZIPFILE, 'rb').read()} # Create a default client that the class can use for single tasks - if region: - self.lambda_client = boto3.client('lambda', config=self.botoConfig) - else: - self.lambda_client = boto3.client('lambda') + self.lambda_client = boto3.client('lambda', config=self.botoConfig) - self.worker_name = worker_name # Prefix name of Lambda functions - self.worker_max = int(worker_max) # Max number of Lambda worker functions - self.worker_current = 0 # Keep track of which Lambda worker was the last used + self.worker_name = worker_name # Prefix name of Lambda functions + self.worker_max = int(worker_max) # Max number of Lambda worker functions + self.worker_current = 0 # Keep track of which Lambda worker was the last used self.thread_max = int(thread_max) # Intialize other stuff @@ -210,7 +213,7 @@ def scan_next_tuple(self): # Use lock, save what worker we should use, then increment for next with self.lck: local_worker = self.worker_current - logger.debug(f"Using worker {self.worker_name}_{local_worker} for target: {tmpTarget}:{tmpPort}") + logger.debug(f"Using {self.worker_name}_{local_worker} for target: {tmpTarget}:{tmpPort}") self.next_worker() # Create new boto3 client to be thread-safe @@ -240,17 +243,11 @@ def scan_next_tuple(self): return({"source_ip": source_ip, "target": tmpTarget, "port": tmpPort, "status": status}) # Start multi-threading job to create lambda functions in AWS faster - def createWorkers(self): - # Determine code location and load - if self.s3bucket: - codeParams = {'S3Bucket': self.s3bucket, 'S3Key': self.s3key} - else: - codeParams = {'ZipFile': open(ZIPFILE, 'rb').read()} - + def createWorkers(self, func_timeout): future_list = [] with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: for i in range(self.worker_max): - future = executor.submit(self.create_worker_function, i, codeParams) + future = executor.submit(self.create_worker_function, i, func_timeout) future_list.append(future) for future in concurrent.futures.as_completed(future_list): try: @@ -267,7 +264,7 @@ def createWorkers(self): raise Exception(f"Error: could not create required worker functions. Counted: {tmpCount} Requested: {self.worker_max}") # Create single AWS lambda function worker - def create_worker_function(self, functionIndex, codeParams): + def create_worker_function(self, functionIndex, func_timeout): fn_name = self.worker_name + "_" + str(functionIndex) try: logger.debug(f"Creating worker: {fn_name}") @@ -276,9 +273,9 @@ def create_worker_function(self, functionIndex, codeParams): FunctionName=fn_name, Runtime='python3.8', Role=self.role_arn, - Handler=f"{self.worker_name}.lambda_handler", - Code=codeParams, - Timeout=20) + Handler=f"{self.handler_name}.lambda_handler", + Code=self.codeParams, + Timeout=func_timeout) return(f"Success creating: {fn_name}") except Exception as e: if hasattr(e, 'message'): @@ -286,75 +283,137 @@ def create_worker_function(self, functionIndex, codeParams): else: raise type(e)('"Error happened with %s' % fn_name) + # Conduct an nmap scan against the targets + def threadedNmapScan(self): + future_list = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=self.thread_max) as executor: + for target in self.targets: + future = executor.submit(self.nmapscan_next_target, target) + future_list.append(future) + for future in concurrent.futures.as_completed(future_list): + try: + res = future.result() + except Exception as e: + logger.info(e) + del future + logger.info("All threads completed") + + # Scan a single tuple target + def nmapscan_next_target(self, target): + # Pop the next IP target off + tmpTarget = target + + # Use lock, save what worker we should use, then increment for next + with self.lck: + local_worker = self.worker_current + logger.debug(f"Using {self.worker_name}_{local_worker} for target: {tmpTarget}") + self.next_worker() + + # Create new boto3 client to be thread-safe + tmp_lambda_client = boto3.client('lambda', config=self.botoConfig) + + # Invoke Lambda function to actually execute the port scan + response = tmp_lambda_client.invoke( + FunctionName=f'{self.worker_name}_{local_worker}', + InvocationType='RequestResponse', + Payload=json.dumps(dict({"args": self.nmapargs, "target": tmpTarget})) + ) + + # Get response + responseJson = json.loads(response['Payload'].read().decode('utf-8')) + if 'errorMessage' in responseJson: + logger.info(f'Error occured for: {tmpTarget}') + return responseJson['errorMessage'] # try to catch lambda errors + else: + logger.debug(f"Scan complete for {tmpTarget}") + #logger.debug(f"Details for {tmpTarget}: {responseJson}") + writeOutput(responseJson) + + # Log source IPs for each scan + logger.info(f"{responseJson['source_ip']:<15} --> {responseJson['target']:<15}") + + return True + + +# Helper function to write nmap/gnmap/xml output files +def writeOutput(responseJson): + target = responseJson['target'] + with open(f"{target}.gnmap", 'w') as fout: + fout.write(responseJson['output_gnmap']) + with open(f"{target}.nmap", 'w') as fout: + fout.write(responseJson['output_nmap']) + with open(f"{target}.xml", 'w') as fout: + fout.write(responseJson['output_xml']) ########################################## # Parse arguments ########################################## def parse_args(parser): - parser.add_argument('--ports', nargs='?', dest='ports', help='Ports to scan. Example:80,81,1000-2000') - parser.add_argument('--role', nargs='?', dest='role', help='AWS IAM role ARN for lambda functions to use') - parser.add_argument('--target', nargs='?', dest='target', help='IP address or CIDR range') - parser.add_argument('--target-file', nargs='?', dest='target_file', help='File with one IP address or CIDR per line') - parser.add_argument('--workers', default=1, dest='worker_max', help='Number of Lambda workers to create') - parser.add_argument('--threads', default=1, dest='thread_max', help='Max number of threads for port scanning') - parser.add_argument('--clean', default=False, action='store_true', help='Do not scan. Delete all Lambda functions matching ^scanworker_. Use if something goes wrong.') - parser.add_argument('--region', dest='region', help='Specify the target region to create and run the lambscan workers (e.g, us-east-1)') - parser.add_argument('--outfile', dest='outfile', help='Specify the output file to store timestamped scan results') - parser.add_argument('--open', dest='open', action="store_true", help='Only show open ports') - parser.add_argument('--s3-zip', dest='s3zip', nargs='?', help='Provide a S3 bucket and key path to the code ZIP file and the controller will use that to create each function worker instead of uploading each manually') - parser.add_argument("-v", "--verbose", dest='verbose', action="store_true", help="increase console output verbosity") + #parser.add_argument('command', help='subcommand for scan type') + + subparsers = parser.add_subparsers(dest="subcommand", metavar='') + + # PORTSCAN SUBCOMMAND PARSING + portscan = subparsers.add_parser('portscan', help='Portscan subcommand. Get port up/down status. scan is randomized across target ports and IPs') + requiredNamed = portscan.add_argument_group('required named arguments') + requiredNamed.add_argument('--ports', nargs='?', required=True, dest='ports', help='Ports to scan. Example:80,81,1000-2000') + requiredNamed.add_argument('--role', nargs='?', required=True, dest='role', help='AWS IAM role ARN for lambda functions to use') + requiredNamed.add_argument('--target', nargs='?', dest='target', help='IP address or CIDR range') + requiredNamed.add_argument('--target-file', nargs='?', dest='target_file', help='File with one IP address or CIDR per line') + requiredNamed.add_argument('--region', nargs='?', dest='region', required=True, help='Specify the target region to create and run the lambscan workers (e.g, us-east-1)') + portscan.add_argument('--workers', default=1, dest='worker_max', help='Number of Lambda workers to create. Default: 1') + portscan.add_argument('--threads', default=1, dest='thread_max', help='Max number of threads for port scanning. Default: 1') + portscan.add_argument('--outfile', dest='outfile', help='Specify the output file to store timestamped scan results') + portscan.add_argument('--open', dest='open', action="store_true", help='Only show open ports') + portscan.add_argument('--s3-zip', dest='s3zip', nargs='?', help='Provide a S3 bucket and key path to the code ZIP file and the controller will use that to create each function worker instead of uploading each manually') + portscan.add_argument("-v", "--verbose", dest='verbose', action="store_true", help="increase console output verbosity") + portscan.set_defaults(func=subCommand_portscan) + + # NMAP SUBCOMMAND PARSING + nmap = subparsers.add_parser('nmap', help='Nmap subcommand. Each Lambda worker will conduct Nmap on its target') + nmap.add_argument('nmapargs', help='''Arguments to pass to nmap. Note: do NOT include any output flags. Add -- before all nmap arguments to parse correctly. + Example: %(prog)s --target [] --role [] --region [] --s3-zip [] -- "-Pn --top-ports 100"''') + nmaprequiredNamed = nmap.add_argument_group('required named arguments') + nmaprequiredNamed.add_argument('--target', nargs='?', dest='target', help='IP address or CIDR range') + nmaprequiredNamed.add_argument('--target-file', nargs='?', dest='target_file', help='File with one IP address or CIDR per line') + nmaprequiredNamed.add_argument('--region', nargs='?', dest='region', required=True, help='Specify the target region to create and run the lambscan workers (e.g, us-east-1)') + nmaprequiredNamed.add_argument('--role', nargs='?', required=True, dest='role', help='AWS IAM role ARN for lambda functions to use') + nmap.add_argument('--workers', default=1, dest='worker_max', help='Number of Lambda workers to create. Default: 1') + nmap.add_argument('--threads', default=1, dest='thread_max', help='Max number of threads for port scanning. Default: 1') + nmap.add_argument('--s3-zip', dest='s3zip', nargs='?', help='Provide a S3 bucket and key path to the code ZIP file and the controller will use that to create each function worker instead of uploading each manually') + nmap.add_argument('--outfile', dest='outfile', help='Specify the output file to store timestamped scan results') + nmap.add_argument("-v", "--verbose", dest='verbose', action="store_true", help="increase console output verbosity") + nmap.set_defaults(func=subCommand_nmap) + + # CLEAN SUBCOMMAND PARSING + clean = subparsers.add_parser('clean', help='Clean subcommand. Do not scan. Delete all Lambda functions matching ^scanworker_. Use if something goes wrong.') + cleanrequiredNamed = clean.add_argument_group('required named arguments') + cleanrequiredNamed.add_argument('--region', nargs='?', dest='region', required=True, help='Specify the target region to clean') + clean.add_argument("-v", "--verbose", dest='verbose', action="store_true", help="increase console output verbosity") + clean.set_defaults(func=subCommand_clean) + args = parser.parse_args() return args -############################################## -# Main program execution -############################################## -if __name__ == "__main__": - - # Parse arguments - parser = argparse.ArgumentParser(description='Create workers in Lambda to conduct distributed port scans on selected targets') - args = parse_args(parser) - region = args.region - # -- LOGGER: CONSOLE HANDLER -- - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG if args.verbose else logging.INFO) - logger.addHandler(ch) - # -- LOGGER: FILE HANDLER -- - if args.outfile: - # create file handler which logs only the scan result messages - fh = logging.FileHandler(args.outfile) - fh.setLevel(logging.WARNING) - # create formatter - formatter = logging.Formatter('%(asctime)s - %(message)s') - # add formatter to fh - fh.setFormatter(formatter) - logger.addHandler(fh) - # log the execution - logger.warning(f'{__file__.split("/")[-1]} ARGS: {vars(args)}') +############################################## +# Main program execution +############################################## +def subCommand_portscan(args): # Initialize scanner scanner = Scanner( role_arn=args.role, worker_name=FN_NAME, + handler_name="lambscan", worker_max=args.worker_max, thread_max=args.thread_max, s3zip=args.s3zip, region=args.region) - # Check if user wants to clean out old functions - if args.clean: - try: - print("[*] Deleting LambScan functions from AWS...") - scanner.threaded_lambda_cleanup() - except Exception as e: - print("ERROR: Could not delete LambScan functions!\nException: " + str(e)) - exit(1) - exit(0) - # Make sure required arguments are set if not (args.target or args.target_file) or not args.ports or not args.role: parser.print_help() @@ -379,7 +438,7 @@ def parse_args(parser): # Create the AWS lambda function workers print(f"[*] Creating {args.worker_max} Lambda workers in AWS...") - scanner.createWorkers() + scanner.createWorkers(func_timeout=20) # 20s timeout for portscans # Begin the port scan print("[*] Scanning ports...") @@ -390,3 +449,96 @@ def parse_args(parser): print("[*] Deleting Lambda workers from AWS...") scanner.threaded_lambda_cleanup() print("[*] Done!") + + +def subCommand_nmap(args): + global ZIPFILE + ZIPFILE = "workercode-nmap.zip" # local ZIP if not using S3zip + + # Make sure required arguments are set + if not (args.target or args.target_file) or not args.role: + parser.print_help() + exit(1) + + if args.target and args.target_file: + print("ERROR: Specify a target or a target file, not both.") + exit() + + logger.debug(f"Using nmap args: {args.nmapargs}") + + # Initialize scanner + scanner = Scanner( + role_arn=args.role, + worker_name=FN_NAME, + handler_name="nmap_aws", + worker_max=args.worker_max, + thread_max=args.thread_max, + s3zip=args.s3zip, + region=args.region, + nmapargs=args.nmapargs) + + # Add target IPs + if args.target_file: + scanner.add_targets_from_file(args.target_file) + if args.target: + scanner.add_target(args.target) + + # Shuffle targets + shuffle(scanner.targets) + logger.debug(f"{len(scanner.targets)} targets loaded") + + # Create the AWS lambda function workers + print(f"[*] Creating {args.worker_max} Lambda workers in AWS...") + scanner.createWorkers(func_timeout=300) # 5m timeout for nmap scans + + # Start nmap scans + print("[*] Executing Nmap scans...") + logger.debug(f"Using {scanner.thread_max} threads") + scanner.threadedNmapScan() + + # Clean up functions + print("[*] Deleting LambScan functions from AWS...") + scanner.threaded_lambda_cleanup() + print("[*] Done!") + + +def subCommand_clean(args): + scanner = Scanner(region=args.region) + try: + print("[*] Deleting LambScan functions from AWS...") + scanner.threaded_lambda_cleanup() + except Exception as e: + print("ERROR: Could not delete LambScan functions!\nException: " + str(e)) + exit(1) + exit(0) + + +if __name__ == "__main__": + + # Parse arguments + parser = argparse.ArgumentParser() + args = parse_args(parser) + + # -- LOGGER: CONSOLE HANDLER -- + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG if args.verbose else logging.INFO) + logger.addHandler(ch) + + # Break here to clean subcommand + if args.subcommand == 'clean': + subCommand_clean(args) + + # -- LOGGER: FILE HANDLER -- + if args.outfile: + # create file handler which logs only the scan result messages + fh = logging.FileHandler(args.outfile) + fh.setLevel(logging.WARNING) + # create formatter + formatter = logging.Formatter('%(asctime)s - %(message)s') + # add formatter to fh + fh.setFormatter(formatter) + logger.addHandler(fh) + # log the execution + logger.warning(f'{__file__.split("/")[-1]} ARGS: {vars(args)}') + + args.func(args) diff --git a/teardown.sh b/teardown.sh new file mode 100755 index 0000000..15a1e64 --- /dev/null +++ b/teardown.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +BUCKET_NAME='lambdaportscanner' +ROLE_NAME='lambdaportscanner' + +function delete_bucket() { + aws s3 rm "s3://$BUCKET_NAME/" --recursive --no-cli-pager + aws s3api delete-bucket --bucket $BUCKET_NAME --no-cli-pager +} + +function delete_role() { + aws iam delete-role --role-name $ROLE_NAME --no-cli-pager +} + +function main() { + echo "[*] Tearing down S3 bucket" + delete_bucket + echo "[*] Tearing down Lambda IAM Execution Role" + delete_role +} + +main +exit 0 \ No newline at end of file diff --git a/lambdaportscanner-workercode-nmap.zip b/workercode-nmap.zip similarity index 100% rename from lambdaportscanner-workercode-nmap.zip rename to workercode-nmap.zip diff --git a/lambdaportscanner-workercode.zip b/workercode-portscan.zip similarity index 100% rename from lambdaportscanner-workercode.zip rename to workercode-portscan.zip