Example #1
0
def handle_db(args):
    """
    awspx db
    """

    if args.load_zip:
        db = args.load_zip.split('_')[-1][0:-4] + ".db"

        if not args.load_zip.startswith("/opt/awspx/data/"):
            args.load_zip = "/opt/awspx/data/" + args.load_zip

        print(f"[*] Importing records from {args.load_zip}")
        (success, message) = Neo4j.load(args.load_zip, db)
        print(f"{message}\n")

        print("Run `awspx attacks` to calculate attack paths.")

        if not success:
            sys.exit(1)

    elif args.list_dbs:
        print("\n".join([
            db for db in os.listdir("/data/databases/")
            if os.path.isdir(os.path.join("/data/databases/", db))
        ]))

    elif args.use_db:
        print(f"[+] Changing database to {args.use_db} "
              "(remember to refresh your browser)")
        Neo4j.switch_database(args.use_db)
        Neo4j.restart()
Example #2
0
def handle_db(args, console=console):
    """
    awspx db
    """

    db = Neo4j(console=console)

    if args.load_zips:

        db.load_zips(archives=args.load_zips,
                     db=args.database if 'database' in args else 'default.db')

    elif args.list_dbs:
        db.list()

    elif args.use_db:
        db.use(args.use_db)
Example #3
0
def loaded():
    """
    Fetch previously loaded resources and services from the database.
    """

    resources = [
        r["r"] for r in Neo4j.run("MATCH (g:Generic) "
                                  "WITH [_ IN LABELS(g) "
                                  "WHERE _ <> 'Generic'][0] AS r "
                                  "RETURN r ORDER BY r")
    ]

    services = [
        s for s in SERVICES
        if s.upper() in list(set([s.split('::')[1].upper()
                                  for s in resources]))
    ]

    return {"Resources": resources, "Services": services}
Example #4
0
def db(args):
    """
    awspx db
    """

    dbdir = "/data/databases/"

    if args.list_dbs:
        databases = [
            d for d in os.listdir(dbdir)
            if os.path.isdir(os.path.join(dbdir, d))
        ]
        print("\n".join(databases))

    elif args.use_db:
        if args.use_db[-3:] == ".db":
            d = args.use_db
        else:
            d = args.use_db + ".db"

        db = os.path.join(dbdir, d)
        if os.path.isdir(db):
            Neo4j.switch_database(db)
            Neo4j.restart()

        print(f"Switched to {db}.")

    elif args.load_zip:
        zips = [z for z in os.listdir("/opt/awspx/data") if args.load_zip in z]
        dbname = args.load_zip.replace(".zip", ".db")

        if zips:
            print(f"[+] Loading /opt/awspx/data/{zips[0]} into {dbname}.")
            Neo4j.load("/opt/awspx/data/" + zips[0], dbname)
        else:
            print(f"[-] {args.load_zip} not found in /opt/awpsx/data/.")
