利用Amazon SNS SigningCertURL验证不当漏洞实现消息伪造

本文详细分析了Amazon SNS服务中SigningCertURL验证机制的漏洞,攻击者可通过S3存储桶伪造证书URL,绕过签名验证,实现恶意消息注入。文章包含具体攻击手法、代码示例和实际影响案例。

利用Amazon Simple Notification Service SigningCertURL验证不当漏洞

无数应用程序依赖Amazon Web Services的Simple Notification Service进行应用间通信,如webhooks和回调。为了验证这些消息的真实性,这些项目使用基于SigningCertURL值的证书签名验证。不幸的是,官方AWS SDK中的一个漏洞允许攻击者向所有SNS HTTP订阅者伪造消息。

保持简单,傻瓜 🔗

Amazon Simple Notification Service(SNS)是当今无服务器生态系统中如此广泛以至于几乎成为基础的技术之一。这个想法本身并不新鲜:发布-订阅消息软件已经存在很长时间了,从Apache Kafka到RabbitMQ。SNS允许不同的应用程序通过在其间传递消息进行远程通信。虽然其他消息软件通常包含队列,但SNS尽可能保持简单(AWS将队列功能单独作为Amazon Simple Queue Service出售 - 是的…)。同时,它比简单地自己实现一个简单的HTTP webhook要有用得多 - SNS支持FIFO消息传递、轻松扩展、广泛的发布者/订阅者支持、消息过滤等。

一个典型的Amazon SNS用例是应用到应用的扇出模式。例如,图像上传和转换工作流可以抽象为发布到SNS主题的S3摄取/上传事件通知消息。SNS主题将此消息转发给几个不同的AWS Lambda订阅者,这些订阅者将图像转换为不同的格式和大小,然后将它们存储在单独的S3存储桶中。

简而言之,SNS充当各种事件源和目标之间的粘合剂(不要与AWS Glue混淆 - 是的…)。SNS支持的一些最受欢迎的目标是电子邮件、SMS和HTTP/S - 例如,如果用户从他们的应用程序取消订阅,则通知开发人员或触发webhook工作流。

香肠是如何制成的 🔗

当然,在幕后SNS基本上是一个功能更丰富的HTTP webhook服务。当您配置SNS发布到HTTP/S目标时,它会向您的端点发送以下请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST / HTTP/1.1
x-amz-sns-message-type: Notification
x-amz-sns-message-id: 22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324
x-amz-sns-topic-arn: arn:aws:sns:us-west-2:123456789012:MyTopic
x-amz-sns-subscription-arn: arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96
Content-Length: 773
Content-Type: text/plain; charset=UTF-8
Host: example.com
Connection: Keep-Alive
User-Agent: Amazon Simple Notification Service Agent
 
{
 "Type" : "Notification",
 "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
 "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic",
 "Subject" : "My First Message",
 "Message" : "Hello world!",
 "Timestamp" : "2012-05-02T00:54:06.655Z",
 "SignatureVersion" : "1",
 "Signature" : "EXAMPLEw6JRN...",
 "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
 "UnsubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96"
}

根据您的事件源,主题和消息包含您需要的数据。然而,这引出了一个问题:如果您的webhook端点是公开的,您如何信任您收到的请求来自Amazon SNS?毕竟:

“几乎没有人第一次就正确使用JWT令牌和webhooks。对于webhooks,人们几乎总是忘记验证传入请求…” Ken Kantzer,《从5年科技初创公司代码审计中学到的经验》

这就是Signature和SigningCertURL值发挥作用的地方。AWS文档对验证算法有直接的描述;从JSON正文中提取相关的名称-值对(如Subject和Message),并以规范格式排列,然后使用SHA1哈希创建派生哈希值。

接下来,对Signature值进行base64解码,然后使用从SigningCertURL下载的公钥解密以创建断言哈希值。

最后,比较断言和派生哈希值以确保它们匹配。这是一个标准的签名验证方案,Computerphile解释得很好。

此时您可能已经发现了一个可能的弱点:我们应该使用SigningCertURL处的证书来生成 supposedly正确的哈希值,但是我们如何信任SigningCertURL?唉,这就是问题所在…

信任,但要验证 🔗

AWS知识中心提供了这个答案:

为了防止欺骗攻击,在验证Amazon SNS消息签名时请确保执行以下操作:

  1. 始终使用HTTPS从Amazon SNS获取证书。
  2. 验证证书的真实性。
  3. 验证证书是否来自Amazon SNS。
  4. (如果可能)使用受支持的AWS SDK for Amazon SNS来验证消息。

