tags: codeql,cve

我是如何使用codeql挖掘CVE-2021-31856 Meshery sql注入的

item details
Project Url https://github.com/layer5io/meshery
CVE-ID CVE-2021-31856
LGTM Alert lgtm alert
Affect Version v0.5.2
Fix Version v0.5.3 fix commit
CVSS 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Author https://github.com/ssst0n3

一、codeql简介

CodeQL是一个自动化静态代码分析工具。在CodeQL中,代码被当作数据处理。安全漏洞、缺陷和其他错误被模拟成可以针对从代码中提取的数据库执行的查询。你可以运行由GitHub研究人员和社区贡献者编写的标准CodeQL查询,或者编写你自己的查询,用于自定义分析。找到潜在漏洞的查询会直接在源文件中突出显示结果。

二、meshery简介

Meshery是CNCF成员之一,作为服务网格管理平面,Meshery 支持任何服务网格及其工作负载的采用、操作和管理。

三、CVE-2021-31856介绍

简介如下:

GetMesheryPatterns() 函数(位于meshery/models/meshery_pattern_persister.go)存在一个SQL注入漏洞,注入点是/api/experimental/patternfile?order=id%3Bselect(randomblob(1000000000))&page=0&page_size=0中的order参数。

具体内容已在另外的文章中详细介绍,参见:

四、挖掘过程

前段时间,我在学习codeql时,想找一些案例。于是在lgtm上翻了一下, 偶然间发现了Meshery中的一个sql注入漏洞。

问题本身、及codeql在这个漏洞中的使用比较简单——直接无脑分析告警即可发现。

但codeql在其中的作用,可以作为其效果的示例。因此,本文可以作为codeql极简入门的一个指引。而后续codeql的高级用法,都可以按照类似的模式扩展开来。

漏洞产生及发现的时间线如下:

在这个过程中,人类只需要挨个查看告警,并分析是否为误报或漏洞。比较方便的是,lgtm提供了数据流动的路径。

五、query分析

lgtm产生的告警为"Database query built from user-controlled sources"。并对以下代码报告"This query depends on a user-provided value."

 query := mpp.DB.Order(order)

经过分析可以确认,order参数确实来源于用户输入。

点击告警行的问号按钮,可以找到这条告警对应的规则为 https://lgtm.com/rules/1510359656852/

我们也可以很方便得浏览到这条规则扫描到的告警列表, https://lgtm.com/rules/1510359656852/alerts/

点击页面中的Query in query console按钮,我们可以查看这条规则的query源码如下:

https://lgtm.com/query/rule:1510359656852/lang:go/

/**
 * @name Database query built from user-controlled sources
 * @description Building a database query from user-controlled sources is vulnerable to insertion of
 *              malicious code by the user.
 * @kind path-problem
 * @problem.severity error
 * @security-severity 8.8
 * @precision high
 * @id go/sql-injection
 * @tags security
 *       external/cwe/cwe-089
 */

import go
import semmle.go.security.SqlInjection
import DataFlow::PathGraph

from SqlInjection::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This query depends on $@.", source.getNode(),
  "a user-provided value"

这条规则,在codeql-go中也有,地址为 https://github.com/github/codeql-go/blob/191a4c1101cfd840aa38f44c08de5d9743bcc726/ql/src/Security/CWE-089/SqlInjection.ql

为了使用codeql复现这个漏洞的发现过程,我们可以自行编译一个mesheryv0.5.2的codeql数据库,并执行一下。

# git clone https://github.com/meshery/meshery
# cd meshery
# git checkout v0.5.2
# codeql database create -l go meshery_v0.5.2
# codeql database analyze meshery_v0.5.2 /home/codeql-home/codeql-go/ql/src/Security/CWE-089/SqlInjection.ql --format=csv --output=result.csv
# cat result.csv 
"Database query built from user-controlled sources","Building a database query from user-controlled sources is vulnerable to insertion of malicious code by the user.","error","This query depends on [[""a user-provided value""|""relative:///handlers/meshery_pattern_handler.go:147:7:147:11""]].","/models/meshery_pattern_persister.go","35","24","35","28"

我们发现了一个分析结果,存在Database query built from user-controlled sources

漏洞的数据流路径从handlers/meshery_pattern_handler.go:147:7:147:11开始。 https://github.com/meshery/meshery/blob/v0.5.2/handlers/meshery_pattern_handler.go#L147

q := r.URL.Query()

最终落在/models/meshery_pattern_persister.go:35

https://github.com/meshery/meshery/blob/v0.5.2/models/meshery_pattern_persister.go#L35

query := mpp.DB.Order(order)

正是CVE-2021-31856。

下面我们分析一下这条ql规则:

import go
import semmle.go.security.SqlInjection
import DataFlow::PathGraph

from SqlInjection::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This query depends on $@.", source.getNode(),
  "a user-provided value"

这条规则使用的是污点分析技术,污点分析规则具体定义在semmle.go.security.SqlInjection::Configuration中。

https://github.com/github/codeql-go/blob/v1.27.0/ql/src/semmle/go/security/SqlInjection.qll#L19

import go

module SqlInjection {
  import SqlInjectionCustomizations::SqlInjection

