天天看點

初探檢測未授權與越權問題

目前階段研究結果來看,通過codeql靜态代碼掃描可以發現部分越權問題,架構類的未授權與接口越權問題也可以通過codeql來覆寫,但非架構授權機制下的未授權漏洞就無法通過靜态代碼掃描進行檢測。通過IAST/RASP可以覆寫非架構未授權通路問題,但需要建構相應的測試用例,似乎與人工測試沒差别了,是以建構此類工具覆寫非架構問題就好像沒必要。

1. 現狀

2. 未授權與越權問題

2.1. 定義問題

  1. 未授權漏洞:沒有登入憑證也能通路到敏感接口
  2. 越權漏洞中,具體 “權” 的定義:接口權限:基于接口路徑與使用者角色的權限管控 (類似垂直越權)資料權限:根據憑證ID進行權限管控、根據資料狀态進行權限管控(優惠券、簽到禮品)

2.2. 問題模型

權限管理中,可以抽離出三種要素,分别是 使用者 、實體、政策

關于邏輯漏洞的未授權通路與越權問題,我們嘗試通過以下模型進行分析,後面再給出目前的解決方案、解決思路。

圖中說到的DAO方法執行也存在資料不來源使用者而來源目前憑證的情況,這種情況我們可不認為有越權問題(在自動檢測中)。

初探檢測未授權與越權問題

下面我們先簡單了解一下 架構是如何進行授權通路、路由鑒權 ,自定義Filter如何授權通路 及 Controller這裡的越權校驗。

2.3. 架構路由授權

下圖為shiro架構的路由通路配置,其中 map 的 value 配置為 anno 的表示該路徑可未授權通路

初探檢測未授權與越權問題

2.4. 架構接口鑒權

org.apache.shiro.authz.annotation.RequiresPermissions

背景接口方法這裡通過配置注解設定了接口權限辨別 system:notice:list

初探檢測未授權與越權問題

使用者已登入的情況下,根據情況設定其擁有的接口權限:

2.5. 非架構授權

開發人員編寫了一個Filter對使用者進行授權問題,可以看到,先排除了一些無需授權通路的路徑;随後通過JWT Token鑒别使用者是否登入,如果未登入則傳回401.

2.6. 越權校驗

該圖中的背景接口功能未設定使用者資訊,實際上執行了如下這樣一個SQL語句

1
           
update user set nickName='xxx',pic='xxx' where userId='xxx'
           

如果userId是使用者可控的,則存在越權問題

初探檢測未授權與越權問題

這裡的修改密碼 updateById 的Bean對象User的userId 來源于userService.getOne的查詢結果,而該查詢的輸入nickName可被使用者控制,導緻了該接口存在越權問題。

2.7. 總結

在filter中,通過RASP/IAST 建構代碼流程圖檢測漏洞

  • 未授權通路漏洞:需要建構的測試用例為 已登入使用者通路接口、未登入使用者通路接口
  • 接口越權問題:需要建構的測試用例為 不同角色通路同一接口

在controller中,通過codeql 來檢測漏洞

  • 越權(根據使用者ID進行權限管控):檢測業務操作CRUD的入參是否與憑證ID有資料關聯

3. 檢測越權漏洞

首先需要認識到,我們難以或無法檢測一些邏輯缺陷的問題,如架構或非架構中對未授權通路路由的鑒别代碼邏輯存在缺陷導緻的未授權通路,具體一點,Filter中判斷HTTP URL路徑含有/image則放行,但攻擊者通過/test?foo=/image進行繞過。

在 第2章 提到的問題中,架構類的情況通過codeql檢測的話,理論上會相對簡單,最複雜的情況是 非架構授權 問題 ,越權檢測也比較複雜且意義重大,本章節我們講述一下目前的思路方案。

3.1. 思路

通過codeql白盒工具檢測越權問題,我們實踐思路是,檢查 使用者的輸入 到 業務方法 的資料流是否經過可靠的檢查。