Example #5
0
    def compute(self, max_iterations=5, max_search_depth=""):

        converged = False
        db = Neo4j(console=self.console)

        self.console.task("Removing all existing attack patterns",
                          db.run,  args=["MATCH (p:Pattern) DETACH DELETE p"],
                          done="Removed all existing attack patterns")

        self.console.task("Creating pseudo Admin",
                          db.run,  args=[
                              "MERGE (admin:Admin:`AWS::Iam::Policy`{"
                              "Name: 'Effective Admin', "
                              "Description: 'Pseudo-Policy representing full and unfettered access.', "
                              "Arn: 'arn:aws:iam::${Account}:policy/Admin', "
                              'Document: \'[{"DefaultVersion": {"Version": "2012-10-17", '
                              '"Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"'
                              '}]}}]\''
                              '}) '
                          ],
                          done="Created pseudo Admin")

        for i in self.console.tasklist(
            "Adding attack paths (this search can take a while)",
            range(max_iterations * len(self.definitions)),
            done="Added attack paths"
        ):
            if converged:
                continue

            # First iteration
            elif (i % len(self.definitions) == 0):
                self.console.info("Temporarily adding Admin label "
                                  "to Generic Policy")
                db.run("MATCH (gp:`AWS::Iam::Policy`:Generic) SET gp:Admin")

            # Last iteration, check for convergence
            elif ((i + 1) % len(self.definitions) == 0):

                self.console.info("Removing Admin label from Generic Policy")
                db.run("MATCH (admin:`AWS::Iam::Policy`:Generic) "
                       "REMOVE admin:Admin")

                self.console.info("Pruning attack paths")

                # Only keep the 'cheapest' paths to admin, favouring transitive
                # relationships over attacks
                db.run("MATCH shortestPath((admin)-[:ATTACK|TRANSITIVE*1..]->(:Admin)) "
                       "WHERE NOT (admin:Pattern OR admin:Admin) "
                       "WITH admin MATCH path=(admin)-[:ATTACK|:TRANSITIVE*..]->(:Admin) "
                       "WITH DISTINCT admin, path, "
                       "REDUCE(sum=0, _ IN EXTRACT(_ IN RELS(path)|"
                       "COALESCE(_.Weight, 0))|sum + _) AS weight "
                       "ORDER BY admin, weight "
                       "WITH admin, COLLECT([weight, path]) AS paths "
                       "WITH admin, FILTER(attack IN NODES(paths[0][1]) "
                       "WHERE attack:Pattern) AS cheapest "
                       "MATCH path=(admin)-[:ATTACK]->(pattern:Pattern) "
                       "WHERE NOT pattern IN cheapest "
                       "WITH pattern MATCH (source)-[attack:ATTACK]->(pattern) "
                       "MERGE (source)-[_attack_:_ATTACK_]->(pattern) "
                       "ON CREATE SET _attack_ = attack "
                       "DELETE attack"
                       )

                db.run("MATCH (admin)-[:ATTACK*..2]->(:Admin) "
                       "WITH COLLECT(DISTINCT admin) AS admins "
                       "WITH admins UNWIND admins AS admin "
                       "MATCH (source)-[:TRANSITIVE*1..]->(target) "
                       "WHERE target IN admins "
                       "WITH DISTINCT source "
                       "MATCH (source)-[attack:ATTACK]->(pattern:Pattern) "
                       "MERGE (source)-[_attack_:_ATTACK_]->(pattern) "
                       "ON CREATE SET _attack_ = attack "
                       "DELETE attack "
                       )

                if sum([s["nodes_created"] + s["relationships_created"]
                        for s in self.stats[-(len(self.definitions)):]]) == 0:

                    converged = True
                    self.console.info("Search converged on iteration: "
                                      f"{iteration} of max: {max_iterations} - "
                                      "Tidying up")

                    # Update attack descriptions
                    db.run("MATCH (:Pattern)-[attack:ATTACK|OPTION|CREATE]->() "
                           "WHERE LENGTH(attack.Commands) > 0 "
                           "WITH COLLECT(DISTINCT attack) AS attacks "
                           "UNWIND attacks AS attack "
                           "WITH attacks, COLLECT(DISTINCT ["
                           "    attack.Commands[LENGTH(attack.Commands) - 1], "
                           "    attack.Description]) as commands "
                           "UNWIND attacks AS attack "
                           "WITH attack, commands WHERE TYPE(attack) = 'ATTACK' "
                           "WITH attack, EXTRACT(command IN attack.Commands|"
                           "    COALESCE([description IN commands "
                           "        WHERE command = description[0]][0][1], "
                           "        attack.Description)) AS descriptions "
                           "WITH attack, descriptions "
                           "SET attack.Descriptions = descriptions "
                           "REMOVE attack.Description"
                           )

                    # Remove redundant attack paths
                    db.run("MATCH ()-[:_ATTACK_]->(pattern:Pattern) "
                           "DETACH DELETE pattern"
                           )
                    # Remove attacks affecting generic resources
                    db.run("OPTIONAL MATCH ()-[:ATTACK]->(:Pattern)-[attack:ATTACK]->(:Generic) "
                           "DELETE attack"
                           )

                    continue

            pattern = list(self.definitions.keys())[i % len(self.definitions)]
            iteration = int(i / len(self.definitions)) + 1
            definition = self.definitions[pattern]
            timestamp = time.time()

            self.console.info(f"Searching for attack: {pattern} "
                              f"(iteration: {iteration} of max: {max_iterations})")

            cypher = self._pattern_cypher(pattern, definition,
                                          max_search_depth)

            summary = db.run(cypher)._summary

            self.stats.append({
                "pattern": pattern,
                "iteration": iteration,
                "nodes_created": 0,
                "relationships_created": 0,
                "properties_set": 0,
                "labels_added": 0,
                "time_elapsed": time.time() - timestamp,
                ** summary.counters.__dict__
            })

        db.close()

        discovered = sum([s["nodes_created"] if "nodes_created" in s
                          else 0 for s in self.stats])

        self.console.notice(f"{discovered} potential attack paths "
                            "were added to the database")
