Comment on page
SCM IP Whitelisting Bypass
Many orgs combines SaaS-based source control management (SCM) systems (like GitHub or GitLab) with an internal, self-hosted CI solution (e.g. Jenkins, TeamCity) allowing these CI systems to receive webhook events from the SaaS source control vendors, for the simple purpose of triggering pipeline jobs.
Therefore, the orgs whitelists the IP ranges of the SCM allowing them to reach the internal CI system with webhooks. However, note how anyone can create an account in Github or Gitlab and make it trigger a webhook that could send a request to that internal CI system.
Moreover, note that while the IP range of the SCM vendor webhook service was opened in the organization’s firewall to allow webhook requests to trigger pipelines – this does not mean that webhook requests cannot be directed towards other CI endpoints, besides the ones that regularly listen to webhook events. We can try and access these endpoints to view valuable data like users, pipelines, console output of pipeline jobs, or if we’re lucky enough to fall on an instance that grants admin privileges to unauthenticated users (yes, it happens), we can access the configurations and credentials sections.
Imagine a Jenkins service which only allows GitHub and GitLab IPs to reach him externally.
In this scenario an attacker will trigger arbitrary webhooks in GitHub and GitLab to login inside the Jenkins and extract information.
- Only POST requests: Webhooks usually only allow you to send POST requests, however, some endpoints with interesting information need to be accessed via GET requests.
- If the Post is answered with a redirect it might follow it.
- Some CIs (Jenkins) allow to have a GET param indicating where to redirect the client once he managed to login, you can use this to redirect him to a specific page with a Get.
- Cannot control the body of the POST request: If you to send specific data in the POST body, you cannot.
- CSRF tokens: If the interesting endpoint is expecting CSRF tokens, you won't be able to extract them and provide them.
The login requires sending a POST request. Choosing to target the login endpoint solves the challenge of holding CSRF tokens, as this specific request doesn’t require it. But we still face the other challenge, as our abilities to modify the body of request remain limited.
A Jenkins login request looks as follows:
POST /j_acegi_security_check HTTP/1.1
We need to send the credentials we brute force somehow. Fortunately, the Jenkins login endpoint accepts a POST request with the fields sent as query parameters:
POST /j_acegi_security_check?j_username=admin&j_password=mypass123&from=%2F&Submit=Sign+in HTTP/1.1
[webhook json in body of request]
So how can we get it to work? We can create a new webhook in GitHub, setting the Jenkins login request URL as the payload URL. We can then create an automation using the GitHub API to brute-force the user account’s password, by modifying the password field, triggering the webhook, and inspecting the response in the repository webhook event log.
We fire the webhook, and see the results. All SCM vendors display the HTTP request and response sent through the webhook in their UI. If the login attempt fails, we’re redirected to the login error page.
But if the login is successful, we’re redirected to the main Jenkins page, and a session cookie is set.
So, we can brute-force Jenkins credentials and get a session cookie! However, we are a bit limited – we can only send one stateless request each time, and the cookie can’t be attached to our request, as we can’t control the headers.
Another option would be to try and obtain a Jenkins access token, which can be attached in the URL and used to send POST requests to Jenkins without the need of adding a CSRF token. This option is a bit more complex as it requires an attacker to somehow find both a self-hosted CI that is only accessible from SCM IP ranges and also obtain a valid access token to that CI. So for the time being – we’ll focus on more practical scenarios.
Let’s try sending the same request, but this time through GitLab. Due to the same limitations, we send the exact same POST request, adding the credentials as query parameters.
We trigger the request, but as opposed to GitHub – the response is 200. As in the last example, we used GitLab’s webhook service to brute-force a user and obtain a session cookie, but this time – the content of the response from Jenkins were relayed back to the GitLab UI, essentially providing us with the full content of the Jenkins main page. ****This is because GitLab followed the redirect adding the Cookie to the request:
It means we can:
- 1.brute force users and discover valid credentials,
- 2.use the valid credentials against the login page to login successfully,
- 3.get the contents of the internal Jenkins main page.
Jenkins login accepts a redirection parameter – “from”. Originally used to redirect users to the page they aimed to reach after they login, but in our case – a feature we can abuse to send a GET request attached with a session cookie to an internal Jenkins page of our choice. Let’s see how:
- 1.Set a webhook with the following URL:
A POST request is sent to Jenkins, and the authentication succeeds.
- We get a 302 redirect response, with a session cookie, and a redirection to the job console output page.
- The GitLab webhook service automatically follows the redirection with a GET request sent to the job console output page, along with the session cookie which is added to the request:
- Job console output is sent back and presented in the attacker’s GitLab webhook event log.
It’s important to mention here that Jenkins can be configured either to allow access to internal components without authentication, or in a way that enforces that only authenticated users can access the internal components. How does that affect us?
- If there’s no authentication configured, we can make the GitLab webhook service access any internal page in the CI, capture the response, and present it to us.
- If authentication is configured, we can try and brute force a user, and then use the credentials to access any internal page (like in the bullet above).