好的,所以它告诉我们要验证证书是否来自Amazon SNS,但没有告诉我们如何验证。它还提供了一个示例Python脚本,该脚本执行签名验证*而不验证SigningCertURL!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import base64
from M2Crypto import EVP, RSA, X509
import requests
cache = dict()
 
# 从Amazon SNS发送到您的端点的HTTP POST请求正文中的JSON文档中提取名称-值对。
def processMessage(messagePayload):
     print ("Start!")
     
     if (messagePayload["SignatureVersion"] != "1"):
         print("Unexpected signature version. Unable to verify signature.")
         return False
     messagePayload["TopicArn"] = messagePayload["TopicArn"].replace(" ", "")
     signatureFields = fieldsForSignature(messagePayload["Type"])
     print(signatureFields)
     strToSign = getSignatureFields(messagePayload, signatureFields)
     print(strToSign)
     certStr = getCert(messagePayload)
 
     print("Printing the cert")
     print(certStr.text)
     print("Using M2Crypto")
     
     # 获取Amazon SNS用于签署消息的X509证书。
     certificateSNS = X509.load_cert_string(certStr.text)
     #从证书中提取公钥。
     public_keySNS = certificateSNS.get_pubkey()
     public_keySNS.reset_context(md = "sha1")
     # 生成Amazon SNS消息的派生哈希值。
     # 生成Amazon SNS消息的断言哈希值。
     public_keySNS.verify_init()
     public_keySNS.verify_update(strToSign.encode())
     # 解码Signature值 
     decoded_signature = base64.b64decode(messagePayload["Signature"])
     # 验证Amazon SNS消息的真实性和完整性
     verification_result = public_keySNS.verify_final(decoded_signature)
     print("verification_result", verification_result)
     if verification_result != 1:
         print("Signature could not be verified")
         return False
     else:   
        return True
 
# 根据消息类型获取签名字段。
def fieldsForSignature(type):
    if (type == "SubscriptionConfirmation" or type == "UnsubscribeConfirmation"):
        return ["Message", "MessageId", "SubscribeURL", "Timestamp", "Token", "TopicArn", "Type"]
    elif (type == "Notification"):
        return ["Message", "MessageId", "Subject", "Timestamp", "TopicArn", "Type"]
    else:
        return []
 
# 创建要签名的字符串。  
def getSignatureFields(messagePayload, signatureFields):
    signatureStr = ""
    for key in signatureFields:
        if key in messagePayload:
            signatureStr += (key + "\n" + messagePayload[key] + "\n")
    return signatureStr
 
#**** 证书获取 ****
#证书缓存
def get_cert_from_server(url):
    print("Fetching cert from server...")
    response = requests.get(url)
    return response
 
def get_cert(url):
    print("Getting cert...")
    if url not in cache:
        cache[url] = get_cert_from_server(url)
    return cache[url]
 
def getCert(messagePayload):
    certLoc = messagePayload["SigningCertURL"].replace(" ", "")
    print("Cert location", certLoc)
    responseCert = get_cert(certLoc)
    return responseCert

SigningCertURL/url值直接获取,没有任何验证步骤。同时,如果我们转向StackOverflow,Google的顶级结果建议验证URL是否匹配格式sns.${region}.amazonaws.com。足够合理,但需要注意各种细微差别。

例如,开发人员可能希望验证URL以https://开头,在子域的第一个slug中包含sns,在第二个slug中包含有效的区域字符串,然后以amazonaws.com域结尾。此外,开发人员可能希望确保路径以.pem扩展名结尾。

表面上看,这足以确保URL确实属于默认的AWS证书位置之一。然而,有一个关键的漏洞:Amazon S3。

更具体地说,Amazon S3存储桶资源可以在https://<存储桶名称>.s3-<区域>.amazonaws.com/<资源名称>访问,这很容易通过上述检查!通过在https://mysns.s3-us-west-2.amazonaws.com/evil.pem上传他们自己生成的公钥,攻击者可以轻松伪造签名的SNS消息到受害者的webhook端点。

在对自定义SNS SigningCertURL验证例程的快速开源代码审查中,除了此类破碎的算法外,我还发现了弱正则表达式,如https?://sns.(.+).amazonaws.com(被http://sns.evil.s3.amazonaws.com/evil.pem绕过)和sns.[a-z0-9-]+.amazonaws.com(被snsthisismyamazonaws.com绕过)。

