信息处理安全保障支援士(情報処理安全確保支援士) 2024年春季(令和6年) 午后问1解说 ── JWT的alg=none与API授权、WAF的临时应对

· · 信息处理安全保障支援士, 注册安全工程师, API, API安全, JWT, 认证, 授权, WAF, Log4Shell, 信息安全, 漏洞, IPA

“JWT的签名已经验证过了,所以用户ID是可信的”

这种说法只对了一半。

信息处理安全保障支援士考试2024年度春季(令和6年)午后问1,以从智能手机调用的API为题材1。认证成功后会签发JWT,之后带着这个JWT调用获取・更新用户信息的API。乍看之下,是很常见的结构。

然而,在诊断中却发现了以下四个问题。

  1. 将JWT请求头的alg改为none后,未签名的JWT也能通过验证
  2. 保持使用正确的JWT,只将mid改为其他用户ID,就能获取・更新他人的信息
  3. 添加规格之外的status=paid后,免费用户就变成了付费用户
  4. 邮件送达的4位认证码,可以不受限制地进行暴力破解

这四个问题看上去都是”认证相关的漏洞”,但成因并不相同。令牌的完整性、对象级别授权、属性级别授权、尝试次数限制这些各自独立的边界被逐一突破了。

在文章后半,还会加入另一个论点。一个被广泛使用的开源库被公布出存在重大漏洞,攻击者可以利用JNDI Lookup从外部执行代码。修复版本尚未发布,完善的WAF规则也还不存在。在此期间,该如何确认影响范围、WAF应该检查哪里、以及为什么最初要选择”检测”而不是”阻断”,都是本文要探讨的问题。

本文将以官方解答示例2与评分讲评3为基础,不仅整理各设问的答案,还会梳理为什么答案是这样、以及在实务中应该把设计做到多严格

问题全貌展示在认证码、JWT、API授权、库漏洞各阶段被突破的信任边界无试行次数限制允许alg=none信任midstatus=paidJNDI/LDAP/HTTP用户应用4位认证码签发JWT用户API日志输出存在漏洞的库外部代码执行获取/更新他人数据变更计费状态

图1: 问题全貌。各阶段被突破的信任边界各不相同。

1. 先说结论

  • RESTful API不持有会话的特性称为无状态(Stateless)。但这并不意味着服务器完全不持有数据库或用户状态
  • 4位认证码共有1万种组合。若每秒尝试10次,平均5,000次、500秒即可成功。这短于10分钟的有效期,因此仅靠失效时间无法防范
  • 针对alg=none的最低限度对策,是确认JWT请求头中的alg不为NONE。但在实务中,应当在服务器端固定允许使用的算法
  • 即使JWT本身正确,也不能信任请求中的mid。应核对JWT内的用户ID与mid是否一致,更安全的做法是不接收mid,而是根据JWT决定对象
  • 添加status=paid,是把规格之外的属性也绑定到内部对象上的批量赋值(Mass Assignment)问题。应将更新用DTO作为许可列表使用,不允许用户变更计费状态
  • 暴力破解对策的答案是连续失败次数超过阈值后锁定账户的处理。实务中还应叠加分级延迟与按发送源单位的控制
  • 在确认新公布的重大漏洞的影响时,不应使用破坏性指令,而应通过记录对测试服务器index.html的访问,来确认是否达到了外部代码执行
  • 由于攻击字符串出现在HTTP请求头中,WAF的检查对象为Header。作为应对大小写互换的正则表达式,例如可以使用\W[jJ][nN][dD][iI]\W
  • 最初将WAF设为”检测”的好处是可以避免因误报而阻断业务通信。收到告警后应精查是否为攻击,调整规则后再转入阻断
  • WAF只是临时对策,根本对策是将受影响的库更新到修复版本

2. 题材与设问的对应关系

题目的舞台设定为正准备新推出健康服务的G公司。用户通过智能手机应用输入饮食、体重等数据,获得健康风险判定与饮食菜单建议。系统构建在云端,组合了API网关、事件驱动处理与托管数据库。

题目中的产品名与服务名均已抽象化处理。本文同样不会转载IPA的图表与原文,只会转述理解设问所需的结构。

设问 主题 本文章节
设问1 RESTful API的性质 第4章
设问2(1) 4位验证码的暴力破解时间 第5章
设问2(2) JWT的alg=none 第6章
设问2(3) 利用mid访问他人信息 第7章
设问2(4) 接受规格之外的status的缺陷 第8章
设问2(5) 暴力破解对策 第9章
设问3(1) 用安全的方法确认漏洞是否存在 第11章
设问3(2)(3) WAF检查的位置与正则表达式 第12章
设问3(4) 检测模式的优点与运维 第13章

评分讲评中提到,整体正确率处于平均水平。而设问2(2)的JWT篡改对策,以及设问3(1)验证用服务器所需机制的正确率则偏低。这两处仅凭记住术语是无法解答的,必须追踪攻击者改变了哪个值、它流向了哪个处理、又在哪里被错误地信任了

3. 这道题不只是”认证问题”

把整个题目按信任边界排列,会呈现如下的结果。

[用户ID・密码]
          |
          v
[验证4位验证码] ---- 无尝试次数限制 ----> 暴力破解
          |
          v
[签发JWT]
          |
          v
[JWT库] ------- 允许alg=none ------> 篡改用户ID
          |
          v
[用户API]
    |             |
    |             +-- 原样传递整个status ----> 属性级别授权缺陷
    |
    +-- 信任mid -----------------------> 对象级别授权缺陷

[将外部输入记录到日志]
          |
          v
[存在漏洞的库] ---- JNDI/LDAP/HTTP ------> 外部代码执行

