Disclosure: TheBestVPN is reader-supported. When you buy a VPN through links on our site, we may earn commissions. Learn more.

CVE-2019-17059: Preauth-RCE in Sophos’ Cyberoam Explained

Rob Mardisalu

Rob Mardisalu

Editor of TheBestVPN.com

We’ve been working hard with internal and external security researchers to uncover serious remotely exploitable loopholes in SSL VPNs and Firewalls like Cyberoam, Fortigate and Cisco VPNs. This article is a technical go-to about a patched critical vulnerability affecting Cyberoam SSL VPN also known as CyberoamOS.

This Cyberoam exploit, dubbed CVE-2019-17059 is a critical vulnerability that lets attackers access your Cyberoam device without providing any username or password. On top of that, the access granted is the highest level (root), which essentially gives an attacker unlimited rights on your Cyberoam device.

In most network environments, Cyberoam devices are used as firewalls and SSL VPN gateways. This gives a potential attacker a strong foothold in a network. It makes it easier to attack hosts inside the network, and since Cyberoam devices are usually trusted in most environments, this gives a would-be attacker extra edge.

According to Shodan (a search engine for internet-connected devices), there are more than 96,000 internet-facing Cyberoam devices from all over the world. Most of these devices are installed in enterprises, universities and some in world-renowned banks. This leads to the attacks having huge impacts on these environments.

Working with Sophos security team has been a large delight as they acted quickly by acknowledging and rolling out patches only a few days after our initial report to them. Kudos to them! (pun-intended!)

And since most of these entities are attractive targets for attackers, it makes the bugs all that more critical.

CyberoamOS Remote Unauthenticated Root Command Execution

The CyberoamOS is a modified Linux-based operating system for Cyberoam devices. This OS has a web-based configuration interface and an SSLVPN portal.

The web interface is divided into two main parts:

  • A frontend written in Java
  • A backend that uses a combination of C and Perl

We will not dive deep into the internals of the front- or back-end code, mainly to save time and limit the amount of information revealed. But we will discuss briefly how the bug is triggered.

Both the configuration and SSLVPN interfaces have a servlet that handles main operations. These operations are defined using a parameter named “mode”.

Most of these are authenticated. But there are a few ops we can access without authentication (like login).

The bugs we have found lie in the email antivirus/antispam module. The request mode for this endpoint (module, op) is 458.

One thing to note is the opcodes are mapped to their names in the Cyberoam database (internal database Postgres). By looking up 458, we can find out what the name of this opcode is.

Here is a line from the database initialization SQL script showing the name opcode 458:

insert into tblcrevent(opcode,description,mode,requesttype) 
values('RELEASEQUARANTINEMAILFROMMAIL','RELEASE QUARANTINE MAIL FROM MAIL','458',2);

The opcode functions are stored in the directory /_conf/csc/cscconf/. We will not be revealing the whole code of the vulnerable function, but we will provide a few snippets showing where and how the bug occurs.

A code from the Java frontend that handles the opcode 458:

if ((jsonObject.getString("hdnSender").equals("") ||
validateEmail(jsonObject.getString("hdnSender"))) &&
validateEmail(jsonObject.getString("hdnRecipient")) &&
isSafeFilePath(jsonObject.getString("hdnFilePath")) && b) {
    httpServletResponse.setContentType("text/html");
    CyberoamLogger.debug("Antivirus/AntiSpam", "CSC Constant value " + 
CSCConstants.isCCC);

As you can see above, a few parameters are checked for validity. If they are valid values, the following happens:

final EventBean eventByMode = EventBean.getEventByMode(363);
...redacted.
final int sendWizardEvent = cscClient.sendWizardEvent(eventByMode, hashMap, sqlReader);

As we can see above, we have a new event code (363) that will be sent to the backend. The bug we have discovered is in the code that handles this in the backend.

The opcode is named sendmail, and to avoid exploitation of this bug, we will be redacting most of the code from the following code.

The opcode handler for send_mail.

...redacted...

<code>$param = $request->{release};</code>
        param = DLOPEN(base64_decode,param)
        LOG applog " Decode values :: $param \n"
        <code>%requestData = split(/[&=]/, $param);
            $mailServerHost = $requestData{hdnDestDomain};
            $mailFrom = $requestData{hdnSender};
            $mailTo   = $requestData{hdnRecipient};
            $file = $QUARANTINE_PATH."/".$requestData{hdnFilePath};

    $mailfile=$requestData{hdnFilePath};
    $validate_email="false";
    my $email_regex='^([\.]?[_\-\!\#\{\}\$\%\^\&\*\+\=\|\?\'\\\\\\/a-zA-Z0-9])*@([a-zA-Z0-9]([-]?[a-zA-Z0-9]+)*\.)+([a-zA-Z0-9]{0,6})$';
    if($requestData{hdnRecipient} =~ /$email_regex/ && ((defined $requestData{hdnSender} && $requestData{hdnSender} eq '') || $requestData{hdnSender} =~ /$email_regex/) && index($requestData{hdnFilePath},'../') == -1){
        $validate_email="true";
    }
....redacted....

As we can see above, the pseudo-Perl code shows us how the backend receives input from the frontend ($requestData) and how it attempts to verify some of the parameters we send.

After the verification, if our parameters are valid, the following code is executed:

%mailreq=("mailaction"=>"$MAIL_FORWARD","subject"=>"$strSubject","toEmail"=>"$mailTo","attachmentfile"=>"$file","smtpserverhost"=>"$mailServerHost","fromaddress"=>"$mailFrom");
    </code>

  out = OPCODE mail_sender json %mailreq

The code above sets our request parameters into mailreq variable and calls the mail_sender function (OPCODE). We will see how this opcode is executed and where exactly the RCE happens:

    <code>
        #mailaction 0=mail_with_var,1=mail_forward,2=mail_attachment
        $mailaction=$request->{mailaction};
        $subject=$request->{subject};
        $mailbody='';
        $attachmentfile=$request->{attachmentfile};
        $toEmail=$request->{toEmail};
    </code>
    #mail body
    IF("defined $request->{mailbody} && '' ne $request->{mailbody}"){
        <code>$mailbody=$request->{mailbody};</code>
    }
    #SMTP server host
    IF("defined $request->{smtpserverhost} && '' ne $request->{smtpserverhost}"){
        <code>$smtpserverhost=$request->{smtpserverhost};</code>
    }ELSE{
        result = QUERY "select servicevalue from tblclientservices where servicekey='MailServer'"
        IF("defined $result->{output}->{servicevalue}[0] && '' ne $result->{output}->{servicevalue}[0]"){
            <code>$smtpserverhost=$result->{output}->{servicevalue}[0];</code>
        }ELSE{
            <code>$smtpserverhost="127.0.0.1";</code>
        }
    }

    #SMTP server port
    IF("defined $request->{smtpserverport} && '' ne $request->{smtpserverport}"){
        <code>$smtpserverport=$request->{smtpserverport};</code>
    }ELSE{
        result = QUERY "select servicevalue from tblclientservices where servicekey='MailServerPort'"
        IF("defined $result->{output}->{servicevalue}[0] && '' ne $result->{output}->{servicevalue}[0]"){
            <code>$smtpserverport=$result->{output}->{servicevalue}[0];</code>
        }ELSE{
            <code>$smtpserverport="25";</code>
        }
    }

    #SMTP auth flag
    <code>$smtpauthflag="0";</code>
    IF("defined $request->{smtpauthflag} && '' ne $request->{smtpauthflag}"){
        <code>$smtpauthflag=$request->{smtpauthflag};</code>
    }ELSE{
        result = QUERY "select servicevalue from tblclientservices where servicekey='SMTPAuthenticationFlag'"
        IF("defined $result->{output}->{servicevalue}[0] && '' ne $result->{output}->{servicevalue}[0]"){
            <code>$smtpauthflag=$result->{output}->{servicevalue}[0];</code>
        }
    }

    IF("$smtpauthflag == 1"){
        IF("defined $request->{mailusername} && '' ne $request->{mailusername}"){
            <code>
                $mailusername=$request->{mailusername};  
                $mailpassword=$request->{mailpassword};
            </code>
        }ELSE{
            result = QUERY "select servicevalue from tblclientservices where servicekey = 'MailServerUsername'"
            <code>$mailusername = $result->{output}->{servicevalue}[0];</code>
            result = QUERY "select servicevalue from tblclientservices where servicekey = 'MailServerPassword'"
            <code>$mailpassword = $result->{output}->{servicevalue}[0];</code>
        }
    }ELSE{
        <code>
            $mailusername = "";
            $mailpassword = "";
        </code>
    }
    IF("defined $request->{fromaddress} && '' ne $request->{fromaddress}"){
        <code>$fromaddress=$request->{fromaddress};</code>
    }ELSE{
        result = QUERY "select servicevalue from tblclientservices where servicekey = 'FromAddress'"
        <code>$fromaddress = $result->{output}->{servicevalue}[0];</code>
    }

    #Security Mode
    IF("defined $request->{smtpsecurity} && '' ne $request->{smtpsecurity}"){
        <code>$smtpsecurity=$request->{smtpsecurity};</code>
    }ELSE{
        result = QUERY "select servicevalue from tblclientservices where servicekey = 'smtpsecurity'"
        <code>$smtpsecurity = $result->{output}->{servicevalue}[0];</code>
    }

    <code>$smtpsecuritymode=0;</code>
    IF("$smtpsecurity eq 'STARTTLS'"){
        <code>$smtpsecuritymode=1;</code>
    }ELSE IF("$smtpsecurity eq 'SSL/TLS'"){
        <code>$smtpsecuritymode=2;</code>
    }

    #SMTP Certificate
    <code>
        $smtpcertificate = '';
        $certpassword='';
    </code>
    IF("$smtpsecuritymode!=0"){
        IF("defined $request->{smtpcertificate} && '' ne $request->{smtpcertificate}"){
            result = QUERY "select certname,password from tblvpncertificate where certid=$request->{smtpcertificate}"
        }ELSE{
            result = QUERY "select certname,password from tblvpncertificate where certid=(select servicevalue::int from tblclientservices where servicekey = 'smtpcertificate')"
        }
        <code>
            $smtpcertificate = $result->{output}->{certname}[0];
            $certpassword=$result->{output}->{password}[0];
        </code>
    }

    #From Address with Name
    IF("defined $request->{fromaddresswithname} && '' ne $request->{fromaddresswithname}"){
        <code>$fromaddresswithname=$request->{fromaddresswithname};</code>
    }ELSE{
        <code>$fromaddresswithname = $OEMNAME . " <" . $fromaddress . ">";</code>
    }

The code above does the same thing the other opcode did when it starts. It initializes variables (some from us or from the device if not specified).

After the variables are assigned, the following code block is executed.

out = EXECSH "/bin/cschelper mail_send '$fromaddress' '$fromaddresswithname' '$toEmail' '$toEmail' '$subject' '$mailbody' '$smtpserverhost' '$smtpserverport' '$mailusername' '$mailpassword' '$mailaction' '$smtpsecuritymode' '$smtpcertificate' '$certpassword' '1' '$attachmentfile'"

And there it is, the command execution. Now the call here is EXECSH which calls /bin/sh -c “ARGUMENTS”. With the execution happening using values we control, we can easily attain remote command execution, all without authentication.

We will be releasing a full report and the Proof of Concept with proper outlines in a few months.

Update: This research was covered first on TechCrunch, read more here.

Leave a Reply

Your email address will not be published. Required fields are marked *