È sotto attacco? Richieda supporto per la risposta agli incidenti

Sviluppo sicuro

Trovare ogni chiamata alla shell costruita da input utente

Come individuare le chiamate alla shell costruite da input utente e sostituirle con API di libreria o array di argomenti, senza affidarsi all'escaping.

Anche in:EnglishTürkçeDeutschFrançaisEspañolPolskiNederlandsEesti

Illustrazione: Trovare ogni chiamata alla shell costruita da input utente

La correzione affidabile per l'OS command injection è smettere del tutto di passare a una shell stringhe influenzabili dall'utente. Migliorare l'escaping è l'opzione più debole. Il lavoro pratico consiste nel trovare ogni punto in cui il codice avvia un processo e poi decidere, caso per caso, se quella chiamata debba davvero esistere.

La command injection si verifica quando un'applicazione passa a una shell di sistema dati utente non sicuri, e i comandi iniettati di solito vengono eseguiti con i privilegi dell'applicazione. CWE-78 aggiunge un dettaglio scomodo: poiché i comandi sono eseguiti dall'applicazione, l'attività può sembrare provenire dall'applicazione stessa o dal suo proprietario.

Perché l'escaping è la risposta sbagliata da cui partire

La funzione escapeshellarg() di PHP racchiude un valore tra apici singoli, in modo che la shell lo tratti come un unico argomento. Questo impedisce che & echo "hello" venga eseguito come secondo comando. Non impedisce però all'attaccante di scegliere quale argomento, uno solo, riceverà il programma.

Ogni OS command injection è anche un'argument injection. Nella cheat sheet di OWASP, escape("--help") passato a curl stampa comunque il testo della guida. A seconda del programma, l'impatto va dalla divulgazione di informazioni alla remote code execution.

Una nota di un lettore nella pagina del manuale PHP su escapeshellarg() mostra lo stesso difetto con tar. Ogni nome di file è sottoposto a escaping corretto, eppure un valore che inizia con un trattino viene comunque interpretato come opzione, e tar ha un'opzione che esegue un altro comando. Aggiungere -- prima dei nomi dei file risolve il problema, ma la nota fa notare che non tutti i programmi supportano questo separatore.

Su Windows, secondo il manuale, escapeshellarg() usa i doppi apici e sostituisce con spazi i segni di percentuale, i punti esclamativi e i doppi apici. Una nota di un lettore aggiunge che il risultato è sicuro ma con perdita di informazioni. In entrambi i casi è un motivo per evitare la shell, non una soluzione.

Trovare le chiamate

Eseguite questi comandi su ogni repository. Adattate i tipi di file alla struttura dei Suoi progetti:

rg -n 'shell=True|os\.system|os\.popen' --type py
rg -n "subprocess\.(run|Popen|call|check_output)\(\s*[f\"']" --type py
rg -n '\b(system|exec|shell_exec|passthru|popen|proc_open)\s*\(' --type php
rg -n '`[^`]*\$' --type php
rg -n 'Runtime\.getRuntime\(\)\.exec|ProcessBuilder' --type java
rg -n 'ProcessStartInfo|Process\.Start|UseShellExecute' --type cs
rg -n '\bsystem\s*\(|popen\s*\(|exec[lv]p?e?\s*\(' --type c

Il secondo pattern Python intercetta una singola stringa passata come comando. Senza shell=True funziona solo se la stringa contiene il nome di un programma e nient'altro, quindi ogni risultato richiede un controllo manuale. Il pattern PHP per i backtick è rumoroso perché trova anche i backtick nei commenti e in SQL. Cercate anche negli shell script, negli script di deploy e nei wrapper di cron, che costruiscono comandi a partire da variabili con la stessa frequenza del codice applicativo.

LinguaggioCosa cercareCon cosa sostituire
PHPsystem(), exec(), shell_exec(), passthru(), popen(), proc_open(), l'operatore backtickFunzioni integrate come mkdir()
Pythonshell=True; os.system; os.popen; una singola stringa di comandosubprocess.run con una lista
JavaRuntime.exec(String)ProcessBuilder con il programma e ogni argomento come elementi separati
.NETcmd.exe, powershell.exe, sh -c, una stringa Arguments costruitaProcessStartInfo con ArgumentList
Csystem()Una chiamata a libreria

Includete anche i comandi hardcoded. L'approfondimento della community OWASP descrive un programma CGI privilegiato che esegue cd /var/yp && make senza alcun input utente nella stringa. Non usava un percorso assoluto per make, quindi un attaccante poteva modificare $PATH e far eseguire un proprio binario con i privilegi del programma.

Classificare i risultati

Un elenco grezzo di risultati non è un elenco di vulnerabilità. Per ciascuno, risalite all'indietro il percorso del valore e chiedetevi se una richiesta possa raggiungerlo. I parametri di route e di query, i campi dei form, i cookie e gli header HTTP sono le fonti più ovvie. I nomi dei file caricati, i valori provenienti da un database scrivibile dagli utenti, i messaggi in coda e le variabili d'ambiente sono quelle che si dimenticano più spesso.