Example #6
0
    def compute(max_iterations=5,
                except_attacks=[],
                only_attacks=[],
                max_search_depth="",
                ignore_actions_with_conditions=True):

        exception = None

        print("[*] Searching database for attack patterns\n")

        sys.stdout.write("\033[F\033[K")
        print("[*] Removing all existing attack patterns")

        Neo4j().run("MATCH (pattern:Pattern) "
                    "OPTIONAL MATCH ()-[admin]->(:Admin) "
                    "DETACH DELETE pattern, admin")

        sys.stdout.write("\033[F\033[K")
        print("[*] Creating pseudo admin")
        Neo4j().run(Attacks._admin_cypher())

        # Temporarily set generic policy to admin. This is
        # because all attack paths that allow for reaching
        # this node implicitly grant admin.

        Neo4j().run("MATCH (policy:Generic:Policy) SET policy:Admin")

        # Identify any new attack paths, we stop when we've
        # converged or when we've exceeded the maximum number
        # of iterations.

        attack_definitions = {
            k: v
            for k, v in Attacks.definitions.items()
            if k not in except_attacks and (
                only_attacks == [] or k in only_attacks)
        }

        try:
            discovered = 0
            _ = 0
            for _ in range(1, max_iterations + 1):

                attack = 0
                created = 0

                for name, definition in attack_definitions.items():

                    exception = name
                    attack += 1

                    sys.stdout.write("\033[F\033[K")
                    print("[*] Searching for attack "
                          f"{attack}/{len(attack_definitions)}: "
                          f"{name} (iteration: {_} of max: {max_iterations})")

                    cypher = Attacks._pattern_cypher(
                        name,
                        definition,
                        max_search_depth=max_search_depth,
                        ignore_actions_with_conditions=
                        ignore_actions_with_conditions)

                    summary = Neo4j().run(cypher).summary()

                    if str(summary.counters) == "{}":
                        continue

                    created += 1
                    discovered += summary.counters.nodes_created

                if created == 0:
                    break

            exception = None

        except Exception as e:
            print(f"[-] Neo4j returned:\n\n{e}")
            print("[!] Don't worry, we'll use what we already have")

        sys.stdout.write("\033[F\033[K")
        print("[+] Consolidating attack patterns")

        # Remove :Admin (restore generic policy definition)
        Neo4j().run(
            "MATCH (source:Pattern)-[edge]->(policy:`AWS::Iam::Policy`:Generic:Admin) "
            + "MERGE (source)-[admin:ADMIN]->(policy) " +
            "ON CREATE SET admin = edge " + "DELETE edge " +
            "REMOVE policy:Admin")

        # Patch generalised CYPHER queries to reflect a unified admin definition
        # Note to self: I'm not sure why we've chosen to flatten
        # (source)-->(pattern)-->(admin)

        Neo4j().run(
            "MATCH (admin:Admin), " +
            "path=(source:Resource)-[:ATTACK]->(pattern:Pattern)-[edge:ATTACK{Admin:True}]->(target) "
            + "MERGE (pattern)-[_:ATTACK]->(admin) " +
            "ON CREATE SET _ = edge, _.Target = ID(edge)")

        # Replace all attack 'Description' entries with a 'Descriptions' set, that maps
        # to all 'Commands' present in the attack.

        # TODO: Need to work out whether a description is referencing the same edge for collection
        Neo4j().run(
            "MATCH ()-[attack:ATTACK]->() " +
            "WITH attack UNWIND attack.Commands AS command " +
            "OPTIONAL MATCH (:Pattern)-[_]->() " +
            "WHERE command IN _.Commands " +
            "WITH attack, command, _ ORDER BY _.Weight " +
            "WITH attack, command, COLLECT(_)[0] AS _ " +
            "WITH attack, COALESCE(_.Description, attack.Description) AS description "
            + "WITH attack, COLLECT(description) AS descriptions " +
            "SET attack.Descriptions = descriptions " +
            "REMOVE attack.Description")

        sys.stdout.write("\033[F\033[K")
        print(f"[+] {discovered} patterns were discovered " +
              str(f"(successfully converged after {_} iterations)"
                  if _ < max_iterations else
                  f"(failed to converge of {max_iterations})"))

        if exception is not None:
            raise Exception(exception)
