Ukratko: Command Injection u PHP-u najsigurnije sprječavate tako da uopće ne pozivate ljusku: mnoge zadatke izravno obavljaju PHP funkcije ili biblioteke. Gdje je potreban vanjski program, naredbu od PHP-a 7.4 predajte kao polje (array) funkciji proc_open(), tada PHP pokreće program bez ljuske. Vrijednosti provjeravajte prema popisu dopuštenog, opcije završite s --, a proces pokrećite s minimalnim pravima.
Zašto sistemski poziv ima toliku moć
Obrada slika, izrada PDF-ova, arhiviranje, virusni sken, upit DNS zapisa: za takve zadatke PHP aplikacije rado pozivaju vanjske programe. Tehnički je to zgodno, dovoljan je jedan redak s exec() ili shell_exec(). Sigurnosno-tehnički to je jedno od najopasnijih mjesta u kodu.
Kad se naredba sastavlja kao tekst i izvršava je ljuska, ljuska odlučuje što je naredba, a što podatak. Ako vanjska vrijednost uđe nepromijenjena, može promijeniti značenje naredbe. Posljedica je u najgorem slučaju ista kao kod PHP2Shella: izvršavanje proizvoljnih naredbi na poslužitelju s pravima PHP procesa.
Razlika: PHP2Shell otklanjate PHP updateom. Command Injection u vlastitom kodu ostaje nakon svakog updatea. Tome se pridodaje da se takva mjesta često kriju u pomoćnim skriptama namijenjenima samo administratorima, primjerice za održavanje, dijagnostiku ili uvoze, a kasnije ostaju dostupna bez zaštite.
Gdje nastaje Command Injection u PHP-u
Ugroženo je svako mjesto na kojem PHP pokreće ljusku ili program. Funkcije za to su:
| Funkcija | Koristi li ljusku? | Napomena |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
da | naredba kao tekst, ljuska je interpretira |
| operator povratnog navodnika (backtick) | da | odgovara shell_exec() |
popen() |
da | naredba kao tekst |
proc_open() sa stringom |
da | naredba kao tekst |
proc_open() s poljem |
ne | od PHP-a 7.4, argumenti se predaju izravno |
mail() s petim parametrom |
neizravno | dodatni parametri za program za e-poštu |
Tipičan nesiguran uzorak izgleda ovako. Naziv datoteke dolazi iz zahtjeva i izravno završava u naredbenoj liniji:
<?php
// NESIGURNO: vrijednost iz zahtjeva postaje dio naredbe ljuske
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Prvi izbor: uopće bez ljuske
Najučinkovitija zaštita je zamijeniti sistemski poziv. OWASP to izričito navodi kao prvu liniju obrane. Za mnoge zadatke u PHP-u postoje vlastite funkcije ili proširenja:
- Direktoriji i datoteke:
mkdir(),copy(),rename(),unlink() - Arhive:
ZipArchiveiliPharData - Slike: GD ili Imagick kao PHP proširenje
- Mrežni upiti:
dns_get_record(),gethostbyname(), cURL - Kontrolni zbrojevi:
hash_file()
Time potpuno nestaje pitanje koje znakove ljuska interpretira. Osim toga funkcije za sistemske naredbe tada se mogu blokirati u konfiguraciji, kako opisuje vodič Ispravno koristiti disable_functions.
Kad je vanjski program potreban: proc_open s poljem
Od PHP-a 7.4 proc_open() prihvaća naredbu kao polje. Prema PHP priručniku proces se tada pokreće izravno, bez zaobilaznice preko ljuske, a PHP preuzima potrebno maskiranje argumenata. Znakovi poput točke sa zarezom ili pipea tada više nemaju posebno značenje.
<?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);
Primjer čine robusnim tri stvari: program je s punom putanjom čvrsto upisan u kod, naziv datoteke provjerava se prema uskom uzorku, a svaki promjenjivi argument počinje apsolutnom putanjom, pa se ne može pročitati kao opcija. Gdje vrijednost stoji sama, opcije završite s --, ako to program prema dokumentaciji podržava, ili odbijte vrijednosti koje počinju crticom.
Kod zadataka koji redovito trebaju vanjske programe, primjerice obrada slika ili PDF-ova većeg opsega, vrijedi odvajanje: web-aplikacija samo stavlja zadatak u red čekanja, a zasebna pozadinska usluga s vlastitim korisnikom i vlastitim pravima ga izvršava. PHP proces koji odgovara na zahtjeve s interneta tada više ne treba funkcije za sistemske naredbe i one se za njega mogu blokirati. Vanjski programi trebali bi uz to imati vremenska i memorijska ograničenja, kako jedan zadatak ne bi blokirao poslužitelj.
Poseban je slučaj mail(). Peti parametar predaje dodatne opcije sistemskom programu za e-poštu. Vrijednosti iz obrazaca, primjerice adresa pošiljatelja, tamo nikad ne smiju završiti neprovjerene. Robusnija je biblioteka za e-poštu koja šalje preko SMTP-a i ne treba sistemski poziv.
escapeshellarg: ispravna primjena i poznavanje granica
Gdje postojeći kod još radi s naredbenim linijama, escapeshellarg() za svaku pojedinačnu vrijednost minimalna je mjera. Funkcija vrijednost stavlja u navodnike tako da je ljuska tretira kao točno jedan argument. escapeshellcmd() nije prikladan za unos korisnika jer dopušta dodatne parametre, kako OWASP pokazuje na primjeru.
Važne granice funkcije escapeshellarg():
- Argument Injection ostaje moguć: maskirana vrijednost koja počinje crticom ciljni program svejedno čita kao opciju.
- Windows se ponaša drukčije: tamo funkcija prema priručniku određene znakove poput
%i"zamjenjuje razmacima; vrijednosti stoga stižu promijenjene. - Skup znakova: ovisno o postavci locale poslužitelja, znakovi izvan ASCII-ja mogu se izgubiti.
Pronaći mjesta u postojećem kodu
Pretraga sistemskih poziva brzo je gotova i daje pregledan popis:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Svako nalazište traži odgovor na tri pitanja: može li se poziv zamijeniti PHP funkcijama? Utječu li vanjske vrijednosti? S kojim pravima proces radi? Posljednje pitanje presudno je za štetu u najgorem slučaju. PHP proces s vlastitim korisnikom i bez prava pisanja na kod čini manje štete, vidi Najmanje ovlasti: prava korisnika web-poslužitelja.
Ako PHP-FPM ili proces web-poslužitelja pokrene ljusku, to je snažan znak upozorenja na provalu. Kako to prepoznati, opisuje Znak upozorenja: web-poslužitelj pokreće ljusku.
Kako PHP SOS konkretno pomaže?
- Tražimo sve sistemske pozive u Vašoj aplikaciji, dodacima i pomoćnim skriptama.
- Pozive gdje je moguće zamjenjujemo PHP funkcijama, a ostale prebacujemo na pozive bez ljuske.
- Nepotrebne funkcije blokiramo u PHP konfiguraciji i provjeravamo prava PHP korisnika.
- Testiramo promjene i dokumentiramo koje programe aplikacija još smije pozivati.
Provjeru vlastitog softvera nudimo kao sigurnosni audit i penetracijski test.
Česta pitanja
Je li escapeshellarg dovoljan protiv Command Injectiona?
Sprječava da se vrijednost izvrši kao dodatna naredba. Ne sprječava da pozvani program vrijednost pročita kao opciju ako počinje crticom. Zato dodatno provjeravajte vrijednosti i, gdje program to podržava, opcije završite s --.
Koja je razlika između escapeshellarg i escapeshellcmd?
escapeshellarg vrijednost tretira kao točno jedan argument. escapeshellcmd samo maskira metaznakove u cijeloj naredbenoj liniji i dopušta dodatne parametre. Za unos korisnika escapeshellarg je pravi izbor, a poziv bez ljuske još bolji.
Trebam li exec i system blokirati pomoću disable_functions?
Ako ih Vaša aplikacija ne treba, da. To ograničava štetu nakon provale. No ne zamjenjuje siguran kod, jer aplikacije kojima su naredbe potrebne i dalje trebaju te funkcije.
Kako prepoznati je li Command Injection iskorišten?
Snažan znak upozorenja su PHP ili web-poslužiteljski procesi koji pokreću ljusku poput sh ili bash. Tome se pridružuju nepoznati procesi, nove datoteke u mapama za upload i neobične odlazne veze.