58集团白盒代码审计系统建设实践2:深入理解SAST

 

背景

源代码安全检测是安全开发流程(SDL)中非常重要的一部分,在58集团的CI/CD流程中每天有数千次量级的构建及发布,白盒检测的自动化能力显得极为重要。企业级的白盒代码审计系统就不仅仅面临漏洞发现的需求,也需要适应企业CI/CD流程。由于58集团大部分业务使用自研的Java框架,本系列文章会重点介绍我们在Java白盒能力建设过程中的实践。

本文是58白盒扫描建设之路系列文章第二篇,主要介绍SAST的一些技术原理及应用、CodeQL的官方教程中文翻译及实践用法

 

技术原理

理解AST抽象语法树

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。

一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树,然后从分析树生成AST。一旦AST被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

抽象语法树是程序源代码结构的树状表示。程序源代码经过词法分析器(Lexer)得到各种不同种类的单词(Token),再由语法分析器(Parser)分析和语法检查后得到抽象语法树(AST)。抽象语法树的根节点表示整个程序,内部节点是抽象语法结构或者单词。AST的核心在于它能与输入源代码中的各个语法元素一一对应

如 以下的C语言代码如图所示

while (i<n){
sum + = A[i++];
}

Java AST抽象语法树

Spoon

Spoon是一个开放源代码库,用于分析,重写,转换,翻译Java源代码。它解析源文件以构建具有强大分析和转换API的精心设计的AST。它完全支持Java 11、12、13、14之前的现代Java版本。Spoon是Inria的一个官方开源项目,并且是OW2开源联盟的成员。

git地址:https://github.com/INRIA/spoon

JAVA项目进行AST分析

可以通过自行编译Spoon源代码或者去https://search.maven.org/artifact/fr.inria.gforge.spoon/spoon-core maven仓库下载已经编译好的Spoon的jar包

通过以下命令进行GUI的语法树分析

java -cp /Users/fangzhao/IDEA/spoon/target/spoon-core-8.4.0-SNAPSHOT-jar-with-dependencies.jar spoon.Launcher -i /Users/fangzhao/IDEA/springboot-mybatis/src/main/java/cn/no7player/controller/HelloController.java  --gui

对 Spring demo的 hello.java进行AST分析

package cn.no7player.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class HelloController {

    @RequestMapping("/hello")
    public String greeting(@RequestParam(value="name", required=false, defaultValue="World") String name, Model model) {
        model.addAttribute("name", name);
        return "hello";
    }

}

分析结果

编程语言可以具有不同的元模型。抽象语法树(AST)或模型是元模型的实例。每个元模型(因此每个AST)或多或少都取决于手头的任务。例如,已针对Sun编译器(javac)的Java元模型进行了设计和优化,以将其编译为字节码,而Eclipse IDE(JDT)的Java元模型的主要目的是在一个软件中支持软件开发的不同任务。集成方式(代码完成,编译错误的快速修复,调试等)。

与基于编译器的AST(例如来自javac)不同,Java的Spoon元模型被设计为普通Java开发人员易于理解,因此他们可以编写自己的程序分析和转换。 Spoon元模型是完整的,因为它包含派生可编译和可执行Java程序所需的所有信息(因此包含批注,泛型和方法体)。

Spoon元模型可以分为三个部分。

结构部分包含程序元素的声明,例如接口,类,变量,方法,注释和枚举声明。

代码部分包含可执行的Java代码,例如在方法主体中找到的代码。

参考部分对对程序元素的引用(例如,对类型的引用)进行建模。

如图所示,所有元素都继承自CtElement(javadoc),后者声明一个父元素,该父元素表示源文件中的包含关系。例如,方法节点的父级是类节点。所有名称均以“ CT”为前缀,表示“编译时”。

从Spoon 6.1.0开始,Spoon元模型包含CtModule元素表示Java 9中的模块,以及CtModuleDirective表示模块的不同指令。

提示:模型的根不再是未命名的包,而是未命名的模块。

