Semgrep代码审计体验

语言: CN / TW / HK

最近有一些代码审计需要,研究了一下自动化审计工具

考虑了一些备选工具

  1. Kunlun-M

    LoRexxar大佬的审计工具,文档似乎比较老旧,学习成本比较高,试用了一下就放着了

  2. CodeQL

    GitHub搞得审计工具,QL逐渐流行起来,很多师傅写过文章

  3. Semgrep

    很早之前看过介绍文章,简单的规则编写让我印象非常深刻

刚好有需求,决定研究学习一下Semgrep

官方提供了非常棒的教程,强烈推荐学习一下 http://semgrep.dev/learn

本文是在学习和尝试实践后,个人的一些体验分享(基于 Semgrep 0.88.0

ltdr

可以学习,暂时不推荐使用,但未来可期跳到总结部分

像写代码一样写规则

这个我觉得是最厉害的地方,官网上也提到了

Write rules that look like your code
No painful and complex DSL

没有痛苦和复杂的DSL完全就是在说CodeQL

Semgrep的“DSL”部分很少,也很容易记,非常符合编程思维,学习成本可以说极低, 30分钟从入门到精通 ,比如

... 是任意代码段

"..." 是任意字符串

$xx 变量

学习完 learn 教程 真的就可以去实践,写自己的规则

不像CodeQL,看完教程好像是懂了,但想要去写检测一个新的洞,文档翻烂,才拼拼凑凑写一个规则出来(CodeQL大佬带带我有啥技巧吗)

数据流分析

代码审计逃不开数据流分析,Semgrep当然也是支持的

官网例子 http://semgrep.dev/s/P8oz

rules:
  - id: taint-example
    languages:
      - python
    message: Found dangerous HTML output
    mode: taint
    pattern-sanitizers:
      - pattern: sanitize_input(...)
    pattern-sinks:
      - pattern: html_output(...)
      - pattern: eval(...)
    pattern-sources:
      - pattern: get_user_input(...)
    severity: WARNING
def route1():
    data = get_user_input()
    data = sanitize_input(data)
    # ok: taint-example
    return html_output(data)

def route2():
    data = get_user_input()
    # ruleid: taint-example
    return html_output(data)  // 检测到这里

看着非常简单,基本上定义好 sourcesinksanitizers 就行

但是稍微变形一下就不行了

def route1():
    data = get_user_input()
    data = sanitize_input(data)
    # ok: taint-example
    return html_output(data)

def route2():
    data = get_user_input()
    # ruleid: taint-example
    return html_output(data)  // 检测到

def html_output_wrap(data):
    return html_output(data)

def route3():
    data = get_user_input()
    return html_output_wrap(data) // 无法检测到

污点传播分析还是比较弱,另外还有一些case也检测不到,就不一一列举了

规则复用

这个在实践中还是非常重要的,规则复用更像是知识的积累

Semgrep当然也是支持的,叫 join 模式,也有例子文档 http://semgrep.dev/docs/experiments/join-mode/overview/

rules:
- id: flask-likely-xss
  mode: join
  join:
    refs:
      - rule: flask-user-input.yaml
        as: user-input
      - rule: unescaped-template-extension.yaml
        as: unescaped-extensions
      - rule: any-template-var.yaml
        renames:
        - from: '$...EXPR'
          to: '$VAR'
        as: template-vars
    on:
    - 'user-input.$VAR == unescaped-extensions.$VALUE'
    - 'unescaped-extensions.$VAR == template-vars.$VAR'
    - 'unescaped-extensions.$PATH > template-vars.path'
  message: |
    Detected a XSS vulnerability: '$VAR' is rendered
    unsafely in '$PATH'. 
  severity: ERROR

导入 flask-user-input.yaml 获取用户输入( source

导入 any-template-var.yaml 获取模板渲染( sink

最后用神奇的 on 语法,把 sourcesink 连起来

看起来很棒不是,但是这个 on 语法没有污点分析,也就说虽然上面的污点跟踪有些弱,但这里甚至都没有

join 模式线上用不了,我本地写个例子,检测eval一个变量的场景

// eval.yaml
rules:
- id: eval
  patterns:
    - pattern: eval($X)
    - pattern-not: eval("...")
  message: $X
  languages:
    - js
    - ts
  severity: WARNING

检测取location

// location.yaml
rules:
- id: location
  languages:
    - js
    - ts
  severity: INFO
  message: $VAR
  pattern: $VAR = location.$X

合并起来就是检测eval(location.$X),很显然会有xss,有以下代码

let p1 = location.hash;
let p2 = p1;

function param_wrap(param) {
  return param;
}

let p3 = param_wrap(p2);

eval(p1);
eval(p2);
eval(p3);

使用 join 模式写规则

rules:
- id: eval-join
  mode: join
  join:
    refs:
      - rule: location.yaml
        as: user-input
      - rule: eval.yaml
        as: eval
    on:
    - user-input.$VAR == eval.$X
  message: eval-join
  severity: ERROR

只能检测到 eval(p1) ,但是如果使用 taint 跑污点分析,都能检测出来

rules:
- id: eval-taint
  mode: taint
  pattern-sources:
    - pattern: location.$X
  pattern-sinks:
    - pattern: eval(...)
  message: eval-taint
  severity: ERROR
  languages:
    - js
    - ts

总结

Semgrep是一个非常年轻的 开源项目 (似乎20年才出现的),功能还不是很完善

  1. 污点分析弱,漏报率太高,意味着不能很好的挖洞

  2. 规则复用弱,知识不好积累,意味着难以形成工程

这两个连起来,可能适合比较小型,调用关系不复杂的源码场景,以及个人使用

比如js编译,混淆,调用关系复杂,中大型项目,团队合作就没法用了

如果能解决上面的问题,规则编写的简单将是巨大的优势