CodeQL的自動化程式碼審計之路(中篇)

盛邦安全發表於2022-11-22

0x01 前言


在上一篇文章中,我們已經瞭解了關於CodeQL的基本語法,從實際案例角度來體驗了CodeQL在程式碼審計中的作用。從這篇文章開始,我們將開始真正打造基於CodeQL的自動化程式碼審計工具,由於這僅僅是來自於個人興趣的研究,並非來自成熟專案,所以在文章中可能缺陷,各位大佬如果有更好的意見建議,請私信。


CodeQL的程式碼審計整體過程可以分成兩個部分,如圖1.1所示,分別是從原始碼解析成CodeQL資料庫和從資料庫中查詢出安全隱患。本次分享主要關注第二階段的內容,假設我們已經有了CodeQL資料庫之後,下一步基於資料庫的自動化查詢。

圖片

圖1.1 CodeQL程式碼審計過程階段


為什麼我們最終結果得到的是安全隱患,而不是漏洞呢?這是因為CodeQL並不是萬能的,它只能幫我們找到可能的安全隱患,而不能一定確認漏洞存在,這在後面的文章中會說到原因。


當前的自動化程式碼審計工具將採用python3開發,針對的目標語言是java,後續如果有時間,也會陸續支撐其他語言。目前程式碼還不是特別完善,後期後繼續對程式碼進行最佳化,見github地址:https://github.com/webraybtl/codeql



0x02 工具設計


基於CodeQL的自動化程式碼審計工具流程其實和傳統的漏洞掃描工具相似,所以我們還是按照傳統漏掃的思路來設計工具。關於第一階段原始碼轉化為資料庫的部分在下一篇文章來詳述,這裡還是隻關注第二階段資料庫查詢的內容,如圖2.1所示。


圖片

圖2.1 自動化工具設計流程


其實從流程中可以看出,工具的主要功能是基於ql外掛的遍歷,對外掛結果的格式化輸出。首先需要解決的問題是關於ql外掛來源的問題,在上一篇文章中,我們有提到CodeQL官方給我們提供了很多測試用的demo例項https://github.com/github/codeql/tree/main/java/ql/src/experimental/Security/CWE。


官方按照CWE提供了多個不同型別的ql外掛,部分外掛是可以直接來用的,但是有的外掛涉及到自定義qll庫,需要進行一定的轉化才能使用,如圖2.2所以,FilePathInjection.ql指令碼就是典型的有自定義庫的指令碼。

圖片

圖2.2 使用了qll自定義庫的ql指令碼


在我們設計的自動化工具中,為了方便會只查詢單個ql指令碼,需要把ql指令碼中呼叫的qll庫進行轉化。轉化的方式是顯示的把qll庫中定義的類和謂詞直接定義到ql指令碼中,我已經把官方提供的全部指令碼都轉化了一遍,後續會將完整的程式碼分享到github。