这里最重要的是以下这些区别。

确认项 询问的内容 本题中被突破的示例
认证 你是谁 4位验证码的暴力破解
令牌验证 该身份信息是否被篡改 alg=none
对象级别授权 是否可以访问该用户的数据 替换mid
属性级别授权 是否可以变更该字段 status=paid
从输入到执行的边界 外部输入是否会被当作指令解释 JNDI Lookup

前一项确认成功,并不是省略下一项确认的理由。 持有正确JWT的用户,不一定就可以读取他人的数据。能够更新自己数据的用户,也不一定可以变更计费状态。

一旦能做出这样的阶段划分,各设问的答案就不再需要死记硬背了。

认证与授权的区别认证确认主体是谁,授权确认该主体被允许执行的操作认证你是谁授权你能做什么

图2: 认证与授权的区别。认证在先,授权是另一项独立的确认。

4. 设问1 ── 什么是无状态(Stateless)

设问1询问的是RESTful API的设计原则之一——不进行会话管理的特性。

答案是无状态(Stateless)

所谓无状态,是指服务器无需记住上一次请求的对话状态,仅凭每次请求本身就能获得处理所需的全部信息。在本题中,智能手机应用会在每次请求时都把JWT附加到Authorization请求头中。服务器验证该JWT,并据此识别本次请求的用户。

容易产生的误解是,把无状态理解为”服务器完全不持有任何状态”。实际上,以下这些状态服务器通常都会持有。

  • 保存用户信息与健康数据的数据库
  • 计费状态
  • 认证码的值、有效期限、失败次数
  • JWT的签名密钥
  • 若设计中使用失效列表,则包括该失效信息
  • 日志与审计记录

不持有的,是不把仅用于维持对话的服务器端会话状态,当作每次API调用的前提

此外,无状态本身并不会自动提升安全性。如果每次都发送JWT,固然更容易实现水平扩展,但一旦JWT的验证出错,这个错误也会均匀地扩散到所有节点。架构层面的性质与安全层面的正确性是两回事。

5. 设问2(1) ── 4位验证码平均500秒即可破解

认证API在用户ID与密码匹配时,会向邮箱发送一个4位数字。此后,若用户ID与4位验证码一致,则签发JWT。验证码自生成起10分钟内有效。

在诊断中,每秒可以尝试10次。要求出平均需要多少秒才能被攻破。

计算方法是”候选数的一半”

4位数字包含开头为0的情况,共有以下1万种组合。

0000, 0001, 0002, ... , 9999

如果正确答案是均匀选取的,那么按顺序不重复地依次尝试的攻击者,到达正确答案为止的平均尝试次数,就是候选数的一半。

平均尝试次数 = 10,000 ÷ 2 = 5,000次
平均耗时     = 5,000 ÷ 10次/秒 = 500秒

因此,空格b的答案是500

最坏情况下需要1,000秒,但题目问的是平均值。而验证码的有效期为600秒,长于平均破解时间的500秒。这正是判定为”很可能被攻破”的理由。

4位认证码的时间感觉1万种可能每秒尝试10次,平均5,000次、500秒即可命中,短于600秒的有效期平均 10,000 / 2 = 5,000次500秒 < 600秒候选数 10,000种平均破解时间 500秒有效期 600秒可在有效期内破解

图9: 4位认证码的时间感觉。平均尝试候选数的一半,就能在有效期内命中。

只缩短失效时间,若候选数少仍会被攻破

认证码的强度,并不是仅由位数或仅由有效期决定的。

有效期内可尝试的次数
= 每秒尝试次数 × 有效期
= 10 × 600
= 6,000次

如果按顺序尝试不重复的取值,就能在有效期内确认1万种组合中的60%。即使设置了失效时间,只要不限制尝试次数,就仍然不够安全。

现行的NIST SP 800-63B要求带外认证使用的短期密钥至少为6位,并且在小于64比特时必须限制尝试次数。同时还要求不要将电子邮件用于带外认证4。考试中是在给定的”4位数字・邮件发送”这一规格前提下作答,但在实务的新设计中,应当重新审视这个前提本身。

6. 设问2(2) ── alg=none是”让攻击者选择验证方式”的问题

本题中的JWT由请求头、载荷、签名三部分组成。

base64url(header).base64url(payload).base64url(signature)

请求头中记录着用于签名的算法RS256。载荷中则包含用户ID、签发时间、有效期限等信息。

诊断人员改动了以下两处内容。

  1. 将请求头的algRS256改为NONE
  2. 将载荷中的用户ID改为其他用户

发送该JWT后,验证竟然成功,从而得以冒充他人身份。

JWT alg=none攻击的流程将正确JWT的alg改为none,并篡改用户ID后通过验证的流程将请求头的alg改为none跳过签名验证正确的JWTalg=RS256user=user01篡改后的JWTalg=noneuser=user02服务器接受为user02

图3: JWT alg=none攻击流程。攻击者自行选择了验证算法。

none并非拼写错误

RFC 7519中定义了algnone、既不签名也不加密的JWT,称为”Unsecured JWT”5。因此,none这个值在规范上并非完全不存在。

问题在于,本应只接受带签名JWT的API,却接受了攻击者指定的none

用概念性的方式描述这一有漏洞的处理流程,大致如下。

1. 读取JWT请求头
2. 查看请求头中记录的alg,并据此选择验证方式
3. 若alg为none,则不进行签名验证
4. 信任载荷中的用户ID

这相当于让攻击者可控的输入,直接决定了安全强度本身。

考试的解答

本设问要求分别用20字以内回答,修复后的库Q”针对哪些数据”、”进行怎样的验证”。

解答示例如下。