  /**
   * A taint-tracking configuration for reasoning about SQL-injection vulnerabilities.
   */
  class Configuration extends TaintTracking::Configuration {
    Configuration() { this = "SqlInjection" }

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

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

    override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
      NoSQL::isAdditionalMongoTaintStep(pred, succ)
    }

    override predicate isSanitizer(DataFlow::Node node) {
      super.isSanitizer(node) or
      node instanceof Sanitizer
    }

    override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
      guard instanceof SanitizerGuard
    }
  }
}

可以看出,这个文件仅是一个模板,具体对于Source, Sink的定义,位于SqlInjectionCustomizations::SqlInjection

https://github.com/github/codeql-go/blob/v1.27.0/ql/src/semmle/go/security/SqlInjectionCustomizations.qll

/**
 * Provides default sources, sinks and sanitizers for reasoning about SQL-injection vulnerabilities,
 * as well as extension points for adding your own.
 */

import go

/**
 * Provides extension points for customizing the taint tracking configuration for reasoning about
 * SQL-injection vulnerabilities.
 */
module SqlInjection {
  /**
   * A data flow source for SQL-injection vulnerabilities.
   */
  abstract class Source extends DataFlow::Node { }

  /**
   * A data flow sink for SQL-injection vulnerabilities.
   */
  abstract class Sink extends DataFlow::Node { }

  /**
   * A sanitizer for SQL-injection vulnerabilities.
   */
  abstract class Sanitizer extends DataFlow::Node { }

  /**
   * A sanitizer guard for SQL-injection vulnerabilities.
   */
  abstract class SanitizerGuard extends DataFlow::BarrierGuard { }

  /** A source of untrusted data, considered as a taint source for SQL injection. */
  class UntrustedFlowAsSource extends Source {
    UntrustedFlowAsSource() { this instanceof UntrustedFlowSource }
  }

  /** An SQL string, considered as a taint sink for SQL injection. */
  class SqlQueryAsSink extends Sink {
    SqlQueryAsSink() { this instanceof SQL::QueryString }
  }

  /** A NoSQL query, considered as a taint sink for SQL injection. */
  class NoSqlQueryAsSink extends Sink {
    NoSqlQueryAsSink() { this instanceof NoSQL::Query }
  }
}

Sanitier, Sanitizer是空的,删去也不影响结果,这里也不涉及nosql, 所以我们可以把ql语句合并、删简为:

import go
import DataFlow::PathGraph


module SqlInjection {
    abstract class Source extends DataFlow::Node { }

    abstract class Sink extends DataFlow::Node { }

    class UntrustedFlowAsSource extends Source {
        UntrustedFlowAsSource() { this instanceof UntrustedFlowSource }
    }

    class SqlQueryAsSink extends Sink {
        SqlQueryAsSink() { this instanceof SQL::QueryString }
    }

    class Configuration extends TaintTracking::Configuration {
      Configuration() { this = "SqlInjection" }
  
      override predicate isSource(DataFlow::Node source) { source instanceof Source }
      override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
      }
  }

from SqlInjection::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This query depends on $@.", source.getNode(),
  "a user-provided value"

因此,source为UnstrustedFlowSource类型的对象, sink为SQL::QueryString对象。

UnstrustedFlowSource有很多种,例如

https://github.com/github/codeql-go/blob/v1.27.0/ql/src/semmle/go/frameworks/Chi.qll#L24

  private class UserControlledRequestMethod extends UntrustedFlowSource::Range,
    DataFlow::MethodCallNode {
    UserControlledRequestMethod() {
      this.getTarget().hasQualifiedName(packagePath(), "Context", "URLParam")
    }
  }

在本例中,应该认为来源于net/http.Request.URL.Query()的参数是用户可控的,即对应 https://github.com/github/codeql-go/blob/v1.27.0/ql/src/semmle/go/frameworks/stdlib/NetHttp.qll

  private class UserControlledRequestField extends UntrustedFlowSource::Range,
    DataFlow::FieldReadNode {
    UserControlledRequestField() {
      exists(string fieldName | this.getField().hasQualifiedName("net/http", "Request", fieldName) |
        fieldName = "Body" or
        fieldName = "GetBody" or
        fieldName = "Form" or
        fieldName = "PostForm" or
        fieldName = "MultipartForm" or
        fieldName = "Header" or
        fieldName = "Trailer" or
        fieldName = "URL"
      )
    }
  }

sink为SQL::QueryString对象,也有多种,其中Gorm的实现为

https://github.com/github/codeql-go/blob/v1.27.0/ql/src/semmle/go/frameworks/SQL.qll#L198

  private class GormSink extends SQL::QueryString::Range {
    GormSink() {
      exists(Method meth, string package, string name |
        meth.hasQualifiedName(package, "DB", name) and
        this = meth.getACall().getArgument(0) and
        package = Gorm::packagePath() and
        name in [
            "Where", "Raw", "Order", "Not", "Or", "Select", "Table", "Group", "Having", "Joins",
            "Exec", "Distinct", "Pluck"
          ]
      )
    }
  }

这条规则似乎已经非常完美了,我们暂时不必思考优化的问题,在其他的案例中可以考虑对规则进行优化。