def exit_gracefully(sig, frame) -> None: """Signal handler that tries to warn the C&C server that the node is shutting down before it does so.""" print("Exiting...") ip = config.get('NAT_IP', config['IP']) port = config.get('NAT_PORT', config['PORT']) signature = signatures.new_signature( config['C2_SECRET'], "DELETE", f"/environments/{ip}/{port}") authorization_content = ( signatures.new_authorization_header("Node", signature)) try: resp = rq.delete( f"{config['C2_URL']}/environments/{ip}/{port}", headers={'Authorization': authorization_content}) resp.raise_for_status() except rq.exceptions.ConnectionError: print("Could not contact Command and Control server before exiting.") except Exception: print(resp.json()['error']) finally: sys.exit()
def install(password: str, ip: str, port: int, packages: List[str]): """Install the given PACKAGES in the environment at IP:PORT.""" prepared = requests.Request("PATCH", f"{C2_URL}/environments/{ip}/{port}/installed", json=packages).prepare() digest = b64encode(sha256(prepared.body).digest()).decode() prepared.headers['Digest'] = f"sha-256={digest}" headers = ['Digest'] signature = signatures.new_signature( password.encode(), "PATCH", f"/environments/{ip}/{port}/installed", signature_headers=headers, header_recoverer=lambda h: prepared.headers.get(h)) prepared.headers['Authorization'] =\ signatures.new_authorization_header("Client", signature, headers) try: resp = requests.Session().send(prepared) except requests.exceptions.ConnectionError: click.echo("Connection refused.") else: if resp.status_code in {400, 401, 404, 415, 500, 502, 504}: click.echo(resp.json()['error']) elif resp.status_code != 204: click.echo("Unexpected response from Command and Control Sever.")
def remove_available_packages(password: str, packages: List[str]): """Delete the given top level PACKAGES from the C&C server.""" key = password.encode() for pack in packages: signature = signatures.new_signature(key, "DELETE", f"/test_sets/{pack}") auth_content = signatures.new_authorization_header("Client", signature) try: resp = requests.delete(f"{C2_URL}/test_sets/{pack}", headers={'Authorization': auth_content}) except requests.exceptions.ConnectionError: click.echo("Connection refused.") else: if resp.status_code in {401, 404}: click.echo(resp.json()['error']) elif resp.status_code != 204: click.echo( "Unexpected response from Command and Control Sever.")
def delete_executions(password: str, executions: List[int]): """Delete the specified EXECUTIONS.""" key = password.encode() for execution in executions: signature = signatures.new_signature(key, "DELETE", f"/executions/{execution}") auth_content = signatures.new_authorization_header("Client", signature) try: resp = requests.delete(f"{C2_URL}/executions/{execution}", headers={'Authorization': auth_content}) except requests.exceptions.ConnectionError: click.echo("Connection refused.") else: if resp.status_code in {401, 404}: click.echo(resp.json()['error']) elif resp.status_code != 204: click.echo( "Unexpected response from Command and Control Sever.")
def uninstall(password: str, ip: str, port: int, packages: List[str]): """Remove the specified PACKAGES from the node at IP:PORT.""" key = password.encode() for pack in packages: signature = signatures.new_signature( key, "DELETE", f"/environments/{ip}/{port}/installed/{pack}") auth_content = signatures.new_authorization_header("Client", signature) try: resp = requests.delete( f"{C2_URL}/environments/{ip}/{port}/installed/{pack}", headers={'Authorization': auth_content}) except requests.exceptions.ConnectionError: click.echo("Connection refused.") else: if resp.status_code in {401, 404, 502, 504}: click.echo(resp.json()['error']) elif resp.status_code != 204: click.echo( "Unexpected response from Command and Control Sever.")
def connect_to_c2() -> bool: """Tries to make contact with the C&C server. Returns ------- bool Wheter the C&C was sucessfully reached and it returned a status code of 204. """ prepared = rq.Request( "POST", f"{config['C2_URL']}/environments", json={ 'ip': config.get('NAT_IP', config['IP']), 'port': config.get('NAT_PORT', config['PORT']), 'platform_info': get_platform_info() }).prepare() digest = b64encode(sha256(prepared.body).digest()).decode() prepared.headers['Digest'] = f"sha-256={digest}" headers = ['Digest'] signature = signatures.new_signature( config['C2_SECRET'], "POST", "/environments", signature_headers=headers, header_recoverer=lambda h: prepared.headers.get(h)) prepared.headers['Authorization'] = ( signatures.new_authorization_header("Node", signature, headers)) try: resp = rq.Session().send(prepared) except rq.exceptions.ConnectionError: return False return resp.status_code == 204
def stop_active_environments() -> None: """Tries to shutdown all currently active nodes. It also updates the database ending all current sessions.""" get_memory_storage().flushdb(asynchronous=True) db = get_database() cursor = db.execute("""SELECT env_ip, env_port FROM session WHERE session_end IS NULL""") environments = cursor.fetchall() if environments: signature = signatures.new_signature(current_app.config['NODE_SECRET'], "DELETE", "/") authorization_content = (signatures.new_authorization_header( "C2", signature)) for env in environments: ip = env['env_ip'] port = env['env_port'] try: resp = rq.delete( f"http://{ip}:{port}/", headers={'Authorization': authorization_content}) except rq.exceptions.ConnectionError: click.echo(f"Node at {ip}:{port} could not be reached.") else: if resp.status_code != 204: click.echo( f"Unexpected response from node at {ip}:{port}.") else: click.echo(f"Node at {ip}:{port} reached.") cursor.execute("""UPDATE session SET session_end = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE session_end IS NULL""") db.commit()
def delete_installed_package(ip, port, package): check_authorization_header(client_key_recoverer) check_registered(ip, port) signature = signatures.new_signature(current_app.config['NODE_SECRET'], "DELETE", f"/test_sets/{package}") authorization_content = signatures.new_authorization_header( "C2", signature) environment_key = f"environments:{ip}:{port}" memory_storage = get_memory_storage() with memory_storage.lock(f"{environment_key}:installed:mutex", timeout=30, sleep=1): try: resp = rq.delete(f"http://{ip}:{port}/test_sets/{package}", headers={'Authorization': authorization_content}) except rq.exceptions.ConnectionError: abort(504, description="The requested environment could not be reached") if resp.status_code == 204: installed_cached = memory_storage.hget(environment_key, "installed_cached") if installed_cached == "1": # Updates cache if it exists. pipe = memory_storage.pipeline() pipe.hdel(environment_key, f"installed:{package}") pipe.zrem(f"{environment_key}:installed_index", package) pipe.execute() return Response(status=204, mimetype="application/json") if resp.status_code in {401, 404}: return abort(404, description=f"'{package}' not found at {ip}:{port}") abort(502, description=f"Unexpected response from node at {ip}:{port}")
def upload_compressed_packages(password: str, file_path: click.Path): """Uploads a tar.gz file full of packages to the C&C server.""" if not file_path.endswith(".tar.gz"): click.echo("Only .tar.gz extension allowed.") else: with open(file_path, "rb") as f: prepared = requests.Request("PATCH", f"{C2_URL}/test_sets", files={ 'packages': f }).prepare() digest = b64encode(sha256(prepared.body).digest()).decode() prepared.headers['Digest'] = f"sha-256={digest}" headers = ['Digest'] signature = signatures.new_signature( password.encode(), "PATCH", "/test_sets", signature_headers=headers, header_recoverer=lambda h: prepared.headers.get(h)) prepared.headers['Authorization'] = ( signatures.new_authorization_header("Client", signature, headers)) try: resp = requests.Session().send(prepared) except requests.exceptions.ConnectionError: click.echo("Connection refused.") else: if resp.status_code in {400, 401, 415}: click.echo(resp.json()['error']) elif resp.status_code != 204: click.echo( "Unexpected response from Command and Control Sever.")
def install_packages(ip, port): check_digest_header() check_authorization_header(client_key_recoverer, "Digest") check_registered(ip, port) check_is_json() packages = request.json memory_storage = get_memory_storage() with rcl.ReaderLock(memory_storage, "repository", 30, 1): with tempfile.SpooledTemporaryFile() as f: # Can throw ValueError. try: test_utils.compress_test_packages( f, packages, current_app.config['TESTS_PATH']) except ValueError as e: abort(400, description=str(e)) f.seek(0) prepared = rq.Request("PATCH", f"http://{ip}:{port}/test_sets", files={ 'packages': f }).prepare() digest = b64encode(sha256(prepared.body).digest()).decode() prepared.headers['Digest'] = f"sha-256={digest}" headers = ['Digest'] signature = signatures.new_signature( current_app.config['NODE_SECRET'], "PATCH", "/test_sets", signature_headers=headers, header_recoverer=lambda h: prepared.headers.get(h)) prepared.headers['Authorization'] = ( signatures.new_authorization_header("C2", signature, headers)) environment_key = f"environments:{ip}:{port}" with memory_storage.lock(f"{environment_key}:installed:mutex", timeout=30, sleep=1): try: resp = rq.Session().send(prepared) except rq.exceptions.ConnectionError: abort( 504, description="The requested environment could not be reached" ) if resp.status_code == 204: installed_cached = memory_storage.hget(environment_key, "installed_cached") if installed_cached == "1": # Updates cache if it exists. pipe = memory_storage.pipeline() for pack in packages: pipe.hset(environment_key, f"installed:{pack}", memory_storage.get(f"repository:{pack}")) pipe.zadd(f"{environment_key}:installed_index", {pack: 0}) pipe.execute() return Response(status=204, mimetype="application/json") if resp.status_code in {400, 401, 415}: abort(500, description="Something went wrong when handling the request") abort(502, description=f"Unexpected response from node at {ip}:{port}")