项目 解答要点
验证对象数据 JWT请求头中alg指定的值
验证内容 验证其不为NONE

作为对题目所述漏洞的直接修复方案,这样回答是正确的。

实务中不要止步于”只要不是NONE就行”

这里需要把考试的解答与实务上推荐的做法区分开来。

RFC 8725规定,JWT库应让调用方指定一组允许使用的算法,并且不得使用该集合之外的算法6。也就是说,应采取以下思路。

不好的思路:
  若 token.header.alg != "none" 则接受

好的思路:
  仅当包含于 serverConfig.allowedAlgorithms 时才接受
  例如: allowedAlgorithms = ["RS256"]

即使只拒绝none,仍可能残留其他弱算法,或者混淆公钥方式与共享密钥方式而导致的算法混淆问题。原则是不要以否定形式不断增加可接受的条件,而应把允许的条件严格限定在很窄的范围内

在JWT验证中,除了算法之外,还应根据用途至少确认以下内容。

项目 确认内容
签名 是否能用预期的密钥与算法验证通过
iss 是否为可信的签发者
aud 令牌是否是为本API签发的
exp 是否在有效期内
nbf 是否早于使用开始时间
sub或用户ID 是否是应用程序中有效的主体
令牌类型 是否混淆了ID令牌与访问令牌等

本题中载荷的键名为user,但在实务中应使用标准的sub,或者明确定义自定义声明(claim)的含义。

安全的JWT验证与危险的JWT验证危险的验证依赖alg字段,安全的验证使用服务器端的许可列表安全的验证服务器配置的许可算法例如RS256确认JWT请求头的alg是否在许可列表中验证签名・iss・aud・exp危险的验证读取JWT请求头的alg若alg为none则接受

图4: 安全的验证与危险的验证。实务中应将允许使用的算法严格限定在小范围内。

Base64url不是加密

关于JWT,还有一个常见的误解。请求头与载荷虽然是以base64url表示的,但这并不是加密。任何人都可以解码后读取内容。

签名所保证的,仅仅是——在验证成功的前提下——内容自签发后未被篡改。这并不意味着可以把想要保密的个人信息,放入带签名JWT的载荷中。

7. 设问2(3) ── 即使JWT正确,只要更改mid就能读取他人信息

接下来是一种不篡改JWT本身的攻击。

用户API通过GET或PUT接收名为mid的用户ID。公共模块P会根据这个mid,从数据库中获取或更新对应的用户信息。

攻击的结构很简单。

JWT中的用户ID: user01    ← 签名正确的JWT
请求中的mid: user02      ← 攻击者篡改后的值

由于JWT的签名是正确的,因此认证会成功。但API却原样信任了mid=user02,并返回了user02的信息。

这正是OWASP API Security Top 10 2023中所说的对象级别授权失效(Broken Object Level Authorization,BOLA)的典型案例。当用户指定对象ID来访问数据时,每次都必须确认对该对象的授权7

BOLA攻击使用正确的JWT,同时将请求中的mid改为其他用户IDJWT为user01mid为user02信任mid攻击者用户API从数据库返回user02的信息

图5: BOLA攻击。认证通过,但未确认授权。

设问的解答

表5中的下划线②要求用40字以内回答,应在公共模块P的调用处理中添加什么处理。

解答示例如下。

验证JWT中包含的用户ID是否与mid的值一致的处理

在公共模块P中进行验证的好处是,可以让同一套授权逻辑同时作用于GET与PUT,以及今后使用P的其他API。如果把相同的核对处理复制到各个画面或各个端点中,总会在某一处遗漏。

更安全的设计是不接收mid

如果API只获取・更新用户自己的信息,那么就不需要从客户端接收用户ID。

GET /users/me
Authorization: Bearer <JWT>

在服务器端,从已验证的JWT中取出主体。

principal = validateJwt(request.authorization)
userId = principal.subject
return repository.getUser(userId)

更新处理也是一样。

principal = validateJwt(request.authorization)
input = validateProfileUpdate(request.body)
repository.updateProfile(
    userId = principal.subject,
    name = input.name,
    age = input.age
)

核对处理只要写了就能起到保护作用。但如果设计成不从外部接收目标ID,就能从根本上减少”忘记编写核对处理”这一类漏洞

如果管理员需要操作他人的用户信息,可以像下面这样将其拆分开。

PUT /users/me                  面向普通用户
PUT /admin/users/{userId}      面向管理员

面向管理员的接口,应要求不同的权限、审计日志,必要时还应要求重新认证。相比”在普通用户API中只为管理员追加例外”,这样拆分能让授权策略的边界更加清晰。

BOLA的防范方法不使用请求中的mid,改为根据JWT的主体确定或核对目标GET /users/me + JWT获取JWT的sub一致不一致无mid用户API若存在mid是否与sub一致返回自己的数据403拒绝以JWT的sub查询数据库

图6: BOLA的防范方法。不接收mid,或将其与JWT的主体进行核对。

用一句话区分认证与授权

无论是考试还是实务,以下这种说法都很有帮助。

  • 认证: 你是谁
  • 授权: 这个人能做什么

JWT签名验证成功,只能说明”可以信任该令牌所代表的主体”这一点。至于”该主体是否可以读取user02“,则必须另行确认。

8. 设问2(4) ── status=paid是属性级别的授权缺陷

用户API的规格中,定义了以下更新参数。

mid   用户ID
name  姓名
age   年龄

然而,诊断人员添加了规格之外的以下值。

status=paid

结果,免费用户的状态变成了付费用户。

根据题目描述,服务L不对接收到的参数进行验证,而是将其全部传递给公共模块P。而P则被设计成可以直接更新数据库。

空格c的答案是公共模块P