為了方便統一的對結果進行格式化輸出,我們期待每一個ql檔案最終返回的結果都是統一格式,所以還需要對每個ql檔案最終的返回結果進行約束,典型的demo如下所示。其中select後面的值是ql指令碼最終返回的資料。


          from DataFlow::PathNode source, DataFlow::PathNode sink, BeanShellInjectionConfig conf
          where conf.hasFlowPath(source, sink)
          select source.toString(),source.getNode().getEnclosingCallable(),source.getNode().getEnclosingCallable().getFile().getAbsolutePath(),
          sink.toString(),source.getNode().getEnclosingCallable(), sink.getNode().getEnclosingCallable().getFile().getAbsolutePath(), "BeanShell injection"



          表2.1 ql指令碼輸出規範約束

          圖片


          由於CodeQL官方並不對引擎開源,我們只能直接使用官方編譯好的版本,官方編譯好的引擎並不支援python這些語言,只能從命令列進行呼叫,如圖2.3所示。其中-d引數用於表示待查詢的資料庫路徑,最後跟的是要查詢的ql指令碼路徑。

          圖片

          圖2.3 透過命令列呼叫codeql查詢


          由於CodeQL每次查詢都需要使用ql指令碼檔案路徑,如果每次查詢都需要先生成一個檔案,然後查詢結束之後再刪除檔案,程式碼顯得怪怪的。好在python給我們提供了tempfile庫,可以稍微優雅的解決這個問題,如圖2.4所示。這是一段我專案中檢查環境是否準備好了的程式碼,透過tempfile生成臨時的ql指令碼,臨時指令碼在執行結束之後會自動自動刪除。

          圖片

          圖2.4 使用tempfile生成臨時檔案查詢codeql  


          本來想自己封裝了一個類來呼叫呼叫CodeQL執行,但是突然看到網上已經有大佬寫好了一個相應的類https://github.com/AlexAltea/codeql-python,其實現思路和我之前的想法差不多,本質上還是從命令列呼叫的CodeQL。然而我直接執行大佬的程式碼卻執行不成功,主要原因還是在於生成的臨時檔案必須要在ql sdk所在測試路徑,路徑下必須有正確配置的qlpack.yml檔案。所以我在原始碼的基礎上修改了一下,主要是固定sdk路徑為配置好的路徑。


          這之後我們一個簡易的基於CodeQL的自動化程式碼審計工具雛型就差不多了,後續會陸續在這個框架的基礎上最佳化功能。



          0x03 外掛最佳化


          官方雖然提供了大約59個java的ql查詢外掛,但是實際上還遠不能滿足我們的需求,我們希望有更多的白帽子參與進來提供更多的ql查詢外掛。當前階段,我按照自己日常漏洞挖掘過程補充一些ql查詢外掛,如下所示,相關外掛均在plugins/java_ext目錄。


          表3.1 新增的Java常見漏洞查詢ql指令碼

          圖片


          本次新增只是一個開端,並不能覆蓋全部,自知還相差很遠。但是不斷的最佳化,總歸會有好的效果。由於部分小夥伴對與CodeQL的語法瞭解甚少,我們用一個簡單的指令碼Unserialze.ql來說明完整的CodeQL指令碼的寫法。


          反序列化漏洞是java中常見的漏洞,典型的漏洞程式碼寫法如下。這是一段從某應用中提取的真實漏洞的部分程式碼。

            protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {    
               ObjectInputStream objin = new ObjectInputStream(request.getInputStream()); //這裡是獲取使用者輸入
               
               response.setContentType("application/x-download");
               
               ServletOutputStream out = response.getOutputStream();

               try {        
                   String dsName = (String)objin.readObject(); //這裡是反序列化的點
                   
                   System.out.println(dsName);
               
               } catch (Exception var11) {
                   
                   var11.printStackTrace();
               
               }

               out.close();    
               objin.close();
            }


            其中最關鍵的是使用者可控的source點為request.getInputStream(),最後的危險操作sink點為objin.readObject()。也就是說外部傳入的postdata直接進行了反序列化操作,則可能導致反序列化漏洞。對於CodeQL中,可以編寫對應的查詢指令碼如下。

              import java
              import semmle.code.java.dataflow.FlowSources
              import semmle.code.java.dataflow.TaintTracking
              import semmle.code.java.dataflow.DataFlow
              class UnserializeSink extends DataFlow::Node {    
              UnserializeSink(){        
              exists(MethodAccess ma,Class c | ma.getMethod().hasName("readObject") and                
              ma.getQualifier().getType() = c and                
              c.getASupertype*().hasQualifiedName("java.io", "InputStream") and                
              this.asExpr() = ma        
              )    
              }}
              class UnserializeSanitizer extends DataFlow::Node {    
              UnserializeSanitizer() {      
              this.getType() instanceof BoxedType or this.getType() instanceof PrimitiveType    
              }  
              }
              class JavaUnserialize extends TaintTracking::Configuration {    
              JavaUnserialize() { this = "Java Unsearialize" }      
              override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
                 override predicate isSink(DataFlow::Node sink) { sink instanceof UnserializeSink }
                 override predicate isSanitizer(DataFlow::Node node) { node instanceof UnserializeSanitizer }    
                 }  
              from DataFlow::PathNode source, DataFlow::PathNode sink, JavaUnserialize confwhere  conf.hasFlowPath(source, sink)select source.toString(),source.getNode().getEnclosingCallable(),source.getNode().getEnclosingCallable().getFile().getAbsolutePath(),      
              sink.toString(),source.getNode().getEnclosingCallable(), sink.getNode().getEnclosingCallable().getFile().getAbsolutePath(), "Potential JAVA Unserialize Vulnerability"


              其中source點直接使用CodeQL預定義的類RemoteFlowSource,而sink點則是透過下面的程式碼實現。判斷的邏輯是存在一個方法名為readObject的呼叫,並且呼叫的主體繼承自java.io.InputStream類。注意這些說的是反序列化漏洞,不涉及利用鏈,不一定就真的能導致RCE效果。

                exists(MethodAccess ma,Class c | ma.getMethod().hasName("readObject") and                 
                ma.getQualifier().getType() = c and                 
                c.getASupertype*().hasQualifiedName("java.io", "InputStream") and                 
                this.asExpr() = ma        
                )


                單獨執行對應的指令碼,則可以發現程式中可能存在的反序列化漏洞,如圖3.1所示。

                圖片

                圖3.1 單獨執行Unserialize.ql指令碼的效果


                其他指令碼就不再依次講解,如果有小夥伴感興趣,非常期待小夥伴能為我們提供外掛。如果小夥伴不知道怎麼編寫CodeQL指令碼,可以把有漏洞的程式碼邏輯給私信我,由我來轉化為CodeQL外掛。


                0x04 工具使用


                回到工具本身,當前完整的程式碼我已經放在github,使用方式如下所示。


                使用之前應該首先安裝CodeQL,並配置config/config.ini,其中最關鍵的是配置臨時生成的ql指令碼儲存的路徑qlpath,如圖4.1所示。確保qlpath當前目錄下面有配置檔案qlpack.yml。如果使用的過程中有問題,建議把debug配置為on。

                圖片

                圖4.1 專案配置檔案


                執行python3 main.py -h,如圖4.2所示。

                其中引數-d代表資料庫檔案地址,必填。

                引數-s代表是否跳過環境檢查,不填預設為false,首次使用建議不跳過環境檢測。

                圖片

                圖4.2 專案支援的引數列表


                執行python3 main.py -d /Users/xxxx/CodeQL/databases/RuoYI/,透過若依原始碼來掩飾效果。

                圖片

                圖4.3 使用工具獲取的掃描結果

                最終的掃描結果是以csv檔案儲存在out/result/目錄,開啟相應的結果,如圖4.4所示。

                圖片

                圖4.4 對結果進行格式化輸出到CSV檔案中 


                關於結果的分析我們在上一篇文章中已經涉及到一些,這裡就不再分析結論。


                0x05 工具不足


                在圖4.4的結果中,有很多FilePathInjection外掛掃描的結果,其中記過都很相似,以其中之一為例,我們基於檔案的source和sink定位其中的問題。


                定位到source檔案和方法,com.ruoyi.web.controller.system. SysProfileController類的updateAvatar方法,如圖5.1所示。

                圖片

                圖5.1 Source類與方法


                繼續跟蹤upload方法,就可以到sink點,如圖5.2所示。

                圖片

                圖5.2 Sink的類與方法


                這裡其實RuoYI已經對上傳檔案的副檔名進行了限制,然後CodeQL仍然把這裡識別為漏洞,這是典型的誤報行為,而這也是CodeQL的程式碼審計工具中最難解決的一個問題。


                CodeQL可以跟蹤Source和Sink流,但是畢竟仍然只是靜態程式碼審計工具,無法自動解析程式碼中的一些過濾操作,導致可能會出現誤報。而這也是文章開頭提到的CodeQL只能作為輔助工具發現安全隱患,不能確定是否一定存在漏洞的原因。



                0x06 結論


                距離自動化的程式碼審計工具,我們仍然有很長的路要走,如果小夥伴能提供一些可用的ql外掛或者提供有漏洞的程式碼樣本由我們來編寫ql外掛,我們都將非常感激。


                後續我們會陸續豐富工具的功能,特別是解決前一階段生成資料庫的問題。


                原文連結

                相關文章