June 22, 2026
[First Blood] Write-Up: BugForge Weekly Challenge — Vaultly with Prototype Pollution Exposing the…
และนี่ก็เป็นอีกครั้งที่ทีมสามารถทำ first blood ใน Bugforge Weekly Challenge ซึ่งโจทย์ในรอบนี้เป็นเว็บที่เพิ่งทำออกมาใหม่…
Athiwat Tiprasaharn
3 min read
และนี่ก็เป็นอีกครั้งที่ทีมสามารถทำ first blood ใน Bugforge Weekly Challenge ซึ่งโจทย์ในรอบนี้เป็นเว็บที่เพิ่งทำออกมาใหม่ และมีโจทย์ออกมาอยู่ไม่กี่โจทย์ใน Daily Cahllenge แต่รอบนี้ถูกจับมาเป็นโจทย์ Weekly เลยครับ นั่นคือโจทย์ Vaultly
สำหรับโจทย์นี้ จะเป็นเว็บที่ใช้ในการเก็บเอกสารและไฟล์ต่าง ๆ พร้อมกับสร้าง API เพื่อเชื่อมต่อกันได้ง่ายขึ้น
ปกติแล้วทีมจะชอบลงทะเบียนเพื่อเข้าใช้งานเว็บไซต์ก่อน แต่รอบนี้เขามีบัญชีทดลองมาให้ใช้ ทีมก็ใช้บัญชี admin@acme.test ไปเลย
บนหน้าเว็บจะมีเอกสารต่าง ๆ ถูกเก็บไว้ใน vault ทีมก็นั่งรีวิวเอกสารอย่างรวดเร็ว จนกระทั่งเจอเข้ากับเอกสารหนึ่งใน Vault Specs ชื่อไฟล์ public-api.md
ที่น่าสนใจคือมันคือเอกสารที่บอกวิธีการเรียกดูไฟล์และเรียกดู vault ที่เปิดสาธารณะ (public) ซึ่งในนั้นมีการพูดถึงเรื่องการใช้ API key สำหรับการเรียกดูไฟล์ด้วย
งั้นเรามาดูการสร้าง API key ก่อน โดยเข้าไปที่ Settings แล้วเลือก API tokens ในการสร้าง API key เราจะใส่ชื่อและกำหนดสิทธิ์ว่าคีย์ที่สร้างขึ้นสามารถนำไปใช้ด้านใดได้บ้าง
ทีมสร้างคีย์ชื่อ test ขึ้นมา โดยกำหนดให้ทำได้ทุกสิทธิ์ไปเลย ซึ่งคีย์ที่ได้ คือ vat_kZyD88upjRNJ06MkZVhi3s47YJaFxxyj แต่ตรงนี้ไม่ได้มีข้อกำหนดว่าจะใช้คีย์นี้ยังไง (เพราะโจทย์ก่อน ๆ หน้านี้อย่าง CopyPasta จะมีบอกว่าให้ใช้คีย์ใน Header ผ่านพารามิเตอร์ X-API-Key เป็นต้น)
ย้ายมาต่อกันใน Burp เรามาเรียกดูเส้น api ที่ยังไม่ใช้ API key กันก่อน นั่นคือเส้น GET /api/v1/published (ไม่ต้องสร้าง request ใหม่ก็ได้นะครับ แค่โยน GET request อื่น ๆ จาก Proxy เข้ามา แล้วเปลี่ยนแค่เส้น api ก็พอ) ซึ่งเส้นนี้จะสามารถเรียกดูว่ามี vault และไฟล์ใดบ้างที่เปิดสาธารณะ ซึ่งถ้าไล่ดูจะพบว่า id มันขาด ๆ หาย ๆ ไป แสดงว่ามีบางไฟล์หรือบาง vault ที่ไม่ได้เปิดสาธารณะครับ
ถัดมา เรามาเช็กเส้น api ที่ใช้ API key กันบ้าง โดยใช้ request เดิม เปลี่ยนแค่เส้น api เป็น GET /api/v1/files แต่เมื่อกดส่งไปแล้ว จะขึ้นว่า Invalid token แสดงว่าเราจะต้องลองมาใช้ API key ที่สร้างขึ้นมาก่อนหน้านี้ครับ
เนื่องจากเว็บไม่ได้แจ้งว่าจะใช้ API key ยังไง ดังนั้น ทีมขอเลือกใช้แบบเบสิกที่สุดที่เคยใช้ คือ Authorization: Bearer เมื่อลองกดส่ง Request ไป ปรากฏว่าสามารถส่งได้แล้วครับ
ทีนี้ point สำคัญของช่องโหว่ในโจทย์ข้อนี้เลยคือ เส้น api ที่เป็น PATCH /api/v1/files/:id ซึ่งมันเป็นเส้นที่ใช้ในการอัปเดตข้อมูลของไฟล์ครับ โดยเราสามารถแก้ไข metadata ของไฟล์ได้ ด้วยการใส่ json body เป็น {"metadata":{…}} และข้อความสำคัญคือ เมื่อเราใส่ json body ดังกล่าวแล้ว ข้อมูลจะถูก merged into the existing metadata !!
เอาล่ะสิ เห็นแบบนี้ทีมพอคิดออกละว่ามันคือช่องโหว่อะไร จากการนั่งเรียนคอร์ส Advanced Web Hacking ของ TCM Security และสอบ TCM PWPE แล้วเรียบร้อย นี่มันคือช่องโหว่ Prototype Pollution ชัด ๆ
งั้นเดี๋ยวเรามาลองส่ง Request กันดูนะครับ เริ่มแรกทีมสลับให้ GET method ด้วยการคลิกขวาบน Burp เลือก change request method ซึ่งมันจะสลับเป็น POST ก่อน จากนั้นเราจึงแก้เป็น PATCH ตามด้วยเส้น api ที่เอกสารระบุ ซึ่งทีมจะใช้เป็น /api/v1/files/1
ในส่วนของ Body จะใช้เป็น { "metadata": {"tag":"test"} } แล้วลองส่งไป ปรากฏว่ามีการอัปเดต metadata ขึ้นจริง
จากนั้นลองส่งไปอีกรอบด้วย body { "metadata": {"tag":"test", "label":"test2"} } จะพบว่ามันมีการอัปเดตข้อมูล metadata เพิ่มเข้าไปจริงครับ
แล้วเราจะโจมตีด้วย Prototype Pollution ยังไงล่ะ โอเค เราย้อนกลับมาดูที่เอกสารอีกรอบหนึ่ง มันจะมีเขียนบอกว่าถ้า vault ไหนที่มีการกำหนดตัวแปร publicProjection แล้วตามด้วยประเภทข้อมูลที่ต้องการให้แสดงผล ได้แก่ id,name,mime,size,content vault และไฟล์ข้างในนั้น จะถือว่าเป็นสาธารณะ และข้อมูลของไฟล์ก็จะแสดงข้อมูลตามที่กำหนดไว้ครับ
ยกตัวอย่างเช่น valut press kit ถูกกำหนดไว้แบบนี้ตอนสร้าง
{ name: "Press Kit", publicProjection: "id,name,mime,size" }
จะทำให้ vault นี้กลายเป็นสาธารณะ และแสดงข้อมูลของไฟล์ใน vault ได้แก่ ข้อมูล id, name, mime และ size
แสดงว่าถ้า vault ไหน ไม่มี publicProjection มันก็จะกลายเป็น vault private ไปด้วยนั่นเอง
ซึ่งจุดที่เราจะโจมตีคือจุดนี้เองครับ เราจะใช้เรื่อง metadata ที่สามารถ merge ข้อมูลได้เป็นจุดโจมตี ให้ทุก vault กลายเป็น public!!
วิธีการโจมตีจะเป็นแบบนี้ครับ กลับไปที่เส้น PATCH /api/v1/files/1 แต่รอบนี้เราใส่ metadata แบบนี้ลงไปแทน { "metadata": { "proto": { "publicProjection": "id,name,mime,size,content" } } }
วิธีการนี้คือเราจงใจให้ metadata ส่งข้อมูลไป merge ผิดที่ แทนที่จะเอาไปใส่ไว้ในไฟล์ เมื่อมี proto โผล่มา ระบบ merge จะส่งข้อมูลไปไว้ใน object.prototype หรือต้นแบบของตัว object ซะเลย ทำให้ต้นแบบของ object ไม่ว่าเราจะสร้างข้อมูล private อันไหนขึ้นมาใหม่ หรือของเก่าที่โดนสร้างไว้แล้ว จะถูกทำให้เป็น public ทั้งหมด
เมื่อลองกดส่งไป จะไม่เห็นอะไรเปลี่ยนแปลงครับ เพราะเราไม่ได้เก็บข้อมูลลงใน metadata แต่เรากำลังเอาข้อมูลไปยัดลงใน prototype
หลังจากนั้นกลับไปที่เส้น GET /api/v1/published (ใช้ cookie เหมือนเดิมนะ ไม่ต้องใช้ API key) จะพว่ามี vault operation โผล่มาในอันสุดท้าย และเจอไฟล์ที่ชื่อ break-glass.txt ด้วย!!
มันเกิดขึ้นได้ยังไง? อย่างที่ทีมบอก เส้น GET /api/v1/published จะอ่านเฉพาะ vault ที่มีการกำหนดตัวแปร publicProjection ลงไปก็จริง แต่ไม่ได้หมายความว่ามันจะเช็กอยู่ที่เดียวครับ ระบบจะมีขั้นตอนการเช็กแบบนี้ครับ
-
เมื่อเราเรียกเส้น GET /api/v1/published มันเรียกแบบนี้ครับ vault.publicProjection โดยเริ่มแรกมันจะไปหาดูว่า vault ไหนที่มี publicProjection อยู่บ้าง ก็จะเรียกมาแสดงผลทันที
-
แต่ถ้าตรวจแล้ว vault นั้นไม่มี publicProjection มันจะไม่หยุดตรงนี้นะครับ ใน Javascript เมื่อเรียก vault.publicProjection แบบนี้ แล้วหาใน vault โดยตรงไม่เจอ มันจะวิ่งย้อนกลับไปเช็กในจุดที่ใหญ่กว่า คือ object.prototype (ง่าย ๆ คือ ถามหา publicProjection จากลูกไม่เจอ ก็ไปถามแม่มัน ถ้าไม่เจอค่อยคอนเฟิร์มว่าไม่เจอจริง ๆ)
-
ก่อนการโจมตีของเรา vault ที่เป็น private จะไม่มี publicProjection และใน prototype เองก็ไม่มี publicProjection อยู่เลย ดังนั้น vault ที่เป็น private จะไม่หลุดออกมาครับ
-
แต่หลังจากที่เราโจมตีโดยยัด publicProjection=id,name,mime,size,content เข้าไปใน prototype แล้ว เมื่อเรียก GET /api/v1/published ใน private vault ไม่เจอ ระบบก็เลยย้อนกลับไปถาม prototype แต่รอบนี้ prototype ดันมี publicProjection เขียนไว้ ว่าให้แสดงผล id,name,mime,size,content ทำให้ระบบต้องแสดงข้อมูลของ vault นั้นออกมา แม้ว่าตัว vault เองจะไม่มี publicProjection เลยก็ตาม
สุดท้ายเมื่อทีมเรียกดูไฟล์ด้วย GET /api/v1/published/28 ก็จะได้ flag ออกมาครับ
และ Flag ของโจทย์นี้คือ bug{WRP6oCDRpzl3Y5SwadAJKzeU1iOeDz3I}
สนใจแนวนี้ฝากกดติดตามด้วยนะครับ หรือติดตามได้ในช่องทางเหล่านี้