Kort sagt: Command injection i PHP forhindrer I sikrest ved slet ikke at kalde en shell: Mange opgaver klarer PHP-funktioner eller biblioteker direkte. Hvor et eksternt program er nødvendigt, overgiver I siden PHP 7.4 kommandoen som array til proc_open(), så starter PHP programmet uden shell. Værdier kontrollerer I mod en tilladelsesliste, afslutter indstillinger med -- og lader processen køre med minimale rettigheder.
Hvorfor et systemkald har så meget magt
Billedbehandling, PDF-generering, arkivering, virusscanning, forespørgsel på en DNS-post: Til sådanne opgaver kalder PHP-applikationer gerne eksterne programmer. Teknisk er det bekvemt, en linje med exec() eller shell_exec() er nok. Sikkerhedsmæssigt er det et af de farligste steder i koden.
Sættes en kommando sammen som tekst og udføres af en shell, afgør shellen, hvad der er kommando, og hvad der er data. Når en værdi udefra kommer ukontrolleret ind, kan den ændre kommandoens betydning. Følgen er i værste fald den samme som ved PHP2Shell: udførelse af vilkårlige kommandoer på serveren med PHP-processens rettigheder.
Forskellen: PHP2Shell afhjælper I med en PHP-opdatering. En command injection i egen kode består efter enhver opdatering. Dertil kommer, at sådanne steder ofte sidder i hjælpescripts, der kun var tænkt til administratorer, f.eks. til vedligeholdelse, diagnose eller importer, og senere forbliver nåelige uden beskyttelse.
Hvor command injection i PHP opstår
Udsat er ethvert sted, hvor PHP starter en shell eller et program. Funktionerne hertil er:
| Funktion | Bruger en shell? | Bemærkning |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
ja | Kommando som tekst, shellen fortolker den |
| Backtick-operator | ja | svarer til shell_exec() |
popen() |
ja | Kommando som tekst |
proc_open() med streng |
ja | Kommando som tekst |
proc_open() med array |
nej | siden PHP 7.4, argumenter overgives direkte |
mail() med femte parameter |
indirekte | yderligere parametre til mailprogrammet |
Et typisk usikkert mønster ser sådan ud. Filnavnet kommer fra forespørgslen og havner direkte i kommandolinjen:
<?php
// USIKKER: Værdi fra forespørgslen bliver del af en shell-kommando
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Første valg: slet ingen shell
Den mest virkningsfulde beskyttelse er at erstatte systemkaldet. OWASP nævner det udtrykkeligt som første forsvarslinje. Til mange opgaver findes der i PHP egne funktioner eller udvidelser:
- Mapper og filer:
mkdir(),copy(),rename(),unlink() - Arkiver:
ZipArchiveellerPharData - Billeder: GD eller Imagick som PHP-udvidelse
- Netværksforespørgsler:
dns_get_record(),gethostbyname(), cURL - Kontrolsummer:
hash_file()
Dermed forsvinder spørgsmålet om, hvilke tegn en shell fortolker, fuldstændigt. Desuden kan funktionerne til systemkommandoer så spærres i konfigurationen, som vejledningen Brug disable_functions rigtigt beskriver.
Når et eksternt program er nødvendigt: proc_open med array
Siden PHP 7.4 accepterer proc_open() kommandoen som array. Ifølge PHP-manualen startes processen så direkte, uden omvej via shellen, og PHP overtager den nødvendige maskering af argumenterne. Tegn som semikolon eller pipe har så ikke længere nogen særbetydning.
<?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);
Tre ting gør eksemplet robust: Programmet står med fuld sti fast i koden, filnavnet kontrolleres mod et snævert mønster, og hvert variabelt argument begynder med en absolut sti og kan altså ikke læses som indstilling. Hvor en værdi står alene, afslutter I indstillingerne med --, hvis programmet ifølge dokumentationen understøtter det, eller afviser værdier, der begynder med en bindestreg.
Ved opgaver, der jævnligt har brug for eksterne programmer, f.eks. billed- eller PDF-behandling i større omfang, er en afkobling værd: Webapplikationen lægger blot en opgave i en kø, en separat baggrundstjeneste med egen bruger og egne rettigheder udfører den. PHP-processen, der besvarer forespørgsler fra internettet, har så ikke længere brug for funktioner til systemkommandoer, og de kan spærres for den. Eksterne programmer bør desuden have tids- og hukommelsesgrænser, så en enkelt opgave ikke blokerer serveren.
Et særtilfælde er mail(). Den femte parameter overgiver yderligere indstillinger til systemets mailprogram. Værdier fra formularer, f.eks. en afsenderadresse, må aldrig havne dér ukontrolleret. Mere robust er et mailbibliotek, der sender via SMTP og ikke har brug for noget systemkald.
escapeshellarg: brug det rigtigt og kend grænserne
Hvor eksisterende kode stadig arbejder med kommandolinjer, er escapeshellarg() for hver enkelt værdi minimumsforanstaltningen. Funktionen sætter værdien i anførselstegn, så shellen behandler den som netop ét argument. escapeshellcmd() er uegnet til brugerinput, fordi den tillader yderligere parametre, som OWASP viser med et eksempel.
Vigtige grænser for escapeshellarg():
- Argument-injection er fortsat mulig: En maskeret værdi, der begynder med en bindestreg, læses alligevel af målprogrammet som en indstilling.
- Windows opfører sig anderledes: Dér erstatter funktionen ifølge manualen bestemte tegn som
%og"med mellemrum; værdier ankommer altså ændrede. - Tegnsæt: Afhængigt af serverens locale-indstilling kan tegn uden for ASCII gå tabt.
Find fundsteder i eksisterende kode
En søgning efter systemkald er hurtigt gjort og giver en overskuelig liste:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Hvert fund kræver et svar på tre spørgsmål: Kan kaldet erstattes af PHP-funktioner? Flyder der værdier udefra ind? Med hvilke rettigheder kører processen? Det sidste spørgsmål er afgørende for skaden i alvorlige tilfælde. En PHP-proces med egen bruger og uden skriverettigheder til koden anretter mindre, se Least Privilege: Webserver-brugerens rettigheder.
Starter en PHP-FPM- eller webserverproces en shell, er det et stærkt advarselstegn på et indbrud. Hvordan I opdager det, beskriver Advarselstegn: Webserveren starter en shell.
Hvordan hjælper PHP SOS konkret?
- Vi søger alle systemkald i jeres applikation, i plugins og i hjælpescripts.
- Vi erstatter kald, hvor det er muligt, med PHP-funktioner og omstiller de øvrige til kald uden shell.
- Vi spærrer unødvendige funktioner i PHP-konfigurationen og kontrollerer PHP-brugerens rettigheder.
- Vi tester ændringerne og dokumenterer, hvilke programmer applikationen fortsat må kalde.
Kontrol af egen software tilbyder vi som sikkerhedsaudit og penetrationstest.
Ofte stillede spørgsmål
Er escapeshellarg nok mod command injection?
Det forhindrer, at en værdi udføres som en yderligere kommando. Det forhindrer ikke, at værdien læses som en indstilling til det kaldte program, hvis den begynder med en bindestreg. Kontrollér derfor desuden værdier, og afslut indstillinger med --, hvor programmet understøtter det.
Hvad er forskellen mellem escapeshellarg og escapeshellcmd?
escapeshellarg behandler en enkelt værdi som netop ét argument. escapeshellcmd maskerer kun metategn i en hel kommandolinje og tillader yderligere parametre. Til brugerinput er escapeshellarg det rigtige valg, et kald uden shell er endnu bedre.
Bør jeg spærre exec og system via disable_functions?
Hvis jeres applikation ikke har brug for dem, ja. Det begrænser skaden efter et indbrud. Det erstatter dog ikke sikker kode, for applikationer, der har brug for kommandoer, har fortsat brug for funktionerne.
Hvordan kan jeg se, om en command injection er blevet udnyttet?
Et stærkt advarselstegn er PHP- eller webserverprocesser, der starter en shell som sh eller bash. Dertil kommer ukendte processer, nye filer i upload-mapper og usædvanlige udgående forbindelser.