认证相关漏洞

作者 ro0t 于 2021-06-05 发布
预计阅读所需时间 16 分钟
4.4k

OAuth 认证

OAuth是一个授权机制,核心是向第三方应用颁发令牌。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

令牌和密码有什么区别?

阮一峰写的( http://www.ruanyifeng.com/blog/2019/04/oauth_design.html ) 很好理解,我照搬一下:

  • 令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
  • 令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
  • 令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。

只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因

OAuth原理

OAuth规定了四种获得令牌的方式,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和 客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的

授权码

第三方应用先申请一个授权码,之后再利用该授权码获取令牌。

场景:用户U想登录A网站,但是又不想在A网站走麻烦的注册流程,A网站就提供了如使用QQ登录(暂定为B网站),此时就需要用到OAuth进行授权。授权流程就是,B网站在用户U允许的情况下,将用户U的相关数据共享给网站A。

OAuth授权码流程

流程如下:

  • A 网站提供一个链接,用户U点击后就会跳转到 B 网站,授权用户数据给 A 网站使用

    1
    https://b.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read

    response_type参数表示要求返回授权码codescope表示要授权的范围

  • 用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码。

  • A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌

  • B 网站收到请求以后,就会颁发令牌

  • A 网站根据令牌获取用户数据,之后返回用户所需要的资源

隐藏式

第一种是web应用有后端的情况,对于没有后端的web服务器,则需要使用隐藏式。隐藏式允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)。

同样的场景。

image-20210601175825466

流程如下:

  • A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

    1
    https://b.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read

    response_type参数为token,表示要求直接返回令牌。

  • 用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

    1
    https://a.com/callback#token=ACCESS_TOKEN

    token就是令牌,网站A是在前端拿到的令牌。

    **注:**令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。

  • A 网站根据令牌获取用户数据,之后返回用户所需要的资源

**注:**这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌也就失效了。

密码式

第三方应用使用用户的密码,申请令牌,这种方式称为"密码式"(password)。风险极高,不推荐使用

image-20210601191925621

流程如下:

  • A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。

    1
    https://oauth.b.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID

    grant_type参数是授权方式,这里的password表示"密码式",usernamepassword是 B 的用户名和密码。

  • B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。

  • A 网站根据令牌获取用户数据,之后返回用户所需要的资源

客户端凭证

适用于没有前端的命令行应用,即在命令行下请求令牌。

image-20210601192447949

流程如下

  • A 应用在命令行向 B 发出请求

    1
    https://oauth.b.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET

    grant_type参为client_credentials表示采用客户端凭证式,client_idclient_secret用来让 B 确认 A 的身份

  • B 网站验证通过以后,直接返回令牌

注:这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

OAuth漏洞

CSRF导致绑定劫持

攻击方式

攻击者抓取认证请求构造恶意url,诱骗已经登录的网用户点击(比如通过邮件或者QQ等方式)。认证成功后用户的帐号会同攻击者的帐号绑定到一起,攻击者就可以看到用户的操作和内容。

防御方式

  • OAuth 2.0提供了state参数用于防御CSRF。认证服务器在接收到的state参数按原样返回给redirect_uri,客户端收到该参数并验证与之前生成的值是否一致。
  • 可使用传统的CSRF防御方案

state是由第三方网站生成(如用户的设备信息+时间戳生成),再由第三方进行校验,保证了请求的一致性。

redirect_uri 绕过导致授权劫持

攻击方式

根据OAuth的认证流程,用户授权凭证会由服务器转发到redirect_uri对应的地址。如果攻击者伪造redirect_uri为自己的地址,然后诱导用户发送该请求,之后获取的凭证就会发送给攻击者伪造的回调地址。攻击者使用该凭证即可登录用户账号,造成授权劫持。

绕过方式

绕过redirect_uri的域名检查。

1
2
3
4
5
6
7
8
9
10
- auth.app.com.eval.com
- eval.com?auth.app.com
- eval.com?@auth.app.com
- auth.app.com@eval.com
- auth.app.com\@eval.com
- eval.com\auth.app.com
- eval.com:\auth.app.com
- eval.com\.auth.app.com
- eval.com:\@auth.app.com
- 宽字节绕过

