Github-Action Injection

Github-Action Injection

in

๐Ÿ” Introduction

Github actions์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•˜๋Š” workflow, ๊ฐœ๋ฐœ์ž๊ฐ€ ์ œ๊ณตํ•˜๋Š” custom actions์—์„œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์— ๋Œ€ํ•ด ์ •ํ™•ํ•˜๊ฒŒ ๊ฒ€์ฆํ•˜๊ณ  ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด Command Injection์˜ ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Github action injection, Github action script injection ๋“ฑ์œผ๋กœ ๋ถˆ๋ฆฝ๋‹ˆ๋‹ค.

Example code

  - name: Check PR title
        run: |
          title="${{ github.event.pull_request.title }}"
          if [[ $title =~ ^octocat ]]; then
          echo "PR title starts with 'octocat'"
          exit 0
          else
          echo "PR title did not start with 'octocat'"
          exit 1
          fi

์œ„์™€ ๊ฐ™์ด github.event.pull_request.title๋กœ Pull Request์˜ Title์„ Workflow์—์„œ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์€ ์ œ๋ชฉ์œผ๋กœ ๊ณต๊ฒฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

Workflow ์ฝ”๋“œ์—์„  ๋ณ€์ˆ˜ ๊ฐ’์ด ๊ทธ๋Œ€๋กœ ์‚ฝ์ž…๋˜๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋กœ ์น˜ํ™˜๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด curl์ด string ๋‚ด๋ถ€์— ์žˆ๋Š”๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ช…๋ น์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

  - name: Check PR title
        run: |
          title=""; curl <OAST-SERVICE>; ""
          if [[ $title =~ ^octocat ]]; then
          ...

Actions > Workflow ์— ๋“ค์–ด๊ฐ€์„œ ๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด curl ๋ช…๋ น์ด ์‹คํ–‰๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ OAST ์„œ๋น„์Šค๋กœ DNS Query์™€ HTTP Request๋กœ ๋„์ฐฉํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์ฃ .

์œ„์— ๋Œ€ํ•œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” ์•„๋ž˜ ๋งํฌ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Risk

Public github

Public Githubd์—์„  github์˜ runner๊ฐ€ ์‚ฌ์šฉ๋˜๊ธฐ ๋–„๋ฌธ์— ์‹ค์ œ๋กœ ์ฝ”๋“œ ๋™์ž‘์€ github ์ชฝ ์„œ๋ฒ„์—์„œ ์ผ์–ด๋‚˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ workflow ๋™์ž‘์„ ์œ„ํ•ด secret ๋“ฑ ๋„ runner์— ๋‚ด๋ ค์˜ค๊ธฐ ๋•Œ๋ฌธ์— ๊ณต๊ฒฉ์ž๊ฐ€ ๋‹จ์ˆœํžˆ ๋ช…๋ น ์‹คํ–‰์„ ํ†ตํ•ด github์— ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ ๋ณด๋‹จ ํ† ํฐ ํƒˆ์ทจ๋‚˜ ์ค‘์š”์ •๋ณด๋ฅผ ํƒˆ์ทจํ•˜๋Š”๋ฐ ํฌ์ปค์Šค๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค.

Enterprise github

Enterprise github์—์„œ๋Š” runner๊ฐ€ enterprise ์‚ฌ์šฉ์ž์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„ ํƒˆ์ทจ์— ๊ด€๋ จ๋œ ์ด์Šˆ, ๊ทธ๋ฆฌ๊ณ  workflow ๋™์ž‘ ์ „ checkout ๋“ฑ์„ ํ†ตํ•ด ์ฝ”๋“œ๋ฅผ ๋‚ด๋ ค๋ฐ›๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋†’์€๋ฐ, ์ด๋Ÿฐ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ๋  ๊ฒฝ์šฐ ๋‚ด๋ถ€ ์ฝ”๋“œ ํƒˆ์ทจ ๋“ฑ์˜ ์ด์Šˆ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ž˜ ์ฒดํฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Self-hosted runner

