Tinklaraštis

Nulinis baitas sulaužo PDO apsaugą: kaip escaped eilutė virsta SQL injekcija, o paprastas parametras — serverio kritimu

pdo-dual-cve
CVE / Duomenų bazės / Linux / PHP / Saugumas / SQL

Nulinis baitas sulaužo PDO apsaugą: kaip escaped eilutė virsta SQL injekcija, o paprastas parametras — serverio kritimu

Programuotojas viską daro teisingai: escapina vartotojo įvestį per PDO::quote(), įterpia rezultatą į SQL užklausą, iškviečia PDO::prepare(). Standartinis modelis, kuriuo pasitikima jau daugelį metų. Bet jei toje įvestyje yra vienas nulinis baitas, escaping sulūžta — ne ten, kur buvo tikrinama, o žingsniu vėliau, pačiame Firebird tvarkyklės viduje. Uždaromoji kabutė pradingsta, ir kitas užklausos gabalas, kuris turėjo likti duomenimis, virsta vykdomu SQL kodu.

Tai CVE-2025-14179 — SQL injekcija pdo_firebird tvarkyklėje, kurią rado Aleksey Solovev ir Nikita Sveshnikov iš Positive Technologies. NVD ją vertina CVSS 9.8 balu, Critical: pasiekiama per tinklą, žemas sudėtingumas, autentifikacijos nereikia, vartotojo sąveikos nereikia, rezultatas — pilnas jūsų duomenų kompromitavimas. Pats PHP Group vertina kiek kukliau — 7.4, High, pagal naujesnę CVSS 4.0 metodiką — bet tai vis tiek rimta problema, tiesiog kita skalė. CWE-89, klasikinė SQL injekcija.

Šalia iškilo antra, nesusijusi istorija — CVE-2025-14180, paslaugos atsisakymas (DoS) pdo_pgsql tvarkyklėje, skirtoje PostgreSQL. Čia CVSS 7.5, High: taip pat pasiekiama per tinklą, taip pat žemas sudėtingumas, bet paveikia tik prieinamumą — serveris nukrenta, o ne atiduoda kažkieno duomenis. Veikiančio viešo eksploito nė vienai iš dviejų spragų kol kas nematyti, bet techninė mechanika abiem atvejais aprašyta pakankamai detaliai, kad PoC parašymas užtruktų vieną vakarą, ne daugiau.

KAS YRA PDO

PDO — tai sluoksnis, per kurį PHP bendrauja su duomenų bazėmis. Idėja paprasta: programuotojas rašo užklausas per vieną bendrą sąsają, o po variklio gaubtu konkrečia tvarkyklė — MySQL, PostgreSQL, Firebird, SQLite — išverčia šiuos iškvietimus į reikiamos DBVS kalbą. Nauda milžiniška: nereikia atmintinai žinoti kiekvienos duomenų bazės escaping sintaksės atskirai, PDO tai paima ant savęs.

Būtent todėl klaida pačiame PDO — tai ne vienos aplikacijos problema, o tūkstančių aplikacijų problema vienu metu. Kai apsauga sulūžta tvarkyklės viduje, o ne programuotojo kode, visas kodas, kuris darė viską teisingai ir tiesiog pasitikėjo abstrakcija, staiga tampa pažeidžiamas — be jokios klaidos iš savo pusės.

KAIP VEIKIA KLAIDA FIREBIRD

Tvarkyklė renka SQL užklausą žetonas po žetono, ir PHP šaltinio kode šis surinkimas gyvena funkcijoje php_firebird_preprocess. Eilučių žetonams ten naudojamas iškvietimas strncat(sql_out, start, p - start) — įprasta C kalbos eilučių kopijavimo funkcija, vietoj saugesnio baitas-po-baito memcpy().

Ir kaip tik čia slypi spąstai. C kalboje eilutė tradiciškai baigiasi ten, kur pasitaiko nulinis baitas — tai įausta į pačią kalbos prigimtį. Jei vartotojo įvestyje toks baitas yra, strncat() nutraukia kopijavimą kaip tik tuo momentu. O uždaromoji kabutė, kurią sąžiningai padėjo PDO::quote() escaping metu, tiesiog nespėja patekti į užklausos buferį — ji stovėjo po nulinio baito, o kopijavimas ten jau nepasiekė. Escaping pats savaime suveikė teisingai; viskas sulūžta žingsniu vėliau, kai tvarkyklė iš naujo surenka užklausą ir suklijuoja žetoną be uždaromosios kabutės su kitu eilutės gabalu.

Rezultatas — užklausa praranda ribą tarp „duomenų” ir „kodo”. Viskas, kas ėjo toliau ir turėjo likti tiesiog eilutės turiniu, Firebird parseris skaito kaip to paties literalo tęsinį arba kaip naują SQL. Anot pačių PHP kūrėjų, to pakanka trivialiai injekcijai — žinoma, jei kitas užklausos gabalas taip pat yra atakuotojo kontroliuojamas.

KAIP TAI IŠNAUDOJAMA

Suveikimo sąlyga klastinga būtent savo įprastumu: reikia lygiai to modelio, kurį programuotojai jau metų metus laiko saugiu — escapinti per PDO::quote(), įterpti į dinamiškai konstruojamą užklausos eilutę, atiduoti PDO::prepare(). Dauguma net nesusimąsto apie riziką, nes įvestis formaliai praėjo per tvarkingą escaping. Problema ta, kad apsauga plyšta ne ten, kur buvo tikrinama, o giliau — tvarkyklės pusėje.

Atakuotojui belieka įterpti nulinį baitą į reikšmę, kuri pateks į tokią užklausą. Pačiame advisory kūrėjai tai parodo trimis eilutėmis: escapiname per PDO::quote() eilutę su vienu nuliniu baitu, escapiname per tą patį PDO::quote() antrą eilutę — pavyzdžiui, or 1=1--, — ir įterpiame abu rezultatus į WHERE name = ... AND name = .... Po escaping užklausa atrodo saugi. Kai tvarkyklė ją iš naujo surenka prieš siųsdama į Firebird, kabutė po nulinio baito pradingsta, ir abi WHERE sąlygos susilieja į vieną — į atrankos rezultatą patenka visos lentelės eilutės, o ne viena konkreti. Realioje atakoje vietoj demonstracinio or 1=1-- įterpiamas pilnavertis UNION SELECT, kuris ištraukia Firebird vidinius duomenis, įskaitant variklio versiją — tai, ko aplikacija apskritai niekada neturėjo atiduoti.

KAIP VEIKIA KLAIDA POSTGRESQL

Antra spraga gyvena pdo_pgsql tvarkyklėje ir suveikia tik viena sąlyga — jei aplikacija aiškiai įjungė PDO::ATTR_EMULATE_PREPARES, paruoštų išraiškų emuliaciją paties PHP priemonėmis vietoj PostgreSQL. Šiame režime PDO nesiunčia užklausos ir parametrų atskirai, o įstato reikšmes į SQL šabloną tiesiai PHP pusėje, escapina jas ir jau paruoštą eilutę meta serveriui.

Escaping čia atliekamas per PostgreSQL biblioteką funkciją PQescapeStringConn. Ir jei įvesties duomenyse pasitaiko baitų seka, netinkama dabartiniam ryšio koduotei — tyrėjų ataskaitoje kaip pavyzdys naudojamas \x99, — ši funkcija vietoj escaped eilutės grąžina NULL. Šis NULL, be jokio patikrinimo, skrieja tiesiai į vidinės PDO struktūros lauką plc->quoted.

Toliau į darbą įsijungia pdo_parse_params(), kuri kreipiasi į šios eilutės ilgį per makrokomandą ZSTR_LEN(plc->quoted) — o rodyklė tuo metu jau yra NULL. Nulinės rodyklės dereferencijavimas, segfault, operacinė sistema akimirksniu nutraukia PHP procesą.

KODĖL TAI PAVOJINGA NET BE RCE

