Skip to content
Go back

Field Notes

One Time Code Phishing Inside Minecraft

Minecraft email-code demonstration title image.

A friend sent me this video, and I thought, damn, that’s sneaky. I wanted to give it a go and see if I could emulate it.

The flow

On join, a Paper 1.21.11 plugin opens an email dialog and locks the player in place. Send code posts the entered address, player UUID, and a fresh session UUID to a private Python backend. The backend starts the existing headless Edge requester with that address:

process = subprocess.Popen(
    [config["node"], config["script"], "--email", job["email"], "--session"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL, text=True, bufsize=1,
)

The first Minecraft dialog asks for an email address.

The requester navigates Microsoft’s account sign-in pages and selects Send a code to [entered email] when that option appears. It keeps the browser session open instead of exiting at the code screen. This is a Microsoft account single-use email code, not an OAuth device code. The email address is a sign-in input; the plugin does not send mail itself.

A redacted example of Microsoft's single-use code email. The displayed code is fictional.

Keeping the code in the same session

Minecraft immediately opens a second dialog with an Email code field. Its button sends the entered code to the backend session associated with that player UUID. If the browser is still loading, the backend holds the code until the requester reports code_required, then writes it to the requester’s standard input:

if status == "code_required" and job["pending_code"]:
    process.stdin.write(json.dumps({"code": job.pop("pending_code")}) + "\n")
    process.stdin.flush()

The requester fills Microsoft’s code field in the same browser context and submits it. It reports verified only when it returns to account.microsoft.com and the code prompt is gone. The plugin polls the backend and releases the player on that result.

The second Minecraft dialog accepts the emailed code.

The Paper gate

The dialog is the input surface; it does not enforce the lock. A separate listener blocks movement and other gameplay actions until the session succeeds. The dialog uses pause(false) because Paper rejected the earlier pause/after-action combination:

DialogBase base = DialogBase.builder(Component.text(title, NamedTextColor.AQUA))
    .canCloseWithEscape(false).pause(false)
    .afterAction(DialogBase.DialogAfterAction.NONE)
    .body(body).inputs(inputs).build();

@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void move(PlayerMoveEvent e) {
    if (locked(e.getPlayer()) && e.hasChangedPosition()) e.setTo(e.getFrom());
}

I did not build out a full account-linking or account-takeover workflow. I stopped once the Minecraft prompt could trigger Microsoft’s one-time email code and accept it in-game; the remaining browser steps follow from that point. I will not publish the code for this demo, because this phishing method is already being sold as a service and I do not want to make it easier to abuse.