批量赋值(Mass Assignment)添加了规格之外的status=paid,并被整体反映到内部对象中攻击者添加status=paid自动绑定保存至数据库API规格mid / name / age请求体公共模块P计费状态变更为paid

图7: 批量赋值(Mass Assignment)。规格之外的属性被整体反映到内部对象中。

与BOLA的区别

上一章的mid替换,与本节的status添加看似相似,但所守护的粒度不同。

漏洞 攻击者改变的内容 本应确认的事项
mid替换 目标对象 该用户是否可以访问该用户记录
status添加 对象内部的属性 该用户是否可以变更该字段

OWASP API Security Top 10 2023将后者归类为对象属性级别授权失效(Broken Object Property Level Authorization),并将以往的Mass Assignment(批量赋值)也纳入了这一分类8

“把JSON原样塞进实体”很危险

有漏洞的实现,用概念性的写法大致如下。

entity = repository.find(body.mid)
bindAllProperties(entity, body)
repository.save(entity)

即使画面上只有nameage两个输入框,攻击者也完全可以直接构造HTTP请求。不在UI上出现的字段,并不构成安全边界。

安全的实现,会明确列出可更新的字段。

input = parseExactSchema(body, fields = ["name", "age"])

entity = repository.find(authenticatedUserId)
entity.name = input.name
entity.age  = input.age
repository.save(entity)

这里重要的有两点。

  1. 更新用的输入类型中,只放置用户可以变更的字段
  2. 对于规格之外的未知字段,不要默默忽略,而应尽量作为错误拒绝

如果对未知字段保持沉默地忽略,虽然能让攻击表面上”失败”,但也会因此错过客户端的实现错误或攻击迹象。如果没有兼容性方面的理由,用严格的模式(schema)拒绝反而更便于排查。

属性级别的授权更新用DTO只保留许可列表,未知属性一律拒绝模式校验专用通道更新用DTO(许可列表)nameage请求体是否仅包含许可的字段更新实体的name/age返回错误支付服务已验证的通知更新status=paid

图8: 属性级别的授权。用许可列表限制可更新的字段,计费状态通过另一条通道变更。

status只能通过支付结果变更

status=paid并不是用户资料的一部分。它是由服务器端”支付已成功”这一事实推导出来的状态。

用户资料更新
  -> 仅可变更name / age

来自支付服务的已验证通知
  -> 核对paymentId
  -> 防止重复处理
  -> 将status变更为paid

即使保存在同一个数据库列中,变更的权限与途径也是各自独立的。如果把内部实体直接用作外部API的输入类型,这道边界就会消失。

9. 设问2(5) ── 暴力破解对策需要将失败次数作为状态保存

针对4位验证码的暴力破解,要求用30字以内回答表5空格d应填入的处理。阈值为10。

解答示例如下。

连续失败次数超过阈值后锁定账户的处理

这里与设问1的无状态并不矛盾。不将API调用的对话状态作为服务器会话保存,与将安全判断所需的失败次数持久化,是两件不同的事。

是否设置尝试次数限制没有限制时平均500秒即被突破,但失败次数限制可以压低攻击速度有限制攻击速度骤降账户锁定失败10次即锁定分级延迟无限制约500秒认证成功每秒尝试10次

图10: 是否设置次数限制。失败次数限制能在实际中有效阻止暴力破解。

实务中不要”只靠永久锁定”

按账户单位限制次数是必要的,但如果攻击者知道他人的用户ID,就可以故意失败10次,把正规用户挤出去(拒绝服务)。因此,实务中应组合以下多种手段。

控制手段 作用
按账户单位统计的失败次数 阻止针对单个账户的暴力破解
分级等待时间 在容许正规用户输入失误的同时,降低攻击速度
按发送源IP・终端・ASN等进行的控制 抑制对大量账户各尝试少量次数的攻击
基于风险的判定 对异于平常的地区・终端・速度进行更严格的限制
向用户发送通知 使其能够察觉到攻击或误操作
安全的恢复流程 避免解锁入口本身成为新的攻击途径

此外,重新发送验证码时,不能把失败次数重置为0。因为这样一来,攻击者每次调用重发API,都能重新获得一批可尝试的次数。现行的NIST SP 800-63B也要求,即使生成了新的认证密钥,也不应重置失败次数4

让认证码只能使用一次

题目主要围绕有效期展开,但在实务中还需要以下这些措施。

  • 验证成功的验证码应立即失效
  • 拒绝重复使用同一个验证码
  • 不将验证码本身写入日志
  • 让响应无法从验证码核对的成功与否推测出该用户是否存在
  • 也要为验证码发送API设置次数限制

既然使用的是较短的密钥,就不能只把安全性寄托在随机生成上。

认证码对策除位数与失效时间外,还通过尝试次数限制・拒绝重复使用・通知等手段防护认证码增加位数缩短有效期限制尝试次数成功后立即失效重新发送时不重置失败次数不将认证码写入日志按发送源单位控制

图11: 认证码对策。除位数与失效时间外,还应结合尝试次数控制与运维手段。

10. 用一张表区分设问2的四个问题

把设问2中容易混淆的论点,按攻击者所操纵的值来整理如下。

攻击 攻击者改变的值 本不该被信任的地方 根本对策
JWT篡改 JWT请求头的alg、载荷中的用户ID 令牌自身声明的验证算法 在服务器端固定允许使用的算法
获取他人信息 请求中的mid 客户端指定的目标ID 与JWT的主体核对,或由JWT决定目标ID
变更为付费用户 规格之外的status 被自动绑定的全部属性 将可更新属性列入许可列表
破解4位验证码 otp的候选值 无限制的认证尝试 引入失败次数限制、延迟、风险判定