SAST静态扫描基本原理

SAST原理

  • 通过调用语言的编译器或者解释器把前端的语言代码(如JAVA,C/C++源代码)转换成中间代码(IR,intermediaterepresentation),将其源代码之间的调用关系、执行环境、上下文等分析清楚。
  • 语义分析:分析程序中不安全的函数,方法的使用的安全问题。
  • 数据流分析:跟踪,记录并分析程序中的数据传递过程所产生的安全问题。
  • 控制流分析:分析程序特定时间,状态下执行操作指令的安全问题。
  • 配置分析:分析项目配置文件中的敏感信息和配置缺失的安全问题。
  • 结构分析:分析程序上下文环境,结构中的安全问题。
  • 结合2~6步的结果,匹配所有规则库中的漏洞特征,一旦发现漏洞就抓取出来。
  • 最后形成包含详细漏洞信息的漏洞检测报告,包括漏洞的具体代码行数以及漏洞修复的建议。

 

简单理解污点分析技术

当我们通过AST技术拿到了源码的抽象语法树,并将其数据格式化存储之后,需要一套高效的算法对漏洞模型进行匹配,在漏洞模型的建立上我们需要引入污点分析技术来对漏洞进行定义

污点分析定义

污点分析可以抽象成一个三元组<sources,sinks,sanitizers>的形式,其中,source 即污点源,代表直接引入不受信任的数据或者机密数据到系统中;sink即污点汇聚点,代表直接产生安全敏感操作(违反数据完整性)或者泄露隐私数据到外界(违反数据保密性);sanitizer即无害处理,代表通过数据加密或者移除危害操作等手段使数据传播不再对软件系统的信息安全产生危害.污点分析就是分析程序中由污点源引入的数据是否能够不经无害处理,而直接传播到污点汇聚点.如果不能,说明系统是信息流安全的;否则,说明系统产生了隐私数据泄露或危险数据操作等安全问题.

简单的说:污点分析是默认不信任本地/外部输入,将本地及外部输入的控制/数据流过程进行分析,如果没有经过无害化处理,即认为存在漏洞的漏洞模型

污点分析的处理过程

污点分析的处理过程可以分为三个阶段

  • 识别污点源和汇聚点;
  • 污点传播分析;
  • 无害处理.

识别污点源

识别污点源和污点汇聚点是污点分析的前提.目前,在不同的应用程序中识别污点源和汇聚点的方法各不

相同.缺乏通用方法的原因一方面来自系统模型、编程语言之间的差异.另一方面,污点分析关注的安全漏洞类

型不同,也会导致对污点源和污点汇聚点的收集方法迥异.表 1 所示为在 Web 应用程序漏洞检测中的污点源示

例[29],它们是 Web 框架中关键对象的属性.

现有的识别污点源和汇聚点的方法可以大致分成 3 类:

  • 使用启发式的策略进行标记,例如把来自程序外部输入的数据统称为“污点”数据,保守地认为这些数据有可能包含恶意的攻击数据(如 PHP Aspis);
  • 根据具体应用程序调用的 API 或者重要的数据类型,手工标记源和汇聚点(如 DroidSafe);
  • 使用统计或机器学习技术自动地识别和标记污点源及汇聚点.

污点传播分析

污点传播分析就是分析污点标记数据在程序中的传播途径.按照分析过程中关注的程序依赖关系的不同, 可以将污点传播分析分为显式流分析和隐式流分析.

显示流分析

污点传播分析中的显式流分析就是分析污点标记如何随程序中变量之间的数据依赖关系传播

以图 3 所 示的程序为例,变量 a 和 b 被预定义的污点源函数 source 标记为污点源.假设 a 和 b 被赋予的污点标记分别为taint_a 和 taint_b.由于第 5 行的变量 x 直接数据依赖于变量 a,第 6 行的变量 y 直接数据依赖于变量 b,显式流分析会分别将污点标记 taint_a 和 taint_b 传播给第 5 行的变量 x 和第 6 行的变量 y.又由于 x 和 y 分别可以到达第 7 行和第 8 行的污点汇聚点(用预定义的污点汇聚点函数 sink 标识),图 3 所示的代码存在信息泄漏的问题.我们将在后面具体介绍目前污点传播分析中显式流分析面临的主要挑战和解决方法.