防御方式

  • 严格校验目标域名,解析后的域名,要再加一层判断,如果不是注册的域名,则应禁止跳转。
  • 服务器严格校验client_id与回调地址是否对应

利用重定向漏洞

利用方式

目标站点app.com,OAuth的redirect_uri 也为app.com,假设app.com某处存在url跳转漏洞,则可绕过redirect_uri的检测。

正常请求下:

1
https://oauth.com/oauth2/authorize?client_id=xxx&redirect_uri=https://app.com/callback    

利用重定向漏洞之后:

1
https://oauth.com/oauth2/authorize?client_id=xxx&redirect_uri=https://app.com?redirect_url=http://eval.com

防御方式

  • 修复URL跳转漏洞

子域可控

攻击方式

对回调的地址验证了主域为app.com,但其子域名xxx.app.com可被任意用户注册 或 子域名存在URL跳转漏洞。

防御方式

  • 只对有授权需求的子域进行注册,禁止对*.app.com进行注册

scope越权访问

攻击方式
scope代表着访问权限,如https://b.com/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read scope为读权限,如果更改为 write,且服务器未做校验,则可获取写权限

防御方式

  • 严格校验注册redirect_uri的访问权限

code不失效

攻击方式

code在使用之后,未失效。攻击者在获取code之后,依旧可继续使用

修复方式

  • 严格校验code与client_id的绑定关系
  • code使用之后,应立即弃用,同时解除与client_id的绑定关系

用邮箱地址、手机进行「先前账户劫持」

场景:第三方应用不仅允许使用OAuth授权登录,还允许使用用户名、密码登录(即,在OAuth授权之后,创建一个第三方的账户)

攻击方式

​ 第三方应用在用户账户创建时缺乏邮件 或 手机号 验证,那么攻击者可以在知晓受害者 邮箱 或 手机号 的情况下,在受害者创建该应用账户之前,用受害者邮箱 或 手机 加任意密码以受害者身份创建该应用账户。之后,一旦受害者在第三方应用中尝试创建或登录时,由于其 邮件 或 手机号 已被攻击者先前创建过,因此就会把受害者的创建或登录流程链接绑定到攻击者之前创建的账户中,从而完成典型的“pre account takeover”(先前账户劫持)攻击,即攻击者利用受害者信息在受害者创建账户之前进行创建,实现账户劫持。

防御方式

  • 严格校验已授权用户的 邮箱、手机号 等,是否为当前用户所有。在OAuth授权完成之后,也需要加一层验证机制。

jwt 认证

JWT(JSON Web Token)是一个开放标准(RFC 7519),定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

jwt原理

jwt就是一个字符串,它由三部分组成:头部、载荷(payload)与签名。

流程如图:

img

在这个流中,用户在使用用户名和密码登录服务器后,服务器会创建一个新令牌并将其返回给客户端。当客户机继续调用服务器时,它会在Authorization头中附加新的令牌。服务器读取令牌并首先验证签名成功验证后,服务器使用令牌中的信息来标识用户。

JWT 头部

由两部分组成:类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

JWT 载荷

Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了以上字段之外,你完全可以添加自己想要的任何字段。

注意:由于jwt的标准,信息是不加密的,所以一些敏感信息最好不要添加到json里面

JWT 签名

为了得到签名部分,你必须有编码过的 头、编码过的 payload、一个秘钥(这个秘钥只有服务端知道),签名算法是头部中指定的那个算法,然对它们进行签名。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

**注意:**base64是一种编码方式,并非加密方式。

举个栗子

以下为一个jwt认证的串:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTm8iOiI4IiwiZXhwIjoxNjIyOTYwNjA5fQ.W8lHUKtf7-VLWekREgge4gwg11xFmBZcZQpSBEKyr7w

首先对其按照.分成三个部分:

  • eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • eyJ1c2VyTm8iOiI4IiwiZXhwIjoxNjIyOTYwNjA5fQ
  • W8lHUKtf7-VLWekREgge4gwg11xFmBZcZQpSBEKyr7w