Public, Enterprise ๋ชจ๋‘ Self-hosted runner ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ ๋‹น์—ฐํžˆ Runner๋ฅผ ์šด์˜ํ•˜๋Š” ์‚ฌ์šฉ์ž์˜ ์„œ๋ฒ„๋ฅผ ๋Œ€์ƒ์œผ๋กœ ๊ณต๊ฒฉ์ด ์ผ์–ด๋‚  ์ˆ˜ ์žˆ๊ธฐ ๋–„๋ฌธ์— ํ•ด๋‹น ๊ด€์ ์—์„œ ๋ฆฌ์Šคํฌ๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค.

๐Ÿ—ก Offensive techniques

Detect

Workflow

์‚ฌ์šฉ์ž๊ฐ€ workflow ๊ตฌ์„ฑ ์‹œ ์™ธ๋ถ€์—์„œ ์ž…๋ ฅ ๊ฐ’์„ ๋ฐ›์•„ run ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

  - name: Check PR title
        run: |
            title="${{ github.event.pull_request.title }}"
            if [[ $title =~ ^octocat ]]; then
            ...

Custom Actions

Action ๋‚ด๋ถ€์—์„œ shell exec ํ•˜๋Š” ๊ตฌ๊ฐ„ ์ค‘ ์™ธ๋ถ€๋กœ ๋ถ€ํ„ฐ ์ž…๋ ฅ ๊ฐ’์„ ๋ฐ›์•„ ๋ฐ˜์˜ํ•˜๋Š” ๊ตฌ๊ฐ„์€ ์ž ์žฌ์ ์œผ๋กœ ๋ชจ๋‘ ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ RCE ์ทจ์•ฝ์ ์˜ ์ฝ”๋“œ ํŒจํ„ด๊ณผ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

const { exec } = require("child_process");

exec(userValue, (error, stdout, stderr) => {
    if (error) {
        console.log(`error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.log(`stderr: ${stderr}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
});

๐Ÿ›ก Defensive techniques

In workflow

Change to actionsโ€™s input

Workflow์—์„  ๊ฐ€๊ธ‰์ ์ด๋ฉด ์™ธ๋ถ€์—์„œ ์ž…๋ ฅ ๊ฐ’์„ ๋ฐ›์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด Action์„ ๋งŒ๋“ค์–ด์„œ ํ•ด๋‹น Action์˜ Input์œผ๋กœ ์ „๋‹ฌ์‹œ์ผœ ์ด๋ฅผ run์—์„œ ๊บผ๋‚ด ์žฌ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ๋‹จ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

uses: fakeaction/checktitle@v3
with:
    title: ${{ github.event.pull_request.title }}

Change to env

์œ„์™€ ์œ ์‚ฌํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ run์— ์ง์ ‘ ๊ฐ’์„ ๋„ฃ์ง€ ์•Š๊ณ  env๋ฅผ ๊ฑฐ์ณ์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ run์œผ๋กœ ์ธํ•œ ์‰˜ ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ์— ๊ด€์—ฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— injection์„ ์™„ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

      - name: Check PR title
        env:
          TITLE: ${{ github.event.pull_request.title }}
        run: |
          if [[ "$TITLE" =~ ^octocat ]]; then
          ...

In Action

Action ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์—์„œ shell execute ๋“ฑ์˜ ๋™์ž‘์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ๊ฒ€์ฆํ•œ ํ›„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

const { exec } = require("child_process");

exec(escapeChar(userValue), (error, stdout, stderr) => {
    if (error) {
        console.log(`error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.log(`stderr: ${stderr}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
});

๐Ÿ•น Tools

๐Ÿ“Œ References

  • https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
  • https://github.com/hahwul/github-aciton-injection-test/pull/1
  • https://github.com/hahwul/github-aciton-injection-test/runs/6537735592?check_suite_focus=true
  • https://github.com/hahwul/github-aciton-injection-test/blob/9a7ec16a7cff8132ac080a16a41c521b7a42f880/.github/workflows/blank.yml