隐式流分析

污点传播分析中的隐式流分析是分析污点标记如何随程序中变量之间的控制依赖关系传播,也就是分析污点标记如何从条件指令传播到其所控制的语句.

在图 4 所示的程序中,变量 X 是被污点标记的字符串类型变量,变量 Y 和变量 X 之间并没有直接或间接的数据依赖关系(显式流关系),但 X 上的污点标记可以经过控制依赖隐式地传播到 Y.

具体来说,由第 4 行的循环条件控制的外层循环顺序地取出 X 中的每一个字符,转化成整型后赋给变量 x,再由第 7 行的循环条件控制的内层循环以累加的方式将 x 的值赋给 y,最后由外层循环将 y 逐一传给 Y.最终,第 12 行的 Y 值和 X 值相同,程序存在信息泄漏问题.但是,如果不进行隐式流污点传播分析,第 12 行 的变量 Y 将不会被赋予污点标记,程序的信息泄漏问题被掩盖.

隐式流污点传播一直以来都是一个重要的问题,和显式流一样,如果不被正确处理,会使污点分析的结果不精确.由于对隐式流污点传播处理不当导致本应被标记的变量没有被标记的问题称为欠污染(under-taint)问题.相反地,由于污点标记的数量过多而导致污点变量大量扩散的问题称为过污染(over-taint)问题.目前,针对隐式流问题的研究重点是尽量减少欠污染和过污染的情况.我们将在后面具体介绍现有技术是如何解决上述问题的.

无害处理

污点数据在传播的过程中可能会经过无害处理模块,无害处理模块是指污点数据经过该模块的处理后,数据本身不再携带敏感信息或者针对该数据的操作不会再对系统产生危害.换言之,带污点标记的数据在经过无害处理模块后,污点标记可以被移除.正确地使用无害处理可以降低系统中污点标记的数量,提高污点分析的效率,并且避免由于污点扩散导致的分析结果不精确的问题.

在应用过程中,为了防止敏感数据被泄露(保护保密性),通常会对敏感数据进行加密处理.此时,加密库函数应该被识别成无害处理模块.这一方面是由于库函数中使用了大量的加密算法,导致攻击者很难有效地计算出密码的可能范围;另一方面是加密后的数据不再具有威胁性,继续传播污点标记没有意义.

此外,为了防止外界数据因为携带危险操作而对系统关键区域产生危害(保护完整性),通常会对输入的数据进行验证.此时,输入验证(input validation)模块应当被识别成无害处理模块.

例如,为了防止代码注入漏洞,PHP 提供的 htmlentities 函数可以将特殊含义的 HTML 字符串转化成HTML实体(例如,将’<’转化成’<’).输入字符串经过上述转化后不会再携带可能产生危害的代码,可以安全地 发送给用户使用.除了语言自带的输入验证函数外,一些系统还提供了额外的输入验证工具,比如ScriptGard,CSAS,XSS Auditor,Bek.这些工具也应被识别成无害处理模块.

综上,目前对污点源、污点汇聚点以及无害处理模块的识别通常根据系统或漏洞类型使用定制的方法.由于这些方法都比较直接,本文将不再进行更深入的探讨.下一节将重点介绍污点传播中的关键技术.

tips:

更多的污点分析技术可参考以下链接及其参考链接:https://www.k0rz3n.com/2019/03/01/%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3%E6%B1%A1%E7%82%B9%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/

 

理解CodeQL漏洞分析过程

