Skip to content

Full read SSRF in Analytics Dashboard bypassing all localhost restrictions (GET and POST)

Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #2697456 by joaxcar on 2024-09-04, assigned to GitLab Team:

Report | Attachments | How To Reproduce

Report

Summary

There exists a full read (GET and POST) SSRF in the Analytics Dashboard integration. An attacker can fully control the port, path and parameters on the request and read the full response. The attack can target localhost even on instances where all local access has been turned off

Screenshot_2024-09-04_at_10.48.11.png

The issue is that the method query_data that is used to fetch data from Cube looks like this

def query_data  
      options = {  
        allow_local_requests: true,  
        headers: cube_security_headers  
      }

      begin  
        response = if params[:path] == 'meta'  
                     Gitlab::HTTP.get(cube_server_url(params[:path]), options)  
                   else  
                     ::Gitlab::HTTP.post(  
                       cube_server_url(params[:path]),  
                       options.merge(body: { query: params[:query], queryType: params[:queryType] }.to_json)  
                     )  
                   end

        body = Gitlab::Json.parse(response.body)  
      rescue Gitlab::Json.parser_error, *Gitlab::HTTP::HTTP_ERRORS => e  
        return ServiceResponse.error(message: e.message, reason: :bad_gateway)  
      end  
...  
    end  

where you can see that the allow_local_requests: true flag is set. This is dangerous as the cube_server_url is user controlled. If the request hits an internal service that do not respond with JSON the return ServiceResponse.error(message: e.message, reason: :bad_gateway) will return the full response as is and return it in the GraphQL error message.

When the requested path is not meta the ::Gitlab::HTTP.post will run. And as this will follow redirects by default we can abuse this to hit any service as either a GET (by sending the request to a 301 redirect that converts the request to GET) or as POST (by sending the request to a 307 redirect that preserve POST).

The base request will go to http://YOURSERVER.com/cubejs-api/v1/load, this is not a problem as we can just make our server respond with a redirect like this http://localhost:PORT/PATH?QUERY and that will be the request that hits the backend.

I have made a small script to scan ports using this but have not run it against prod. I have tested redirecting to http://127.0.0.1:8080 on prod and that gave me the gitlab main page back in the response (as this is the port of the webserver).

Please get back to me if I am allowed to test further to see what I can find, or if there is a list of ports I can try out.

Steps to reproduce

I have verified this on my local GDK instance, where it is easier to see that it is hitting the local host. But as its quite cumbersome to set it up I will provide a POC for prod first

(note step 4 IP 159.223.181.253:4567 this is my own collector server on Digitalocean. Its possible to set up your own, but its only needed to get a valid configuration and does not do anything else, if you want to run your own deploy https://gitlab.com/gitlab-org/analytics-section/product-analytics/devkit on a droplet)

  1. Create a new Group with an ultimate subscription. Use a trial account
  2. In the new group create a new project
  3. Go to https://gitlab.com/GROUPNAME/PROJECTNAME/-/settings/analytics
  4. Fill out the form with
  • Snowplow configurator connection string: http://test:test@159.223.181.253:4567
  • Collector host: https://example.com
  • Cube API URL: https://joaxcar.com/get/1234/
  • Cube API key: anything

Screenshot_2024-09-04_at_12.04.07.png

  1. Save the configuration
  2. Go to https://gitlab.com/GROUPNAME/PROJECTNAME/-/analytics/dashboards/product-analytics-onboarding
  3. Select the left option connect to your own provider, make sure to have devtools open
  4. Search for graphql in the network tab and look for the last one it should contain an error in the response saying that 127.0.0.1:1234 did not accept a connection. This is as there is nothing running on 1234
  5. Go back to https://gitlab.com/GROUPNAME/PROJECTNAME/-/settings/analytics and change the Cube API URL to https://joaxcar.com/get/8181/
  6. Do step 6-8 again. You should now have the full content of gitlab.com main page in the error instead. This has now been served from localhost:8181

Screenshot_2024-09-04_at_12.04.20.png

(only for gitlab triage)To play with this localy on GDK follow these steps:

  1. Install GDK
  2. Follow the guide here to set up a local analytics system https://gitlab.com/gitlab-org/analytics-section/product-analytics/devkit
  3. Connect it to your GDK like so https://gitlab.com/gitlab-org/analytics-section/product-analytics/devkit#connecting-gdk-to-your-devkit
  4. Now you should be able to use this server to set everything up in GDK
  5. Host a "redirect service" somewhere that will throw a redirect on any request ending in cubejs-api/v1/load. My POC above use this config in .htaccess
[RewriteRule ^get/([0-9]+)/cubejs-api/v1/load$ /poc/gitlab/port.php?port=$1 [L,QSA]  
RewriteCond %{REQUEST_URI} ^/post/([0-9]+)/cubejs-api/v1/load$](<RewriteCond %{REQUEST_URI} ^/get/([0-9]+)/cubejs-api/v1/load$  
RewriteRule ^get/([0-9]+)/cubejs-api/v1/load$ /poc/gitlab/port.php?port=$1 [L,QSA]>)  

that will redirect all requests based on the path to my script at /poc/gitlab/port.php?port=$1 looking like this

<?php  
header("Location: http://127.0.0.1:" . $_GET['port'], true, 301);  
exit;  
?>  

It is also possible to build a more efficient port-scan by just using a single redirect that will return different ports each time its requested, its then possible to loop the graphql request and scan the full port range quickly

Impact

Full read (and potentially write) SSRF POST and GET that bypass all localhost filters.

I know that this kind of SSRF got rated as critical here https://hackerone.com/reports/878779 but that was unauthenticated which should put this one at high at least. But as stated I have not done extensive testing against prod, it might be that this SSRF using POST could also have integrity impact.

What is the current bug behavior?

Cube requests are allowed to hit localhost despite not being allowed in settings.

What is the expected correct behavior?

As Cube URL is user controlled it should follow the same rules as other webhooks and services

Output of checks

This bug happens on GitLab.com

Impact

Full read (and potentially write) SSRF POST and GET that bypass all localhost filters.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: