身份认证缺陷
Authentication Bypasses
审计
- 创建AccountVerificationHelper实例,用于处理账户验证逻辑
parseSecQuestions函数的作用是从请求体中遍历参数名,找到包含secQuestion的参数,将其值存入Map中并返回
这里直接把AccountVerificationHelper整个分析一下
/** Created by appsec on 7/18/17. */
public class AccountVerificationHelper {// simulating database storage of verification credentialsprivate static final Integer verifyUserId = 1223445;private static final Map<String, String> userSecQuestions = new HashMap<>();static {userSecQuestions.put("secQuestion0", "Dr. Watson");userSecQuestions.put("secQuestion1", "Baker Street");}private static final Map<Integer, Map> secQuestionStore = new HashMap<>();static {secQuestionStore.put(verifyUserId, userSecQuestions);}// end 'data store set up'// this is to aid feedback in the attack process and is not intended to be part of the// 'vulnerable' codepublic boolean didUserLikelylCheat(HashMap<String, String> submittedAnswers) {boolean likely = false;if (submittedAnswers.size() == secQuestionStore.get(verifyUserId).size()) {likely = true;}if ((submittedAnswers.containsKey("secQuestion0")&& submittedAnswers.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0")))&& (submittedAnswers.containsKey("secQuestion1")&& submittedAnswers.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1")))) {likely = true;} else {likely = false;}return likely;}// end of cheating check ... the method below is the one of real interest. Can you find the flaw?public boolean verifyAccount(Integer userId, HashMap<String, String> submittedQuestions) {// short circuit if no questions are submittedif (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {return false;}if (submittedQuestions.containsKey("secQuestion0")&& !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))) {return false;}if (submittedQuestions.containsKey("secQuestion1")&& !submittedQuestions.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {return false;}// elsereturn true;}
}
这里直接定义了用户ID和用户的问题关联并将它们存入了secQuestionStore这个变量
- didUserLikelylCheat函数通过
(用户提交的参数数量)=(参数数量) (此条可能会被后面覆盖,但是由于后面参数不存在,检验跳过,所以这里是有用的)
(用户提交参数名包含所需要参数名)and(对应参数名答案正确)
判断用户是否作弊
- verifyAccount函数和didUserLikelylCheat函数很像,也是通过检查用户参数数量和对应参数答案是否匹配(不同的是这里用的不等于的比较,为后面的绕过留下了余地)
didUserLikelylCheat和verifyAccount两个函数判定过了就能绕过了
这里的核心原理是如果secQuestion0不存在就会跳过检验,所以其实如果包含secQuestion参数名的数量正确之后,剩下的检验就会全部跳过,所以只要构造两个包含secQuestion的参数就行了
靶场
Insecure Login
靶场
这里就是简单的拦截请求再填入
审计
login部分,主要是js
function submit_secret_credentials() {var xhttp = new XMLHttpRequest();xhttp['open']('POST', 'InsecureLogin/login', true);//sending the request is obfuscated, to descourage js readingvar _0xb7f9=["\x43\x61\x70\x74\x61\x69\x6E\x4A\x61\x63\x6B","\x42\x6C\x61\x63\x6B\x50\x65\x61\x72\x6C","\x73\x74\x72\x69\x6E\x67\x69\x66\x79","\x73\x65\x6E\x64"];xhttp[_0xb7f9[3]](JSON[_0xb7f9[2]]({username:_0xb7f9[0],password:_0xb7f9[1]}))
}--->
function submit_secret_credentials() {var xhttp = new XMLHttpRequest();xhttp.open('POST', 'InsecureLogin/login', true);// 发送混淆后的请求xhttp.send(JSON.stringify({username: "CaptianJack", password: "BlackPearl"}))
}
使用了十六进制编码的字符串数组 _0xb7f9,但请求包中仍然是明文
JWT
理论部分:
技术本质
JWT是一种基于RFC 7519标准的轻量级安全凭证格式,采用紧凑的URL-safe字符串形式传输经过数字签名的JSON数据。其核心价值在于通过密码学签名机制实现了信息自包含(self-contained)和防篡改(tamper-proof)的特性。
安全机制
- 数字签名保障:支持两种签名方式
- 对称加密:使用HMAC算法(HS256/HS384/HS512)+ 服务端密钥
- 非对称加密:使用RSA/ECDSA算法(RS256/ES256等)+ 公私钥体系
基本结构
Header.Payload.Signature
- Header (头部):包含令牌类型和签名算法
{"alg": "HS256","typ": "JWT"
}
- Payload(负载):包含声明(claims),有三种类型:
- Registered claims:预定义声明如 iss(签发者), exp(过期时间), sub(主题)等
- Public claims:公开定义的声明
- Private claims:自定义声明
{"sub": "1234567890","name": "John Doe","admin": true
}
- Signature (签名):用于验证消息完整性,创建方式:
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
令牌
- 两种令牌类型
- 访问令牌(Access):用于向服务器发起API请求,有效期较短
- 刷新令牌(Refresh):用于获取新的访问令牌,有效期较长
获取jwt的流程
在客户端和服务端之间
JWT Claim Misuse
JWT Claim Misuse(JWT声明滥用)指的是对JSON Web Token中的声明(payload)部分进行不当或未经授权的操纵行为。
- 未经授权的声明添加
攻击者尝试添加未授权的声明以获取不应有的权限
例如:普通用户添加管理员权限声明
- 声明篡改
修改现有声明的值来操纵身份或权限
例如:修改"user_id"来冒充其他用户
- 过度声明
添加大量不必要或虚假声明
目的可能是增大令牌体积影响系统性能
- 过期时间篡改
修改"exp"声明延长令牌有效期
使攻击者能够维持超出预期的访问权限
- 重放攻击
重复使用已过期的有效JWT
用于冒充原始用户或利用有时限的功能
- 关键声明操纵
如操纵"kid"(密钥ID)声明
可能使服务器使用错误的密钥进行验证
JKU相关漏洞
JKU是JWT规范的一部分,允许通过URL动态获取验证签名所需的公钥。
- 漏洞原理
- 当JWT使用弱密钥签名,且JKU指向外部公钥时
- 攻击者可制作恶意JWT并控制验证公钥的来源
- 攻击步骤
- 识别JKU端点
- 制作恶意JWT
- 使用自有私钥签名
- 发送给服务器
- 服务器从攻击者控制的URL获取公钥验证
- 攻击成功
- 防御措施
- 白名单机制
- 静态密钥
- 增强验证
- 监控审计
- 安全测试
实践部分
jwt_decode
靶场
简单的解密之后提取用户名就行了
审计
判断user值是否匹配
jwt_vote
审计
先找到前端触发vote函数
getvoting函数
- 通过$.get("JWT/votings")从服务端获取投票项列表
- 使用字符串替换方式构建HTML模板
- votesList是前端投票面板的id值
vote函数
- 验证用户是否为"Guest"
- 通过$.ajax POST请求发送到JWT/votings/{title}端点
(Claims)设置
- setIssuedAt设置签发时间(10天后),设置admin和user
jwt签名
- 设置声明(claims)
- 使用密钥签名, 对JWT_PASSWORD进行HS512对称加密
其中:JWT_PASSWORD = TextCodec.BASE64.encode("victory");//注意密钥
cookie设置
- 将jwt存入cookie
生成claims,jwt,cookie,并返回200 OK
若validUsers.contains(user)判断没过,则清空Cookie值,并返回401未授权
从cookie中获取access_token,如果存在
try {Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD) // 使用密钥验证签名.parse(accessToken); // 解析 JWTClaims claims = (Claims) jwt.getBody(); // 获取 payload(声明)
核心
boolean isAdmin = Boolean.valueOf(String.valueOf(claims.get("admin")));
if (!isAdmin) {return failed(this).feedback("jwt-only-admin").build();}
else {votes.values().forEach(vote -> vote.reset());return success(this).build();
}
从JWT的payload中获取admin字段,如果为true则成功
靶场
在删除时抓包
对token解码
把admin改成true,再输入密钥为上文提到的victory,再放回去
成功
JWT 破解
审计
成功条件是WEBGOAT_USER.equalsIgnoreCase(user)
但是注意到这里的有效时间只有一分钟,要不就快速的,要不就要最后把时间戳换一下
靶场
用脚本爆破密钥
我这里是business
重新替换之后编码输入就行
refreshing token
审计
- 检查用户是否是Tom
- 如果是Tom,额外检查JWT头部的算法是否为none
靶场
让我们让Tom付钱,把token改成tom的
题目给了一个文件,解码发现是Tom的token
拦截一个请求,发现里面有jerry的token
第一反应就是替换,但是肯定没这么简单
果然,认证过期了。
改时间戳,欸嘿,成功了!
JWTHeaderJKUEndpoint
审计
重点在这里生成了一个JWT
Jwt jwt =Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {@Overridepublic byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {final String kid = (String) header.get("kid");try (var connection = dataSource.getConnection()) {ResultSet rs =connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'");while (rs.next()) {return TextCodec.BASE64.decode(rs.getString(1));}} catch (SQLException e) {errorMessage[0] = e.getMessage();}return null;}}).parseClaimsJws(token);
- @Override public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims):重写方法,根据JWT头部和声明返回签名密钥的字节数组
- kid从jwt头获得id字段
- 执行SQL查询,根据kid从jwt_keys表获取对应的密钥
- nTextCodec.BASE64.decode(rs.getString(1));返回base64解码后的密钥字节数组
- .parseClaimsJws(token);使用配置的解析器解析并验证JWT
靶场
这里是Jerry想删掉Tom的账户,那可能就要构造一个Tom的token
拦截一个请求把它里面的token解码之后得到
讲密钥设为1并在kid中构造联合注入
在webwolf里搞了很久,发现自己拦截没开,哈哈哈
Password reset
Question
审计
就是获取用户名和答案,再根据用户名对密码进行比对
可以对密码进行爆破
SecurityQuestion
前面部分设置了一些问题和答案
- answer.isPresent():检查是否存在有效答案
这里设置了一个triedQuestion类
就是简单的计数和判断
ResetLink
- String resetLink = UUID.randomUUID().toString();:使用UUID生成随机重置令牌
- 将令牌存储在全局集合中
- 获取主机头信息
- 判断 host 当中是否存在 WebWlof 服务对应的端口与 Host
抓包改一下host
在wobwolf中找到对应请并访问