整体流程

  • 通过适配各个语言的AST解析器,并将代码的AST解析结果按照预设好的数据模型将代码AST数据及其依赖关系存储到CodeDB里
  • 通过QL语言定义污点追踪漏洞模型
  • 执行QL时通过高效的搜索算法对CodeDB的AST元数据进行高效查询,从而在代码中搜索出漏洞结果

QL中的污点分析模型

我们以命令执行的QL作为例子来看在Codeql Rules里的污点分析是如何使用的

首先,Codeql也是使用<sources,sinks,sanitizers>三元组对污点分析过程进行三个阶段的定义

文件目录:/java/ql/src/Security/CWE/CWE-078/ExecTainted.ql

/**
 * @name Uncontrolled command line
 * @description Using externally controlled strings in a command line is vulnerable to malicious
 *              changes in the strings.
 * @kind path-problem
 * @problem.severity error
 * @precision high
 * @id java/command-line-injection
 * @tags security
 *       external/cwe/cwe-078
 *       external/cwe/cwe-088
 */

import java /** 导入codeql的java依赖 **/
import semmle.code.java.dataflow.FlowSources /** 导入java的Sources定义模块 **/
import semmle.code.java.security.ExternalProcess /** 导入定义java中执行系统命令模块 **/
import ExecCommon  /** 导入对java中命令执行的 sources、sink、sanitizer定义模块 **/
import DataFlow::PathGraph /** 导入java的数据流控制模块 **/

/** 从DataFlow里导入 source、 sink、并且定义命令执行的参数为execArg **/    
from DataFlow::PathNode source, DataFlow::PathNode sink, ArgumentToExec execArg
/** source、 sink、execArg 满足 execTainted的参数定义 **/
where execTainted(source, sink, execArg)
/** 搜索满足execTainted谓词定义的参数、污染源及sink点并输出(可以理解成取交集的过程) **/    
select execArg, source, sink, "$@ flows to here and is used in a command.", source.getNode(),
  "User-provided value"

我们再来看一下 ExecCommon 里是如何定义满足条件的 source、sink 及 sanitizer

文件目录:/java/ql/src/Security/CWE/CWE-078/ExecCommon.qll

/** 导入各种所依赖的配置 **/
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.ExternalProcess
import semmle.code.java.security.CommandArguments

/** 定义一个私有的Class作为远程命令执行的dataflow Config配置,并且这个Config配置继承自基础的Configuration **/
private class RemoteUserInputToArgumentToExecFlowConfig extends TaintTracking::Configuration {
  RemoteUserInputToArgumentToExecFlowConfig() {
  /** 定义该Config的别名为ExecCommon **/
    this = "ExecCommon::RemoteUserInputToArgumentToExecFlowConfig"
  }
  /** 重写对Source的定义,满足Source是远程数据输入,instanceof语句为满足后续谓词条件,依旧是取交集操作 **/
  override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

  /** 重写对Sink的定义,sink.asExpr为sink的表达式需要满足于ArgumentToExec的谓词定义 **/
  override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof ArgumentToExec }

  /** 重写对Sanitizer的定义,节点的数据类型满足PrimitiveType,BoxedType 或者是安全的命令调用方式,便认为是经过了净化**/ 
  override predicate isSanitizer(DataFlow::Node node) {
    node.getType() instanceof PrimitiveType
    or
    node.getType() instanceof BoxedType
    or
    isSafeCommandArgument(node.asExpr())
  }
}

/**
 * Implementation of `ExecTainted.ql`. It is extracted to a QLL
 * so that it can be excluded from `ExecUnescaped.ql` to avoid
 * reporting overlapping results.
 */

/** 定义谓词 execTainted满足于RemoteUserInputToArgumentToExecFlowConfig条件,并且存在Source到Sink点的数据流 **/

predicate execTainted(DataFlow::PathNode source, DataFlow::PathNode sink, ArgumentToExec execArg) {
  exists(RemoteUserInputToArgumentToExecFlowConfig conf |
    conf.hasFlowPath(source, sink) and sink.getNode() = DataFlow::exprNode(execArg)
  )
}

