“JWT的签名已经验证过了,所以用户ID是可信的”
这种说法只对了一半。
信息处理安全保障支援士考试2024年度春季(令和6年)午后问1,以从智能手机调用的API为题材1。认证成功后会签发JWT,之后带着这个JWT调用获取・更新用户信息的API。乍看之下,是很常见的结构。
然而,在诊断中却发现了以下四个问题。
- 将JWT请求头的
alg改为none后,未签名的JWT也能通过验证 - 保持使用正确的JWT,只将
mid改为其他用户ID,就能获取・更新他人的信息 - 添加规格之外的
status=paid后,免费用户就变成了付费用户 - 邮件送达的4位认证码,可以不受限制地进行暴力破解
这四个问题看上去都是”认证相关的漏洞”,但成因并不相同。令牌的完整性、对象级别授权、属性级别授权、尝试次数限制这些各自独立的边界被逐一突破了。
在文章后半,还会加入另一个论点。一个被广泛使用的开源库被公布出存在重大漏洞,攻击者可以利用JNDI Lookup从外部执行代码。修复版本尚未发布,完善的WAF规则也还不存在。在此期间,该如何确认影响范围、WAF应该检查哪里、以及为什么最初要选择”检测”而不是”阻断”,都是本文要探讨的问题。
本文将以官方解答示例2与评分讲评3为基础,不仅整理各设问的答案,还会梳理为什么答案是这样、以及在实务中应该把设计做到多严格。
flowchart TB
accTitle: 问题全貌
accDescr: 展示在认证码、JWT、API授权、库漏洞各阶段被突破的信任边界
user[用户应用]
auth[4位认证码]
jwt[签发JWT]
api[用户API]
log[日志输出]
vuln[存在漏洞的库]
ext[外部代码执行]
user --> auth
auth -->|无试行次数限制| jwt
jwt -->|允许alg=none| api
api -->|信任mid| db1[获取/更新他人数据]
api -->|status=paid| db2[变更计费状态]
api --> log
log --> vuln
vuln -->|JNDI/LDAP/HTTP| ext
图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的用户,不一定就可以读取他人的数据。能够更新自己数据的用户,也不一定可以变更计费状态。
一旦能做出这样的阶段划分,各设问的答案就不再需要死记硬背了。
flowchart LR
accTitle: 认证与授权的区别
accDescr: 认证确认主体是谁,授权确认该主体被允许执行的操作
auth[认证<br/>你是谁]
authz[授权<br/>你能做什么]
auth --> authz
图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秒。这正是判定为”很可能被攻破”的理由。
flowchart LR
accTitle: 4位认证码的时间感觉
accDescr: 1万种可能每秒尝试10次,平均5,000次、500秒即可命中,短于600秒的有效期
A[候选数 10,000种] -->|平均 10,000 / 2 = 5,000次| B[平均破解时间 500秒]
C[有效期 600秒] -->|500秒 < 600秒| D[可在有效期内破解]
图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、签发时间、有效期限等信息。
诊断人员改动了以下两处内容。
- 将请求头的
alg从RS256改为NONE - 将载荷中的用户ID改为其他用户
发送该JWT后,验证竟然成功,从而得以冒充他人身份。
flowchart LR
accTitle: JWT alg=none攻击的流程
accDescr: 将正确JWT的alg改为none,并篡改用户ID后通过验证的流程
A[正确的JWT<br/>alg=RS256<br/>user=user01] -->|将请求头的alg改为none| B[篡改后的JWT<br/>alg=none<br/>user=user02]
B -->|跳过签名验证| C[服务器<br/>接受为user02]
图3: JWT alg=none攻击流程。攻击者自行选择了验证算法。
none并非拼写错误
RFC 7519中定义了alg为none、既不签名也不加密的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)的含义。
flowchart TB
accTitle: 安全的JWT验证与危险的JWT验证
accDescr: 危险的验证依赖alg字段,安全的验证使用服务器端的许可列表
subgraph "危险的验证"
D1[读取JWT请求头的alg]
D2[若alg为none则接受]
D1 --> D2
end
subgraph "安全的验证"
S1[服务器配置的许可算法<br/>例如RS256]
S2[确认JWT请求头的alg<br/>是否在许可列表中]
S3[验证签名・iss・aud・exp]
S1 --> S2 --> S3
end
图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。
flowchart LR
accTitle: BOLA攻击
accDescr: 使用正确的JWT,同时将请求中的mid改为其他用户ID
A[攻击者] -->|JWT为user01<br/>mid为user02| B[用户API]
B -->|信任mid| C[从数据库返回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中只为管理员追加例外”,这样拆分能让授权策略的边界更加清晰。
flowchart LR
accTitle: BOLA的防范方法
accDescr: 不使用请求中的mid,改为根据JWT的主体确定或核对目标
A[用户] -->|GET /users/me + JWT| B[API]
B -->|获取JWT的sub| C{若存在mid<br/>是否与sub一致}
C -->|一致| D[返回自己的数据]
C -->|不一致| E[403拒绝]
B -->|无mid| F[以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。
flowchart TB
accTitle: 批量赋值(Mass Assignment)
accDescr: 添加了规格之外的status=paid,并被整体反映到内部对象中
A[API规格<br/>mid / name / age] -->|攻击者添加status=paid| B[请求体]
B -->|自动绑定| C[公共模块P]
C -->|保存至数据库| D[计费状态变更为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)
即使画面上只有name和age两个输入框,攻击者也完全可以直接构造HTTP请求。不在UI上出现的字段,并不构成安全边界。
安全的实现,会明确列出可更新的字段。
input = parseExactSchema(body, fields = ["name", "age"])
entity = repository.find(authenticatedUserId)
entity.name = input.name
entity.age = input.age
repository.save(entity)
这里重要的有两点。
- 更新用的输入类型中,只放置用户可以变更的字段
- 对于规格之外的未知字段,不要默默忽略,而应尽量作为错误拒绝
如果对未知字段保持沉默地忽略,虽然能让攻击表面上”失败”,但也会因此错过客户端的实现错误或攻击迹象。如果没有兼容性方面的理由,用严格的模式(schema)拒绝反而更便于排查。
flowchart TB
accTitle: 属性级别的授权
accDescr: 更新用DTO只保留许可列表,未知属性一律拒绝
subgraph "更新用DTO(许可列表)"
D1["name"]
D2["age"]
end
A[请求体] -->|模式校验| B{是否仅包含<br/>许可的字段}
B -->|是| C[更新实体的name/age]
B -->|否| D[返回错误]
E[支付服务<br/>已验证的通知] -->|专用通道| F[更新status=paid]
图8: 属性级别的授权。用许可列表限制可更新的字段,计费状态通过另一条通道变更。
status只能通过支付结果变更
status=paid并不是用户资料的一部分。它是由服务器端”支付已成功”这一事实推导出来的状态。
用户资料更新
-> 仅可变更name / age
来自支付服务的已验证通知
-> 核对paymentId
-> 防止重复处理
-> 将status变更为paid
即使保存在同一个数据库列中,变更的权限与途径也是各自独立的。如果把内部实体直接用作外部API的输入类型,这道边界就会消失。
9. 设问2(5) ── 暴力破解对策需要将失败次数作为状态保存
针对4位验证码的暴力破解,要求用30字以内回答表5空格d应填入的处理。阈值为10。
解答示例如下。
连续失败次数超过阈值后锁定账户的处理
这里与设问1的无状态并不矛盾。不将API调用的对话状态作为服务器会话保存,与将安全判断所需的失败次数持久化,是两件不同的事。
flowchart LR
accTitle: 是否设置尝试次数限制
accDescr: 没有限制时平均500秒即被突破,但失败次数限制可以压低攻击速度
subgraph "无限制"
A1[每秒尝试10次] -->|约500秒| B1[认证成功]
end
subgraph "有限制"
A2[失败10次即锁定] -->|攻击速度骤降| B2[账户锁定]
C2[分级延迟] --> B2
end
图10: 是否设置次数限制。失败次数限制能在实际中有效阻止暴力破解。
实务中不要”只靠永久锁定”
按账户单位限制次数是必要的,但如果攻击者知道他人的用户ID,就可以故意失败10次,把正规用户挤出去(拒绝服务)。因此,实务中应组合以下多种手段。
| 控制手段 | 作用 |
|---|---|
| 按账户单位统计的失败次数 | 阻止针对单个账户的暴力破解 |
| 分级等待时间 | 在容许正规用户输入失误的同时,降低攻击速度 |
| 按发送源IP・终端・ASN等进行的控制 | 抑制对大量账户各尝试少量次数的攻击 |
| 基于风险的判定 | 对异于平常的地区・终端・速度进行更严格的限制 |
| 向用户发送通知 | 使其能够察觉到攻击或误操作 |
| 安全的恢复流程 | 避免解锁入口本身成为新的攻击途径 |
此外,重新发送验证码时,不能把失败次数重置为0。因为这样一来,攻击者每次调用重发API,都能重新获得一批可尝试的次数。现行的NIST SP 800-63B也要求,即使生成了新的认证密钥,也不应重置失败次数4。
让认证码只能使用一次
题目主要围绕有效期展开,但在实务中还需要以下这些措施。
- 验证成功的验证码应立即失效
- 拒绝重复使用同一个验证码
- 不将验证码本身写入日志
- 让响应无法从验证码核对的成功与否推测出该用户是否存在
- 也要为验证码发送API设置次数限制
既然使用的是较短的密钥,就不能只把安全性寄托在随机生成上。
flowchart TB
accTitle: 认证码对策
accDescr: 除位数与失效时间外,还通过尝试次数限制・拒绝重复使用・通知等手段防护
A[认证码] --> B[增加位数]
A --> C[缩短有效期]
A --> D[限制尝试次数]
A --> E[成功后立即失效]
A --> F[重新发送时不重置失败次数]
A --> G[不将认证码写入日志]
A --> H[按发送源单位控制]
图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。题目描述的流程如下。
- 攻击者将含有JNDI Lookup的字符串放入HTTP请求头中发送
- 目标服务器将该值输出到日志中
- 存在漏洞的库对JNDI Lookup求值,并向攻击者控制的LDAP服务器发起查询
- LDAP响应返回攻击者控制的HTTP服务器的URL
- 目标服务器获取类文件并执行其中的命令
这可以理解为隐去了具体名称的Log4Shell(CVE-2021-44228)型攻击。Apache的官方说明中也提到,一旦攻击者能够控制日志消息或参数,就可能导致执行从LDAP服务器读取的任意代码这一漏洞9。
flowchart LR
accTitle: Log4Shell型漏洞的确认流程
accDescr: 用无害的回调确认从JNDI到外部代码执行的链路是否畅通
A[攻击者] -->|在x-api-version中<br/>注入JNDI Lookup字符串| B[存在漏洞的服务器]
B --> C[日志处理]
C -->|JNDI Lookup| D[恶意LDAP服务器]
D -->|HTTP URL响应| E[恶意HTTP服务器<br/>index.html]
E -->|记录GET请求| F[测试服务器<br/>访问日志]
F -->|确认到达| G[确认漏洞]
图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、编码、更换协议等仅靠特征码难以完全覆盖的变形手法。
因此,在实务中的定位应当是以下这样。
- 用WAF暂时阻止目前已知的攻击模式
- 调查是否真的包含受影响的库
- 限制出站的LDAP・RMI以及不必要的HTTP通信
- 更新到修复版本
- 更新之后仍要检查日志,调查是否已被入侵
WAF只是在等待修复版本发布期间争取时间的一层防线。
flowchart TB
accTitle: WAF的定位
accDescr: WAF是临时缓解层,根本对策是将库更新到修复版本
A[公布重大漏洞] --> B[确认影响]
B --> C[临时缓解]
C -->|WAF规则<br/>检测/阻断| D[暂时阻止攻击模式]
C -->|限制出站通信| E[封堵利用路径]
D --> F[更新到修复版本的库]
E --> F
F --> G[事后确认与再发防止]
图13: WAF的定位。WAF只是为等待修复版本争取时间,根本对策是更新。
13. 设问3(4) ── 一开始使用”检测”模式的理由
关于变更后的WAF规则,注册安全工程师Z建议,在正式投入运行后的一段时间内,应将动作设为”检测”而非”阻断”。
本设问要求分别用25字以内回答,将其设为检测模式的好处,以及为将损失降到最低应实施的内容。
解答示例如下。
| 项目 | 解答要点 |
|---|---|
| 好处 | 可以避免因误报而造成阻断 |
| 应实施的内容 | 收到告警后应精查是否为攻击 |
检测模式不是”什么都不做的模式”
在检测模式下,系统一边放行与规则匹配的通信,一边将其记录到日志并发出告警。即使正常的API调用中偶然出现jndi或ldap这样的字符串,业务也不会因此立即被中断。
但作为交换,运维一方需要做到以下几点。
收到告警
|
v
确认目标请求
|
+-- 正常通信 -> 收窄规则,考虑设置例外条件
|
+-- 攻击 -> 隔离目标,保全日志,调查影响,转入阻断
如果没有人查看告警,那么检测模式就不具备任何防御效果。检测,必须与”观察并做出判断”的运维工作配套使用。
从检测转向阻断的流程
一般的引入步骤如下。
- 在检测模式下应用于真实流量
- 对误报与正确检出进行分类
- 调整目标请求头、路径、API、字符边界等
- 确认对正常通信的影响处于可接受范围
- 转入阻断模式
- 监控阻断件数与业务影响
不过,这只是平时的原则。如果漏洞非常严重、已经被实际利用、且没有替代手段,也可能判断入侵造成的损失大于误报导致的中断,从而一开始就选择阻断。在考题所设定的情境中,为了确认服务能否照常使用,首先选择了检测模式。
flowchart LR
accTitle: WAF从检测转向阻断
accDescr: 在检测模式下观察告警,调整误报后再转入阻断模式
A[检测模式] -->|实际流量| B[产生告警]
B --> C{是攻击<br/>还是误报}
C -->|误报| D[调整规则]
D --> A
C -->|攻击| E[切换至阻断模式]
E --> F[监控阻断件数与业务影响]
图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及非预期的算法 - 是否根据用途验证签名、
iss、aud、exp、nbf - 是否避免混淆ID令牌、访问令牌、刷新令牌
- 是否具备密钥轮换与失效时的处理流程
- 是否未将应保密的信息放入JWT载荷
对象级别的授权
- 更改请求中的ID时,是否无法到达他人的数据
- 列表、详情、更新、删除、下载,是否全部都进行了授权检查
- 授权检查是否在到达数据的公共层实施,而不是仅在画面层面
- 对于仅面向本人的API,是否考虑过从令牌中导出目标ID
- 管理员操作是否与普通用户API采用了不同的策略
属性级别的授权
- 是否将外部输入类型与数据库实体分开
- 是否用许可列表列举了可更新的字段
- 是否拒绝或审计了规格之外的属性
- 权限、计费、审批、所有者等状态是否无法通过用户输入变更
- 响应中是否也不包含不必要的敏感属性
认证尝试
- 是否设有按账户单位的失败次数限制
- 是否具备分级延迟或按发送源单位的控制
- 重新发行验证码时是否不会重置失败次数
- 认证码是否只能使用一次
- 是否未将认证码或密码写入日志
- 解锁・恢复流程是否没有沦为另一条薄弱的认证途径
重大的依赖库漏洞
- 能否将正在运行的服务与依赖版本对应起来
- 是否具备以无害方式验证影响的流程
- 能否应用WAF或限制出站通信等临时对策
- 是否有负责人确认检测告警的运维机制
- 是否具备更新到修复版本的紧急发布通道
- 更新前是否从日志中调查过被利用的可能性
17. 从这道题看到的公共组件的两面性
本题中出现了两个公共组件——JWT管理库Q与公共模块P。
公共组件有很大的优点。
- 只需修改一处,就能让修复反映到所有使用它的API上
- 无需在各功能中重复实现授权与验证逻辑
- 可以集中测试对象
- 可以统一日志与审计的格式
但另一方面,错误也会扩散到整体。
- 如果库Q接受
alg=none,那么所有使用JWT的API都会变得危险 - 如果公共模块P接受任意的
mid或status,那么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中加入规则,并不意味着漏洞已经被修复。检测与阻断只是在争取时间,还需要确认影响范围,并最终更新库本身。
flowchart TB
accTitle: 漏洞与对策对应表
accDescr: 将各漏洞与相应的信任边界、对策对应起来
A[JWT篡改] -->|令牌验证| B[固定许可算法]
C[mid替换] -->|对象级别授权| D[核对JWT主体/无需mid]
E[status=paid] -->|属性级别授权| F[将更新DTO许可列表化]
G[4位验证码暴力破解] -->|认证尝试控制| H[失败次数限制・延迟]
I[Log4Shell型漏洞] -->|从输入到执行| J[更新库/WAF]
图15: 漏洞与对策对应表。按被突破的边界分别制定对策。
贯穿本题的原则只有一个。
不要因为前一项验证已经成功,就把它当作省略下一道信任边界的理由。
在本系列此前的文章中,已经解说过2023年秋季(令和5年)午后问1的存储型XSS与信息处理安全保障支援士(情報処理安全確保支援士) 2023年秋季(令和5年) 下午问2解说 ── 从来宾用Wi-Fi带出的文件。关于整个网站的确认要点,也请参考把IPA《安全网站构建指南》当作检查清单来使用。
flowchart TB
accTitle: 最终总结
accDescr: 说明即使前一项验证成功,也不能省略下一道信任边界
A[认证成功] --> B[JWT签名验证]
B --> C[对象级别授权]
C --> D[属性级别授权]
D --> E[尝试次数限制]
E --> F[从输入到执行的边界]
F --> G[WAF/更新库]
图16: 最终总结。信任边界应逐层确认,不可省略任何一个环节。
参考链接
-
IPA, 2024年度春期(令和6年) 信息处理安全保障支援士考试 午后 试题册。本文所讨论的题目原文。 ↩
-
IPA, 2024年度春期(令和6年) 信息处理安全保障支援士考试 午后 解答示例。各设问的官方解答示例。 ↩
-
IPA, 2024年度春期(令和6年) 信息处理安全保障支援士考试 午后 评分讲评。关于正确率与常见错答倾向的说明。 ↩
-
NIST, SP 800-63B: Authentication and Authenticator Management。给出了短期密钥的位数、尝试次数限制、重新发行时的失败次数、不将电子邮件用于带外认证等规定。 ↩ ↩2
-
RFC Editor, RFC 7519: JSON Web Token (JWT)。定义了Unsecured JWT与包含
alg=none的JWT规范。 ↩ -
RFC Editor, RFC 8725: JSON Web Token Best Current Practices。规定了固定允许算法、验证签发者・主体・audience等内容的最佳实践(BCP)。 ↩
-
OWASP, API1:2023 Broken Object Level Authorization。说明了针对用户指定的每个对象ID都必须确认授权的必要性。 ↩
-
OWASP, API3:2023 Broken Object Property Level Authorization。说明了包括Mass Assignment在内的属性级别授权缺陷及其对策。 ↩
-
Apache Logging Services, Security。说明了CVE-2021-44228的影响、经由JNDI与LDAP实现的代码执行、以及修复版本。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
信息处理安全保障支援士(情報処理安全確保支援士) 2023年秋季(令和5年)午后问1解说 ── 明明有16条评论,却只显示2条的存储型XSS
以信息处理安全保障支援士考试2023年秋季(令和5年)午后问1为素材,解说存储型XSS的攻击流程。整理输入字数限制被分段投稿突破的原因、会话ID在不经外部通信的情况下以图片形式被窃取的路径,以及真正起作用的应对措施。
信息处理安全保障支援士(情報処理安全確保支援士) 2023年秋季(令和5年) 下午问2解说 ── 从来宾用Wi-Fi带出的文件
以信息处理安全保障支援士考试2023年秋季(令和5年)下午问2为题材,解说堵住了USB闪存的公司如何被人从来宾用Wi-Fi带出文件的路径。梳理服务器证书验证与HSTS、MAC地址过滤的局限性,以及EAP-TLS和TPM带来的应对方法。
网站发包方也应该了解的内容 —— 把 IPA《安全网站构建指南》当作检查清单来使用
公司网站的安全性应该以什么标准来核查?本文用发包方、运营方也能理解的语言,解析 IPA《安全网站构建指南》所列出的 11 种漏洞及其对策,并介绍在发包、验收、运维各阶段的具体用法。
信息安全10大威胁2026——排行榜的正确打开方式,与中小企业真正应该防范的威胁
在IPA《信息安全10大威胁2026》中,勒索攻击连续11年蝉联第1位,供应链攻击位居第2位,首次入选的“围绕AI利用的网络风险”跃居第3位。本文解读组织篇TOP10的内容,并说明中小企业应把哪些威胁当作自身问题来加以应对。
中小企业的安全对策,该从何入手 ── IPA《中小企业信息安全对策指南》第4.0版导读
中小企业的安全对策应该从何入手?本文基于 IPA《中小企业信息安全对策指南》第4.0版,从信息安全六条准则、5分钟自我诊断,到 SECURITY ACTION,进行分阶段讲解。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
网站制作
因为在会员API与手机应用联动中,JWT验证、对象级别授权、可更新属性的限制直接关系到Web系统的安全性。
技术咨询 & 设计评审
因为通过设计评审排查现有API的授权漏洞、依赖库的影响范围、WAF临时规则,属于技术咨询的范畴。
常见问题
汇总了咨询这一主题时常见的问题。
- 为什么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的验证条件、掌握依赖库并保持可更新的状态,是实务中的核心所在。