← all walkthroughs

Barrier

Linux· Medium· Credential Access· Privilege Escalation
owned
2026-10-12
time to own
43m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a public GitLab repository whose commit history still contained a developer's password. Those credentials worked on both GitLab and the Authentik single-sign-on server fronting it.

The installed GitLab version was vulnerable to a SAML signature-wrapping authentication bypass, which let me obtain an administrator session without the administrator's password. From there, a paused CI/CD runner was resumed and used to run a pipeline job whose log printed an Authentik API token out of the environment.

That token had enough privilege to create a new Authentik account, promote it to admin, and impersonate an existing user, which handed over that user's remote-desktop session. A database behind that remote-desktop service stored a second user's SSH key and passphrase in plaintext, and that second user's own shell history (world-readable) contained the administrator password in plain text, completing full root compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"

Some commands also carry a value in angle brackets — <guac_password>, <guac_user>, <leaked-password>, <runner-tag>, <second-user> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService/vhost enumeration and public repository discovery (MITRE T1595, T1593)
Mapped the exposed services and found a public GitLab repository
Scanning the host showed SSH, Apache Tomcat on 8080, and two Authentik identity-provider endpoints on 9000/9443. The web ports redirected to a GitLab virtual host whose help page disclosed the exact GitLab version. GitLab's public project list included a repository named 'gitconnect' owned by a user named satoru, and Tomcat's /guacamole path redirected to the Authentik single sign-on login, confirming Authentik fronts both GitLab and the remote-desktop (Guacamole) service.
GitLab /help disclosed version 17.3.2; public project 'gitconnect' owned by satoru; /guacamole redirected to Authentik on 9443.
Exact commands 3
shell

Resolve the discovered hostnames.

$ echo "$TARGET barrier.vl gitlab.barrier.vl" | sudo tee -a /etc/hosts
shell

Identify open services: 22, 80/443, 8080, 9000, 9443.

$ nmap -sV -sC -p- -T4 $TARGET
shell

Finds /manager (needs creds) and /guacamole (redirects to Authentik SSO).

$ ffuf -u http://barrier.vl:8080/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt
FixRemove leaked secrets from source history and stop password reuseHigh
WeaknessA commit from early in the 'gitconnect' project's history still contained a developer's plaintext password, and that same password was reused across GitLab and the Authentik identity provider, so one leaked secret compromised both systems.
FixTreat any secret ever committed as compromised: rotate it immediately, then purge it from history with git filter-repo or BFG Repo-Cleaner and force-push. Enable GitLab's push-protection/secret-detection scanning to block future commits containing credentials, and enforce unique passwords per system (ideally via SSO-only authentication with no separate local password).
2Initial AccessCredentials exposed in version-control history; password reuse (T1552.001, T1078)
Recovered a developer's password from old commit history
The current files in the 'gitconnect' repository were clean, but an earlier commit still contained a plaintext password for the user satoru. Because that password was reused between GitLab and the Authentik SSO, one leaked secret opened both systems.
A historical commit in gitconnect contained satoru's password, which was accepted on both GitLab and Authentik.
Exact commands 3
shell

Clone the public repository.

$ git clone https://gitlab.barrier.vl/satoru/gitconnect.git && cd gitconnect
shell

Search every commit (not just current files) for leaked secrets.

$ git log -p --all | grep -iE 'pass|secret|token'
shell

Validate the recovered credential on both GitLab and Authentik.

$ # Sign in to https://gitlab.barrier.vl and https://barrier.vl:9443 as satoru / <leaked-password>
3Privilege EscalationSAML signature-wrapping authentication bypass — CVE-2024-45409 (T1190, T1606)
Bypassed GitLab SAML authentication to obtain an administrator session
The installed GitLab release used a SAML (Ruby-SAML) library with a known signature-wrapping flaw: it validates the cryptographic signature on the identity provider's original assertion but can be tricked into reading the logged-in username from a second, my own assertion placed alongside it. Starting a real SSO login as satoru and substituting a crafted SAML response for the one GitLab expected, in the same live login flow, returned a session for the built-in administrator account.
After substituting the crafted SAML response in the live SSO callback, the GitLab API confirmed an administrator session for the built-in admin account.
Exact commands 4
shell

One cookie jar/session for the whole flow; keep the original RelayState and session cookies.

$ # Intercept a real satoru SSO login (Burp/mitmproxy); capture the SAMLResponse posted to /users/auth/saml/callback
shell

Public PoC: search 'CVE-2024-45409 ruby-saml poc' / the ruby-saml GHSA advisory.

$ # Use the public CVE-2024-45409 PoC against the captured response to produce a response naming the built-in admin account, keeping the original signed assertion byte-for-byte intact
shell

Sync local clock with the server first — session cookies depend on it.

$ # Replay the callback request with only SAMLResponse swapped for the crafted one; keep RelayState and cookies unchanged
shell

Confirm is_admin:true on the resulting session.

$ curl -sk -b jar.txt https://gitlab.barrier.vl/api/v4/user
FixPatch GitLab's SAML library against the signature-wrapping bypassCritical
WeaknessThe installed GitLab release bundled a Ruby-SAML version vulnerable to CVE-2024-45409, letting an unauthorised user forge the identity asserted in an otherwise validly-signed SAML response and obtain an administrator session.
FixUpgrade GitLab to a release bundling the patched ruby-saml gem (17.3.3+ / 17.2.5+ / 17.1.7+ or later) and verify by checking the gem version. Where feasible, also enable SAML response encryption, which independently closes this class of signature-wrapping attack.
4Credential AccessSecrets exposed via CI/CD job environment (T1552.001, T1072)
Dumped a secret token from a CI/CD runner's job environment
As GitLab administrator, I found a paused build runner and resumed it, then pushed a minimal pipeline to a new project that simply printed the job's environment variables. The resulting job log disclosed an Authentik API token that had been configured as a runner environment variable.
The resumed runner's job log contained a valid Authentik API bearer token.
Exact commands 2
shell

