简介
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条规则。
如果想使用这个规则集来扫描的话,可以添加 --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
但是我们知道WebGoat 的sql-injection lessons 应该不止8个漏洞。
在这37个规则中,适合java语言的主要是formatted-sql-string这条规则,因此先单独拉这个规则出来进行分析和优化。
点击对应的名称,可以详细看到规则内容,也可以在线编辑规则、并可运行测试代码来验证规则正确有效性:
更方便一点是在Playground 中对规则进行编写和验证。
完整的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 后不加任意参数,输出文本格式,很难排查问题,例如规则多的时候不知道匹配到的是哪个规则,出现误报时比较难以排查问题)。
也就是默认输出的格式里,SqlInjectionLesson10.java 匹配到了两条有问题的代码,但是加----线下面的没有标注是匹配到的哪个规则。开始还以为是规则有问题,后来通过--json 文件分析才发现是匹配到了两个不同的check_id(也就是两个规则都各自匹配到了问题代码,一个规则是匹配出第58-85行,另外一个是第63行)
值得注意的是,虽然发现了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 |
| query 是用户输入的参数,直接传入executeUpdate()方法中 |
2 | SqlInjectionLesson4.java |
| query 是用户输入的参数,直接传入executeUpdate()方法中 |
3 | SqlInjectionLesson5.java |
| query 是用户输入的参数,直接传入executeQuery()方法中 |
4 | SqlInjectionLesson5a.java |
| query 是拼接来自用户输入的参数,直接传入executeQuery()方法中 |
5 | SqlInjectionLesson5b.java |
...
...
| accountName 来自用户输入,未用占位符 |
6 | SqlInjectionLesson6a.java |
...中间省略
| 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个新的漏洞:
利用这个规则成功地检出了Lesson2、Lesson3、Lesson4、Lesson5 的问题。
针对第二类规则:
和第一类规则类似,还是一个变量是从用户输入的(并且参数个数和位置是不固定的),设为$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)$
进行代码扫描测试:
利用这个规则成功地检出了Lesson6a、Lesson5a、Lesson5b 的问题。
写在最后
未来会尝试把Semgrep 接入CI流程中,而精细化策略运营将会成为后续重点研究的方向。鉴于目前笔者能力尚且有限,文章难免会存在错误之处,还望大家不吝赐教。若您对文章有进一步的见解,也欢迎随时与我交流~