万一アクセスキーが漏洩した時に攻撃者に都合良く悪用されそうなIAMユーザを抽出するスクリプトを作ってみた

「Administratorなんとか」「Powerなんとか」「なんとかFullAccess」
2021.09.21

AWSアクセスキーセキュリティ意識向上委員会って何?

昨今、AWSのアクセスキーを漏洩させてしまうことが原因でアカウントへの侵入を受け、 多額の利用費発生・情報漏洩疑いなど重大なセキュリティ事案が発生するケースが実際に多々起きています。

そこで、アクセスキー運用に関する安全向上の取組みをブログでご紹介する企画をはじめました。

アクセスキーを利用する場合は利用する上でのリスクを正しく理解し、 セキュリティ対策を事前に適用した上で適切にご利用ください。

はじめに

まずはこちらの記事をご確認ください。

【実録】攻撃者のお気に入り API 10選 | DevelopersIO

一部抜粋

仮想通貨のマイニングを例とすると、攻撃者は自分たちの IAM ユーザーを作り、とにかく「ハイスペックなインスタンス」をとにかくたくさん稼働させようとします。 裏を返せば、アクセスキーを用いた運用をする際にはアクセスキーにこれらの過剰な権限を付与しないことで、不正利用の抑止効果が見込まれます。

なにげなく「Administratorなんとか」「Powerなんとか」「なんとかFullAccess」といった権限を選びがちな方は、早期に見直しすることをお勧めします!

アクセスキーの漏洩が発生した時、IAMユーザーの作成やEC2インスタンスの起動は特に狙われがちです。

今回は、次に記載する条件をもとに、万一アクセスキーが漏洩した際被害にあいやすそうなIAMユーザーを検索・抽出するスクリプトを作ってみました。
権限の見直しのきっかけや、かつて作ったきりうっかり放置しているようなアクセスキーの棚卸しにどうぞ。

抽出の条件

以下の条件に当てはまるIAMユーザのリストを出力します。

1, 2, 3 はAND条件です。

  1. 有効なアクセスキーがある
  2. MFA設定なし
  3. AWS管理ポリシーの AdministratorAccess or PowerUserAccess or IAMFullAccess が付与されている

スクリプト全文

ソースコードはこちらのGitHubリポジトリにアップロードしています。
GitHubからclone、あるいはスクリプトの内容をコピーして、CloudShellなどで実行ください。

実行と出力の例(CloudShellを利用する場合)
条件に当てはまるIAMユーザがない場合は何も出力されません。

$ git clone https://github.com/pypypyo14/list-highlisk-iamuser.git
$ cd list-highlisk-iamuser
$ python3 list_highlisk_iamuser.py
{'username': 'test_warn_user', 'is_mfa_active': False, 'is_accesskey_active': True}

スクリプトの内容はこちらです。
もっと読みやすかったり、いい書き方のプルリクエスト等歓迎いたします!

import boto3


class IamUser:
    def __init__(self, iam, username):
        self.username = username
        self.is_mfa_active = self.__check_is_mfa_active(iam, username)
        self.is_accesskey_active = self.__check_is_accesskey_active(iam, username)

    def __str__(self):
        return str(self.__dict__)

    def __repr__(self):
        return str(self.__dict__)

    def __check_is_mfa_active(self, iam, username):
        """check the user has a active MFA device or not"""
        res = iam.list_mfa_devices(UserName=username)
        if len(res["MFADevices"]) > 0:
            return True
        return False

    def __check_is_accesskey_active(self, iam, username):
        """check the user has a active accesskey or not"""
        res = iam.list_access_keys(UserName=username)
        for accesskey in res["AccessKeyMetadata"]:
            if accesskey["Status"] == "Active":
                return True
        return False


def fetch_userlist_from_entity(iam, policy):
    """return a list of IAM users directly attached to the specified AWS managed policy"""
    users = []
    response = iam.list_entities_for_policy(
        PolicyArn=f"arn:aws:iam::aws:policy/{policy}", MaxItems=1000
    )
    while True:
        res_users = response["PolicyUsers"]
        for res_user in res_users:
            users.append(res_user["UserName"])
        if not response["IsTruncated"]:
            break
        else:
            response = iam.list_entities_for_policy(
                Marker=response["Marker"],
                PolicyArn=f"arn:aws:iam::aws:policy/{policy}",
                MaxItems=1000,
            )
    return users


def fetch_grouplist_from_entity(iam, policy):
    """return a list of IAM groups attached to the specified AWS managed policy"""
    groups = []
    response = iam.list_entities_for_policy(
        PolicyArn=f"arn:aws:iam::aws:policy/{policy}", MaxItems=1000
    )
    while True:
        res_groups = response["PolicyGroups"]
        for res_group in res_groups:
            groups.append(res_group["GroupName"])
        if not response["IsTruncated"]:
            break
        else:
            response = iam.list_entities_for_policy(
                Marker=response["Marker"],
                PolicyArn=f"arn:aws:iam::aws:policy/{policy}",
                MaxItems=1000,
            )
    return groups


def get_userlist_from_group(iam, group):
    """return a list of IAM users associated with the specified user group"""
    users = []
    response = iam.get_group(GroupName=group)
    group_users = response["Users"]
    while True:
        for group_user in group_users:
            users.append(group_user["UserName"])
        if not response["IsTruncated"]:
            break
        else:
            response = iam.get_group(
                Marker=response["Marker"], GroupName=group, MaxItems=1000
            )
    return users


def get_users(iam, policy):
    """return a list of IAM users attached to the specified AWS managed policy"""
    userlist = fetch_userlist_from_entity(iam, policy)
    glouplist = fetch_grouplist_from_entity(iam, policy)

    for group in glouplist:
        userlist.extend(get_userlist_from_group(iam, group))

    return userlist


def get_iamuser_set(iam, policy_list):
    userlist = []
    for policy in policy_list:
        userlist.extend(get_users(iam, policy))
    return set(userlist)


def extract_warning_users(iam, userset):
    for username in userset:
        user = IamUser(iam, username)
        if user.is_accesskey_active and not user.is_mfa_active:
            print(user)


def main():
    iam = boto3.Session().client("iam")
    target_policys = ["AdministratorAccess", "IAMFullAccess", "PoweruserAccess"]

    userset = get_iamuser_set(iam, target_policys)
    extract_warning_users(iam, userset)


if __name__ == "__main__":
    main()

おわりに

この条件で悪用されやすいユーザ一覧を全て網羅できているとは言えませんが、あるあるなパターンのIAMユーザではないでしょうか。

このスクリプトがなにかのお役に立てれば幸いです。