我们可以看到CodeQL 使用结构化的查询,通过分别定义三元组<sources,sinks,sanitizers>,对满足其定义的所有AST元数据进行交集,并判断source到sink之间是否存在可达路径,如果存在可达路径即判断存在相关漏洞

 

在实际应用场景里的QL使用案例

如何通过QL获取Spring项目的 Web Path

规则分析

新建规则路径:java/ql/src/Security/CUSTOM/query/spring/SpringPath.ql

/**
 * @name Spring controller bind path 
 * @description list all Spring controller path in method bind and class bind.
 * @kind path-list
 * @problem.severity information
 * @precision Null
 * @id java/Spring-path
 * @tags Information-path
 */


import java
import semmle.code.java.frameworks.spring.SpringCustomController
import semmle.code.java.dataflow.FlowSources


from SpringWebApiBindMethod m
select 
m as controllerMethod, m.getLocation() as location,
m.getBindPath() as methodBindPath,
m.getControllerClassBindPath() as classBindPath, "Spring bindPath"

扫描结果,classBindPath + methodBindPath为Spring的Web path

Spring Web Path的模块 :java/ql/src/semmle/code/java/frameworks/spring/SpringCustomController.qll

import java
import semmle.code.java.Maps
import SpringController


/**
 * Sping框架web请求绑定方法
 */
class SpringWebApiBindMethod extends SpringMvcControllerMethod {
    SpringWebApiBindMethod() {
        getAnAnnotation() instanceof SpringRequsetMappingCustomAnnotation
        or 
        getAnAnnotation() instanceof SpringRestMappingCustomAnnotation
    }

    /**
     * 获取方法绑定的路径RequsteMapping
     */
    Expr getBindPath(){
        result = getAnAnnotation().(SpringRequsetMappingCustomAnnotation).getValue("value")
        or 
        result = getAnAnnotation().(SpringRestMappingCustomAnnotation).getValue("value")

    }

    /**
     * 获取mvc控制器类绑定的路径,不存在返回空字符串
     */
    string getControllerClassBindPath(){
        if this.isSpringMvcControllerBindPath() then 
        result = this.getDeclaringType().getAnAnnotation().(SpringRequsetMappingCustomAnnotation).getValue("value").toString()
        else result = ""
    }


    /**
     * 该方法的mvc控制器是否绑定了路径前缀
     */
    predicate isSpringMvcControllerBindPath() {
        exists(SpringRequsetMappingCustomAnnotation a| this.getDeclaringType().getAnAnnotation() = a 
        and this.getDeclaringType().getAnAnnotation().(SpringRequsetMappingCustomAnnotation).getValue("value").toString().length()>0  )
    }
}

    /**
     * 该方法的声明类型的直接类型或者间接类型满足Spring Controller的谓词定义
     */

class SpringMvcControllerMethod extends Method {
    SpringMvcControllerMethod() {
        getDeclaringType().getAnAncestor() instanceof SpringController
    }
}



    /** 
     * 该方法的声明类型名称包含Spring Request Mapping的注解
    */
class SpringRequsetMappingCustomAnnotation extends Annotation {
    SpringRequsetMappingCustomAnnotation() {

        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping")
        or
        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "RestController")

    }
}


    /** 
     * 该方法的声明类型名称包含Spring Rest Mapping的注解
    */
class SpringRestMappingCustomAnnotation extends Annotation {
    SpringRestMappingCustomAnnotation() {

        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping")
        or
        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "PostMapping")
        or
        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "PutMapping")
        or
        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "DeleteMapping")
        or
        getType().getAnAncestor().hasQualifiedName("org.springframework.web.bind.annotation", "PatchMapping")

    }
}

通过对Spring框架对Web Controller的使用方法及注解的使用方法,定义了SpringWebApiBindMethod类,并通过getControllerClassBindPath()、getBindPath() 获取方法绑定的路径RequsteMapping及该方法的mvc控制器是否绑定了路径前缀

