728x90
반응형
client-side 취약점 문제다. 기능 분석은 스킵하고 바로 문제 풀이로 넘어가겠다.
from flask import Flask, render_template, send_from_directory, request, redirect, make_response
from dotenv import load_dotenv
import os
load_dotenv()
API_SECRET_KEY = os.getenv("API_SECRET_KEY")
FLAG = os.getenv("FLAG")
app = Flask(__name__, static_folder="static", template_folder="templates")
@app.after_request
def add_header(response):
response.cache_control.no_store = True
response.cache_control.must_revalidate = True
return response
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
return redirect("/confidential.html")
@app.route("/confidential.html")
def confidential():
return render_template("confidential.html")
@app.route("/admin/flag", methods=["POST"])
def flag():
key = request.headers.get("X-API-Key")
if key == API_SECRET_KEY:
return FLAG
return "Unauthorized", 403
app.py의 코드다. `/admin/flag`에 접근하면 flag를 얻을 수 있다. 근데 X-API-KEY가 필요하다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Confidential</title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<div id="audio-warning">
⚠️ This experience contains sound. Please ensure your volume is adjusted appropriately.
</div>
<button id="start-btn">Begin Experience</button>
<div id="dancer"></div>
<img id="dancer-img" src="/static/ballerina-cappucina/ballerina-cappucina.png" alt="Ballerina" />
<audio id="balletAudio" preload="auto">
<source src="/static/ballerina-cappucina/ballerina-cappucina.mp3" type="audio/mpeg" />
</audio>
<script src="/static/js/main.min.js"></script>
</body>
</html>
templates 폴더의 confidential.html 파일이다. 밑에 script 태그가 눈에 띈다. 접근해보자.

그럼 위와 같은 코드를 볼 수 있다.
function pingMailStatus() { fetch("/api/mail/status") } function fetchInboxPreview() { fetch("/api/mail/inbox?limit=5") } pingMailStatus(), fetchInboxPreview(), document.getElementById("start-btn")?.addEventListener("click", () => { document.getElementById("balletAudio").play(), document.getElementById("start-btn").style.display = "none", document.getElementById("audio-warning").style.display = "none"; let i = document.getElementById("dancer"), o = document.getElementById("dancer-img"), l = (i.style.display = "block", o.style.display = "block", 0), a = window.innerWidth / 2, d = window.innerHeight / 2; !function e() { l += .05; var t = a + 100 * Math.cos(l), n = d + 100 * Math.sin(l); i.style.left = t + "px", i.style.top = n + "px", o.style.left = t + "px", o.style.top = n + "px", requestAnimationFrame(e) }() });
//test map file -> test-main.min.js.map, remove in prod
맨 밑에 주석으로 테스트 파일이 있다는 것을 알려주고 있다. 똑같이 접근해보자.
{"version":3,"file":"main.min.js.map","sources":["main.js"],"sourcesContent":["function pingMailStatus() {\r\n fetch(\"/api/mail/status\");\r\n}\r\n\r\nfunction fetchInboxPreview() {\r\n fetch(\"/api/mail/inbox?limit=5\");\r\n}\r\n\r\npingMailStatus();\r\nfetchInboxPreview();\r\n\r\ndocument.getElementById(\"start-btn\")?.addEventListener(\"click\", () => {\r\n const audio = document.getElementById(\"balletAudio\");\r\n audio.play();\r\n\r\n document.getElementById(\"start-btn\").style.display = \"none\";\r\n document.getElementById(\"audio-warning\").style.display = \"none\";\r\n\r\n const dancer = document.getElementById(\"dancer\");\r\n const dancerImg = document.getElementById(\"dancer-img\"); // Get the image element\r\n\r\n dancer.style.display = \"block\";\r\n dancerImg.style.display = \"block\"; // Show the image\r\n\r\n let angle = 0;\r\n const radius = 100;\r\n const centerX = window.innerWidth / 2;\r\n const centerY = window.innerHeight / 2;\r\n\r\n function animate() {\r\n angle += 0.05;\r\n const x = centerX + radius * Math.cos(angle);\r\n const y = centerY + radius * Math.sin(angle);\r\n dancer.style.left = x + \"px\";\r\n dancer.style.top = y + \"px\";\r\n\r\n dancerImg.style.left = x + \"px\"; // Sync image movement\r\n dancerImg.style.top = y + \"px\";\r\n\r\n requestAnimationFrame(animate);\r\n }\r\n animate();\r\n});\r\n\r\nfunction qyrbkc() { \r\n const xtqzp = [\"85\"], vmsdj = [\"87\"], rlfka = [\"77\"], wfthn = [\"67\"], zdqo = [\"40\"], yclur = [\"82\"],\r\n bpxmg = [\"82\"], hkfav = [\"70\"], oqzdu = [\"78\"], nwtjb = [\"39\"], sgfyk = [\"95\"], utxzr = [\"89\"],\r\n jvmqa = [\"67\"], dpwls = [\"73\"], xaogc = [\"34\"], eqhvt = [\"68\"], mfzoj = [\"68\"], lbknc = [\"92\"],\r\n zpeds = [\"84\"], cvnuy = [\"57\"], ktwfa = [\"70\"], xdglo = [\"87\"], fjyhr = [\"95\"], vtuze = [\"77\"], awphs = [\"75\"];\r\n const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0], \r\n bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0], \r\n jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0], \r\n zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];\r\n\r\n const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>\r\n String.fromCharCode(\r\n Number(pjgrx) ^ (fkhzu + 1) ^ 0 \r\n )\r\n ).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, \"\"); \r\n console.log(\"Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)\");\r\n}\r\n\r\n"],"names":["pingMailStatus","fetch","fetchInboxPreview","qyrbkc","map","pjgrx","fkhzu","String","fromCharCode","Number","reduce","qdmfo","lxzhs","console","log","document","getElementById","addEventListener","play","style","display","dancer","dancerImg","angle","centerX","window","innerWidth","centerY","innerHeight","animate","x","Math","cos","y","sin","left","top","requestAnimationFrame"],"mappings":"AAAA,SAASA,iBACPC,MAAM,kBAAkB,CAC1B,CAEA,SAASC,oBACPD,MAAM,yBAAyB,CACjC,CAsCA,SAASE,SAKc,CAJJ,KAAgB,KAAgB,KAAgB,KAAe,KAAgB,KAC/E,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAChF,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAChF,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,MAMzFC,IAAI,CAACC,EAAOC,IAC9BC,OAAOC,aACHC,OAAOJ,CAAK,EAAKC,EAAQ,EAAK,CAClC,CACJ,EAAEI,OAAO,CAACC,EAAOC,IAAUD,EAAQC,EAAO,EAAE,EAC5CC,QAAQC,IAAI,mFAAmF,CACnG,CApDAd,eAAe,EACfE,kBAAkB,EAElBa,SAASC,eAAe,WAAW,GAAGC,iBAAiB,QAAS,KAChDF,SAASC,eAAe,aAAa,EAC7CE,KAAK,EAEXH,SAASC,eAAe,WAAW,EAAEG,MAAMC,QAAU,OACrDL,SAASC,eAAe,eAAe,EAAEG,MAAMC,QAAU,OAEzD,IAAMC,EAASN,SAASC,eAAe,QAAQ,EACzCM,EAAYP,SAASC,eAAe,YAAY,EAKlDO,GAHJF,EAAOF,MAAMC,QAAU,QACvBE,EAAUH,MAAMC,QAAU,QAEd,GAENI,EAAUC,OAAOC,WAAa,EAC9BC,EAAUF,OAAOG,YAAc,EAcrCC,CAZA,SAASA,IACPN,GAAS,IACT,IAAMO,EAAIN,EANG,IAMgBO,KAAKC,IAAIT,CAAK,EACrCU,EAAIN,EAPG,IAOgBI,KAAKG,IAAIX,CAAK,EAC3CF,EAAOF,MAAMgB,KAAOL,EAAI,KACxBT,EAAOF,MAAMiB,IAAMH,EAAI,KAEvBX,EAAUH,MAAMgB,KAAOL,EAAI,KAC3BR,EAAUH,MAAMiB,IAAMH,EAAI,KAE1BI,sBAAsBR,CAAO,CAC/B,EACQ,CACV,CAAC"}
그럼 파일이 다운로드 되는데 파일 내용은 위와 같다.
function pingMailStatus() {
fetch("/api/mail/status");
}
function fetchInboxPreview() {
fetch("/api/mail/inbox?limit=5");
}
pingMailStatus();
fetchInboxPreview();
document.getElementById("start-btn")?.addEventListener("click", () => {
const audio = document.getElementById("balletAudio");
audio.play();
document.getElementById("start-btn").style.display = "none";
document.getElementById("audio-warning").style.display = "none";
const dancer = document.getElementById("dancer");
const dancerImg = document.getElementById("dancer-img"); // Get the image element
dancer.style.display = "block";
dancerImg.style.display = "block"; // Show the image
let angle = 0;
const radius = 100;
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
function animate() {
angle += 0.05;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
dancer.style.left = x + "px";
dancer.style.top = y + "px";
dancerImg.style.left = x + "px"; // Sync image movement
dancerImg.style.top = y + "px";
requestAnimationFrame(animate);
}
animate();
});
function qyrbkc() {
const xtqzp = ["85"], vmsdj = ["87"], rlfka = ["77"], wfthn = ["67"], zdqo = ["40"], yclur = ["82"],
bpxmg = ["82"], hkfav = ["70"], oqzdu = ["78"], nwtjb = ["39"], sgfyk = ["95"], utxzr = ["89"],
jvmqa = ["67"], dpwls = ["73"], xaogc = ["34"], eqhvt = ["68"], mfzoj = ["68"], lbknc = ["92"],
zpeds = ["84"], cvnuy = ["57"], ktwfa = ["70"], xdglo = ["87"], fjyhr = ["95"], vtuze = ["77"], awphs = ["75"];
const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0],
bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0],
jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0],
zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];
const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>
String.fromCharCode(
Number(pjgrx) ^ (fkhzu + 1) ^ 0
)
).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, "");
console.log("Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)");
}
보기 좋게 json 파서로 파싱하고 unescape 처리한 것이다.
난독화되어 있는 qyrbkc 함수를 볼 수 있다. 근데 그 결과를 출력해주지 않고 있다. 그 부분만 수정해서 출력해보도록 하겠다.
function qyrbkc() {
const xtqzp = ["85"], vmsdj = ["87"], rlfka = ["77"], wfthn = ["67"], zdqo = ["40"], yclur = ["82"],
bpxmg = ["82"], hkfav = ["70"], oqzdu = ["78"], nwtjb = ["39"], sgfyk = ["95"], utxzr = ["89"],
jvmqa = ["67"], dpwls = ["73"], xaogc = ["34"], eqhvt = ["68"], mfzoj = ["68"], lbknc = ["92"],
zpeds = ["84"], cvnuy = ["57"], ktwfa = ["70"], xdglo = ["87"], fjyhr = ["95"], vtuze = ["77"], awphs = ["75"];
const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0],
bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0],
jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0],
zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];
const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>
String.fromCharCode(
Number(pjgrx) ^ (fkhzu + 1) ^ 0
)
).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, "");
console.log(lmsvdt);
}
위는 수정한 코드다.

그럼 위와 같은 값을 얻을 수 있다. 이것이 토큰일 것이다.
curl -X POST -H "X-API-Key: TUNG-TUNG-TUNG-TUNG-SAHUR" https://web-mini-me-ab6d19a7ea6e.2025-us.ductf.net/admin/flag

그럼 flag를 얻을 수 있다.
DUCTF{Cl13nt-S1d3-H4ck1nG-1s-FuN}
728x90
반응형
'분류 전 > CTF' 카테고리의 다른 글
| [ShaktiCTF25] FRIENDS 풀이 (2) | 2025.07.30 |
|---|---|
| [DownUnderCTF 2025] rocky 풀이 (0) | 2025.07.22 |
| [L3ak CTF 2025] babyrev 풀이 (1) | 2025.07.16 |
| [L3ak CTF 2025] BrainCalc 풀이 (0) | 2025.07.15 |
| [L3ak CTF 2025] Flag L3ak 풀이 (0) | 2025.07.15 |