Web aplikacijai toks kritimas — ne tiesiog trumpalaikis neprieinamumas. PHP worker’is miršta apdorojimo viduryje, o try/catch čia bejėgis: segfault vyksta paties interpretatoriaus C kodo lygmenyje, o ne PHP išimties lygmenyje, todėl aplikacijos klaidų apdorojimas jo tiesiog nepamato.

Positive Technologies tai išanalizavo sąlyginio skaitmeninių licencijų servisas pavyzdžiu: pinigai iš vartotojo balanso jau nurašyti, o po to — kritimas bandant išsaugoti užsakymo komentarą su netinkamu baitu. Rezultatas — pinigai nurašyti, užsakymo įrašo nėra, sandėlio likutis nepaliestas, sąskaita nesuformuota, o vartotojas tiesiog mato backend’o klaidą, nesuprasdamas, kas atsitiko su jo mokėjimu.

Rizika auga daugiažingsnėse operacijose be aiškių transakcijų. Su autocommit dalis pakeitimų ramiai spėja patekti į duomenų bazę prieš proceso kritimą — o tolesni operacijos žingsniai jau niekada neįvyks. Paskirstytoje architektūroje, kur servisai kviečia vienas kitą, toks pats gedimas gali išbalansuoti iš karto kelias sistemas — ypač jei prieš kritimą komponentas jau spėjo išsiųsti išorinę užklausą ar užfiksuoti tarpinį žingsnį kažkur kitur.

KAS LEMIA REZULTATĄ

Firebird SQL injekcijai rezultatas visada vienodas — jei kodas naudoja pdo_firebird su modeliu „quote plius dinaminis eilutės surinkimas”, jis pažeidžiamas, švelnesnio varianto čia nėra.

PostgreSQL kritimui viskas priklauso nuo vieno nustatymo — ar įjungtas PDO::ATTR_EMULATE_PREPARES. Su nativiais PostgreSQL paruoštais teiginiais ataka tiesiog nesuveiks: escaping per PQescapeStringConn šiame scenarijuje apskritai nenaudojamas. Daugelis šiuolaikinių framework’ų pagal nutylėjimą jau naudoja natives prepares — bet emuliacija vis dar gana dažnai įjungiama rankiniu būdu, dažniausiai norint apeiti tam tikrus PostgreSQL apribojimus paruoštiems teiginiams. Būtent šios aplikacijos ir yra rizikos zonoje.

ATNAUJINIMAS

PHP Group abi problemas jau uždarė. Firebird SQL injekcija ištaisyta 8.2.31, 8.3.31, 8.4.21 ir 8.5.6 versijose. PostgreSQL kritimas pataisytas anksčiau — 8.1.34, 8.2.30, 8.3.29, 8.4.16 ir 8.5.1 versijose. Savo versiją patikrinti galite taip:

php -v

Jei versija žemesnė nei reikalinga jūsų šakai — laikas atsinaujinti. Niuansas tas, kad standartinės Debian ir Ubuntu saugyklos PHP atnaujina gana neskubiai, ir gauti šviežią pataisos leidimą, tokį kaip 8.3.31, iš karto ne visada pavyksta. Daugelis production serverių laiko PHP per trečiosios šalies saugyklą Ondřej Surý (packages.sury.org) — būtent ji publikuoja aktualias minorines versijas šioms distribucijoms. Jei tokia saugykla pas jus prijungta, atnaujinimas jūsų šakos ribose — įprastas apt update ir reikiamo paketo diegimas (php8.3, php8.4 ir taip toliau). Prieš tai verta pažiūrėti apt-cache policy php8.3 (pakeiskite į savo versiją), kad pamatytumėte, kokia versija apskritai prieinama kaip kandidatė jūsų saugykloje.

Po atnaujinimo pakartotinai paleidus php -v turėtų parodyti versijos numerį ne žemesnį nei pataisytas. Jei jūsų distribucija ar image dar neatnešė reikiamo leidimo — PHP GitHub visada galima pasitikrinti su konkrečiu commit’u, kuris uždaro reikiamą CVE, ir įsitikinti, kad versija, kurią siūlo paketų tvarkyklė, iš tikrųjų apima taisymą, o ne tiesiog naujesnį numerį su kitais pakeitimais.