好的,所以URL验证很难。然而,AWS有帮助地提供了AWS SDKs for Amazon SNS来验证消息。例如,AWS在NPM上发布了sns-validator包。包代码使用以下正则表达式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
defaultHostPattern = /^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/,

// hostPattern默认为defaultHostPattern
var validateUrl = function (urlToValidate, hostPattern) {
    var parsed = url.parse(urlToValidate);

    return parsed.protocol === 'https:'
        && parsed.path.substr(-4) === '.pem'
        && hostPattern.test(parsed.host);
};

^sns.检查第一个slug匹配而不是包含sns;这阻止了诸如mysns的存储桶名称。然而,正则表达式的其余部分仍然允许.s3-us-west-2.amazonaws.com子域后缀。幸运的是,s3-us-west-2.amazonaws.com工作(工作过?)就像s3.amazonaws.com一样,因此满足域中第二个slug的最小3字符要求([a-zA-Z0-9-]{3,})。这使得sns S3存储桶成为此正则表达式的唯一可能匹配。

当然,如此关键的存储桶名称应该被保留,对吧?

问题1:sns未被保留。 问题2:sns是公开可读的存储桶。 问题3:sns是公开可写的存储桶。

通过此漏洞,攻击者可以伪造消息给任何官方SDK SNS验证器用户。影响取决于应用程序的webhook处理程序。例如,Firefox Monitor是一个允许用户注册他们的电子邮件以监控在线数据泄露的工具,它有一个公开访问的SNS webhook端点https://monitor.firefox.com/ses/notification,使用sns-validator验证传入的POST消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'use strict'

const MessageValidator = require('sns-validator')

const DB = require('../db/DB')

const mozlog = require('../log')

const validator = new MessageValidator()
const log = mozlog('controllers.ses')

async function notification (req, res) {
  const message = JSON.parse(req.body)
  return new Promise((resolve, reject) => {
    validator.validate(message, async (err, message) => {
      if (err) {
        log.error('notification', { err })
        const body = 'Access denied. ' + err.message
        res.status(401).send(body)
        return reject(body)
      }

      await handleNotification(message)

      res.status(200).json(
        { status: 'OK' }
      )
      return resolve('OK')
    })
  })
}

验证消息后,它然后使用从已验证消息中获取的值从数据库中删除用户:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
async function handleNotification (notification) {
  log.info('received-SNS', { id: notification.MessageId })
  const message = JSON.parse(notification.Message)
  if (message.hasOwnProperty('eventType')) {
    await handleSESMessage(message)
  }
  if (message.hasOwnProperty('event')) {
    await handleFxAMessage(message)
  }
}

async function handleFxAMessage (message) {
  switch (message.event) {
    case 'delete':
      await handleDeleteMessage(message)
      break
    default:
      log.info('unhandled-event', { event: message.event })
  }
}

async function handleDeleteMessage (message) {
  await DB.deleteSubscriberByFxAUID(message.uid)
}

async function handleSESMessage (message) {
  switch (message.eventType) {
    case 'Bounce':
      await handleBounceMessage(message)
      break
    case 'Complaint':
      await handleComplaintMessage(message)
      break
    default:
      log.info('unhandled-eventType', { type: message.eventType })
  }
}

async function handleBounceMessage (message) {
  const bounce = message.bounce
  if (bounce.bounceType === 'Permanent') {
    return await removeSubscribersFromDB(bounce.bouncedRecipients)
  }
}

async function handleComplaintMessage (message) {
  const complaint = message.complaint
  return await removeSubscribersFromDB(complaint.complainedRecipients)
}

async function removeSubscribersFromDB (recipients) {
  for (const recipient of recipients) {
    await DB.removeEmail(recipient.emailAddress)
  }
}

因此,攻击者可以通过伪造SNS消息从Firefox Monitor数据库中删除任意用户。

修补 🔗

在报告后,我对AWS的响应印象深刻。他们通过在其基础设施端的一个优雅解决方案迅速解决了漏洞,防止了未来与S3的子域命名空间冲突。通过避免从SDK端修补正则表达式,AWS防止了触发数千个警报并确保了向后兼容性。创建此类S3存储桶名称也不再可能。然而,此解决方案仅适用于AWS SDK用户;如果开发人员自己编写SigningCertURL验证算法(或根本未验证它),攻击者可以继续伪造…

总体而言,这是一次富有成果的代码审查和阅读当今无服务器云底层机制的旅程。查看AWS任何服务的优秀开发人员文档 - 您可能会发现一些有趣的东西。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计