Stručne: Command Injection v PHP najbezpečnejšie zabránite tak, že nevoláte vôbec žiadny shell: mnohé úlohy vybavia priamo funkcie PHP alebo knižnice. Kde je externý program nutný, odovzdajte príkaz od PHP 7.4 ako pole do proc_open(), vtedy PHP spustí program bez shellu. Hodnoty kontrolujte voči zoznamu povolených, možnosti ukončujte pomocou -- a proces nechajte bežať s minimálnymi právami.
Prečo má systémové volanie takú veľkú moc
Úprava obrázkov, tvorba PDF, archivácia, antivírusová kontrola, dotaz na záznam DNS: na takéto úlohy aplikácie PHP rady volajú externé programy. Technicky je to pohodlné, stačí jeden riadok s exec() alebo shell_exec(). Z hľadiska bezpečnosti je to jedno z najnebezpečnejších miest v kóde.
Ak sa príkaz zloží ako text a vykoná ho shell, o tom, čo je príkaz a čo údaje, rozhoduje shell. Ak sa dovnútra dostane hodnota zvonka bez kontroly, môže zmeniť význam príkazu. Následok je v najhoršom prípade rovnaký ako pri PHP2Shell: vykonanie ľubovoľných príkazov na serveri s právami procesu PHP.
Rozdiel: PHP2Shell odstránite aktualizáciou PHP. Command Injection vo vlastnom kóde zostane po každej aktualizácii. K tomu pristupuje to, že takéto miesta často sedia v pomocných skriptoch určených len pre administrátorov, napríklad na údržbu, diagnostiku alebo importy, ktoré neskôr zostanú dostupné bez ochrany.
Kde vzniká Command Injection v PHP
Ohrozené je každé miesto, kde PHP spúšťa shell alebo program. Funkcie na to sú:
| Funkcia | Používa shell? | Poznámka |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
áno | príkaz ako text, shell ho interpretuje |
| operátor Backtick | áno | zodpovedá shell_exec() |
popen() |
áno | príkaz ako text |
proc_open() s reťazcom |
áno | príkaz ako text |
proc_open() s poľom |
nie | od PHP 7.4, argumenty sa odovzdávajú priamo |
mail() s piatym parametrom |
nepriamo | dodatočné parametre pre poštový program |
Typický nebezpečný vzor vyzerá takto. Názov súboru pochádza z požiadavky a skončí priamo v príkazovom riadku:
<?php
// NEBEZPEČNÉ: hodnota z požiadavky sa stane súčasťou príkazu shellu
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Prvá voľba: vôbec žiadny shell
Najúčinnejšou ochranou je nahradiť systémové volanie. OWASP to výslovne uvádza ako prvú obrannú líniu. Pre mnohé úlohy existujú v PHP vlastné funkcie alebo rozšírenia:
- Priečinky a súbory:
mkdir(),copy(),rename(),unlink() - Archívy:
ZipArchivealeboPharData - Obrázky: GD alebo Imagick ako rozšírenie PHP
- Sieťové dotazy:
dns_get_record(),gethostbyname(), cURL - Kontrolné súčty:
hash_file()
Tým úplne zmizne otázka, ktoré znaky shell interpretuje. Funkcie pre systémové príkazy sa potom dajú v konfigurácii aj zablokovať, ako opisuje sprievodca Správne použitie disable_functions.
Keď je externý program nutný: proc_open s poľom
Od PHP 7.4 prijíma proc_open() príkaz ako pole. Podľa príručky PHP sa proces potom spustí priamo, bez okľuky cez shell, a PHP preberá potrebné maskovanie argumentov. Znaky ako bodkočiarka alebo pipe už nemajú osobitný význam.
<?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);
Príklad robustným robia tri veci: program je v kóde pevne uvedený s plnou cestou, názov súboru sa kontroluje voči úzkemu vzoru a každý premenlivý argument začína absolútnou cestou, takže sa nemôže čítať ako možnosť. Tam, kde hodnota stojí sama, ukončite možnosti pomocou --, ak to program podľa dokumentácie podporuje, alebo odmietnite hodnoty, ktoré začínajú pomlčkou.
Pri úlohách, ktoré pravidelne potrebujú externé programy, napríklad spracovanie obrázkov alebo PDF vo väčšom rozsahu, sa oplatí oddelenie: webová aplikácia uloží len zákazku do fronty a oddelená služba na pozadí s vlastným používateľom a vlastnými právami ju vykoná. Proces PHP, ktorý odpovedá na požiadavky z internetu, potom už nepotrebuje funkcie pre systémové príkazy a dajú sa preňho zablokovať. Externé programy by mali mať navyše limity času a pamäte, aby jedna zákazka neblokovala server.
Zvláštnym prípadom je mail(). Piaty parameter odovzdáva dodatočné možnosti poštovému programu systému. Hodnoty z formulárov, napríklad adresa odosielateľa, sa tam nikdy nesmú dostať bez kontroly. Robustnejšia je poštová knižnica, ktorá odosiela cez SMTP a nepotrebuje systémové volanie.
escapeshellarg: správne použitie a znalosť hraníc
Kde existujúci kód ešte pracuje s príkazovými riadkami, je escapeshellarg() pre každú jednotlivú hodnotu minimálnym opatrením. Funkcia vloží hodnotu do úvodzoviek, takže ju shell spracuje ako práve jeden argument. escapeshellcmd() je pre vstupy od používateľa nevhodné, pretože pripúšťa dodatočné parametre, ako ukazuje OWASP na príklade.
Dôležité hranice escapeshellarg():
- Argument injection zostáva možná: maskovaná hodnota, ktorá začína pomlčkou, sa cieľovým programom napriek tomu číta ako možnosť.
- Windows sa správa inak: tam funkcia podľa príručky nahrádza určité znaky ako
%a"medzerami; hodnoty teda prídu zmenené. - Znaková sada: v závislosti od nastavenia locale servera sa môžu stratiť znaky mimo ASCII.
Nájdenie miest v existujúcom kóde
Hľadanie systémových volaní je rýchle a dá prehľadný zoznam:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Každé nájdené miesto potrebuje odpoveď na tri otázky: dá sa volanie nahradiť funkciami PHP? Prúdia dovnútra hodnoty zvonka? S akými právami proces beží? Posledná otázka je rozhodujúca pre škodu v kritickom prípade. Proces PHP s vlastným používateľom a bez práv zápisu na kód napácha menej, pozri Least Privilege: práva používateľa webservera.
Ak proces PHP-FPM alebo webservera spustí shell, je to silný varovný signál vlámania. Ako to rozpoznáte, opisuje Varovný signál: webserver spúšťa shell.
Ako konkrétne pomáha PHP SOS?
- Vyhľadáme všetky systémové volania vo vašej aplikácii, v pluginoch a v pomocných skriptoch.
- Volania tam, kde je to možné, nahradíme funkciami PHP a ostatné prestavíme na volania bez shellu.
- Nepotrebné funkcie zablokujeme v konfigurácii PHP a skontrolujeme práva používateľa PHP.
- Zmeny otestujeme a zdokumentujeme, ktoré programy smie aplikácia ešte volať.
Kontrolu vlastného softvéru ponúkame ako bezpečnostný audit a penetračný test.
Časté otázky
Stačí escapeshellarg proti Command Injection?
Zabráni tomu, aby sa hodnota vykonala ako ďalší príkaz. Nezabráni však tomu, aby ju volaný program čítal ako možnosť, ak začína pomlčkou. Preto hodnoty navyše kontrolujte a tam, kde to program podporuje, ukončujte možnosti pomocou --.
Aký je rozdiel medzi escapeshellarg a escapeshellcmd?
escapeshellarg spracuje jednu hodnotu ako práve jeden argument. escapeshellcmd maskuje len metaznaky v celom príkazovom riadku a pripúšťa ďalšie parametre. Pre vstupy od používateľa je správnou voľbou escapeshellarg, ešte lepšie je volanie bez shellu.
Mám exec a system zablokovať cez disable_functions?
Ak ich vaša aplikácia nepotrebuje, áno. Obmedzí to škodu po vlámaní. Nenahrádza to však bezpečný kód, pretože aplikácie, ktoré príkazy potrebujú, tieto funkcie naďalej potrebujú.
Ako rozpoznám, či bol Command Injection zneužitý?
Silným varovným signálom sú procesy PHP alebo webservera, ktoré spúšťajú shell ako sh alebo bash. K tomu pristupujú neznáme procesy, nové súbory v priečinkoch s uploadmi a nezvyčajné odchádzajúce spojenia.