Requires the admin session from step 3.

$ # In GitLab Admin > CI/CD > Runners: resume the paused runner
shell

Push to a new project targeting the resumed runner; read the job log for the leaked token.

$ printf 'dump:\n  image: {name: redis:alpine, pull_policy: if-not-present}\n  tags: [<runner-tag>]\n  script: [env]\n' > .gitlab-ci.yml && git add .gitlab-ci.yml && git commit -m ci && git push
FixStop storing long-lived secrets as plain CI/CD runner environment variablesHigh
WeaknessAn Authentik API token was configured as a plaintext environment variable on a shared CI/CD runner, so any pipeline job able to run on that runner — including one created by a newly-escalated admin — could print the secret to its log with a trivial 'env' command.
FixMark CI/CD variables that are secrets as 'Masked' and 'Protected' so they never appear in logs and never reach unprotected branches/jobs, scope runners/tags tightly, and prefer short-lived, narrowly-scoped tokens (or GitLab OIDC/CI_JOB_JWT federation) over static long-lived API tokens.
5Lateral MovementAbuse of an over-privileged API token to create an account and impersonate a user (T1136, T1550)
Used the leaked API token to escalate privilege in Authentik and impersonate another user
The leaked bearer token was valid against the Authentik management API and was over-privileged: it could create accounts and manage group membership. I created a new account, added it to the Authentik administrators group, then used Authentik's built-in 'impersonate user' admin feature to act as an existing lower-privileged user and inherit that user's active remote-desktop (Guacamole) session.
A new account was added to the Authentik admin group via the management API and used to impersonate an existing user, whose Guacamole connection then opened an interactive terminal session.
Exact commands 3
shell

Enumerate users and the admin group's UUID.

$ curl -s -H 'Authorization: Bearer <AUTHENTIK_TOKEN>' http://barrier.vl:9000/api/v3/core/users/
shell

Three calls to /api/v3/core/users/, /users/{pk}/set_password/, /groups/{uuid}/add_user/.

$ # Via the API: create a new user, set its password, add it to the admin group UUID found above
shell

Then open the Guacamole tile/connection to land an interactive shell as that user.

$ # Log into https://barrier.vl:9443 as the new admin account, open Admin Interface > Impersonate, select the target user
FixScope Authentik API tokens to least privilege and restrict impersonationCritical
WeaknessThe leaked Authentik API token had full administrative rights — enough to create a new user, add it to the administrators group, and impersonate any existing user — far beyond what the CI job that held it needed.
FixIssue API tokens scoped to the minimum permissions a given integration requires, rotate and expire them regularly, and restrict the 'impersonate' permission and admin group membership to a small, monitored set of break-glass accounts with alerting on use.
6Credential AccessPlaintext secrets stored in an application database (T1552.001, T1552.003)
Recovered another user's SSH key from the Guacamole database
Guacamole's own configuration file disclosed the credentials for its backing MySQL database, which was reachable locally. That database stored connection parameters for other hosts in plaintext, including a full RSA private key, its passphrase, and the username and port to use it with.
Guacamole_connection_parameter table in the local MySQL instance contained an RSA private key, its passphrase, and an SSH username/port for a second account.
Exact commands 3
shell

Discloses the local MySQL username/password for the Guacamole database.

$ cat /etc/guacamole/guacamole.properties
shell

Dumps stored connection secrets, including an RSA private key, its passphrase, and a second SSH username.

$ mysql -u <guac_user> -p<guac_password> guac_db -e 'select * from guacamole_connection_parameter;'
shell

Use the recovered key and passphrase to open a shell as the second account.

$ chmod 600 recovered_key && ssh -i recovered_key <second-user>@barrier.vl -oHostKeyAlgorithms=+ssh-rsa
FixRemove plaintext secrets from the Guacamole database and lock down shell history filesHigh
WeaknessGuacamole stored a full SSH private key and passphrase as plaintext connection parameters in its MySQL database, and a separate account's .bash_history file was world-readable and contained a plaintext sudo password typed during an earlier command.
FixStore Guacamole connection credentials via a secrets manager or its vault/LDAP integration instead of plaintext database fields, encrypt the guac_db at rest, and restrict guacamole.properties/database permissions to the service account only. Separately, set shell history files to mode 600, avoid typing passwords as command arguments (use sudo's interactive prompt or an askpass helper), and audit history files for leaked secrets.
7Privilege EscalationSensitive data left in a world-readable shell history file; credential reuse for privilege escalation (T1552.003, T1078.003)
Read the administrator password from a world-readable shell history and escalated to root
The second account's shell history file was owned by root but left world-readable, and it contained a prior invocation where the sudo password had been typed as part of a piped command — leaving it in plaintext in the history. That password was valid for sudo, giving full root access.
The second account's .bash_history contained the plaintext administrator password; it unlocked sudo, confirmed by 'id' returning uid=0(root).
Exact commands 3
shell

World-readable despite being root-owned; reveals the plaintext sudo password used in an earlier command.

$ cat ~/.bash_history
shell

Enter the recovered password when prompted.

$ sudo -i
shell

Confirm uid=0(root) before reading the flag -> <root.txt>

$ id && cat /root/root.txt
uid=0(root)

Exposed services

22/tcp
8080/tcp
9000/tcp
9443/tcp