業務方法:DAO方法的執行(SQL執行)、資料緩存操作(Redis Set)

可靠的檢查有兩種方式,分别為 :

1、資料流代碼節點上,有調用“可信方法”,即來自不可靠的輸入被處理為了可靠的資料,又再作為輸入,如 String userId = SecurityUtlis.getUser().getUserID()

​ 2、資料流代碼節中,調用可信方法對,存在某一節點代碼通過可信方法對資料進行校驗,不符合預期則抛異常,如if(!Object.equals(userId,SecurityUtlis.getUser().getUserID())){ throw new Exception("error id");}

可信方法:

​ 可将不安全的輸入轉換為安全的輸出、或是檢測不安全的輸入與已緩存的安全的資料(使用者憑證)是否符合。

​ 基礎可信方法:

​ shiro/spring-security 安全架構擷取憑證的方法

​ redis的get、ThreadLocal.get(緩存了憑證)、其他未發掘的情況

​ 通過基礎可信方法,遞歸檢測源碼,獲得所有可信方法:将可信方法作為傳回值的、或是參數資料與可信方法有關聯并抛異常的

1
2
3
4
5
           
// 不安全的輸入 -> 安全的輸出 , request是不安全的,通過JWT擷取到安全的authToken
// getUserNameFromToken 是可信方法
String authHeader = request.getHeader(this.tokenHeader);
String authToken = authHeader.substring(this.tokenHead.length());
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
           
1
2
3
4
5
6
7
           
// 檢測不安全的輸入與已緩存的安全的資料是否符合
// getCurrentMember 是可信方法
UmsMember member = memberService.getCurrentMember();
OmsOrder order = orderMapper.selectByPrimaryKey(orderId);
if(!member.getId().equals(order.getMemberId())){
    Asserts.fail("不能确認他人訂單!");
}
           

執行資料操作的DAO方法有,CRUD操作,關于DAO方法有以下要點:

​ 是否傳回前端:SELECT操作,僅查詢結果的資料傳回給到前端時,才有必要判斷是否越權

​ 是否敏感資料:如商品資訊的查詢不需要檢測是否越權,這些接口需要标記出來或是作為誤報人工忽略

​ DAO方法參數是否SQL限制:DAO方法的參數為多個或為Bean對象時,需要忽略非where查詢的字段,如對于userService.updateById(user),我們隻需要檢查user對象的userId來源是否可靠即可

3.2. codeql實踐

3.2.1. 擷取可信方法

通過codeql的遞歸文法,我們在定義基礎的可信方法後,遞歸這一結果,進而找到項目源碼中所有預期的方法:

有如下查詢demo結果,遞歸過程如下 ThreadLocal.get -> AuthUserContext.get -> SecurityUtils.getSysUser()

遞歸擷取可信方法 demo2:

3.2.2. 擷取相關DAO方法

可以通過codeql的xml解析子產品去解析項目源碼中的mapper.xml檔案進而擷取DAO方法。

目前為了簡便,我先用Java代碼寫了一個mapper xml解析代碼:

定義相關DAO方法作為sink點:

初探檢測未授權與越權問題

3.2.3. 查詢是否越權

在定義了DAO方法污點、可信方法 等謂詞(邏輯判斷詞)的情況下,基于我們 3.1思路 去編寫codeql查詢規則,主要就是在 HTTP入口->DAO查詢 這條資料鍊上查詢相關節點資料是否經過可信方法的處理。

初探檢測未授權與越權問題

3.2.4. DAO CUD案例

使用codeql查詢不存在越權的 DAO CUD情況,該取消訂單業務接口中,通過調用 service 方法 cancelOrders 進而調用DAO方法執行SQL進行訂單取消,可以看到 order 對象的order.userId已經經過 !Object.equal(order.getUserId(), userId) 語句的校驗

初探檢測未授權與越權問題

codeql查詢存在越權的DAO CUD執行

初探檢測未授權與越權問題

3.2.5. DAO SELECT案例

