Pe scurt: Preveniți cel mai sigur Command Injection în PHP dacă nu apelați deloc un shell: multe sarcini sunt îndeplinite direct de funcții PHP sau de biblioteci. Acolo unde este necesar un program extern, transmiteți comanda, începând cu PHP 7.4, ca array către proc_open(), iar PHP pornește atunci programul fără shell. Valorile le verificați față de o listă de permisiuni, încheiați opțiunile cu -- și rulați procesul cu drepturi minime.
De ce are un apel de sistem atâta putere
Prelucrare de imagini, generare de PDF, arhivare, scanare antivirus, interogarea unei înregistrări DNS: pentru astfel de sarcini, aplicațiile PHP apelează cu plăcere programe externe. Tehnic este comod, o linie cu exec() sau shell_exec() este suficientă. Din punct de vedere al securității, este unul dintre cele mai periculoase locuri din cod.
Dacă o comandă este compusă ca text și executată de un shell, shell-ul decide ce este comandă și ce sunt date. Dacă o valoare din exterior ajunge înăuntru nevalidată, ea poate schimba sensul comenzii. Consecința este, în cel mai rău caz, aceeași ca la PHP2Shell: execuția de comenzi arbitrare pe server, cu drepturile procesului PHP.
Diferența: PHP2Shell o remediați cu o actualizare PHP. O Command Injection din codul propriu rămâne după fiecare actualizare. La aceasta se adaugă faptul că astfel de locuri se află adesea în scripturi auxiliare destinate doar administratorilor, de exemplu pentru mentenanță, diagnosticare sau importuri, și rămân ulterior accesibile fără protecție.
Unde apare Command Injection în PHP
Este expus orice loc în care PHP pornește un shell sau un program. Funcțiile pentru aceasta sunt:
| Funcție | Folosește un shell? | Observație |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
da | Comandă ca text, shell-ul o interpretează |
| Operatorul backtick | da | Corespunde cu shell_exec() |
popen() |
da | Comandă ca text |
proc_open() cu șir |
da | Comandă ca text |
proc_open() cu array |
nu | Din PHP 7.4, argumentele sunt transmise direct |
mail() cu al cincilea parametru |
indirect | Parametri suplimentari către programul de e-mail |
Un tipar tipic nesigur arată așa. Numele fișierului provine din cerere și ajunge direct în linia de comandă:
<?php
// NESIGUR: valoarea din cerere devine parte a unei comenzi shell
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Prima alegere: niciun shell
Cea mai eficientă protecție este înlocuirea apelului de sistem. OWASP o menționează explicit ca prima linie de apărare. Pentru multe sarcini există în PHP funcții sau extensii proprii:
- Directoare și fișiere:
mkdir(),copy(),rename(),unlink() - Arhive:
ZipArchivesauPharData - Imagini: GD sau Imagick ca extensie PHP
- Interogări de rețea:
dns_get_record(),gethostbyname(), cURL - Sume de control:
hash_file()
Astfel dispare complet întrebarea ce caractere interpretează un shell. În plus, funcțiile pentru comenzi de sistem pot fi blocate apoi în configurare, așa cum descrie ghidul Folosirea corectă a disable_functions.
Când este necesar un program extern: proc_open cu array
Începând cu PHP 7.4, proc_open() acceptă comanda ca array. Potrivit manualului PHP, procesul este pornit atunci direct, fără ocolul prin shell, iar PHP preia mascarea necesară a argumentelor. Caractere precum punct și virgulă sau pipe nu mai au atunci o semnificație specială.
<?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);
Trei lucruri fac exemplul robust: programul este fixat în cod cu calea completă, numele fișierului este verificat față de un tipar strâmt, iar fiecare argument variabil începe cu o cale absolută, deci nu poate fi citit ca opțiune. Acolo unde o valoare stă singură, încheiați opțiunile cu --, dacă programul o acceptă conform documentației, sau respingeți valorile care încep cu o cratimă.
La sarcinile care au nevoie regulat de programe externe, de exemplu prelucrarea de imagini sau PDF-uri în volum mare, merită o decuplare: aplicația web depune doar o sarcină într-o coadă, iar un serviciu separat de fundal, cu utilizator propriu și drepturi proprii, o execută. Procesul PHP care răspunde cererilor din internet nu mai are atunci nevoie de funcții pentru comenzi de sistem, iar acestea pot fi blocate pentru el. Programele externe ar trebui să aibă în plus limite de timp și de memorie, pentru ca o singură sarcină să nu blocheze serverul.
Un caz special este mail(). Al cincilea parametru transmite opțiuni suplimentare programului de e-mail al sistemului. Valorile din formulare, de exemplu o adresă de expeditor, nu au voie să ajungă acolo nevalidate. Mai robustă este o bibliotecă de e-mail care trimite prin SMTP și nu are nevoie de apel de sistem.
escapeshellarg: folosire corectă și cunoașterea limitelor
Acolo unde codul existent lucrează încă cu linii de comandă, escapeshellarg() pentru fiecare valoare în parte este măsura minimă. Funcția pune valoarea între ghilimele, astfel încât shell-ul o tratează ca exact un argument. escapeshellcmd() este nepotrivit pentru introducerile utilizatorului, deoarece permite parametri suplimentari, așa cum arată OWASP printr-un exemplu.
Limite importante ale escapeshellarg():
- Argument injection rămâne posibilă: O valoare mascată care începe cu o cratimă este totuși citită de programul țintă ca opțiune.
- Windows se comportă diferit: Acolo funcția înlocuiește, potrivit manualului, anumite caractere precum
%și"cu spații; valorile ajung deci modificate. - Setul de caractere: În funcție de setarea locale a serverului, caracterele non-ASCII se pot pierde.
Găsirea locurilor din codul existent
O căutare a apelurilor de sistem se face repede și oferă o listă ușor de urmărit:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Fiecare apariție are nevoie de un răspuns la trei întrebări: Poate fi apelul înlocuit cu funcții PHP? Intră valori din exterior? Cu ce drepturi rulează procesul? Ultima întrebare este decisivă pentru paguba din cazul cel mai rău. Un proces PHP cu utilizator propriu și fără drepturi de scriere asupra codului provoacă mai puține stricăciuni, vedeți Least Privilege: drepturile utilizatorului serverului web.
Dacă un proces PHP-FPM sau al serverului web pornește un shell, acesta este un semnal de alarmă puternic pentru o intruziune. Cum recunoașteți acest lucru descrie Semnal de alarmă: serverul web pornește un shell.
Cum ajută PHP SOS concret?
- Căutăm toate apelurile de sistem din aplicația dumneavoastră, din pluginuri și din scripturile auxiliare.
- Înlocuim apelurile, unde este posibil, cu funcții PHP și trecem restul la apeluri fără shell.
- Blocăm funcțiile nenecesare în configurarea PHP și verificăm drepturile utilizatorului PHP.
- Testăm modificările și documentăm ce programe mai are voie să apeleze aplicația.
Verificarea software-ului propriu o oferim ca audit de securitate și test de penetrare.
Întrebări frecvente
Este suficient escapeshellarg împotriva Command Injection?
Împiedică executarea unei valori ca o comandă suplimentară. Nu împiedică citirea valorii ca opțiune a programului apelat, dacă începe cu o cratimă. De aceea verificați în plus valorile și, acolo unde programul permite, încheiați opțiunile cu --.
Care este diferența dintre escapeshellarg și escapeshellcmd?
escapeshellarg tratează o singură valoare ca exact un argument. escapeshellcmd mascheză doar metacaracterele dintr-o linie de comandă întreagă și permite parametri suplimentari. Pentru introducerile utilizatorului, escapeshellarg este alegerea corectă, iar un apel fără shell este și mai bun.
Ar trebui să blochez exec și system prin disable_functions?
Dacă aplicația dumneavoastră nu le folosește, da. Aceasta limitează paguba după o intruziune. Nu înlocuiește însă codul sigur, deoarece aplicațiile care au nevoie de comenzi au în continuare nevoie de funcții.
Cum recunosc dacă o Command Injection a fost exploatată?
Un semnal de alarmă puternic sunt procesele PHP sau ale serverului web care pornesc un shell precum sh sau bash. Se adaugă procese necunoscute, fișiere noi în directoarele de upload și conexiuni de ieșire neobișnuite.