Example #7
0
    def compute(
        max_iterations=5,
        skip_attacks=[],
        only_attacks=[],
        max_search_depth="",
        ignore_actions_with_conditions=True
    ):

        stats = []
        iteration = 0
        exception = None

        print("[*] Searching database for attack patterns\n")

        sys.stdout.write("\033[F\033[K")
        print("[*] Removing all existing attack patterns")

        Neo4j().run("MATCH (p:Pattern) DETACH DELETE p")

        sys.stdout.write("\033[F\033[K")
        print("[*] Creating pseudo admin")
        Neo4j().run(Attacks._admin_cypher())

        # Temporarily set generic policy to admin. This is
        # because all attack paths that allow for reaching
        # this node (eg: AttachUserPolicy) would grant admin.

        Neo4j().run("MATCH (gp:`AWS::Iam::Policy`:Generic) SET gp:Admin")

        # Identify any new attack paths, we stop when we've
        # converged or exceeded the maximum number
        # of iterations.

        attack_definitions = {k: v for k, v in Attacks.definitions.items()
                              if k not in skip_attacks
                              and (only_attacks == [] or k in only_attacks)}

        try:

            for iteration in range(1, max_iterations + 1):

                index = 0
                converged = True

                for pattern, definition in attack_definitions.items():

                    exception = pattern
                    index += 1

                    sys.stdout.write("\033[F\033[K")
                    print("[*] Searching for attack "
                          f"{index}/{len(attack_definitions)}: "
                          f"{pattern} (iteration: {iteration} of max: {max_iterations})")

                    cypher = Attacks._pattern_cypher(
                        pattern,
                        definition,
                        max_search_depth=max_search_depth,
                        ignore_actions_with_conditions=ignore_actions_with_conditions
                    )

                    start = time.time()
                    summary = Neo4j().run(cypher).summary()

                    stats.append({
                        "pattern": pattern,
                        "iteration": iteration,
                        "seconds_elapsed": time.time() - start,
                        **summary.counters.__dict__
                    })

                    if str(summary.counters) == "{}":
                        continue

                    converged = False

                if converged:
                    break

            exception = None

        except Exception as e:
            print(f"[-] Neo4j returned:\n\n{e}")
            print("[!] Don't worry, we'll use what we already have")

        sys.stdout.write("\033[F\033[K")
        print("[+] Consolidating attack patterns")

        # Restore generic policy (unset :Admin)

        Neo4j().run("MATCH (gp:`AWS::Iam::Policy`:Generic) REMOVE gp:Admin")

        # Replace all attack 'Description' entries with a 'Descriptions' set that maps
        # to 'Commands'. We do this to support presenting each command with an associated
        # description in the front end (rather than only the last description and all comamnds).
        # It is easier to do it like this than to incorporate logic into _pattern_cypher().

        Neo4j().run(
            "MATCH ()-[attack:ATTACK]->() " +
            "WITH attack UNWIND attack.Commands AS command " +
            "OPTIONAL MATCH (:Pattern)-[_]->() " +
            "WHERE command IN _.Commands " +
            "WITH attack, command, _ ORDER BY _.Weight " +
            "WITH attack, command, COLLECT(_)[0] AS _ " +
            "WITH attack, COALESCE(_.Description, attack.Description) AS description " +
            "WITH attack, COLLECT(description) AS descriptions " +
            "SET attack.Descriptions = descriptions " +
            "REMOVE attack.Description"
        )

        discovered = sum([s["nodes_created"] if "nodes_created" in s
                          else 0 for s in stats])

        sys.stdout.write("\033[F\033[K")
        print(f"[+] {discovered} potential attacks were discovered " + str(
              f"(successfully converged after {iteration} iterations)" if iteration < max_iterations else
              f"(failed to converge of {max_iterations})")
              )

        # print(json.dumps(stats, indent=2))

        if exception is not None:
            raise Exception(exception)