不要用”验证输入值”这一句话把所有问题都笼统概括在一起,这一点很重要。

  • alg关乎加密处理的策略
  • mid关乎对象级别的授权
  • status关乎属性级别的授权
  • otp关乎对在线猜测的抵御能力

即使处于同一个HTTP请求之中,需要守护的理由也各不相同。

11. 设问3(1) ── 在不破坏系统的前提下确认外部代码执行

服务上线后,一个被广泛使用的开源库H被公布出存在重大漏洞V。题目描述的流程如下。

  1. 攻击者将含有JNDI Lookup的字符串放入HTTP请求头中发送
  2. 目标服务器将该值输出到日志中
  3. 存在漏洞的库对JNDI Lookup求值,并向攻击者控制的LDAP服务器发起查询
  4. LDAP响应返回攻击者控制的HTTP服务器的URL
  5. 目标服务器获取类文件并执行其中的命令

这可以理解为隐去了具体名称的Log4Shell(CVE-2021-44228)型攻击。Apache的官方说明中也提到,一旦攻击者能够控制日志消息或参数,就可能导致执行从LDAP服务器读取的任意代码这一漏洞9

Log4Shell型漏洞的确认流程用无害的回调确认从JNDI到外部代码执行的链路是否畅通在x-api-version中注入JNDI Lookup字符串JNDI LookupHTTP URL响应记录GET请求确认到达攻击者存在漏洞的服务器日志处理恶意LDAP服务器恶意HTTP服务器index.html测试服务器访问日志确认漏洞

图12: Log4Shell型漏洞的确认流程。不使用破坏性指令,而是通过HTTP访问记录确认是否到达。

验证代码只应引发无害的HTTP访问

G公司执行不会影响系统本身的验证代码,以确认漏洞V是否能被外部利用。验证代码所执行的指令,仅仅是获取测试服务器上的index.html

设问3(1)询问的是,应该在测试服务器上实现什么,才能确认命令确实被执行了。

解答示例如下。

在测试服务器上记录并确认对index.html的访问的机制

只要Web服务器的访问日志中留下了来自目标服务器的GET请求,就至少可以确认以下这条链路是畅通的。

外部HTTP请求
  -> 日志处理
  -> JNDI Lookup
  -> LDAP响应
  -> 获取类文件
  -> 执行验证指令
  -> 对测试服务器的HTTP访问

为什么”在画面上显示字符”还不够

攻击目标是服务器,用户浏览器画面未必会出现变化。而且即使漏洞确实存在,途中的出站通信也有可能被防火墙拦截。

而在测试服务器一侧记录访问,则能成为”从目标服务器到达了外部”这一可观测的证据。

在实务中进行同类验证时,必须遵守以下原则。

  • 从目标系统的所有者处获得明确的授权
  • 采用不会对生产环境造成影响、或影响可接受的验证方法
  • 不使用写入・删除・修改配置等破坏性指令
  • 验证用的域名与服务器由本公司自行管理
  • 记录验证时刻、来源、目标、以及预期的回调
  • 验证结束后撤除临时搭建的LDAP・HTTP服务器与凭据

“确认能够执行任意代码”与”实际执行任意危险代码”并不是一回事。应将副作用控制在满足目的所需的最小范围内。

12. 设问3(2)(3) ── WAF检查HTTP请求头

服务N的WAF,可以将GET、POST、PUT、ANY、Header、COOKIE、Multipart作为检查对象来选择。

攻击代码存在于名为x-api-version的HTTP请求头的值中。因此,表6的空格e与f,答案都是Header

把本文中给出的位置,直接对应到WAF的检查对象

与其说这里考的是一般知识,不如说是要求读懂题目描述的数据流。

攻击字符串的存放位置:
  x-api-version请求头
          |
          v
WAF的检查对象:
  Header

既不是GET参数,也不是POST正文。不应该看着WAF的功能列表,凭”看起来像攻击就选ANY”的直觉作答,而应该回答题目描述中攻击者放入值的位置

应对大小写互换

最初的方案,用概念性的方式描述,规则大致如下。

Header  \Wjndi\W  阻断
Header  \Wldap\W  阻断

但只要像jNdI这样互换大小写,就能绕过这种仅匹配小写的简单模式。

设问3(3)的解答示例,是以下两种写法之一。

\W[jJ][nN][dD][iI]\W
\W(j|J)(n|N)(d|D)(i|I)\W

