freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

semgrep SAST编写自定义漏洞检测规则实践
2021-08-29 20:40:08

简介

semgrep 是一款由Facebook开源的白盒代码扫描工具,项目地址:https://github.com/returntocorp/semgrep,其规则编写简单,易用,扫描速度快。相较于CodeQL 而言,入门门槛较低,编写规则简单,且非常方便地接入到CI流程中。

安装步骤

方式一、mac机器上可使用 homebrew 安装:

brew install semgrep

方式二、Ubuntu / Windows via WSL / Linux / macOS 也可以使用pip进行安装:

python3 -m pip install semgrep

方式三、Docker部署:

docker pull returntocorp/semgrep:latest

更多详细内容可参考官方文档:https://semgrep.dev/docs/getting-started/

使用体验

若想使用docker 开箱即用先来体验下效果,这里需要注意的是官方文档中的docker扫描命令需要自行添加-v ,把宿主机上的源代码文件挂载到docker 中:

以扫描WebGoat 为例:

$ git clone https://github.com/WebGoat/WebGoat
$ docker run -v "${PWD}:/src" returntocorp/semgrep:latest --config p/security-audit WebGoat/webgoat-lessons
-o scan.txt--json --time

-o:输出扫描结果到文件,可以输出json格式/xml/sarif 等格式

--config: 配置扫描规则文件 官方也提供了一些规则文件,在https://semgrep.dev/r里可以查看各种分类的规则集。以sql 注入的规则为例,关键字搜索sql,可以看到sql-injection 这个规则集就提供了多达了37条规则。

1630238093_612b758dec3403a1e7813.png!small

如果想使用这个规则集来扫描的话,可以添加 --config "p/sql-injection",利用这个规则集我们发现了WebGoat存在8处sql 注入的问题,整个扫描过程耗时大概在3分钟左右。

docker run -v "${PWD}:/src" returntocorp/semgrep --config "p/sql-injection" --debug WebGoat/webgoat-lessons -o scan.txt

1630238203_612b75fb5a59f538a0839.png!small

1630238234_612b761a586528369afa3.png!small

但是我们知道WebGoat 的sql-injection lessons 应该不止8个漏洞。

1630238268_612b763cebceffa220916.png!small

在这37个规则中,适合java语言的主要是formatted-sql-string这条规则,因此先单独拉这个规则出来进行分析和优化。

点击对应的名称,可以详细看到规则内容,也可以在线编辑规则、并可运行测试代码来验证规则正确有效性:

1630238298_612b765a8f260b4c0d79f.png!small

更方便一点是在Playground 中对规则进行编写和验证。

1630238334_612b767e925feb0a2bda8.png!small

完整的formatted-sql-string 注入检测规则:

rules:
  - id: formatted-sql-string
    languages:
      - java
    message: |
      Detected a formatted string in a SQL statement. This could lead to SQL
      injection if variables in the SQL statement are not properly sanitized.
      Use a prepared statements (java.sql.PreparedStatement) instead. You
      can obtain a PreparedStatement using 'connection.prepareStatement'.
    metadata:
      asvs:
        control_id: 5.3.5 Injection
        control_url: https://github.com/OWASP/ASVS/blob/master/4.0/en/0x13-V5-Validation-Sanitization-Encoding.md#v53-output-encoding-and-injection-prevention-requirements
        section: "V5: Validation, Sanitization and Encoding Verification Requirements"
        version: "4"
      category: security
      cwe: "CWE-89: Improper Neutralization of Special Elements used in an SQL Command
        ('SQL Injection')"
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      owasp: "A1: Injection"
      references:
        - https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
        - https://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html#create_ps
        - https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement
      source-rule-url: https://find-sec-bugs.github.io/bugs.htm#SQL_INJECTION
    patterns:
      - pattern-not: $W.execute(<... "=~/.*TABLE *$/" ...>);
      - pattern-not: $W.execute(<... "=~/.*TABLE %s$/" ...>);
      - pattern-either:
          - pattern: $W.execute($X + $Y, ...);
          - pattern: |
              String $SQL = $X + $Y;
              ...
              $W.execute($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += $Y;
              ...
              $W.execute($SQL, ...);
          - pattern: $W.execute(String.format($X, ...), ...);
          - pattern: |
              String $SQL = String.format($X, ...);
              ...
              $W.execute($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += String.format(...);
              ...
              $W.execute($SQL, ...);
          - pattern: $W.executeQuery($X + $Y, ...);
          - pattern: |
              String $SQL = $X + $Y;
              ...
              $W.executeQuery($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += $Y;
              ...
              $W.executeQuery($SQL, ...);
          - pattern: $W.executeQuery(String.format($X, ...), ...);
          - pattern: |
              String $SQL = String.format($X, ...);
              ...
              $W.executeQuery($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += String.format(...);
              ...
              $W.executeQuery($SQL, ...);
          - pattern: $W.createQuery($X + $Y, ...);
          - pattern: |
              String $SQL = $X + $Y;
              ...
              $W.createQuery($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += $Y;
              ...
              $W.createQuery($SQL, ...);
          - pattern: $W.createQuery(String.format($X, ...), ...);
          - pattern: |
              String $SQL = String.format($X, ...);
              ...
              $W.createQuery($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += String.format(...);
              ...
              $W.createQuery($SQL, ...);
          - pattern: $W.query($X + $Y, ...);
          - pattern: |
              String $SQL = $X + $Y;
              ...
              $W.query($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += $Y;
              ...
              $W.query($SQL, ...);
          - pattern: $W.query(String.format($X, ...), ...);
          - pattern: |
              String $SQL = String.format($X, ...);
              ...
              $W.query($SQL, ...);
          - pattern: |
              String $SQL = $X;
              ...
              $SQL += String.format(...);
              ...
              $W.query($SQL, ...);
    severity: WARNING

默认规则下的检测结果分析

文章开头提到的使用"sql-injection"默认规则集一共检出了8个sql注入漏洞,经过梳理后发现分别在以下代码文件中:

序号

代码文件

问题代码行

是否误报

check_id

1

SqlInjectionChallenge.java

63~65

java.lang.security.audit.formatted-sql-string.formatted-sql-string

2

SqlInjectionLesson10.java

58~86

java.lang.security.audit.formatted-sql-string.formatted-sql-string

3

SqlInjectionLesson10.java

63

java.lang.security.audit.sqli.jdbc-sqli.jdbc-sqli

4

SqlInjectionLesson2.java

62

java.lang.security.audit.formatted-sql-string.formatted-sql-string

5

SqlInjectionLesson8.java

60~66

java.lang.security.audit.formatted-sql-string.formatted-sql-string

6

SqlInjectionLesson9.java

61~86

java.lang.security.audit.formatted-sql-string.formatted-sql-string

7

SqlInjectionLesson9.java

66

java.lang.security.audit.sqli.jdbc-sqli.jdbc-sqli

8

JWTFinalEndpoint.java

94

java.lang.security.audit.formatted-sql-string.formatted-sql-string

也可以指定输出结果文件格式(例如--json),即 -o result.txt --json (如果-o 后不加任意参数,输出文本格式,很难排查问题,例如规则多的时候不知道匹配到的是哪个规则,出现误报时比较难以排查问题)。

1630238484_612b771486b0d25b3a1d2.png!small

也就是默认输出的格式里,SqlInjectionLesson10.java 匹配到了两条有问题的代码,但是加----线下面的没有标注是匹配到的哪个规则。开始还以为是规则有问题,后来通过--json 文件分析才发现是匹配到了两个不同的check_id(也就是两个规则都各自匹配到了问题代码,一个规则是匹配出第58-85行,另外一个是第63行)

1630238535_612b7747ee8bb55d5f726.png!small

值得注意的是,虽然发现了8个漏洞,但是有两个是重复的(或者说是代码行位置是子集的关系),这是因为分别匹配到了不同的Pattern,也就是表格中加颜色的SqlInjectionLesson10.java和SqlInjectionLesson9.java 。为什么会出现这样,首先来分析下这些Pattern。

以SqlInjectionLesson10.java 为例,单独拿出来看下为什么同一个代码片段会匹配到两次:

首先在 formatted-sql-string 这条规则里面,是下面这条Pattern 起了关键作用。

- pattern: |
   String $SQL = $X + $Y;
   ...
   $W.executeQuery($SQL, ...);

这条规则描述的是一个String 类型的元变量(metavariable)$SQL,可以理解为一个抽象的String 类型值。这个$SQL元变量是由另外两个元变量相加而成,.... 则代表任意值,代表后续可以有代码行也可以没有,最后需要有$W元变量的executeQuery方法,并且第一个参数是上面的$SQL。这是一个比较典型的检测SQL注入漏洞的Pattern,主要的检测逻辑链路是根据executeQuery方法作为有害sql语句的落脚点【Sink】,入参变量是另外两个参数拼接形成的$SQL【Source】。

那么在SqlInjectionLesson10.java 中,可以通过以下代码来抽象整个匹配过程:

$X = SELECT * FROM access_log WHERE action LIKE '%+action
$Y = %'
$SQL = query
$W = statement

2.而在jdbc-sqli 这个规则里面,是- pattern: $S.$METHOD($SQL,...)起了关键作用,$METHOD需要满足什么条件,是通过metavariable-regex 定义了一个正则表达式来限定的,也就是需要方法名符合这个正则^(executeQuery|execute|executeUpdate|executeLargeUpdate|addBatch|nativeSQL)$,因此也就匹配到了SqlInjectionLesson10.java的第63行代码(当前在前面也有pattern-inside:String $SQL = $X + $Y;),以及排除了【String $SQL = "..." + "...";】这种常量相加的方式-不是从用户输入获取的,避免一些误报)。

- metavariable-regex:
  metavariable: $METHOD
  regex: ^(executeQuery|execute|executeUpdate|executeLargeUpdate|addBatch|nativeSQL)$

同样地,在SqlInjectionLesson10.java 中,可以通过以下代码来抽象整个匹配过程:

$X = SELECT * FROM access_log WHERE action LIKE '%+action
$Y = %'
$S = statement
$METHOD = executeQuery

完整的jdbc-sqli 规则如下所示,融合了pattern-inside/pattern-not/pattern-either/metavariable-regex ,比较标准和完整,具有一定地参考价值,在自己编写规则地时候可以借鉴。

rules:
  - id: jdbc-sqli
    languages:
      - java
    message: |
      Detected a formatted string in a SQL statement. This could lead to SQL
      injection if variables in the SQL statement are not properly sanitized.
      Use a prepared statements (java.sql.PreparedStatement) instead. You
      can obtain a PreparedStatement using 'connection.prepareStatement'.
    metadata:
      category: security
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
    patterns:
      - pattern-either:
          - patterns:
              - pattern-either:
                  - pattern-inside: |
                      String $SQL = $X + $Y;
                      ...
                  - pattern-inside: |
                      String $SQL = String.format(...);
                      ...
                  - pattern-inside: |
                      $VAL $FUNC(...,String $SQL,...) {
                        ...
                      }
              - pattern-not-inside: |
                  String $SQL = "..." + "...";
                  ...
              - pattern: $S.$METHOD($SQL,...)
          - pattern: |
              $S.$METHOD(String.format(...),...);
          - pattern: |
              $S.$METHOD($X + $Y,...);
      - pattern-either:
          - pattern-inside: |
              java.sql.Statement $S = ...;
              ...
          - pattern-inside: |
              $TYPE $FUNC(...,java.sql.Statement $S,...) {
                ...
              }
      - pattern-not: |
          $S.$METHOD("..." + "...",...);
      - metavariable-regex:
          metavariable: $METHOD
          regex: ^(executeQuery|execute|executeUpdate|executeLargeUpdate|addBatch|nativeSQL)$
    severity: WARNING

编写自定义规则

回到文章开头处,我们注意到检测了8个漏洞。但是实际上经过我们分析,其实去掉重复之后就只剩下6个了,并且分布在6个java文件中,熟悉WebGoat的话应该知道这两个规则实际上是漏检了不少sql注入漏洞。

因此整理出了漏检的文件,见下表:

序号

文件名

问题代码

描述

1

SqlInjectionLesson3.java

statement.executeUpdate(query);

query 是用户输入的参数,直接传入executeUpdate()方法中

2

SqlInjectionLesson4.java

statement.executeUpdate(query);

query 是用户输入的参数,直接传入executeUpdate()方法中

3

SqlInjectionLesson5.java

statement.executeQuery(query);

query 是用户输入的参数,直接传入executeQuery()方法中

4

SqlInjectionLesson5a.java

statement.executeQuery(query);

query 是拼接来自用户输入的参数,直接传入executeQuery()方法中

5

SqlInjectionLesson5b.java

StringqueryString = "SELECT * From user_data WHERE Login_Count = ? and userid= " + accountName;

...

PreparedStatementquery = connection.prepareStatement(queryString, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

query.setInt(1, count);

...

ResultSetresults = query.executeQuery();

accountName 来自用户输入,未用占位符

6

SqlInjectionLesson6a.java

query = "SELECT * FROM user_data WHERE last_name = '" + accountName + "'";

...中间省略

ResultSetresults = statement.executeQuery(query);

query 是拼接来自用户输入的参数,直接传入executeQuery()方法中

...

...

...

...

在WebGoat 的mitigation部分是有一些关键字绕过等等,这部分在编写检测规则时需要花点心思,并且由于缺乏通用性,因此不在此次讨论范围内。

上面漏检的6+个漏洞可以将其分为两类:

第一类:用户输入直接入参Sink函数:序号1,2,3

第二类: 用户输入拼接后入参Sink函数: 序号4,5,6

接下来我们需要根据这两种具体情况编写新的规则。

针对第一类规则:

首先我们需要提炼出这个通用的原型函数,首先是一个变量是从用户输入的(并且参数个数和位置是不固定的),设为$X ,然后这个$X 中间不经过处理函数,最终流向到(executeUpdate|executeQuery)方法中。基于此我们可以增加如下规则:

patterns:
     
      - pattern-inside: |
          $VAL $FUNC(...,String $X,...) {
                        ...
                      }
      - pattern: $S.$METHOD($X,...)
      - metavariable-regex:
          metavariable: $METHOD
          regex: ^(executeQuery|executeUpdate)$

其中使用pattern-inside用于限定pattern匹配的上下文。因此这里就要限定它来自一个函数,并且形参是String类型的。

扫描了下即发现了4个新的漏洞:

1630238921_612b78c98ec6e9d25c78a.png!small

利用这个规则成功地检出了Lesson2、Lesson3、Lesson4、Lesson5 的问题。

1630238948_612b78e456384c350ffd9.png!small

针对第二类规则:

和第一类规则类似,还是一个变量是从用户输入的(并且参数个数和位置是不固定的),设为$X ,然后这个$X 经过拼接($X+$Y),最终流向到(executeUpdate|executeQuery)方法中。其中也要考虑多种情况,基于此我们可以增加如下规则:

(1)针对形如 String var = $X:即定义一个变量var,值为$X:尤其适合虽然使用了预编译prepareStatement 但是没有使用?进行占位,依然采取字符串拼接方式。

patterns:
     
      - pattern-inside: |
          $VAL $FUNC(...,String $X,...) {
                        ...
                       String $SQL = "..." + $X;
                       ...
                      }
      - pattern: $S.$METHOD($SQL,...)
      - metavariable-regex:
          metavariable: $METHOD
          regex: ^(executeQuery|executeUpdate|prepareStatement)$

(2)针对形如 String var = ""; var = $X; 即先定义一个空值变量,之后重新赋值为$X:

patterns:
     
      - pattern-inside: |
          $VAL $FUNC(...,String $X,...) {
                        ...  
                       String $SQL = ...;
                       ...
                       $SQL = ... + $X + ...;
                       ...
                      }
                      
      - pattern: $S.$METHOD($SQL,...)
      - metavariable-regex:
          metavariable: $METHOD
          regex: ^(executeQuery|executeUpdate|prepareStatement)$

将这两种情况规则合并:

- pattern-either:
          - patterns:
              - pattern-either:
                  - pattern-inside: |
                     String $SQL = $X + $Y;
                  - pattern-inside: |
                      String $SQL = String.format(...);
                      ...
                  - pattern-inside: |
                      $VAL $FUNC(...,String $X,...) {
                      ...
                       String $SQL =  "..." + $X;
                       ...
                      }
                  - pattern-inside: |
                          $VAL $FUNC(...,String $X,...) {
                            ...  
                           String $SQL = ...;
                           ...
                           $SQL = ... + $X + ...;
                             ...
                          }
              - pattern-not-inside: |
                  String $SQL = "..." + "...";
                  ...
              - pattern: $S.$METHOD($SQL,...)
      - metavariable-regex:
          metavariable: $METHOD
          regex: ^(executeQuery|executeUpdate|prepareStatement)$

进行代码扫描测试:

1630239953_612b7cd1b6de80981548e.png!small

利用这个规则成功地检出了Lesson6a、Lesson5a、Lesson5b 的问题。

1630239984_612b7cf00b5ca3c44b54d.png!small

写在最后

未来会尝试把Semgrep 接入CI流程中,而精细化策略运营将会成为后续重点研究的方向。鉴于目前笔者能力尚且有限,文章难免会存在错误之处,还望大家不吝赐教。若您对文章有进一步的见解,也欢迎随时与我交流~

# SQL注入 # 代码审计 # 企业安全 # SAST
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录