[{"data":1,"prerenderedAt":886},["ShallowReactive",2],{"post-flappy-bird":3},{"id":4,"title":5,"body":6,"description":870,"extension":871,"meta":872,"navigation":880,"path":881,"seo":882,"stem":883,"__hash__":884,"dateDisplay":878,"image":879,"tags":874,"badge":873,"readingTime":885},"posts\u002Fposts\u002FFlappy-Bird.md","Flappy Bird — Write-up (ISP MiniCTF 2025)",{"type":7,"value":8,"toc":842},"minimark",[9,20,25,50,57,64,68,74,87,93,96,102,114,120,123,127,134,149,154,158,164,175,179,196,199,205,215,223,230,236,245,255,259,273,279,289,293,300,311,318,322,325,330,335,339,344,348,356,360,368,372,377,381,386,390,395,399,417,444,454,460,467,471,477,483,486,534,546,550,553,570,577,596,608,614,621,624,632,635,639,645,648,652,655,672,675,680,692,697,701,791,802,806,832,838],[10,11,12],"blockquote",{},[13,14,15,16,19],"p",{},"Một write-up cho challenge Web tại miniCTF, kể lại hành trình khám phá và khai thác logic client-side để lấy flag.",[17,18],"br",{},"\nChủ đề: JavaScript analysis, client-side logic, fetch endpoint.",[21,22,24],"h2",{"id":23},"_1-mở-đầu-một-buổi-chiều-mưa-và-một-con-chim-tò-mò-️","1. Mở đầu — một buổi chiều mưa và một con chim tò mò 🐦🌧️",[13,26,27,28,32,33,37,38,42,43,46,47],{},"Một ngày tôi mở challenge ",[29,30,31],"strong",{},"Flappy Bird"," (link: ",[34,35,36],"code",{},"http:\u002F\u002F103.249.117.57:6637\u002F","). Màn hình chào đón rất “game”: canvas chơi ở giữa, leaderboard bên phải, dòng chữ ",[39,40,41],"em",{},"Tea Cups"," và nút ",[29,44,45],{},"START ADVENTURE"," — giống như một trang game nhỏ được dựng để thu hút. Không có hint, chỉ có một lời thách: ",[39,48,49],{},"Bạn có thể tìm ra bí mật ẩn trong trò chơi này không?",[13,51,52],{},[53,54],"img",{"alt":55,"src":56},"Dashboard - Flappy Bird challenge","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Fdashbroad.png",[13,58,59,60,63],{},"Tôi nhẩm nghĩ: trong CTF, những trang dạng game thường giấu manh mối trong HTML\u002FJS hoặc endpoints client — không phải lúc nào cũng là lỗi server phức tạp. Vì vậy, chiến thuật của tôi là: ",[29,61,62],{},"chơi thử → bắt traffic → đọc mã client",".",[21,65,67],{"id":66},"_2-vào-chơi-click-chết-replay-và-một-phản-hồi-lạ","2. Vào chơi — click, chết, replay — và một phản hồi lạ",[13,69,70,71,73],{},"Tò mò, tôi nhấn ",[29,72,45],{}," và chơi vài lượt:",[75,76,77,81],"ul",{},[78,79,80],"li",{},"Màn hình game hoạt động bình thường: nhân vật nhảy, tránh ống, điểm (tea cups) tăng, cuối cùng hiện “GAME OVER”.",[78,82,83,84,86],{},"Ghi nhận: UI hiển thị số ",[34,85,41],{}," và một phần footer có chỗ dành cho “flag message” (nhưng ban đầu bị ẩn).",[13,88,89],{},[53,90],{"alt":91,"src":92},"Play test - screenshot","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Ftestgame.png",[13,94,95],{},"Mình nhấn Play vài lần, con chim chết nhiều lần như mọi người chơi bình thường. Nhưng sau mỗi lượt, mình mở Developer Tools → Network → HTTP History để quan sát các request\u002Fresponse.Ở phần GET \u002F (trang chính), trong response text có một đoạn trông giống flag.",[13,97,98],{},[53,99],{"alt":100,"src":101},"Fake flag in GET \u002F response","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Frequestindex.png",[10,103,104],{},[13,105,106,107,110,111],{},"Lúc đầu mình mừng rỡ tưởng trận này nhẹ, flag hiện luôn ở ",[34,108,109],{},"GET \u002F","\nnhưng cảm giác đó nhanh chóng bị thay bằng nghi ngờ: ",[29,112,113],{},"tại sao flag lại dễ lấy đến vậy?",[13,115,116],{},[53,117],{"alt":118,"src":119},"Fake flag","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Ffakeflag1.png",[13,121,122],{},"Hành động này giúp tôi xác nhận: trang có logic client-side để cập nhật điểm và hiển thị\u002Fẩn một khu vực UI — nên rất nên kiểm tra mã JS đi kèm.",[21,124,126],{"id":125},"_3-false-flag-lột-mặt-nạ","3. False flag — Lột mặt nạ",[13,128,129,130,133],{},"Mình copy đoạn \"flag\" từ response và thử submit lên scoreboard\u002Fsubmit box (hoặc kiểm tra format). Kết quả: ",[29,131,132],{},"không hợp lệ",". Sau soi kĩ, có vài dấu hiệu cho thấy đó là fake flag:",[75,135,136,143,146],{},[78,137,138,139,142],{},"Flag xuất hiện ngay trong HTML response của ",[34,140,141],{},"\u002F"," — thường flag thật không được hiển thị ở trang chính công khai.",[78,144,145],{},"Nếu đây là một challenge “web”, flag thật thường phải được sinh hoặc bảo vệ bởi logic server (POST\u002Fscoreboard, token, database query, hoặc endpoint phụ).",[78,147,148],{},"Flag trong response có thể là một decoy (đánh lừa người chơi), hoặc dữ liệu dùng cho UI (ví dụ text placeholder) chứ không phải flag thực.",[10,150,151],{},[13,152,153],{},"thấy flag trông hợp lệ nhưng khi thử submit thì bị từ chối — khớp với giả thuyết fake\u002Fdecoy.",[21,155,157],{"id":156},"_4-phân-tích-ngắn-tại-sao-tác-giả-làm-thế","4. Phân tích ngắn — tại sao tác giả làm thế?",[13,159,160,161,163],{},"Cách tác giả đặt một fake flag trong ",[34,162,109],{}," có vài mục đích có ý nghĩa:",[75,165,166,169,172],{},[78,167,168],{},"Lừa người chơi hời hợt: ai không kiểm tra kỹ sẽ nộp luôn và thất bại.",[78,170,171],{},"Hướng chơi: buộc người chơi phải đào sâu — inspect JavaScript, API, WebSocket, localStorage, database endpoints.",[78,173,174],{},"Kiểm tra kỹ năng: thách thức người chơi tìm được flag thật bằng cách reverse client, tìm API ẩn, hoặc trigger event trong gameplay.",[21,176,178],{"id":177},"_5-một-dòng-code-lạc-lõng-dấu-vết-xuất-hiện-ở-dòng-65","5. Một dòng code lạc lõng — dấu vết xuất hiện ở dòng 65",[13,180,181,182,184,185,188,189,192,193,63],{},"Mình kéo chuột xuống response của ",[34,183,109],{}," một lần nữa, đọc từng dòng như đọc nhật ký của một kẻ lén lút. Ở dòng 65 — giữa những thẻ HTML, có một đoạn nhỏ gợi ý: một đường dẫn tới ",[34,186,187],{},"script.js",". “Nếu trong ",[34,190,191],{},"view-source"," không có gì thì sao?” — câu hỏi tự nhiên bật ra. Trang HTML có vẻ đơn thuần, nhưng server vẫn thả ra một pointer đến file JavaScript riêng — ",[29,194,195],{},"chắc chắn là nơi đáng xem",[13,197,198],{},"Cảm giác giống như phát hiện một mẩu giấy gấp trong lòng sách: nhỏ, mộc, nhưng có thể chứa bí mật. Mình nhấp vào đường dẫn đó như một thám tử khẽ mở một ngăn tủ bất thường.",[13,200,201],{},[53,202],{"alt":203,"src":204},"Line 65 - pointer to script.js","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Fendpontjs.png",[10,206,207],{},[13,208,209],{},[39,210,211,212,214],{},"nơi phát hiện ",[34,213,187],{}," ở dòng 65",[21,216,218,219,222],{"id":217},"_6-scriptjs-file-trông-bình-thường-nhưng-chứa-thứ-khủng","6. ",[34,220,221],{},"\u002Fscript.js"," — file trông bình thường nhưng chứa thứ \"khủng\"",[13,224,225,226,229],{},"Truy cập ",[34,227,228],{},"GET \u002Fscript.js"," và… bingo. File JavaScript không dài, nhưng ngay ở đầu có một chuỗi nhìn giống flag. Mình copy nó, tim đập nhanh — có cảm giác vừa gần vừa xa với chiến thắng.",[13,231,232],{},[53,233],{"alt":234,"src":235},"Fake flag in script.js","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Frequestjs.png",[10,237,238],{},[13,239,240],{},[39,241,242,243,63],{},"flag hiện trong ",[34,244,187],{},[13,246,247,248,251,252],{},"Nhưng trong đầu mình lại gợn một điều: ",[29,249,250],{},"tại sao author để flag trong JS client?"," Đây là web challenge nhưng flag hiển thị công khai trên client — giống như chủ nhà để chìa khóa dưới thảm chào khách. Hiền lành quá, hoặc là... ",[39,253,254],{},"cái bẫy đang chờ.",[21,256,258],{"id":257},"_7-kiểm-tra-định-dạng-flag-hay-là-mồi","7. Kiểm tra định dạng — flag hay là mồi?",[13,260,261,262,264,265,268,269,272],{},"Mình không nộp ngay. Thứ nhất vì đã trải nghiệm lần trước: flag “trong trang” hóa ra là fake. Thứ hai vì có một quy ước nhỏ mà mình luôn nhớ: mọi flag chính thức trong cuộc thi này bắt đầu bằng miniCTF{...}. Nhưng flag mình tìm thấy trong ",[34,263,187],{}," bắt đầu bằng ",[34,266,267],{},"minCTF{...}"," — thiếu một chữ ",[34,270,271],{},"i",". Một lỗi chính tả? Hay cố tình?",[13,274,275],{},[53,276],{"alt":277,"src":278},"Fake flag 2","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Ffakeflag2.png",[13,280,281,282,285,286,288],{},"Cảm giác lạnh lạnh rồi: đây lại là một fake flag — tinh tế hơn, đánh vào mắt người chơi vội vàng. (Hình 7 minh họa sự khác biệt: ",[34,283,284],{},"miniCTF{...}"," vs ",[34,287,267],{},".)",[21,290,292],{"id":291},"_8-nội-tâm-của-thám-tử-từ-nghi-ngờ-đến-quyết-tâm","8. Nội tâm của thám tử — từ nghi ngờ đến quyết tâm",[13,294,295,296,299],{},"Ở điểm này mình có hai cảm xúc song hành: tức vì bị đánh lừa, và hừng hực vì thử thách được nâng cấp. Tác giả dùng ",[29,297,298],{},"fake flag với biến thể gần giống"," để lọc những ai copy-paste mà không kiểm tra quy ước. Rõ ràng họ muốn người chơi:",[75,301,302,305,308],{},[78,303,304],{},"Phải biết quy ước flag (miniCTF{}),",[78,306,307],{},"Phải biết so sánh chuỗi, không nộp đại,",[78,309,310],{},"Phải điều tra sâu hơn: file JS chứa decoy, còn flag thật sẽ nằm ở chỗ khác — có thể trên server, trong endpoint chỉ trả khi có điều kiện, hoặc là phải kết hợp một hành động trong game để trigger.",[13,312,313,314,317],{},"Mình tự nhủ: “",[39,315,316],{},"Ok, họ chơi fair-play theo cách của họ — mình sẽ trả lời bằng sự tỉ mỉ",".”",[21,319,321],{"id":320},"_9-kế-hoạch-tấn-công-tư-duy-những-việc-mình-làm-tiếp-theo-ngắn-gọn-nhưng-hành-động","9. Kế hoạch tấn công tư duy — những việc mình làm tiếp theo (ngắn gọn nhưng hành động)",[13,323,324],{},"Mình viết ra checklist để lần theo dấu vết — đây là thứ mình định làm ngay sau đó:",[326,327,329],"h3",{"id":328},"_1-search-toàn-bộ-javascript","1. Search toàn bộ JavaScript",[75,331,332],{},[78,333,334],{},"Dò tiếp script.js xem có hàm nào mã hóa\u002Fgiải mã flag không (ví dụ atob, decode, xor, hexToStr), hoặc có URL khác được build động trong JS.",[326,336,338],{"id":337},"_2-tìm-chuỗi-minictf-trong-toàn-bộ-nguồn","2. Tìm chuỗi 'miniCTF' trong toàn bộ nguồn",[75,340,341],{},[78,342,343],{},"Kiểm tra xem có chỗ nào ghép chuỗi, so sánh, hoặc validate flag theo format không.",[326,345,347],{"id":346},"_3-quan-sát-network-khi-tương-tác-game","3. Quan sát Network khi tương tác game",[75,349,350,353],{},[78,351,352],{},"Chơi, đạt một mốc (ví dụ score nhất định) xem có request mới nào xuất hiện (POST \u002Fsubmit, GET \u002Fget_flag, WebSocket messages...).",[78,354,355],{},"Bật tab WS và XHR — devs thường dùng WS để gửi event game-time.",[326,357,359],{"id":358},"_4-thử-các-endpoint-liên-quan","4. Thử các endpoint liên quan",[75,361,362,365],{},[78,363,364],{},"Truy cập \u002Fget_flag, \u002Fflag, \u002Fadmin, \u002Fget_flag.php, \u002Fapi\u002Fflag, \u002Fsubmit_score v.v. với đủ headers (Referer, Cookie).",[78,366,367],{},"Để ý header Referer — có thể server kiểm tra game origin.",[326,369,371],{"id":370},"_5-kiểm-tra-logic-client-server","5. Kiểm tra logic client-server",[75,373,374],{},[78,375,376],{},"Nếu trong JS có request đến get_flag.php hoặc tương tự, mở và đọc (nếu truy cập được) — hoặc thử gọi trực tiếp bằng curl với body\u002Fheaders tương tự.",[326,378,380],{"id":379},"_6-kiểm-tra-storage-cookies","6. Kiểm tra storage & cookies",[75,382,383],{},[78,384,385],{},"Xem localStorage \u002F sessionStorage \u002F cookie — có thể flag thật được giải mã & lưu ở client sau khi hoàn thành điều kiện.",[326,387,389],{"id":388},"_7-so-sánh-fake-vs-thật","7. So sánh fake vs thật",[75,391,392],{},[78,393,394],{},"Lưu mọi bằng chứng: screenshot, response bodies, timestamps để đưa vào write-up cuối cùng.",[21,396,398],{"id":397},"_10-bí-mật-hiện-ra-ở-dòng-94-một-hàm-lặng-lẽ-gọi-get_flagphp","10. Bí mật hiện ra ở dòng 94 — một hàm lặng lẽ gọi get_flag.php",[13,400,401,402,404,405,408,409,412,413,416],{},"Tôi kéo tiếp xuống, đọc ",[34,403,187],{}," như đọc một bức thư rải rác mã. Ở dòng 94 xuất hiện một hàm có tên ",[34,406,407],{},"returntof()"," — ngắn gọn, nhưng hành động của nó lại to như sấm: hàm này dùng ",[29,410,411],{},"Fetch API"," để gọi ",[34,414,415],{},"get_flag.php",". Logic rất đơn giản và lạnh lùng:",[75,418,419,425],{},[78,420,421,422,63],{},"Gọi ",[34,423,424],{},"fetch('get_flag.php')",[78,426,427,428,431,432,435,436,439,440,443],{},"Nếu ",[34,429,430],{},"response.ok"," thì ",[34,433,434],{},"response.text()"," → gán vào ",[34,437,438],{},"#flagContainer"," và ",[34,441,442],{},"console.log"," ra một chuỗi mà trông rất giống flag.",[13,445,446,447,453],{},"Một chi tiết nhỏ nhưng quan trọng: ",[29,448,449,450,452],{},"flag được trả trực tiếp từ endpoint ",[34,451,415],{}," nếu được gọi đúng cách",". Đây không còn là mồi tinh vi nữa — đây là cánh cửa — chỉ cần ai đó biết cách gõ đúng thì cửa mở.",[13,455,456],{},[53,457],{"alt":458,"src":459},"script.js - returntof() fetch get_flag.php","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Fcodejs94.png",[13,461,462,463,466],{},"Với dòng gọi ",[34,464,465],{},"fetch"," làm mình giật mình: “Ồ, vậy là server có flag thật, vấn đề chỉ là khi nào nó chịu trả.”",[21,468,470],{"id":469},"_11-phút-giây-quyết-định-ở-dòng-127-điều-kiện-mở-kho","11. Phút giây quyết định ở dòng 127 — điều kiện mở kho",[13,472,473,474,63],{},"Tiếp tục cuộn, đến dòng 127 tôi thấy một hàm khác xử lý điểm số: hàm nhận tham số ",[34,475,476],{},"newScore",[13,478,479],{},[53,480],{"alt":481,"src":482},"script.js - updateScore condition","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Fcodejs127.png",[13,484,485],{},"Trong hàm này có điều kiện kỳ lạ:",[487,488,493],"pre",{"className":489,"code":490,"language":491,"meta":492,"style":492},"language-js shiki shiki-themes github-light github-dark","if (newScore >= 10000000000) {\n    returntof();\n}\n","js","",[34,494,495,518,528],{"__ignoreMap":492},[496,497,500,504,508,511,515],"span",{"class":498,"line":499},"line",1,[496,501,503],{"class":502},"szBVR","if",[496,505,507],{"class":506},"sVt8B"," (newScore ",[496,509,510],{"class":502},">=",[496,512,514],{"class":513},"sj4cs"," 10000000000",[496,516,517],{"class":506},") {\n",[496,519,521,525],{"class":498,"line":520},2,[496,522,524],{"class":523},"sScJk","    returntof",[496,526,527],{"class":506},"();\n",[496,529,531],{"class":498,"line":530},3,[496,532,533],{"class":506},"}\n",[13,535,536,537,542,543,545],{},"Nghĩa là: ",[29,538,539,540],{},"chỉ khi điểm đạt >= 10,000,000,000 thì client mới gọi ",[34,541,407],{},", tức mới yêu cầu ",[34,544,415],{}," và hiển thị flag. Tác giả đã đặt một điều kiện \"gameplay\" vô cùng khắc nghiệt — một con số gần như vô lý để đạt bằng cách chơi thật. Đây là một cái bẫy thông minh: flag thật bị khóa phía client bằng điều kiện về score, thay vì ở server bằng token hay session. Hình 9 phơi bày cái câu điều kiện đó — và trong đầu tôi bắt đầu thấy một con đường tắt.",[21,547,549],{"id":548},"_12-ngón-tay-vụt-qua-console-cheat-code-của-thám-tử","12. Ngón tay vụt qua console — cheat code của thám tử",[13,551,552],{},"Lúc này hai lựa chọn hiện ra:",[75,554,555,558],{},[78,556,557],{},"Cố gắng “chơi” đến con số vô lý kia (không khả thi), hoặc",[78,559,560,563,564,566,567,63],{},[29,561,562],{},"Gọi hàm nội bộ bằng DevTools"," — mà ",[34,565,187],{}," đã thực thi trên trang, nên các hàm đó đang sống trong scope ",[34,568,569],{},"window",[13,571,572,573,576],{},"Tôi mở ",[29,574,575],{},"DevTools → Console",", gõ mạnh vào như gõ một mật khẩu bí mật:",[487,578,580],{"className":489,"code":579,"language":491,"meta":492,"style":492},"updateScore(10000000000)\n",[34,581,582],{"__ignoreMap":492},[496,583,584,587,590,593],{"class":498,"line":499},[496,585,586],{"class":523},"updateScore",[496,588,589],{"class":506},"(",[496,591,592],{"class":513},"10000000000",[496,594,595],{"class":506},")\n",[13,597,598,599,601,602,604,605,607],{},"Bùm. Màn hình như nổ tung. ",[34,600,407],{}," được gọi, ",[34,603,424],{}," trả về, và ngay lập tức flag sáng lên ở ",[34,606,438],{}," và trong console.",[13,609,610],{},[53,611],{"alt":612,"src":613},"Console - flag revealed","\u002Fimages\u002FminiCTF\u002FFlappyBird\u002Fpayload.png",[10,615,616],{},[13,617,618],{},[39,619,620],{},"khoảnh khắc flag xuất hiện.",[13,622,623],{},"Dòng chữ tôi mong đợi hiện ra rành rọt:",[487,625,630],{"className":626,"code":628,"language":629},[627],"language-text","miniCTF{d0wn_th3_r4bb1t_h0l3_w3_g0}\n","text",[34,631,628],{"__ignoreMap":492},[13,633,634],{},"Cảm giác — một nửa là thỏa mãn, một nửa là tội lỗi tinh tế: tôi vừa “bẻ” quy tắc chơi, nhưng đúng là tác giả đã để lộ kẽ hở — họ tin vào bảo vệ bằng số điểm, mà quên rằng client-side JavaScript luôn có thể được thao tác.",[21,636,638],{"id":637},"_13-kể-lại-khoảnh-khắc-cinematic-closure","13. Kể lại khoảnh khắc — cinematic closure",[13,640,641,642,644],{},"Đèn trên bàn bật tắt, tiếng mưa ngoài cửa nhỏ lại. Tôi ngồi im một lát, nhìn dòng flag trong console như một câu trả lời cho sự tò mò: cái này là thật. Cả hành trình, từ giao diện Flappy Bird ngây thơ → fake flag lừa nhanh ở HTML → fake flag biến âm ở ",[34,643,187],{}," → đến flag thật được khóa sau điều kiện score — tất cả tạo nên một câu chuyện có cao trào và cú twist.",[13,646,647],{},"Tác giả đã dựng một kịch bản: mồi giả để thử phản xạ, câu đố client-side để đo trình — và cuối cùng là một cửa hậu cho những ai chịu đào sâu. Mình lý giải: họ muốn phân biệt người chơi “copy-paste” và người chịu debug, nhưng quên rằng client-side không bao giờ là rào chắn an toàn.",[21,649,651],{"id":650},"_14-kết-thúc-con-chim-đã-hạ-cánh","14. Kết thúc — con chim đã hạ cánh",[13,653,654],{},"Sau cả hành trình:",[75,656,657,660,666,669],{},[78,658,659],{},"Từ fake flag trong HTML,",[78,661,662,663,665],{},"Đến fake flag biến dạng trong ",[34,664,187],{},",",[78,667,668],{},"Rồi điều kiện vô lý ở score 10 tỷ,",[78,670,671],{},"Cuối cùng là “cheat code” qua DevTools Console,",[13,673,674],{},"tôi đã lấy được flag thật:",[487,676,678],{"className":677,"code":628,"language":629},[627],[34,679,628],{"__ignoreMap":492},[13,681,682,683,63,686,688,689,691],{},"Câu chuyện Flappy Bird khép lại như một minh họa rõ ràng: ",[29,684,685],{},"đặt niềm tin vào client-side là một sai lầm",[17,687],{},"\nTrong thế giới thật, bất cứ logic nào để bảo vệ secret\u002Fflag phải được xử lý server-side.",[17,690],{},"\nMọi thứ hiển thị trong HTML\u002FJS đều chỉ là “sân chơi” — và người chơi luôn có quyền gian lận.",[10,693,694],{},[13,695,696],{},"Con chim nhỏ cuối cùng cũng đã hạ cánh. Trò chơi kết thúc. Flag nằm gọn trong tay.",[21,698,700],{"id":699},"appendix-evidence-bước-tái-hiện-nhanh","Appendix — Evidence & bước tái hiện nhanh",[702,703,704,709,719,724,737,771],"ol",{},[78,705,225,706,708],{},[34,707,36],{}," → giao diện game Flappy Bird hiển thị.",[78,710,711,712,715,716,718],{},"Mở ",[29,713,714],{},"DevTools → Network"," → ",[34,717,109],{}," → trong response thấy một “flag” giả.",[78,720,721,722,63],{},"Kéo xuống dòng ~65 trong response → phát hiện link tới ",[34,723,187],{},[78,725,225,726,728,729,732,733,736],{},[34,727,228],{}," → thấy thêm một flag giả khác (",[34,730,731],{},"minCTF{}"," thay vì ",[34,734,735],{},"miniCTF{}",").",[78,738,739,740,742,743],{},"Đọc tiếp ",[34,741,187],{},":\n",[75,744,745,759],{},[78,746,747,750,751,753,754,756,757,63],{},[29,748,749],{},"Dòng 94",": hàm ",[34,752,407],{}," gọi ",[34,755,424],{}," và in flag vào ",[34,758,438],{},[78,760,761,750,764,767,768,63],{},[29,762,763],{},"Dòng 127",[34,765,766],{},"updateScore(newScore)"," có điều kiện ",[34,769,770],{},"if (newScore >= 10000000000) returntof();",[78,772,711,773,776,777],{},[29,774,775],{},"Console"," và gõ:\n",[487,778,779],{"className":489,"code":579,"language":491,"meta":492,"style":492},[34,780,781],{"__ignoreMap":492},[496,782,783,785,787,789],{"class":498,"line":499},[496,784,586],{"class":523},[496,786,589],{"class":506},[496,788,592],{"class":513},[496,790,595],{"class":506},[10,792,793],{},[13,794,795,796,798,799,801],{},"→ Hàm ",[34,797,407],{}," được gọi → ",[34,800,415],{}," trả về flag thật.",[326,803,805],{"id":804},"bài-học-bảo-mật","Bài học bảo mật",[75,807,808,814,820,826],{},[78,809,810,813],{},[29,811,812],{},"Đừng để secret ở client:"," Mọi dữ liệu trong HTML\u002FJS đều có thể bị đọc, sửa hoặc gọi trực tiếp qua Console.",[78,815,816,819],{},[29,817,818],{},"Không kiểm tra điều kiện trên client:"," Việc đặt rào cản “score >= 10 tỷ” chỉ là hình thức. Người chơi luôn có thể gọi trực tiếp hàm JavaScript.",[78,821,822,825],{},[29,823,824],{},"Luôn xác thực ở server-side:"," Nếu muốn gắn flag với điều kiện gameplay, hãy để server tính toán và trả về, thay vì cho client quyết định.",[78,827,828,831],{},[29,829,830],{},"Fake flag chỉ là gia vị:"," Có thể dùng để đánh lạc hướng, nhưng cần rõ ràng rằng flag thật luôn được bảo vệ bởi logic an toàn hơn.",[13,833,834,835,63],{},"Kết lại, challenge “Flappy Bird” là một ví dụ thú vị để nhắc nhở: ",[29,836,837],{},"đừng bao giờ tin vào client-side",[839,840,841],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":492,"searchDepth":520,"depth":520,"links":843},[844,845,846,847,848,849,851,852,853,862,863,864,865,866,867],{"id":23,"depth":520,"text":24},{"id":66,"depth":520,"text":67},{"id":125,"depth":520,"text":126},{"id":156,"depth":520,"text":157},{"id":177,"depth":520,"text":178},{"id":217,"depth":520,"text":850},"6. \u002Fscript.js — file trông bình thường nhưng chứa thứ \"khủng\"",{"id":257,"depth":520,"text":258},{"id":291,"depth":520,"text":292},{"id":320,"depth":520,"text":321,"children":854},[855,856,857,858,859,860,861],{"id":328,"depth":530,"text":329},{"id":337,"depth":530,"text":338},{"id":346,"depth":530,"text":347},{"id":358,"depth":530,"text":359},{"id":370,"depth":530,"text":371},{"id":379,"depth":530,"text":380},{"id":388,"depth":530,"text":389},{"id":397,"depth":520,"text":398},{"id":469,"depth":520,"text":470},{"id":548,"depth":520,"text":549},{"id":637,"depth":520,"text":638},{"id":650,"depth":520,"text":651},{"id":699,"depth":520,"text":700,"children":868},[869],{"id":804,"depth":530,"text":805},"Một write-up cho challenge Web tại miniCTF, kể lại hành trình khám phá và khai thác logic client-side để lấy flag. (500 điểm)","md",{"badge":873,"tags":874,"date":877,"dateDisplay":878,"image":879},"CTF Write-up",[875,876],"CTF","Web","2025-08-08","August 8, 2025","\u002Fthumbnails\u002Fispminictf2.png",true,"\u002Fposts\u002Fflappy-bird",{"title":5,"description":870},"posts\u002FFlappy-Bird","tPwGnp098pvIAaNPd_PcdncDSSvuT_6NdWFhOCwPR9I",12,1787385493755]