Röviden: A command injectiont PHP-ban a legbiztonságosabban úgy előzi meg, ha egyáltalán nem hív shellt: sok feladatot PHP-függvények vagy könyvtárak közvetlenül elvégeznek. Ahol külső programra van szükség, a parancsot a PHP 7.4 óta tömbként adja át a proc_open() függvénynek, ekkor a PHP shell nélkül indítja a programot. Az értékeket engedélyezési listával ellenőrzi, az opciókat a -- jellel zárja le, a folyamatot pedig minimális jogokkal futtatja.
Miért van egy rendszerhívásnak ennyi hatalma
Képfeldolgozás, PDF-generálás, archiválás, víruskeresés, egy DNS-bejegyzés lekérdezése: az ilyen feladatokhoz a PHP-alkalmazások szívesen hívnak külső programokat. Technikailag ez kényelmes, egy sor exec() vagy shell_exec() elég. Biztonságtechnikailag ez a kód egyik legveszélyesebb pontja.
Ha egy parancsot szövegként állítanak össze, és egy shell hajtja végre, a shell dönti el, mi a parancs és mi az adat. Ha egy külső érték ellenőrizetlenül belekerül, megváltoztathatja a parancs jelentését. A legrosszabb esetben a következmény ugyanaz, mint a PHP2Shellnél: tetszőleges parancsok végrehajtása a szerveren a PHP-folyamat jogaival.
A különbség: a PHP2Shellt egy PHP-frissítéssel hárítja el. A saját kódban lévő command injection minden frissítés után megmarad. Ehhez jön, hogy az ilyen helyek gyakran olyan segédszkriptekben vannak, amelyeket csak adminoknak szántak, például karbantartásra, diagnosztikára vagy importokra, és később védelem nélkül elérhetők maradnak.
Hol keletkezik a command injection PHP-ban
Veszélyeztetett minden hely, ahol a PHP shellt vagy programot indít. Ehhez a következő függvények szolgálnak:
| Függvény | Használ shellt? | Megjegyzés |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
igen | a parancs szövegként, a shell értelmezi |
| Backtick-operátor | igen | megfelel a shell_exec()-nek |
popen() |
igen | a parancs szövegként |
proc_open() stringgel |
igen | a parancs szövegként |
proc_open() tömbbel |
nem | a PHP 7.4 óta, az argumentumok közvetlenül kerülnek átadásra |
mail() ötödik paraméterrel |
közvetetten | további paraméterek a levelezőprogramnak |
Egy tipikusan nem biztonságos minta így néz ki. A fájlnév a kérésből jön, és közvetlenül a parancssorba kerül:
<?php
// NEM BIZTONSÁGOS: a kérésből származó érték egy shell-parancs részévé válik
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Első választás: egyáltalán nincs shell
A leghatékonyabb védelem a rendszerhívás kiváltása. Az OWASP ezt kifejezetten első védelmi vonalként nevezi meg. Sok feladathoz van a PHP-ban saját függvény vagy bővítmény:
- Könyvtárak és fájlok:
mkdir(),copy(),rename(),unlink() - Archívumok:
ZipArchivevagyPharData - Képek: GD vagy Imagick mint PHP-bővítmény
- Hálózati lekérdezések:
dns_get_record(),gethostbyname(), cURL - Ellenőrző összegek:
hash_file()
Ezzel teljesen megszűnik a kérdés, hogy a shell mely karaktereket értelmezi. Ráadásul a rendszerparancsokhoz tartozó függvények ekkor a konfigurációban letilthatók, ahogyan a A disable_functions helyes használata útmutató leírja.
Ha külső program szükséges: proc_open tömbbel
A PHP 7.4 óta a proc_open() a parancsot tömbként is elfogadja. A PHP kézikönyve szerint a folyamatot ekkor közvetlenül indítja el, a shell kerülőútja nélkül, és a PHP átveszi az argumentumok szükséges maszkolását. Az olyan karaktereknek, mint a pontosvessző vagy a pipe, ekkor már nincs különleges jelentésük.
<?php
declare(strict_types=1);
$name = basename((string) ($_POST['datei'] ?? ''));
if (!preg_match('/^[a-z0-9_-]{1,64}\.(jpg|png)$/', $name)) {
throw new InvalidArgumentException('Ungültiger Dateiname.');
}
$befehl = ['/usr/bin/convert', '/srv/app/uploads/' . $name, '-resize', '800x800',
'/srv/app/vorschau/' . $name];
$prozess = proc_open($befehl, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($prozess)) {
throw new RuntimeException('Programm konnte nicht gestartet werden.');
}
$fehlerausgabe = stream_get_contents($pipes[2]);
$status = proc_close($prozess);
Három dolog teszi a példát robusztussá: a program teljes elérési úttal rögzítetten áll a kódban, a fájlnevet szűk mintával szemben ellenőrzi, és minden változó argumentum abszolút úttal kezdődik, így nem olvasható opcióként. Ahol egy érték egyedül áll, zárja le az opciókat a -- jellel, feltéve, hogy a program a dokumentáció szerint támogatja, vagy utasítsa el a kötőjellel kezdődő értékeket.
Azoknál a feladatoknál, amelyek rendszeresen külső programokat igényelnek, például nagyobb volumenű kép- vagy PDF-feldolgozásnál, érdemes szétválasztani: a webalkalmazás csak egy megbízást tesz egy sorba, egy különálló háttérszolgáltatás saját felhasználóval és saját jogokkal hajtja végre. Az internetről érkező kéréseket kiszolgáló PHP-folyamatnak ekkor már nincs szüksége a rendszerparancsok függvényeire, és ezek letilthatók számára. A külső programoknak ezenkívül idő- és memóriahatáraik legyenek, hogy egyetlen megbízás se blokkolja a szervert.
Különleges eset a mail(). Az ötödik paraméter további opciókat ad át a rendszer levelezőprogramjának. Az űrlapokból származó értékek, például egy feladó címe, ott soha nem kerülhetnek ellenőrizetlenül. Robusztusabb egy levelezőkönyvtár, amely SMTP-n keresztül küld, és nem igényel rendszerhívást.
escapeshellarg: helyes használat és a határok ismerete
Ahol a meglévő kód még parancssorokkal dolgozik, az escapeshellarg() minden egyes értékre a minimális intézkedés. A függvény idézőjelek közé teszi az értéket, így a shell pontosan egy argumentumként kezeli. Az escapeshellcmd() felhasználói bemenetekhez alkalmatlan, mert további paramétereket megenged, ahogyan az OWASP egy példán mutatja.
Az escapeshellarg() fontos határai:
- Az argumentum-injection továbbra is lehetséges: egy maszkolt érték, amely kötőjellel kezdődik, a célprogram számára ettől még opcióként olvasható.
- A Windows másképp viselkedik: ott a függvény a kézikönyv szerint bizonyos karaktereket, mint a
%és a", szóközökre cseréli; az értékek tehát módosulva érkeznek. - Karakterkészlet: a szerver locale-beállításától függően a nem ASCII karakterek elveszhetnek.
A találatok megkeresése a meglévő kódban
A rendszerhívások keresése gyorsan megvan, és átlátható listát ad:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Minden találatnak három kérdésre kell választ adnia: kiváltható-e a hívás PHP-függvényekkel? Kerülnek-e bele külső értékek? Milyen jogokkal fut a folyamat? Az utolsó kérdés döntő a kár mértékéhez a legrosszabb esetben. Egy saját felhasználóval rendelkező, a kódra írásjog nélküli PHP-folyamat kevesebb kárt okoz, lásd Least Privilege: a webszerver-felhasználó jogai.
Ha egy PHP-FPM- vagy webszerver-folyamat shellt indít, az erős figyelmeztető jel egy betörésre. Hogyan ismeri fel, azt a Figyelmeztető jel: a webszerver shellt indít írja le.
Hogyan segít konkrétan a PHP SOS?
- Megkeressük az összes rendszerhívást az alkalmazásában, a bővítményekben és a segédszkriptekben.
- A hívásokat, ahol lehet, PHP-függvényekre cseréljük, a többit pedig shell nélküli hívásokra állítjuk át.
- Letiltjuk a nem szükséges függvényeket a PHP-konfigurációban, és ellenőrizzük a PHP-felhasználó jogait.
- Teszteljük a módosításokat, és dokumentáljuk, mely programokat hívhatja még az alkalmazás.
A saját szoftver ellenőrzését biztonsági auditként és penetrációs tesztként kínáljuk.
Gyakori kérdések
Elég az escapeshellarg a command injection ellen?
Megakadályozza, hogy egy értéket további parancsként hajtsanak végre. Azt nem akadályozza meg, hogy az értéket a meghívott program opcióként olvassa, ha kötőjellel kezdődik. Ezért az értékeket emellett ellenőrizze, és ahol a program támogatja, zárja le az opciókat a -- jellel.
Mi a különbség az escapeshellarg és az escapeshellcmd között?
Az escapeshellarg egyetlen értéket pontosan egy argumentumként kezel. Az escapeshellcmd egy teljes parancssorban csak a metakaraktereket maszkolja, és további paramétereket megenged. Felhasználói bemenetekhez az escapeshellarg a helyes választás, a shell nélküli hívás még jobb.
Tiltsam le az exec és a system függvényt a disable_functions-szal?
Ha az alkalmazása nem igényli őket, igen. Ez korlátozza a kárt egy betörés után. Biztonságos kódot viszont nem helyettesít, mert a parancsokat igénylő alkalmazásoknak továbbra is szükségük van a függvényekre.
Honnan tudom, hogy kihasználtak-e egy command injectiont?
Erős figyelmeztető jel az olyan PHP- vagy webszerver-folyamat, amely shellt, például sh-t vagy bash-t indít. Ehhez jönnek az ismeretlen folyamatok, az új fájlok a feltöltési mappákban és a szokatlan kimenő kapcsolatok.