Example #8
0
def ingest(args):
    """
    awspx ingest
    """

    account = "0000000000000"
    ingested = Elements()
    iam = None

    profile = args.profile if args.profile else "default"

    # offer to create profile it doesn't exist
    try:
        session = boto3.session.Session(profile_name=profile)
    except ProfileNotFound:
        create_new_profile(profile)
        session = boto3.session.Session(profile_name=profile)

    if not args.region:
        r = boto3.session.Session(profile_name=profile).region_name
        region = r if r != None else "eu-west-1"
    else:
        region = args.region

    if not args.database:
        database = profile + ".db"
    else:
        if args.database[-3:] == ".db":
            database = args.database
        else:
            database = args.database + ".db"

    if args.services and args.services != "all":
        services = [
            s for s in SERVICES
            if s.__name__.upper() in map(str.upper,
                                         args.services.strip(" ").split(','))
        ]
    else:
        services = SERVICES

    # Always ingest IAM
    if IAM not in services:
        services.insert(0, IAM)

    # Resolve only & except resources types & ARNs
    optional_resource_args = ""
    selections = {}
    if args.except_types:
        selections["except_types"] = args.except_types.split(",")
        validate_types(selections["except_types"])
        optional_resource_args = optional_resource_args + \
            f"                     --except-types {args.except_types} \\\n"
    else:
        selections["except_types"] = []

    if args.only_types:
        selections["only_types"] = args.only_types.split(",")
        validate_types(selections["only_types"])
        optional_resource_args = optional_resource_args + \
            f"                     --only-types {args.only_types} \\\n"
    else:
        selections["only_types"] = []

    if args.except_arns:
        selections["except_arns"] = args.except_arns.lower().split(",")
        optional_resource_args = optional_resource_args + \
            f"                     --except-arns {args.except_arns} \\\n"
    else:
        selections["except_arns"] = []

    if args.only_arns:
        selections["only_arns"] = args.only_arns.lower().split(",")
        optional_resource_args = optional_resource_args + \
            f"                     --only-arns {args.only_arns} \\\n"
    else:
        selections["only_arns"] = []

    if args.role_to_assume:
        assume_role_arg = f"                     --assume-role {args.role_to_assume} \\\n"
    else:
        assume_role_arg = ""

    max_attack_iterations, max_attack_depth, ignore_conditionals, except_attacks, only_attacks = attacks(
        args)

    if args.skip_attacks:
        attack_args = "                     --skip-attacks"
    else:
        attack_args = f"""                     --max-attack-iterations {str(max_attack_iterations)} \\
                     --ignore-conditionals {str(ignore_conditionals)} \\
    """

        if max_attack_depth != "":
            attack_args = attack_args + \
                f"                     --max-attack-depth {max_attack_depth} \\"

        if except_attacks:
            attack_args = attack_args + \
                f"                 --except-attacks {','.join(except_attacks)}"
        elif only_attacks:
            attack_args = attack_args + \
                f"                 --only-attacks {','.join(only_attacks)}"

    print(f"""
[+] Running awspx ingest --profile {profile} --region {region} --database {database} \\
                     --services {','.join([s.__name__ for s in services])} \\
{assume_role_arg+optional_resource_args+attack_args}
          """)

    try:
        session = boto3.session.Session(profile_name=profile,
                                        region_name=region)
        identity = session.client('sts').get_caller_identity()
        account = identity["Account"]
        print(f"[+] User set to {identity['Arn']}.")
        print(f"[+] Region set to {region}.")
    except:
        print(
            "[-] Request to establish identity (sts:GetCallerIdentity) failed."
        )

    if args.role_to_assume:
        response = session.client('sts').assume_role(
            RoleArn=args.role_to_assume,
            RoleSessionName=f"awspx",
            DurationSeconds=7200)
        if response:
            print(f"[+] Assumed role {args.role_to_assume}")
            session = boto3.session.Session(
                aws_access_key_id=response["Credentials"]["AccessKeyId"],
                aws_secret_access_key=response["Credentials"]
                ["SecretAccessKey"],
                aws_session_token=response["Credentials"]["SessionToken"],
                region_name=region)
        try:
            identity = session.client('sts').get_caller_identity()
            account = identity["Account"]
            print(f"[+] Running as {identity['Arn']}.")
            print(f"[+] Region set to {region}.")
        except:
            print(
                "[-] Request to establish identity (sts:GetCallerIdentity) failed."
            )

        if not args.database:
            database = f"{args.role_to_assume.split('::')[1].split(':')[0]}-{database}"

    print(f"[+] Using database {database}")

    if IAM in services:
        iam = IAM(session, db=database)
        account = iam.root.account()

    for service in [s for s in services if s != IAM]:
        resources = service(session, **selections, account=account)
        ingested += resources

    if IAM not in services:
        iam = IAM(session, db=database, resources=ingested)
    else:
        iam += ingested

    archive = iam.post()
    print(f"[+] Results exported to {archive}")
    Neo4j.load(archive, database)

    if not args.skip_attacks:
        print("[+] Computing attack paths")
        Attacks.compute(max_iterations=max_attack_iterations,
                        except_attacks=except_attacks,
                        only_attacks=only_attacks,
                        max_search_depth=max_attack_depth,
                        ignore_actions_with_conditions=ignore_conditionals)

    print("[+] Done.")