Semgrep代碼審計體驗

語言: CN / TW / HK

最近有一些代碼審計需要,研究了一下自動化審計工具

考慮了一些備選工具

  1. Kunlun-M

    LoRexxar大佬的審計工具,文檔似乎比較老舊,學習成本比較高,試用了一下就放着了

  2. CodeQL

    GitHub搞得審計工具,QL逐漸流行起來,很多師傅寫過文章

  3. Semgrep

    很早之前看過介紹文章,簡單的規則編寫讓我印象非常深刻

剛好有需求,決定研究學習一下Semgrep

官方提供了非常棒的教程,強烈推薦學習一下 https://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當然也是支持的

官網例子 https://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 模式,也有例子文檔 https://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編譯,混淆,調用關係複雜,中大型項目,團隊合作就沒法用了

如果能解決上面的問題,規則編寫的簡單將是巨大的優勢