poisoned-pipeline-execution-II

Lặn sâu vào CI/CD Pipelines Vulnerabilities (II) : Indirect Poisoned Pipeline Thi hành (I-PPE)

Trong bài viết trước của chúng tôi, we saw how to detect and protect against Direct Poisoned Pipeline Execution (D-PPE). We also saw how to detect that vulnerability using Máy quét Xygeni, as well as some protection mechanisms. 

 Ngộ độc Pipeline Thực thi (PPE) is produced when the attacker can modify the pipeline logic in either of two ways:

  • By modifying the CI config file (the pipeline) -> Thiết bị bảo hộ cá nhân trực tiếp (D-PPE)
  • By modifying files referenced by the pipeline (ví dụ: các tập lệnh được tham chiếu từ bên trong...) pipeline configuration file) -> Thiết bị bảo hộ cá nhân gián tiếp (I-PPE)
pp2

In this post, we will deep dive into Indirect PPE . But, before that, and as a complement to my previous post, let’s see first how GitHub manages the execution of pipelines and what are the protection mechanisms against D-PPE.

How does GitHub protect the execution of pipelines coming from PRs?

How does GitHub work regarding the execution of modified pipelines?

Thay đổi pipelines can come from Pushes or Pull Requests (PR). As a major best practice, it’s strongly recommended to avoid any direct “push” to a protected branch and use Pull Requests as a mechanism to enforce some review before accepting any contributed code. 

Pull Requests may arrive from two different sources:

  • PRs coming from dĩa
  • PRs coming from chi nhánh

PRs from dĩa can come either from công khai or riêng kho lưu trữ.

As we are dealing with PPE (Poisoned Pipeline Execution), our main point is not the “acceptance” of a PR but the execution of a modified pipeline during the PR’s acceptance/approval process. At the core of a PPE attack, there is an unintended execution of a  “malicious” modified pipeline. 

Nói tóm lại, bị đầu độc. Pipeline Việc thực thi (PPE) được thực hiện khi Kẻ tấn công có thể sửa đổi pipeline logic.

Có hai biến thể:

  • Thiết bị bảo hộ cá nhân trực tiếp (D-PPE): Trong kịch bản D-PPE, Kẻ tấn công sửa đổi tệp cấu hình CI. trong kho lưu trữ mà họ có quyền truy cập, bằng cách đẩy trực tiếp thay đổi lên một nhánh từ xa không được bảo vệ trên kho lưu trữ, hoặc bằng cách gửi yêu cầu kéo (PR) với thay đổi từ một nhánh hoặc một bản sao (fork). Kể từ CI pipeline Quá trình thực thi được xác định bởi các lệnh trong tệp cấu hình CI đã được sửa đổi, các lệnh độc hại của kẻ tấn công cuối cùng sẽ được chạy trên nút xây dựng sau khi quá trình xây dựng hoàn tất. pipeline được kích hoạt.
  • PPE gián tiếp (I-PPETrong một số trường hợp, khả năng sử dụng D-PPE không khả dụng đối với kẻ thù có quyền truy cập vào... SCM kho lưu trữ (ví dụ: nếu pipeline (được cấu hình để lấy tệp cấu hình CI từ một nhánh riêng biệt, được bảo vệ trong cùng kho lưu trữ). Trong trường hợp như vậy, thay vì đầu độc... pipeline Bản thân kẻ tấn công sẽ chèn mã độc vào các tệp được tham chiếu bởi... pipeline (ví dụ: các tập lệnh được tham chiếu từ bên trong...) pipeline tệp cấu hình)

Trong cả hai trường hợp, GitHub sẽ thực thi lệnh đã sửa đổi. pipeline không cần phải xem xét hoặc phê duyệt trước đó.

PRs from forks on công khai còn lại

GitHub allows configuring the behaviour when processing PRs coming from forks in public repos.

When a PR is coming from a fork, GitHub always forces some level of “approval” before executing the pipeline associated with the PR. This level of approval trades off from a weak to a strict approval.

At Org level (Org>>Settings>>Actions>>General), you can decide among several “approval” options:

ppe3

The strictest is the last one (“Require approval from all outside collaborators”) because GitHub will always require approval when the PR is coming from forks from outside collaborators. 

But even in this strict case, there are differences between collaborators with read and write permissions.

  • When the PR comes from a đọc người dùng, thực hiện pipeline is STOPPED until there is an approval of changes. If the approval is ok, then the modified pipeline được thực thi. 
  • When the PR comes from a viết người dùng, approval is not needed and the modified pipeline is always executed !! 
pp4

As a conclusion, PRs coming from forks on public repositories are lightly protected against PPE. There is some protection against external (read) users, but nothing related to internal (write) users.

Thế còn PRs coming from forks from private repos?

PRs from forks on riêng còn lại

In this scenario, GitHub provides some useful configuration settings.

ppe9

Above settings can be configured either at cơ quan hoặc tại repo cấp độ.

Thời Gian no option is checked, GitHub will ask for approval it will not execute the modified pipeline. This is the safest configuration!!

unsafest configuration là khi "Run workflows from fork pull request” is checked. In this case, same for both read and write users, Github will automatically execute the modified pipeline!! And this situation can be even tệ hơn nếu như "Gửi mã thông báo ghi đến các quy trình công việc từ nhánh con. pull requests"Và"Send secrets and variables to workflows from fork pull requests” are checked. Do not do this unless clearly justified!!

Nếu "Require approval for fork pull request Luồng công việc” is checked, the above situation is somewhat enhanced: GitHub will ask for approval and not execute the modified pipeline for the read user, but it will still execute it for a write user.

ppe6

Forks seen, what about PRs coming from branches?

PRs from chi nhánh

To protect this scenario you must rely on Quy tắc bảo vệ chi nhánh

At repo level, you can create branch protection rules for any branch. These rules add some constraints to modification of protected branches.

Although you configure a rule to “Yêu cầu một pull request trước khi hợp nhất"Và"Require approvals", đã sửa đổi pipeline will be automatically executed upon PR creation.The “approval” will only apply to the merge action.

ppe7

What about Indirect Poisoned Pipeline Thực hiện

As we saw above, D-PPE can be mitigated by using mục tiêu yêu cầu kéo, Nhưng nó does not apply to I-PPE.

If you use pull_request_target, the default checkout will be the base code. But if you want to validate some checks on the contributed code (PR code) you need to explicitly checkout the PR code. Therefore, if the PR code has modified any shell script invoked by the pipeline, the “base” (safe) pipeline will invoke the “modified” shell script → Indirect PPE!!

The solution to this is a bit more complicated (there is not a magic bullet like pull_request_target). 

Của chúng tôi pipeline is now safe to D-PPE because we are using pull_request_target. But it is still vulnerable to I-PPE. 

In our test example, we need to checkout the PR code basically to make the build, but the tests are executed on the artifact generated by the build. 

Vậy .. why don’t check out both codebases? 

  • Checkout PR code because is the contributed code what we want to build and test
  • Checkout Base code to run the original version of the pipeline and the build/tests scripts 

This might be done by checking out those codebases to different folders: the base code might be checked out to the root folder, and the PR to a different folder. In this case we would execute the build and the test script from the root folder against the code placed into the new folder.

This is an easy solution, of course!! But, for learning purposes I would like to introduce a quite interesting variant (…) 

GitHub workflow_run sự kiện kích hoạt

Bên cạnh đó mục tiêu yêu cầu kéo, GitHub provides another trigger event: workflow_run. This event allows execution of a pipeline conditioned to another pipeline’s execution

workflow_runmục tiêu yêu cầu kéo triggers are similar in one aspect : both will be executed in privileged mode and, despite the PR modifications, the base pipeline will be executed !! 

Let’s see our current pipeline:

name: PR TARGET CI   on:   pull_request_target:     branches: [ main ]   env:   MY_SECRET: ${{ secrets.MY_SECRET }}   jobs:   prt_build_test_and_merge:     runs-on: ubuntu-latest       steps:       # checkout PR code       - name: Checkout repository         uses: actions/checkout@v4         with:             # This is to get the PR code instead of the repo code           ref: ${{ github.event.pull_request.head.sha }}         # Simulation of a compilation       - name: Building ...         run: |           mkdir ./bin           touch ./bin/mybin.exe           ls -lR             # Simulation of running tests       - name: Running tests ...         id : run_tests         run: |           echo Running tests..           chmod +x runtests.sh           ./runtests.sh            echo Tests executed.                 #       # Let’s omit the check conditions at this moment …       #       - name: pr_check_conditions_to_merge         [...]

The build section is safe to D-PPE, but the test section is still vulnerable to I-PPE.

pipeline itself is safe to D-PPE due to the mục tiêu yêu cầu kéo trigger. But the test step is still vulnerable to I-PPE due to invoking an external shell script.

Avoiding I-PPE 

The purpose of the above pipeline is to build and test the contributed code, being safe to PPE. 

Vậy .. Why don’t split the pipeline into two ? One for building and another for testing..

  • Ngày 1 pipeline (Build CI) sẽ checkout the PR code (to build it), make the build and generate an artifact.
  • 2nd pipeline (Test CI) sẽ checkout the Base code (to avoid shell script modification) and execute the original scripts against the artifact. 
  • Để đồng bộ hóa Kiểm thử CI pipeline chạy SAU khi Build CI pipeline, chúng tôi sẽ sử dụng workflow_run Kích hoạt. 
ppe8

Theo cách này:

  • pipeline Build CI is an toàn cho cả hai D-PPE (do mục tiêu yêu cầu kéo) Và I-PPE (because it no longer executes the shell script).
  • pipeline Test CI cũng là an toàn cho cả hai D-PPE (do workflow_run) Và I-PPE (because it checkout the base code to get the original shell script) 

Let’s see the code of both pipelines according to these modifications …

1st pipeline (Build CI):

name: Build CI   on:   pull_request_target:     branches: [ main ]   env:   MY_SECRET: ${{ secrets.MY_SECRET }}   GITHUB_PAT: ${{ secrets.GH_PAT }}   jobs:                   prt_build_and_upload:     runs-on: ubuntu-latest     steps:       - name: Checking out PR code         uses: actions/checkout@v4         if: ${{ github.event_name == 'pull_request_target' }}         with:           # This is to get the PR code instead of the repo code           ref: ${{ github.event.pull_request.head.sha }}         - name: Building ...         run: |           mkdir ./bin           touch ./bin/mybin.exe 	    # Save some PR info for later use by the 2nd pipeline           echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt           echo "${{github.event.number}}" > ./bin/PR_ID.txt   	# Upload the binary as a pipeline artifact       - name: Archive building artifacts         uses: actions/upload-artifact@v3         with:           name: archive-bin           path: |             bin

2nd pipeline (Test CI):

ame: Test CI   on:   workflow_run:     workflows: [ 'PR TARGET CI' ]     types: [completed]     env:   MY_SECRET: ${{ secrets.MY_SECRET }}   GITHUB_PAT: ${{ secrets.GH_PAT }}     jobs:   deploy:     runs-on: ubuntu-latest     if: ${{ github.event.workflow_run.conclusion == 'success' }}     steps:           # By default, checks out base code (not PR code)       - name: Checkout repository         uses: actions/checkout@v4   	# Download the artifact       - name: 'Download artifact'         uses: actions/github-script@v6         with:           script: |             let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({                owner: context.repo.owner,                repo: context.repo.repo,                run_id: context.payload.workflow_run.id,             });             let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {               return artifact.name == "archive-bin"             })[0];             let download = await github.rest.actions.downloadArtifact({                owner: context.repo.owner,                repo: context.repo.repo,                artifact_id: matchArtifact.id,                archive_format: 'zip',             });             let fs = require('fs');             fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/myartifact.zip`, Buffer.from(download.data));   	# Unzip the artifact       - name: 'Unzip artifact'         run: |           unzip -o myartifact.zip         # Runs tests       - name: Running tests ...         id : run_tests         run: |           echo Running tests..           chmod +x runtests.sh           ./runtests.sh           echo Tests executed.   #       # Let’s omit the check conditions at this moment …       #       - name: pr_check_conditions_to_merge         [...] 

Wow… nice solution!! But ….. Are we safe? I’m afraid that no 😭

Indeed, we have introduced a new vulnerability!! Which one?  This will be the subject of our next post  🙂 … Stay tuned!! 

PS: Sorry, I can’t keep quiet 🤐 ..Have you heard about Nhiễm độc cổ vật ? 😂

Artifact Poisoning and Code Injection​​

Lặn sâu vào CI/CD PipelineCác điểm yếu (III)

Bảo vệ chống lại việc làm nhiễm độc hiện vật thông qua xác thực phần mềm

Lặn sâu vào CI/CD PipelineCác điểm yếu (IV)

Ngộ độc Pipeline Thi hành (PPE)

Lặn sâu vào CI/CD PipelineCác lỗ hổng (I)
sca-tools-software-composition-analysis-tools
Ưu tiên, khắc phục và bảo mật các rủi ro phần mềm của bạn
Đăng ký tài khoản miễn phí ngay.
Không cần thẻ tín dụng.

Bảo mật quá trình phát triển và phân phối phần mềm của bạn.

với bộ sản phẩm Xygeni