再分别base64解码得到:

  • {“typ”:“JWT”,“alg”:“HS256”}
  • {“userNo”:“8”,“exp”:1622960609}
  • 乱码(因为这个是签名,是使用头部算法生成的,不是base64直接编码生成)

jwt漏洞

算法改为none

攻击方式

JWT支持将算法设定为None。如果alg字段设为None,那么签名会被置空,这样任何token都是有效的。

设定该功能的最初目的是为了方便调试。但是,若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为“None”来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站。

如:

1
eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2MjMwNDUxMzEsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.

解码:

1
2
{"alg":"HS512"}
{"iat":1623045131,"admin":"false","user":"Tom"}

更改了header(将算法改为none:{"alg": "none"})、payload(将admin:"false",改为admin:"true"),同时置空sign,可成功绕过

如:

image-20210607140005918

防御方式

  • jwt要指定所需的签名算法,并严格校验签名

密钥混淆攻击

攻击方式

JWT最常用的两种算法是HMAC和RSA。

  • HMAC(对称加密算法)用同一个密钥对token进行签名和认证。
  • RSA(非对称加密算法)需要两个密钥,先用私钥加密生成JWT,然后使用其对应的公钥来解密验证。

如果将算法RS256修改为HS256(非对称加密=>对称加密),后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。由于公钥有时可以被攻击者获取到,所以攻击者可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。后端代码会使用RSA公钥+HS256算法进行签名验证。

防御方式

  • JWT配置应该只允许使用HMAC算法或公钥算法,不能同时使用这两种算法。

无效签名

攻击方式

当用户端提交请求给应用程序,服务端可能没有对token签名进行校验,这样,攻击者便可以通过提供无效签名简单地绕过安全机制。

如:

1
{"user": "user","action": "profile"}

user改为admin后,拼接到原有payload位置,发送给服务端,若页面访问正常,则说明漏洞存在。

防御方式

  • 严格校验签名

暴力破解密钥

攻击方式

HMAC签名密钥(例如HS256 / HS384 / HS512)使用对称加密,这意味着对令牌进行签名的密钥也用于对其进行验证。由于签名验证是一个自包含的过程,因此可以测试令牌本身的有效密钥,而不必将其发送回应用程序进行验证。

通过JWT破解工具(如,myjwt),可以快速检查已知的泄漏密码列表或默认密码。

image-20210607151619702

https://jwt.io/#debugger-io中生成jwt token,即可。(注:必须在日期的过期时间之前,否则将无法绕过)

image-20210607152045756

image-20210607152136098

防御方式

  • 使用复杂的密码
  • 定期更新密码

操纵KID

KID代表“密钥序号”(Key ID)。它是JWT头部的一个可选字段,开发人员可以用它标识认证token的某一密钥。KID参数的正确用法如下所示:

1
2
3
4
5
{
"alg": "HS256",
"typ": "JWT",
"kid": "1" //使用密钥1验证token
}

由于此字段是由用户控制的,因此攻击者可能会操纵它并导致危险的后果。

攻击方式

  • 目录遍历

    由于KID通常用于从文件系统中检索密钥文件,因此,如果在使用前不清理KID,文件系统可能会遭到目录遍历攻击。这样,攻击者便能够在文件系统中指定任意文件作为认证的密钥。

    1
    "kid": "../../public/css/main.css"   //使用公共文件main.css验证token
  • SQL注入

    KID也可以用于在数据库中检索密钥。在该情况下,攻击者很可能会利用SQL注入来绕过JWT安全机制。

    如果可以在KID参数上进行SQL注入,攻击者便能使用该注入返回任意值。

    1
    "kid":"aaaaaaa' UNION SELECT 'key';--"  //使用字符串"key"验证token

    上面这个注入会导致应用程序返回字符串key(因为数据库中不存在名为aaaaaaa的密钥)。然后使用字符串key作为密钥来认证token

  • 命令注入

    KID参数直接传到不安全的文件读取操作可能会让一些命令注入代码流中。

    一些函数就能给此类型攻击可乘之机,比如Ruby open()。攻击者只需在输入的KID文件名后面添加命令,即可执行系统命令:

    1
    "key_file" | whoami;

如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !