Penetrazione e Sicurezza in JavaScript

Scritto da Francesco Di Donato • 7 ottobre 2021 • 6 minuti di lettura
Questo post è stato ispirato da questo articolo di Thomas Hunter II 🔗.
Indice
Sarai sia il lupo che la pecora. Ho creato la funzione qui sotto in modo che avesse tutto il necessario per imparare le tecniche di attacco e le relative difese:
- Sondaggio e Doppio Getter (Probing & Double Getter)
- Corruzione del Prototipo (Prototype Bribing)
- Illusione Primitiva (Primitive Illusion)
La funzione è Connector
, che riceve un oggetto di configurazione options
. Questo deve contenere una proprietà chiamata address
che deve essere uguale a uno di quelli elencati in validAddresses
, altrimenti viene lanciata un’eccezione.
Una volta stabilita la connessione con uno degli address
validi, l’istanza fornisce il metodo transfer
per spostare un certo amount
passato come input, che non deve superare il valore 500
.
function Connector(options) {
const validAddresses = ['partner-account', 'investments', 'mutual']
if (!options.address || typeof options.address !== 'string') _err1()
if (!validAddresses.includes(options.address)) _err2(options, validAddresses)
console.info(`Connessione all'indirizzo [${options.address}] stabilita`)
return {
transfer,
}
function transfer(amount) {
if (!amount || amount <= 0) _err3()
if (amount > 500) _err4()
console.info(
`Trasferito un importo di [${amount}] all'indirizzo [${options.address}]`
)
}
}
Non concentrarti sulle funzioni
_err
. Non sono importanti qui.
L’ happy path è il seguente:
const c = Connector({ address: 'investments' })
// Connessione all'indirizzo [investments] stabilita
c.transfer(300)
// Trasferito un importo di [300] all'indirizzo [investments]
Sondaggio e Doppio Getter
ATTACCO
Supponi di essere un utente malintenzionato dello script. Vuoi inviare una somma di denaro a un address
non incluso in validAddresses
.
Un attacco frontale è ovviamente bloccato.
Connector({ address: 'malicious' })
// L'indirizzo malicious non è valido. Quelli validi sono: partner-account, investments, mutual
Ricorda, mentre impersoni l’hacker non sei a conoscenza dell’implementazione del codice!
È possibile inviare un address
valido e contare il numero di volte in cui vi si accede. In questo modo puoi capire quando è il momento giusto per - ZAC! - trasformarlo nell’indirizzo malicious
!
Costruisci una sonda:
let i = 0
const probe = {
get address() {
console.count('sonda')
return 'investments'
},
}
const c = Connector(probe)
// sonda: 1
// sonda: 2
// sonda: 3
// sonda: 4
// Connessione all'indirizzo [investments] stabilita
c.transfer(300)
// sonda: 5
È chiaro. Basta cambiare la quinta lettura di address
; la sua validità è verificata nelle quattro letture precedenti. È possibile farlo usando la tecnica del Doppio Getter.
let i = 0
const doubleGetter = {
get address() {
if (++i === 5) return 'malicious'
return 'investments'
},
}
const c = Connector(doubleGetter)
// Connessione all'indirizzo [investments] stabilita
c.transfer(300)
// Trasferito un importo di [300] all'indirizzo [malicious]
Grazie a questa tecnica hai effettivamente bypassato le guardie della fase di inizializzazione.
DIFESA
Il problema è che si accede ripetutamente ad address
. Anche due volte sarebbero troppe.
Ma se fosse solo una, i Doppi Getter non potrebbero ingannare le guardie.
Per accedere ad address
una sola volta, basta copiarlo in una variabile. Poiché è una string
, è un primitivo - la nuova variabile è una copia separata, senza il getter.
In ES6 puoi usare il destructuring:
function Connector({ address }) { ... }
Esegui la sonda e vedrai che effettivamente suona una sola volta. La minaccia del Doppio Getter è neutralizzata.
Corruzione del Prototipo
ATTACCO
Devi trovare un modo per infiltrarti nel codice. Ma hanno alzato le mura - abbiamo bisogno di un infiltrato, qualcuno dall’interno che per un momento, solo un momento, finga di non vedere.
La funzione includes
è il tuo uomo. Corromperla è semplice:
const includesBackup = Array.prototype.includes
// corrompila...
Array.prototype.includes = () => true
const c = Connector({ address: 'malicious' })
// Connessione all'indirizzo [malicious] stabilita
// ...e subito dopo tutto torna alla normalità
Array.prototype.includes = includesBackup
c.transfer(300)
// Trasferito un importo di [300] all'indirizzo [malicious]
Solo durante la fase di inizializzazione, includes
restituirà true
indiscriminatamente. La guardia validAddresses.includes(address)
è di fatto accecata e l’indirizzo malicious
può entrare arrogantemente dalla porta principale.
DIFESA
Si tira su un muro intorno a Connector
, ovvero un block scope. All’interno di questo, vuoi avere la tua copia di Array.prototype.includes
che non sia corruttibile dall’esterno e usare solo quella.
{
const safeIncludes = Array.prototype.includes
function Connector({ address }) {
const validAddresses = ['partner-account', 'investments', 'mutual']
...
const isValidAddress = safeIncludes.bind(validAddresses)
if (!isValidAddress(address)) _err2(address, validAddresses)
...
}
global.Connector = Connector // window se nel browser
}
Lo stesso trucco che abbiamo usato prima questa volta non funzionerà e verrà lanciato l’errore _err2
.
ATTACCO
Con un po’ di astuzia è possibile corrompere il supervisore di includes
. Si tratta di bind
.
Consiglio di tenere una copia della funzione corrotta per rimettere le cose a posto non appena commesso il reato.
const includesBackup = Array.prototype.includes
const bindBackup = Function.prototype.bind
Array.prototype.includes = () => true
Function.prototype.bind = () => () => true
const c = Connector({ address: 'malicious' })
// Connessione all'indirizzo [malicious] stabilita
Array.prototype.includes = includesBackup
Function.prototype.bind = bindBackup
c.transfer(300)
// Trasferito un importo di [300] all'indirizzo [malicious]
Ancora una volta, sei riuscito a eludere le guardie.
Illusione Primitiva
L’istanza di Connector
fornisce il metodo transfer
. Questo richiede l’argomento amount
che è un numero e, affinché il trasferimento vada a buon fine, non deve superare il valore 500
. Supponiamo che io sia già riuscito a stabilire un contatto con un address
a mia scelta. A questo punto voglio trasferire un importo superiore a quello consentito.
// Connector#transfer
function transfer(amount) {
if (!amount || amount <= 0) _err3()
if (amount > 500) _err4()
console.info(
`Trasferito un importo di [${amount}] all'indirizzo [${options.address}]`
)
}
La tecnica dell’Illusione Primitiva ottiene un effetto simile al Doppio Getter ma in altri modi. Un limite della tecnica del DG è infatti quello di essere applicabile solo a variabili passate per riferimento. Prova a implementarla per un primitivo - Number
per esempio.
Trovo più funzionale modificare Number.prototype.valueOf
🔗. Questo è un metodo che probabilmente non avrai mai bisogno di chiamare direttamente. JavaScript stesso lo invoca quando ha bisogno di recuperare il valore primitivo di un oggetto (in questo caso, un Number
). L’intuizione è più immediata con un esempio:
Number.prototype.valueOf = () => {
console.count('sonda')
return this
}
this
nel caso diNumber
rappresenta lo stesso numero passato nel costruttore.
Probabilmente l’hai riconosciuta, è una sonda. Provi diverse operazioni su un’istanza di Number
:
const number = new Number(42)
console.log(number)
// [Number: 42]
console.log(+number)
// sonda: 1
// 42
console.log(number > 0)
// sonda: 2
// true
Come intuisci al volo, il metodo valueOf
viene invocato quando ci si aspetta un valore primitivo
- come nel caso di un’operazione matematica. A questo punto non resta che inserire la sonda nel metodo transfer
.
c.transfer(number)
// sonda: 1
// sonda: 2
// Trasferito un importo di [42] all'indirizzo [hacker-address]
I due log della sonda corrispondono precisamente a amount <= 0
e amount > 500
. A questo punto ti rendi conto che non hai bisogno di scambiare il valore con un altro a un certo punto - devi solo restituire un valore che soddisfi le condizioni di cui sopra quando valueOf
viene chiamato.
Number.prototype.valueOf = () => 1
const number = new Number(100000)
c.transfer(number)
// Trasferito un importo di [100000] all'indirizzo [hacker-address]
Ancora una volta, sei riuscito a ottenere quello che volevi.