再新建一个FlowSourceCustom.qll

/**
 * 输入源和数据流定义
 */

import java

import semmle.code.java.dataflow.FlowSources
import semmle.code.java.frameworks.spring.SpringComponentScan
import semmle.code.java.frameworks.spring.SpringCustomController


/**
 * Spring框架Servlet Input参数输入源
 */

class SpringCustomServletInputParameterSource extends RemoteFlowSource {
    SpringCustomServletInputParameterSource() {
      this.asParameter() = any(SpringRequestMappingParameter srmp | srmp.isTaintedInput())
    }
    override string getSourceType() { result = "Spring servlet input parameter" }
  }

/**
 * Spring框架Multipart FileS参数输入源
 */

class SpringCustomMultipartFileSource extends RemoteFlowSource {
    SpringCustomMultipartFileSource() {
      exists(MethodAccess ma, Method m |
        ma = this.asExpr() and
        m = ma.getMethod() and
        m.getDeclaringType()
            .getASourceSupertype*()
            .hasQualifiedName("org.springframework.web.multipart", "MultipartFile") and
        m.getName().matches("get%")
      )
    }
    override string getSourceType() { result = "Spring MultipartFile getter" }
  }


  /**
 * Spring框架Multipart request参数输入源
 */
class SpringCustomMultipartRequestSource extends RemoteFlowSource {
    SpringCustomMultipartRequestSource() {
      exists(MethodAccess ma, Method m |
        ma = this.asExpr() and
        m = ma.getMethod() and
        m.getDeclaringType()
            .getASourceSupertype*()
            .hasQualifiedName("org.springframework.web.multipart", "MultipartRequest") and
        m.getName().matches("get%")
      )
    }

    override string getSourceType() { result = "Spring MultipartRequest getter" }
  }

最后将FlowSourceCustom.qll import至 /java/ql/src/semmle/code/java/dataflow/FlowSources.qll就可以使用SpringPath.ql进行Spring的path查询

我们来看一个实际的例子fastjson.java文件:

package org.joychou.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
@RequestMapping("/fastjson")
public class Fastjson {

    @RequestMapping(value = "/deserialize", method = {RequestMethod.POST})
    @ResponseBody
    public String Deserialize(@RequestBody String params) {
        // 如果Content-Type不设置application/json格式,post数据会被url编码
        try {
            // 将post提交的string转换为json
            JSONObject ob = JSON.parseObject(params);
            return ob.get("name").toString();
        } catch (Exception e) {
            return e.toString();
        }
    }
controllerMethod controllerMethod.getLocation methodBindPath classBindPath Type
Deserialize Fastjson:19[19-29] “/deserialize” “/fastjson” Spring bindPath

QL对该文件扫描结果如上表

我们可以看到 /fastjson/deserialize 即是该方法的Web Path

结合SCA判断fastjson是否可利用

Fastjson不安全使用

Fastjson不安全的使用需要满足以下三个条件:

1、项目中导入了不安全的Fastjson版本

2、项目中使用了Fastsjon不安全的反序列化方法去反序列化外部传入的Json数据

3、项目中存在利用Fastjson反序列化利用方法的反序列化调用链

1、3 通过SCA的能力去解决,2可以通过QL分析源代码拿到FastJson在项目中的使用情况,下面我们来分析一下如何使用QL查询Fastjson是否在在代码中被安全使用

java-sec-code里的fastjson反序列化demo

package org.joychou.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
@RequestMapping("/fastjson")
public class Fastjson {

    @RequestMapping(value = "/deserialize", method = {RequestMethod.POST})
    @ResponseBody
    public String Deserialize(@RequestBody String params) {
        // 如果Content-Type不设置application/json格式,post数据会被url编码
        try {
            // 将post提交的string转换为json
            JSONObject ob = JSON.parseObject(params);
            return ob.get("name").toString();
        } catch (Exception e) {
            return e.toString();
        }
    }

我们在来看看QL里的Fastjson查询

/**
 * @name FastJson deserializing of user-controlled data
 * @description FastJson deserializing user-controlled data may allow attackers to
 *              execute arbitrary code.
 * @kind path-problem
 * @problem.severity error
 * @precision high
 * @id java/unsafe-fastjson-deserialization
 * @tags security
 *       external/cwe/cwe-502/Fastjson deserialization
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.FastJson
import DataFlow::PathGraph

class UnsafeFastJsonSinkConfig extends TaintTracking::Configuration {
  UnsafeFastJsonSinkConfig() { this = "UnsafeFastJsonConfig" }

  override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  override predicate isSink(DataFlow::Node sink) { sink instanceof UnSafeFastJsonSink }
}

from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeFastJsonSinkConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode().(UnSafeFastJsonSink).getMethodAccess(), source, sink,
  "Unsafe fastjson deserialization of $@.", source.getNode(), "user input"

首先我们定义了一个继承Configuration的 UnsafeFastJsonSinkConfig类,其需要满足于 dataFlow里的source满足RemoteFlowSource(远程用户输入)的谓词定义、sink点需要满足UnSafeFastJsonSink(不安全的Fastjson使用)的谓词定义

通过搜索where conf.hasFlowPath(source, sink) 满足UnsafeFastJsonSinkConfig条件的sources和sink并且source和sink之间是可达的,那我们就认为该处存在fastjson的不安全使用

我们再来看看Fastjson.qll是如何定义UnSafeFastJsonSink

import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.DataFlow2
import semmle.code.java.dataflow.DataFlow3
import semmle.code.java.frameworks.FastJson

predicate unsafeFastjson(MethodAccess ma, Expr sink) {
  exists(Method m | m = ma.getMethod() |
    ma.getMethod() instanceof FastJsonParseMethod and
    not fastJsonLooksSafe() and
    sink = ma.getArgument(0)
  )
}

class UnSafeFastJsonSink extends DataFlow::ExprNode {
    UnSafeFastJsonSink() { unsafeFastjson(_, this.getExpr()) }
  MethodAccess getMethodAccess() { unsafeFastjson(result, this.getExpr()) }
}

谓词unsafeFastjson需要满足于存在fastjson的调用方法并且未配置safety配置的方法

我们再来看看 FastJsonParseMethod、和fastJsonLooksSafe是如何编写的

/**
 * The class `com.alibaba.fastjson.JSON`.
 */
class FastJson extends RefType {
  FastJson() { this.hasQualifiedName("com.alibaba.fastjson", "JSON") }
}

/**
 * A FastJson parse method. This is either `JSON.parse` or `JSON.parseObject`.
 */
class FastJsonParseMethod extends Method {
  FastJsonParseMethod() {
    this.getDeclaringType() instanceof FastJson and
    this.hasName(["parse", "parseObject"])
  }
}

/**
 * A call to `ParserConfig.setSafeMode`.
 */
class FastJsonSetSafeMode extends MethodAccess {
  FastJsonSetSafeMode() {
    exists(Method m |
      this.getMethod() = m and
      m.hasName("setSafeMode") and
      m.getDeclaringType().hasQualifiedName("com.alibaba.fastjson.parser", "ParserConfig")
    )
  }

可以看到我们将JSON.parse和JSON.parseObject定义为危险函数

而使用setSafeMode配置的,我们认为是安全的

通过查询我们可以成功搜索出代码里的不安全的fastjson使用方式

结合SCA我们可以完成以上三个条件对fastjson安全风险的发现

 

总结

在理解了SAST的一些技术原理以及CodeQL一些实际使用的案例,本次分享附件包含Codeql的官方教程基础语法以及Java模块的中文教程,可自行向运营小姐姐获取。后面我们将分享58SAST在工程化选型以及设计的一些经验。

 

资源

扫码关注58安全应急响应中心公众号,后台发送“58白盒系列2”,获取Codeql教程下载链接!

(完)