Dividete i risultati in tre gruppi: raggiungibili da una richiesta non autenticata, raggiungibili solo dopo il login, non raggiungibili affatto. Correggete per primo il primo gruppo. Mantenete il terzo nell'elenco, perché un successivo refactoring può collegarlo a una richiesta senza che nessuno se ne accorga.

Sostituire la chiamata con un'API

Le funzioni di libreria integrate non possono essere manipolate in modo da fare altro rispetto al compito previsto. Usate mkdir() al posto di system("mkdir /dir_name"). Usate la API di posta Java in javax.mail.* invece di invocare mail tramite la shell. CWE-78 dà lo stesso consiglio per la fase di progettazione: usare chiamate a libreria anziché processi esterni.

Quando è necessario avviare un processo

Alcuni strumenti non hanno un equivalente in libreria. In tal caso usate insieme due livelli: una lista di argomenti senza shell e la validazione.

La documentazione di subprocess in Python afferma che in genere è preferibile una sequenza di argomenti, perché il modulo gestisce escaping e quoting. Contiene inoltre un avviso che invita a leggere la sezione Security Considerations prima di usare shell=True. Raccomanda anche un percorso completo per l'eseguibile, che elimina la ricerca tramite PATH sfruttata nell'esempio di make visto sopra.

import re, subprocess

if not re.fullmatch(r"[a-z0-9]{3,10}", name):
    raise ValueError("rejected")

subprocess.run(["/usr/bin/tar", "cf", "out.tar", "--", name], check=True)

Il pattern rifiuta già un trattino iniziale, quindi -- sembra ridondante. È invece defence in depth: se qualcuno in seguito allentasse la regex, il separatore continuerebbe a impedire che name venga letto come opzione.

In .NET usate un percorso fisso e completo in FileName, impostate UseShellExecute = false e aggiungete ogni argomento tramite ArgumentList. Non passate mai input non attendibile a cmd.exe, powershell.exe o sh -c. Separare gli argomenti non equivale a validarli, né in Java né in .NET.

Poi validate i valori. Verificate il comando rispetto a un elenco di comandi consentiti e ciascun argomento rispetto a valori consentiti oppure a un pattern con lunghezza massima, tenendo fuori metacaratteri e spazi.

Per curl -- non basta, perché gli argomenti successivi restano comunque operandi di tipo URL e sono possibili ulteriori trasferimenti. Passate esattamente un solo URL validato, usate --globoff e applicate protezioni contro SSRF.

Eseguite l'applicazione con i privilegi minimi di cui ha bisogno e assegnate ai job con un singolo compito un account isolato dedicato. Una sandbox come chroot, AppArmor o SELinux aiuta, ma CWE-78 ne giudica l'efficacia limitata. Restringe ciò che un attaccante può raggiungere, ma non elimina il difetto.

Da dove cominciare

  1. Eseguite le ricerche sopra su ogni repository, compresi script di deploy, job di CI e wrapper di cron. Elencate ogni risultato con l'input che lo raggiunge e ordinate l'elenco in base al fatto che una richiesta possa raggiungerlo o meno.
  2. Per ogni risultato, chiedetevi se una funzione di libreria o integrata svolga lo stesso compito. In caso affermativo, sostituite la chiamata ed eliminate l'avvio del processo.
  3. Riscrivete le chiamate rimanenti usando una lista di argomenti, nessuna shell e un percorso assoluto dell'eseguibile. Aggiungete la validazione tramite allowlist e il separatore -- dove il programma lo supporta.
  4. Eseguite i processi con un account dedicato a bassi privilegi, in modo che un caso trascurato abbia meno margine di manovra.
  5. Testate ogni chiamata rimanente con un valore come bob.txt;sleep 5 e con un valore che inizia con un trattino. Misurate il tempo di risposta anziché attendere un output. Il valore con il trattino iniziale deve essere rifiutato o trattato come nome di file. Per una verifica esterna, un penetration test proverà entrambi i casi, oppure il nostro team di sviluppo software sicuro può rivedere il codice insieme a Voi.

Come possiamo aiutarla

Penetration test

Attacchiamo reti, applicazioni e persone come farebbe un avversario reale, poi le mostriamo come chiudere ogni falla.

Ci scriva
Team AngelForce

Scritto dal team AngelForce: consulenti di sicurezza con certificazioni CEH, LPT Master e CISA che testano, monitorano e difendono i sistemi di organizzazioni in tutta Europa.

Scritto con l’assistenza dell’intelligenza artificiale a partire dalle fonti pubbliche elencate di seguito e rivisto dal team di AngelForce prima della pubblicazione.

Fonti

  1. cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
  2. community.owasp.org/attacks/Command_Injection
  3. cwe.mitre.org/data/definitions/78.html
  4. docs.python.org/3/library/subprocess.html
  5. www.php.net/manual/en/function.escapeshellarg.php

Articoli: Sviluppo sicuro

Ci dica cosa deve proteggere

Descriva il suo ambiente e cosa vuole ottenere. Risponderemo via email.

Ci scriva
info@angelforce.eu +372 5705 2044

AngelForce OÜ · Tallinn, Estonia