在题目册子中,反斜杠在日文环境的字形下可能显示为日元符号,但作为正则表达式,它就是\W\W匹配的是英数字与下划线以外的字符。在JNDI Lookup的语法中,jndi前后会出现${:等非单词字符,因此这里把它们也一并纳入考虑。

按照同样的思路,ldap一侧同样也可以写成忽略大小写的形式。

\W[lL][dD][aA][pP]\W

不要把这个正则表达式当作”完整的Log4Shell对策”

考试要求回答的,是与题目所示的规避方式相对应的正则表达式。而在真实攻击中,还可能出现字符串拆分、使用其他Lookup、编码、更换协议等仅靠特征码难以完全覆盖的变形手法。

因此,在实务中的定位应当是以下这样。

  1. 用WAF暂时阻止目前已知的攻击模式
  2. 调查是否真的包含受影响的库
  3. 限制出站的LDAP・RMI以及不必要的HTTP通信
  4. 更新到修复版本
  5. 更新之后仍要检查日志,调查是否已被入侵

WAF只是在等待修复版本发布期间争取时间的一层防线。

WAF的定位WAF是临时缓解层,根本对策是将库更新到修复版本WAF规则检测/阻断限制出站通信公布重大漏洞确认影响临时缓解暂时阻止攻击模式封堵利用路径更新到修复版本的库事后确认与再发防止

图13: WAF的定位。WAF只是为等待修复版本争取时间,根本对策是更新。

13. 设问3(4) ── 一开始使用”检测”模式的理由

关于变更后的WAF规则,注册安全工程师Z建议,在正式投入运行后的一段时间内,应将动作设为”检测”而非”阻断”。

本设问要求分别用25字以内回答,将其设为检测模式的好处,以及为将损失降到最低应实施的内容。

解答示例如下。

项目 解答要点
好处 可以避免因误报而造成阻断
应实施的内容 收到告警后应精查是否为攻击

检测模式不是”什么都不做的模式”

在检测模式下,系统一边放行与规则匹配的通信,一边将其记录到日志并发出告警。即使正常的API调用中偶然出现jndildap这样的字符串,业务也不会因此立即被中断。

但作为交换,运维一方需要做到以下几点。

收到告警
   |
   v
确认目标请求
   |
   +-- 正常通信 -> 收窄规则,考虑设置例外条件
   |
   +-- 攻击     -> 隔离目标,保全日志,调查影响,转入阻断

如果没有人查看告警,那么检测模式就不具备任何防御效果。检测,必须与”观察并做出判断”的运维工作配套使用

从检测转向阻断的流程

一般的引入步骤如下。

  1. 在检测模式下应用于真实流量
  2. 对误报与正确检出进行分类
  3. 调整目标请求头、路径、API、字符边界等
  4. 确认对正常通信的影响处于可接受范围
  5. 转入阻断模式
  6. 监控阻断件数与业务影响

不过,这只是平时的原则。如果漏洞非常严重、已经被实际利用、且没有替代手段,也可能判断入侵造成的损失大于误报导致的中断,从而一开始就选择阻断。在考题所设定的情境中,为了确认服务能否照常使用,首先选择了检测模式。

WAF从检测转向阻断在检测模式下观察告警,调整误报后再转入阻断模式实际流量误报攻击检测模式产生告警是攻击还是误报调整规则切换至阻断模式监控阻断件数与业务影响

图14: 从检测到阻断。先观察并调整,确认影响可接受后再转入阻断模式。

14. WAF是临时措施,更新才是根本对策

根据题目描述,库H的官方网站当时既没有修复版本,也没有临时对策,云服务商提供的全面WAF规则最长也要72小时才能就绪。因此,G公司选择自行确认影响范围,并对已经掌握的攻击模式先做临时防御。

这个顺序,正是事件响应的基本模式。

阶段 目的 本题中的应对
确认影响 判断本公司是否真的处于危险之中 用无害的回调确认能否被外部利用
临时缓解 为修复争取时间 WAF规则、检测・阻断、限制出站通信
根本修复 消除漏洞根源 更新到修复版本的库
事后确认 调查是否已经被利用 调查WAF・应用・DNS・代理等日志
再发防止 加快下一次的判断速度 梳理依赖关系、SBOM、更新流程、联络渠道

“不知道是否在用”会造成最大的延误

题目中提到,即使G公司向F公司询问是否使用了库H,由于需要详细的架构分析,回复也需要时间。

在实务中,如果直到重大漏洞公布之后才第一次开始寻找JAR文件,应对就会滞后。至少应在平时就具备以下这些信息。

  • 直接依赖与传递依赖的清单
  • 实际包含在发布物中的组件及其版本
  • 部署在哪些服务・容器・终端上
  • 更新依赖库并重新构建・重新发布的流程
  • 批准紧急变更的联络渠道
  • 出站通信的允许目标,以及停用时的影响
  • 日志的保存位置与检索方法

SBOM本身不是目的,而是用于在短时间内回答”这个漏洞会影响哪些正在运行的系统”的索引

更新之后不要就此结束调查

在漏洞公布前后,系统有可能已经遭到过攻击。即使更新到修复版本、阻止了今后的利用,已经泄露的凭据或已被植入的后门也不会因此消失。

如果是Log4Shell型漏洞,至少应从以下角度进行调查。

  • 包含JNDI或LDAP等可疑字符串的HTTP请求
  • 从应用服务器发往外部LDAP・RMI・HTTP的通信
  • 与平常不同的子进程启动
  • 可疑JAR、class、脚本、可执行文件的生成
  • 对云凭据或环境变量的访问
  • 更新前后的认证・权限变更・外部发送

不能仅凭WAF日志就断定”没有遭到攻击”,这一点很重要。因为还存在不经过WAF的内部路径,以及未被保存下来的历史日志。

15. 便于在考试中拿分的阅读方法

与其说这是一道知识题,不如说是一道要求读懂”规格”与”实现”之间差异的题目。

15.1 把表格中的”规格”与”实现”分开看

status的问题中,API规格之外的值,在实现中却被放行了。

规格:
  mid / name / age

实现:
  将接收到的参数全部发送给P

只要看清这个差异,就能明白空格c的答案是公共模块P。

15.2 给攻击者改变的值画线

各次攻击中被改变的值如下。

  • JWT请求头的alg
  • JWT载荷中的用户ID
  • API参数中的mid
  • 规格之外的status
  • 认证API的otp
  • HTTP请求头的x-api-version

各设问几乎都在问”这个值本应在哪里被验证”。

15.3 把答案落回题目文本的用语

在实务中,可以称之为”BOLA”、”Mass Assignment”、”rate limiting”。但设问所要求的,是与题目结构相匹配的具体处理。

不好的例子:

恰当地进行授权。

好的例子:

验证JWT中包含的用户ID是否与mid的值一致。

不好的例子:

采取暴力破解对策。

好的例子:

连续失败次数超过阈值后锁定账户。

只知道抽象名词,写不出能够在字数限制内被判为得分的答案。

15.4 追踪WAF”进入了哪里”

WAF的检查对象,不应从攻击类型去推测,而应根据攻击字符串存放的位置来决定。

放入了x-api-version请求头
        ↓
检查对象为Header

评分讲评中提到,设问3(1)的正确率之所以偏低,也是因为很多答案与图6所示的攻击流程不相符。哪怕只是把攻击步骤用箭头重新写一遍,也能看清应该观测什么。

16. 可用于实务API评审的检查清单

这是一份用于将本题的经验带回实际设计・代码评审的检查清单。

JWT验证

  • 是否在服务器配置中固定了允许使用的签名算法
  • 是否拒绝none及非预期的算法
  • 是否根据用途验证签名、issaudexpnbf
  • 是否避免混淆ID令牌、访问令牌、刷新令牌
  • 是否具备密钥轮换与失效时的处理流程
  • 是否未将应保密的信息放入JWT载荷

对象级别的授权

  • 更改请求中的ID时,是否无法到达他人的数据
  • 列表、详情、更新、删除、下载,是否全部都进行了授权检查
  • 授权检查是否在到达数据的公共层实施,而不是仅在画面层面
  • 对于仅面向本人的API,是否考虑过从令牌中导出目标ID
  • 管理员操作是否与普通用户API采用了不同的策略

属性级别的授权

  • 是否将外部输入类型与数据库实体分开
  • 是否用许可列表列举了可更新的字段
  • 是否拒绝或审计了规格之外的属性
  • 权限、计费、审批、所有者等状态是否无法通过用户输入变更
  • 响应中是否也不包含不必要的敏感属性

认证尝试

  • 是否设有按账户单位的失败次数限制
  • 是否具备分级延迟或按发送源单位的控制
  • 重新发行验证码时是否不会重置失败次数
  • 认证码是否只能使用一次
  • 是否未将认证码或密码写入日志
  • 解锁・恢复流程是否没有沦为另一条薄弱的认证途径

重大的依赖库漏洞

  • 能否将正在运行的服务与依赖版本对应起来
  • 是否具备以无害方式验证影响的流程
  • 能否应用WAF或限制出站通信等临时对策
  • 是否有负责人确认检测告警的运维机制
  • 是否具备更新到修复版本的紧急发布通道
  • 更新前是否从日志中调查过被利用的可能性

17. 从这道题看到的公共组件的两面性

本题中出现了两个公共组件——JWT管理库Q与公共模块P。

公共组件有很大的优点。

  • 只需修改一处,就能让修复反映到所有使用它的API上
  • 无需在各功能中重复实现授权与验证逻辑
  • 可以集中测试对象
  • 可以统一日志与审计的格式

但另一方面,错误也会扩散到整体。

  • 如果库Q接受alg=none,那么所有使用JWT的API都会变得危险
  • 如果公共模块P接受任意的midstatus,那么GET与PUT两者都会变得危险
  • 如果基础设施中使用了存在漏洞的库H,那么把HTTP请求头输出到日志的多条路径都会成为攻击面

因此,应当共通化的不应只是单纯的数据访问。必须共通化安全不变量,并对该公共组件本身进行严格的单元验证

例如,可以将P的契约设计成如下形式。

P.getOwnUser(authenticatedSubject)
P.updateOwnProfile(authenticatedSubject, ProfileUpdate{name, age})

而像下面这样的低水平API,不直接向一般调用方公开会更安全。

P.getUser(arbitraryMid)
P.updateUser(arbitraryMap)

只有管理处理等有限的路径才需要用到后者。如果把低水平的自由度分配给所有API,就会变成一种期待每个调用方每次都能正确使用的设计。

18. 总结

2024年度春季(令和6年)午后问1,是一道要求把API安全的各个论点逐一分离开来阅读的题目。

使用了JWT,并不意味着认证就是安全的。只要让攻击者来选择签名算法,用户ID就可能被篡改。

JWT的签名正确,也不意味着授权就是正确的。只要信任请求中的mid,正规用户就可能访问到他人的信息。

能够更新自己的对象,并不意味着可以变更所有属性。像status这样的内部状态一旦被自动绑定,权限或计费状态就可能被篡改。

认证码设有有效期限,并不意味着就能抵御暴力破解。必须计算候选数与尝试速度,并对失败次数加以限制。

在WAF中加入规则,并不意味着漏洞已经被修复。检测与阻断只是在争取时间,还需要确认影响范围,并最终更新库本身。

漏洞与对策对应表将各漏洞与相应的信任边界、对策对应起来令牌验证对象级别授权属性级别授权认证尝试控制从输入到执行JWT篡改固定许可算法mid替换核对JWT主体/无需midstatus=paid将更新DTO许可列表化4位验证码暴力破解失败次数限制・延迟Log4Shell型漏洞更新库/WAF

图15: 漏洞与对策对应表。按被突破的边界分别制定对策。

贯穿本题的原则只有一个。

不要因为前一项验证已经成功,就把它当作省略下一道信任边界的理由。

在本系列此前的文章中,已经解说过2023年秋季(令和5年)午后问1的存储型XSS信息处理安全保障支援士(情報処理安全確保支援士) 2023年秋季(令和5年) 下午问2解说 ── 从来宾用Wi-Fi带出的文件。关于整个网站的确认要点,也请参考把IPA《安全网站构建指南》当作检查清单来使用

最终总结说明即使前一项验证成功,也不能省略下一道信任边界认证成功JWT签名验证对象级别授权属性级别授权尝试次数限制从输入到执行的边界WAF/更新库

图16: 最终总结。信任边界应逐层确认,不可省略任何一个环节。

参考链接

  1. IPA, 2024年度春期(令和6年) 信息处理安全保障支援士考试 午后 试题册。本文所讨论的题目原文。 

  2. IPA, 2024年度春期(令和6年) 信息处理安全保障支援士考试 午后 解答示例。各设问的官方解答示例。 

  3. IPA, 2024年度春期(令和6年) 信息处理安全保障支援士考试 午后 评分讲评。关于正确率与常见错答倾向的说明。 

  4. NIST, SP 800-63B: Authentication and Authenticator Management。给出了短期密钥的位数、尝试次数限制、重新发行时的失败次数、不将电子邮件用于带外认证等规定。  2

  5. RFC Editor, RFC 7519: JSON Web Token (JWT)。定义了Unsecured JWT与包含alg=none的JWT规范。 

  6. RFC Editor, RFC 8725: JSON Web Token Best Current Practices。规定了固定允许算法、验证签发者・主体・audience等内容的最佳实践(BCP)。 

  7. OWASP, API1:2023 Broken Object Level Authorization。说明了针对用户指定的每个对象ID都必须确认授权的必要性。 

  8. OWASP, API3:2023 Broken Object Property Level Authorization。说明了包括Mass Assignment在内的属性级别授权缺陷及其对策。 

  9. Apache Logging Services, Security。说明了CVE-2021-44228的影响、经由JNDI与LDAP实现的代码执行、以及修复版本。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

网站制作

因为在会员API与手机应用联动中,JWT验证、对象级别授权、可更新属性的限制直接关系到Web系统的安全性。

常见问题

汇总了咨询这一主题时常见的问题。

为什么JWT签名验证已经成功,却仍然能够冒充他人?
在本题中,JWT管理库会按照攻击者指定的方式接受JWT请求头中的alg,把alg=none的JWT当作未签名却依然正确的令牌来处理。因此,即使改写了载荷中的用户ID,验证依然会成功。考试的解答是,将验证对象设为JWT请求头中的alg,并确认其值不为NONE。但在实务中,仅仅拒绝NONE是不够的。应通过服务器端的设置,将允许使用的算法固定为RS256等,不让令牌自行申报的算法直接用于选择。此外还应根据用途验证issuer、audience、有效期限、subject等内容。
只要核对JWT内的用户ID与请求中的mid,作为授权对策就足够了吗?
作为本题的解答已经足够。在公共模块P中验证JWT所含用户ID与mid是否一致,就能阻止指定他人mid的攻击。但如果API只处理用户本人的信息,实务中更安全的做法是不从客户端接收mid,而是根据已验证JWT的subject来决定用户ID。例如设计成GET /users/me或PUT /users/me,就能从根本上降低漏写核对处理这类实现失误发生的可能性。管理员操作他人的API,应拆分为独立的端点与授权策略。
添加status的攻击,为什么仅靠通常的输入值验证无法防范?
因为即使验证了name的长度或age的范围,只要原本就不该接收的status被自动绑定并传给了内部对象,就无法防范这种攻击。问题不在于值的格式,而在于该属性是否允许由用户变更这一属性级别的授权问题。应在更新用的输入类型中只定义name与age,拒绝未知的属性。计费状态必须只能通过服务器可信的事件(例如支付服务返回的成功结果)来变更。
4位认证码10分钟就会失效,为什么仍然危险?
因为从0000到9999的候选只有1万种,若每秒可以尝试10次,平均5,000次、也就是500秒即可命中。有效期10分钟等于600秒,如果按顺序尝试不重复的候选,就能在有效期内确认6,000种组合。仅靠失效时间无法阻止暴力破解,必须把候选数、尝试速度、尝试次数上限一并纳入设计。
设问给出的对策是锁定账户,但在实务中仅靠立即锁定就足够了吗?
不,并非如此。题目空格中应填入的是"连续失败次数超过阈值后锁定账户的处理",但如果只做固定的永久锁定,攻击者就能故意让他人的账户被锁定,从而造成服务妨害(拒绝服务)。实务中应组合按账户单位统计的失败次数、分级等待时间、对发送源与终端的风险判定、通知、恢复流程。即使发行了新的验证码,也不应将失败次数重置为零,这一点同样重要。
把WAF设为检测而非阻断,意义何在?
意义在于,即使把正常字符串误判为攻击,也不会因此中断业务通信。题目的解答中,好处是可以避免因误报而造成阻断,应实施的内容是收到告警后精查是否为攻击。检测模式并不是用来放任不管的设置,而应把它当作一段观察期,用来确认日志、排除误报、调整规则,并为转入阻断做准备。在已知的重大漏洞正被实际利用的紧急情况下,也可能权衡可用性风险后,判断应优先选择阻断。
本题中的库H是Log4j吗?
题目原文隐去了具体产品名,但JNDI Lookup、LDAP服务器、从HTTP服务器获取类文件、嵌入HTTP请求头的字符串、CVSS v3.1较高的基础评分——这一整套攻击流程,很自然地可以理解为是对广为人知的Log4Shell(CVE-2021-44228)的抽象化描述。本文会说明这种对应关系,但在考试中并不需要回答具体产品名,仅凭题目给出的攻击步骤与WAF规格就足以作答。
从本题中,应该带回实务的是什么?
要点在于,认证成功、JWT未被篡改、可以访问目标对象、可以变更目标属性,这些全都是彼此独立的确认。此外,短位数的认证码必须配合尝试次数限制,而重大的库漏洞则需要同时推进影响确认、临时防御与根本修复。将授权集中到公共组件中、把输入模式做成许可列表、在服务器端固定JWT的验证条件、掌握依赖库并保持可更新的状态,是实务中的核心所在。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表