使用codeql查詢不存在越權的DAO SELECT情況:

初探檢測未授權與越權問題

使用codeql查詢存在越權情況的 DAO Select方法執行:

初探檢測未授權與越權問題

3.2.6. 問題總結

1、DAO方法擷取:還需要适配mybatis插件,如 baomidou 中可以直接 調用 servie 來進行DAO查詢(通過注解Bean指明表名字段名插件則自動動态生成SQL查詢語句);以及DAO接口繼承插件的BaseMapper,污點則為BaseMapper的方法

2、DAO方法參數跟蹤:目前還沒根據SQL WHERE/IN判斷的具體字段 來決定跟蹤 DAO方法入參的字段 ,一方面如果項目SQL傳參比較複雜或使用了Mybatis插件,我們通過靜态的方式無法擷取完整的SQL語句;另外就是這個方案也比較複雜,還在考量中。可能做成,隻要DAO查詢中某個參數經過可靠檢查了,就忽略掉其他參數是否存在越權問題,當然,這個做法沒有把解決問題做到極緻。

3、流中繼中斷問題:Codeql去查詢污點(DAO方法)到源(HTTP接口)的這個過程,如果開發者使用了三方庫,我們還需要編寫額外的codeql謂詞進行流中繼,這也是codeql規則編寫的通點

4、誤報問題:對于背景子產品來說,基本不進行此類權限檢測,是以本節的方法不使用于背景子產品代碼;此外,某些資料是操作是不敏感的,如商品資訊的查詢,這也是誤報之一。

4. 檢測未授權與接口越權

4.1. 架構問題

通過codeql去識别接口是否有 架構路由授權、架構接口鑒權 ,從目前看到的shiro/spring-security架構來說,codeql具備可行性。具體的demo還未實踐,後面這塊就不繼續關注了。

4.2. 非架構未授權漏洞

自動化檢測中,對于自定義Filter/Interceptor, 需要結合IAST/RASP進行處理,但這需要建構相應的測試資料,這似乎又和手工測試同一個次元了,這點讓人糾結。

可通過RASP建構一次請求中某Filter的函數調用圖,起初想Hook所有class,但是出現位元組碼錯誤,就粗略寫了個demo稍微進行測試,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
           
String authFilterId = "com.test.filter.AuthFilter#doFilter(Ljavax/servlet/ServletRequest;" +
               "Ljavax/servlet/ServletResponse;Ljavax/servlet/FilterChain;)V";
   // hook哪些包下的代碼
   private final String[] includePkgs = new String[]{"com.test"};
   // 多叉樹
   ThreadLocal<TreeNode> tree = new ThreadLocal<>();
   // 修改被hook的方法的位元組碼,将 before 添加到調用前
   public void before(String methodId, Object[] args) throws Exception {
           if (authFilterId.equals(methodId)) {
               if (tree.get() != null) {
                   throw new Exception("遞歸問題未處理!");
               }
               TreeNode root = new TreeNode(methodId,null);
               tree.set(root);
               root.setCurrent(root);

           } else {
               if (tree.get() == null) {
                   return;
               }
               TreeNode root = tree.get();
               TreeNode parent = root.getCurrent();
               TreeNode node = new TreeNode(methodId,parent);
               parent.addChild(node);
               root.setCurrent(node);
           }

   	}

// 修改被hook的方法的位元組碼,将 after 添加到調用後或抛異常後
   public void after(String methodId, Object[] args, Object ret, Exception exception) {

       if (authFilterId.equals(methodId)) {
           TreeNode root = tree.get();
           if (root == null) {
               return;
           }
           System.out.println("show call graph: ");
           TreeNode.PrettyPrint(root);
           System.out.println("------------------------------");
           tree.set(null);

       }else{
           if (tree.get() == null) {
               return;
           }
           TreeNode root = tree.get();
           root.setCurrent(root.getCurrent().getParent());
       }
   }
           
from https://turn1tup.github.io/