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中找到對應請并訪問