身份认证缺陷
Authentication Bypasses
审计
创建AccountVerificationHelper实例,用于处理账户验证逻辑
parseSecQuestions函数的作用是从请求体中遍历参数名,找到包含secQuestion的参数,将其值存入Map中并返回
这里直接把AccountVerificationHelper整个分析一下
/** Created by appsec on 7/18/17. */
public class AccountVerificationHelper {
// simulating database storage of verification credentials
private 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' code
public 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 submitted
if (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;
}
// else
return 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 reading
var _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); // 解析 JWT
Claims 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() {
@Override
public 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中找到对应请并访问