Jei atsinaujinti dabar neįmanoma, PostgreSQL spragai yra veikiantis laikinas sprendimas — tiesiog išjungti patį emuliacijos režimą:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

PDO pereis prie nativių PostgreSQL paruoštų teiginių — kodo kelio, kur pažeidžiama PQescapeStringConn apskritai nekviečiama. Bet tai būtent apėjimas, o ne taisymas: jis galioja tik tol, kol kažkas neįjungs šio atributo atgal.

Firebird SQL injekcijai tokio paprasto apėjimo nėra — apsauga lūžta tvarkyklės viduje jau po escaping, čia jokie nustatymai nepadės. Vienintelis dalykas, kuris realiai sumažina riziką iki atnaujinimo — aiškiai atmesti aplikacijos lygmenyje įvestį su nuliniu baitu dar prieš tai, kol ji pateks į PDO::quote():

if (strpos($input, "\0") !== false) {
    throw new InvalidArgumentException('Aptiktas nulinis baitas įvesties duomenyse');
}

Tai uždaro būtent šį atakos vektorių, bet ne pačią pažeidžiamumą tvarkyklėje — PHP atnaujinimas vis tiek reikalingas.

KODĖL TAI SVARBU

Abi istorijos smogia į tą pačią skaudžią vietą: PHP aplikacijos saugumas priklauso ne tik nuo to, ką parašė programuotojas, bet ir nuo to, kaip elgiasi runtime ir žemo lygio tvarkyklės po įprastų abstrakcijų gaubtu. Escaping mechanizmas, kuriuo pasitikima jau metų metus, gali sulūžti ne ten, kur buvo tikrinama, o sluoksniu giliau — ten, kur programuotojas fiziškai negali pasiekti, nežinodamas konkrečios tvarkyklės vidaus.

WordPress ir Joomla svetainių savininkams su savarankiškai sukonfigūruotomis jungtimis prie Firebird ar PostgreSQL, o iš tikrųjų ir bet kuriam custom PHP kodui, dirbančiam su šiomis duomenų bazėmis — rizika tiesioginė: SQL injekcija reiškia svetimus duomenis, pakeistus ar ištrintus įrašus, o PostgreSQL tvarkyklės kritimas — realus prastovas, kuris smogia konversijai ir reputacijai. Hostingo paslaugų teikėjams, turintiems krūvą PHP svetainių bendroje infrastruktūroje, tai dar ir proga surengti planinį PHP versijų auditą per visą parką — klientai patys tokių runtime pataisų beveik niekada neseka.

IŠVADOS

Turite PHP aplikacijų su Firebird tvarkykle per PDO? Atsinaujinkite iki 8.2.31, 8.3.31, 8.4.21 ar 8.5.6 dabar pat — tai kritinė injekcija be protingo apėjimo.

Dirbate su PostgreSQL per PDO su įjungtu PDO::ATTR_EMULATE_PREPARES? Arba atsinaujinkite iki 8.1.34, 8.2.30, 8.3.29, 8.4.16 ar 8.5.1, arba, jei tai dabar neįmanoma, aiškiai išjunkite šį atributą kaip laikiną priemonę.

Valdote hostingą su svetimomis PHP svetainėmis? Atlikite PHP versijų inventorizaciją per visą parką ir nesitikėkite, kad klientai atsinaujins patys: abi spragos išnaudojamos nuotoliniu būdu, be autentifikacijos, jei tik pažeidžiamas kodas kažkur aplikacijoje egzistuoja.

Ir bet kuriuo atveju nelaikykite PDO escaping besąlygine garantija. Kaip rodo abu atvejai, reali saugumo riba gali eiti ne ten, kur tikėjotės — ne jūsų pačių kode, o tvarkyklės lygmenyje, kurio vidaus dauguma programuotojų net nesusimąsto pažiūrėti.

Leave your thought here

El. pašto adresas nebus skelbiamas. Būtini laukeliai pažymėti *