1 คะแนน โดย GN⁺ 2024-10-18 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ขอบเขตสิทธิ์ของ WebUI ใน Chromium, ฟีเจอร์ทดสอบนโยบายสำหรับองค์กร และข้อบกพร่องใน DevTools extension API เชื่อมโยงกัน ทำให้ส่วนขยาย Chrome ที่เป็นอันตรายสามารถไปถึงขั้นรันคำสั่งเชลล์ได้โดยต้องอาศัยปฏิสัมพันธ์จากผู้ใช้เพียงเล็กน้อย
  • เส้นทางการโจมตีคือการเรียก policy test API ที่ไม่มีเอกสารกำกับจาก chrome://policy เพื่อเปลี่ยนนโยบายผู้ใช้ แล้วใช้ path และ argument ของเบราว์เซอร์ทางเลือกใน Browser Switcher ในทางที่ผิดให้กลายเป็นคำสั่งเชลล์
  • chrome.devtools.inspectedWindow.reload() อนุญาตให้รัน injectedScript และโค้ดสามารถถูกรันใน privileged WebUI ได้ เนื่องจากการบล็อกการเข้าถึงล่าช้าในจังหวะที่ย้ายไปยัง WebUI หรือมีคำขอ Page.reload ที่ค้างอยู่หลังการแครช
  • Google จัดประเภทช่องโหว่เป็น P1/S1 และเพิ่มการตรวจสอบ loaderId ของ Page.reload, การตรวจ URL ใน inspectedWindow.reload() และการตรวจว่าเปิดใช้การทดสอบนโยบายใน WebUI handler หรือไม่
  • ช่องโหว่ที่เกี่ยวข้องได้รับรหัส CVE-2024-5836 และ CVE-2024-6778 ทั้งคู่ได้ CVSS 8.8 High และเงินรางวัลสุดท้ายอยู่ที่ $20,000

Chromium WebUI และขอบเขตของแซนด์บ็อกซ์

  • Chromium รันโค้ดที่ไม่น่าเชื่อถือภายใน แซนด์บ็อกซ์ และ JavaScript ของส่วนขยาย Chrome ก็ควรทำงานได้เฉพาะภายในสิทธิ์ที่ได้รับและ API ที่เข้าถึงได้เท่านั้น
  • แม้สิทธิ์ของส่วนขยายเพียงอย่างเดียวอาจขโมยข้อมูลล็อกอินหรือประวัติเบราว์เซอร์ได้ แต่โดยหลักแล้วขอบเขตผลกระทบควรจำกัดอยู่ภายในเบราว์เซอร์
  • GUI บางส่วนของ Chromium ถูกสร้างเป็น WebUI เช่น chrome://settings, chrome://history
    • WebUI เขียนด้วย HTML, CSS และ JavaScript แต่มีสิทธิ์สูงกว่าเว็บเพจทั่วไป เพราะต้องแสดงและแก้ไขข้อมูลภายในของเบราว์เซอร์
    • JavaScript ฝั่ง frontend ของ WebUI สามารถสื่อสารกับโค้ด C++ native ภายในเบราว์เซอร์ผ่าน API ส่วนตัวได้
  • หากสามารถรันโค้ดใน WebUI ได้ ก็อาจนำไปสู่การบายพาสแซนด์บ็อกซ์ของ Chromium ดังนั้นจึงสำคัญที่ต้องป้องกันไม่ให้ผู้โจมตีรัน JavaScript ที่ไม่น่าเชื่อถือบนหน้า chrome://
  • ตัวอย่างเช่น หากคลิกรายการดาวน์โหลดไฟล์ .exe ใน chrome://downloads ไฟล์ executable อาจถูกเปิดได้ ดังนั้น Chromium จึงตรวจว่าการเปิดไฟล์มาจาก input ของผู้ใช้จริงหรือไม่

การบายพาสฟีเจอร์ทดสอบนโยบายองค์กร

  • การค้นหาช่องโหว่เริ่มจาก enterprise policy system ของ Chromium
    • ระบบนี้เป็นฟีเจอร์ให้ผู้ดูแลระบบบังคับใช้การตั้งค่าบางอย่างบนอุปกรณ์ของบริษัทหรือโรงเรียน
    • โดยทั่วไปนโยบายจะผูกกับบัญชี Google และดาวน์โหลดจากเซิร์ฟเวอร์จัดการของ Google
  • นโยบายแบ่งเป็น device policies และ user policies
    • device policies ใช้จัดการการตั้งค่าทั้งเครื่องของอุปกรณ์ Chrome OS
    • user policies ใช้กับผู้ใช้หรือ instance ของเบราว์เซอร์รายใดรายหนึ่ง และใช้ได้บนทุกแพลตฟอร์ม
    • บน Linux สามารถใช้ user policies กับ instance ของ Google Chrome ได้โดยวางไฟล์ JSON ไว้ที่ /etc/opt/chrome/policies แต่การเขียนลงไดเรกทอรีนี้ต้องใช้สิทธิ์ root
  • สามารถตรวจสอบนโยบายที่ใช้กับอุปกรณ์ปัจจุบันได้ที่ WebUI chrome://policy
    • หน้านี้แสดงรายการนโยบายที่ใช้, log ของ policy service และฟีเจอร์ส่งออก JSON
    • โดยทั่วไปไม่มีวิธีแก้ไขนโยบายจากหน้านี้
  • release notes ของ Chrome Enterprise สำหรับ Chrome v117 มีรายการว่าเพจ chrome://policy/test อนุญาตให้ทดสอบนโยบายในช่อง Beta, Dev และ Canary
    • ฟีเจอร์นี้ไม่ได้ถูกกล่าวถึงในเอกสาร Chromium นอกเหนือจาก release notes ดังกล่าว
    • หากต้องการเปิดใช้อย่างถูกต้อง จำเป็นต้องใช้นโยบาย PolicyTestPageEnabled ที่ไม่มีเอกสารกำกับ
    • หากไม่มีนโยบายนี้ chrome://policy/test จะ redirect ไปยัง chrome://policy

ความล้มเหลวในการตรวจสอบของ setLocalTestPolicies

  • โค้ด JavaScript ของ chrome://policy/test ใช้ sendWithPromise('setLocalTestPolicies', ...) เพื่อตั้งค่านโยบายทดสอบ
    • sendWithPromise() เป็น wrapper ของ chrome.send() ซึ่งเป็น API ส่วนตัวของ WebUI
    • การเรียกนี้ส่งคำขอไปยังฟังก์ชัน handler ฝั่ง C++ และ handler สามารถทำงานภายในเบราว์เซอร์ได้
  • เมื่อลองเรียก setLocalTestPolicies โดยตรงจาก console ของ chrome://policy ในตอนแรกเบราว์เซอร์แครช และใน log มีข้อความว่าต้องใช้อาร์เรย์นโยบาย
  • เมื่อจัดรูปแบบอาร์เรย์นโยบายให้ถูกต้องและส่ง user policy เช่น AllowDinosaurEasterEgg เข้าไป กลับสามารถตั้งค่านโยบายใด ๆ ได้ แม้ไม่ได้เปิดใช้ฟีเจอร์อย่างชัดเจน
  • handler HandleSetLocalTestPolicies ฝั่ง C++ ตรวจแค่ว่ามี local_test_provider อยู่หรือไม่ และไม่ได้ตรวจว่าฟีเจอร์ทดสอบนโยบายได้รับอนุญาตจริงหรือเปล่า
  • LocalTestPolicyProvider::CreateIfAllowed() เรียก IsPolicyTestingEnabled(nullptr, channel)
    • argument แรกคือ pref_service เป็น null ทำให้การตรวจ PolicyTestPageEnabled ถูกข้ามไป
    • การตรวจที่เหลือมีเพียงว่า release channel เป็น CANARY หรือ DEFAULT หรือไม่
  • ใน unbranded Chromium build โค้ด GOOGLE_CHROME_BRANDING ไม่ถูกคอมไพล์ ทำให้ channel ยังคงเป็น UNKNOWN
    • ใน enum UNKNOWN = 0, DEFAULT = UNKNOWN ดังนั้น Chromium และ build อนุพันธ์จึงผ่านการตรวจ channel
    • ใน branded Google Chrome stable build release channel ถูกตั้งอย่างถูกต้อง ดังนั้นบั๊กนี้จึงใช้ไม่ได้ใน stable Google Chrome

การรันคำสั่งเชลล์ต่อผ่าน Browser Switcher

  • เมื่อสามารถตั้งค่า user policy ใด ๆ ได้ โมดูล Legacy Browser Support ใน Chrome enterprise policy จึงกลายเป็นเส้นทางหลุดออกจากแซนด์บ็อกซ์
  • Legacy Browser Support เรียกอีกอย่างว่า Browser Switcher ออกแบบมาให้รันเบราว์เซอร์ทางเลือกเมื่อผู้ใช้เข้าชม URL บางรายการใน Chromium
    • เป็นฟีเจอร์ที่สร้างขึ้นเพื่อรองรับผู้ใช้ Internet Explorer
    • พฤติกรรมถูกควบคุมด้วยนโยบาย
  • เมื่อนำ policy AlternativeBrowserPath และ AlternativeBrowserParameters มารวมกัน Chromium จะสามารถรัน คำสั่งเชลล์ใด ๆ ในฐานะ “เบราว์เซอร์ทางเลือก” ได้
    • policy ของ Browser Switcher เหล่านี้มีอยู่เฉพาะบน Linux, macOS และ Windows
  • ตัวอย่าง flow มีดังนี้
    • ตั้ง BrowserSwitcherEnabled เป็น true
    • ใส่ example.com ใน BrowserSwitcherUrlList
    • ตั้ง AlternativeBrowserPath เป็น /bin/bash บน Linux
    • ตั้ง AlternativeBrowserParameters เป็น ["-c", "xcalc # ${url}"]
  • เมื่อเบราว์เซอร์นำทางไปยัง example.com Browser Switcher จะทำงาน และคำสั่งในรูป /bin/bash -c 'xcalc # https://example.com' จะถูกรัน
    • ค่าที่แทนใน ${url} ถูกวางไว้หลัง # เพื่อให้ถูกถือเป็นคอมเมนต์ของเชลล์
  • หลังตั้งค่านโยบายใน chrome://policy แล้วเรียก window.open("https://example.com";) ก็สามารถนำไปถึงการรันคำสั่งเชลล์ใด ๆ ได้ด้วย JavaScript เท่านั้น

เส้นทางบายพาสของ DevTools extension API

  • หากมีแค่ขั้นตอนก่อนหน้า เหยื่อต้องคัดลอกโค้ดอันตรายไปวางใน browser console ที่ chrome://policy ทำให้ใช้งานจริงได้ยาก
  • เส้นทางให้รันอัตโนมัติถูกค้นพบผ่านส่วนขยาย Chrome ที่เป็นอันตราย
    • ส่วนขยายสามารถ inject JavaScript ลงในเพจได้ แต่ไม่ควรรัน JavaScript บนหน้า privileged WebUI ได้
  • API หลักที่ส่วนขยายใช้รัน JavaScript บนเพจมีสี่ตัว
    • chrome.scripting
    • chrome.tabs ของ Manifest v2
    • chrome.debugger
    • chrome.devtools.inspectedWindow
  • เป้าหมายการตรวจสอบคือ chrome.devtools.inspectedWindow ซึ่งคาดว่าอาจถูก harden น้อยกว่า
    • ส่วนขยายที่ใช้ API chrome.devtools ต้องมีฟิลด์ devtools_page ใน manifest
    • เมื่อผู้ใช้เปิด DevTools หน้านี้จะถูกโหลดเป็น iframe และสามารถใช้ API chrome.devtools ภายในนั้นได้
  • รายงานบั๊กเดิมของ David Erceg มีกรณีที่ใช้ chrome.devtools.inspectedWindow.eval() ไปถึงการรันโค้ดใน WebUI
    • โดยปกติ เมื่อเพจที่ถูก inspect นำทางไปยัง WebUI การใช้งาน DevTools API ควรถูกปิด
    • แกนหลักของการบายพาสคือส่งคำขอ eval ก่อนที่ Chrome จะปิด API แล้วให้คำขอนั้นไปถึงหน้า WebUI

inspectedWindow.reload() และลักษณะพิเศษของ about:blank

  • chrome.devtools.inspectedWindow.reload() ก็สามารถรัน JavaScript ในเพจที่ถูก inspect ได้เช่นกัน หากรับ argument injectedScript
  • เมื่อเรียก inspectedWindow.reload() บนหน้า about:blank ที่เปิดโดย WebUI สามารถรัน JavaScript ใน privileged page ได้
    • about:blank ไม่มี URL พิเศษในตัวเอง แต่สืบทอดสิทธิ์และ origin ของเพจที่เปิดมัน
    • about:blank ที่เปิดโดย chrome://settings เป็น privileged page ที่มี origin เป็น chrome://settings
  • โค้ดปิดการใช้งาน DevTools API ตรวจเฉพาะ URL ของเป้าหมายที่ถูก inspect ไม่ได้ตรวจ origin
    • แม้ URL จะดูธรรมดา แต่ origin อาจเป็น privileged origin ได้
  • เส้นทาง about:blank เพียงอย่างเดียวนำมาใช้ใน exploit chain ได้ยากทันที เพราะ chrome://policy ไม่ได้เปิด popup about:blank
  • อย่างไรก็ตาม แม้ในสถานการณ์ที่ inspectedWindow.eval() ล้มเหลว inspectedWindow.reload() ก็ยังรัน JavaScript ใน chrome://settings ได้
    • แสดงให้เห็นว่า eval() มีการตรวจของตัวเองในระดับเดียวกับการตรวจ origin แต่ reload() ไม่มีการตรวจระดับเดียวกัน

จาก race condition สู่การทำให้เสถียรด้วยวิธีอาศัยการแครช

  • exploit chain แรกเรียก inspectedWindow.reload() ซ้ำ ๆ เพื่อโจมตีช่วงเวลาสั้น ๆ หลังจากเพจที่ถูก inspect ย้ายไปยัง WebUI แต่ก่อนที่หน้า DevTools จะปิดการใช้งาน API
    • สมมติฐานคือเพจที่ถูก inspect และหน้า DevTools อยู่คนละ process
    • หากคำขอ reload() เข้ามาระหว่างจังหวะที่ย้ายไป chrome://policy กับก่อนที่ DevTools API จะถูกปิด โค้ดจะถูกรันใน WebUI
  • วิธีนี้ใช้งานได้แต่ความน่าเชื่อถือต่ำ
    • หลังปรับแต่งแล้วสำเร็จประมาณ 70%
    • แม้เป็นช่องโหว่ร้ายแรง แต่ความไม่เสถียรอาจทำให้ severity ถูกลดลงได้
  • ต่อมาได้ลองดูว่าสามารถนำพฤติกรรมที่ debugger request ยังเหลืออยู่หลังแท็บแครชแบบแนวทางเดิมของ David Erceg มาใช้กับ inspectedWindow.reload() ได้หรือไม่
  • เมื่อ trigger คำสั่ง debugger สองครั้งติดต่อกัน แท็บจะแครช และคำขอ Page.reload จะค้างอยู่ในคิว ทำให้สามารถถูกรันหลังนำทางไปยัง WebUI ได้
    • วิธีนี้ไม่ต้องพึ่ง race condition อีกต่อไป จึงทำงานได้แบบ 100% reliable
  • ในแพตช์บั๊กก่อนหน้า Google แก้ให้ล้าง pending debugger requests หลังการแครช แต่ปล่อยให้คำขอ Page.reload เป็นข้อยกเว้น
    • inspectedWindow.reload() ภายในส่งคำขอ Page.reload จึงได้รับผลจากข้อยกเว้นนี้
    • แพตช์ในตอนนั้นไม่ได้ป้องกันประเด็นที่ Page.reload สามารถรันสคริปต์ได้
  • การทำให้แท็บแครชทำได้ด้วยวิธีก่อให้เกิดหน่วยความจำไม่พอ นอกเหนือจากวิธี debugger แต่ใน PoC สุดท้ายใช้การแครชด้วย debugger ซึ่งเร็วกว่า

exploit chain สุดท้ายและปฏิสัมพันธ์จากผู้ใช้

  • PoC สุดท้ายทำงานตามลำดับดังนี้
    • ใช้ช่องโหว่ chrome.devtools.inspectedWindow.reload() เพื่อรัน JavaScript payload ใน chrome://policy
    • payload เรียก sendWithPromise("setLocalTestPolicies", policy) เพื่อตั้งค่านโยบายผู้ใช้
    • ตั้งค่า BrowserSwitcherEnabled, BrowserSwitcherUrlList, AlternativeBrowserPath, AlternativeBrowserParameters
    • trigger Browser Switcher ด้วย window.open() หรือการนำทางของเพจ เพื่อรันคำสั่งเชลล์
  • PoC ใช้คำสั่งเปิดเครื่องคิดเลขตามแต่ละ OS
    • Windows: C:\Windows\System32\cmd.exe และ calc.exe
    • Linux: /bin/bash และ xcalc
    • macOS: /bin/bash และ open -na Calculator
  • ปฏิสัมพันธ์จากผู้ใช้อยู่ในระดับแค่ทำให้เปิด DevTools
    • “extension install error” ในภาพตัวอย่างเป็นกลไกเพื่อหลอกให้ผู้ใช้เปิด DevTools
    • เมื่อ DevTools ถูกเปิด chain ที่นำไปสู่ sandbox escape ก็เริ่มทำงาน

การแก้ไขของ Google และการกำหนด CVE

  • หลังได้รับรายงาน Google ยืนยันช่องโหว่อย่างรวดเร็วและจัดประเภทเป็น P1/S1
    • P1/S1 หมายถึง high priority และ high severity
  • ต่อมาในช่วงไม่กี่สัปดาห์ มีการแก้ไขหลักสามอย่างถูกนำเข้า
  • ช่องโหว่ที่เกี่ยวกับ race condition ได้รับรหัส CVE-2024-5836
    • CVSS severity score คือ 8.8 High
  • ช่องโหว่ที่เกี่ยวกับการแครชของเพจที่ถูก inspect ได้รับรหัส CVE-2024-6778
    • ช่องโหว่นี้ก็ได้รับ CVSS severity score 8.8 เช่นกัน
  • หลังการแก้ไขถูก merge เข้าสู่ release branch แล้ว คณะกรรมการ Chrome VRP ตัดสินเงินรางวัล และเงินรางวัลสุดท้ายคือ $20,000

กำหนดการเปิดเผยและข้อมูลประกอบ

  • ไทม์ไลน์มีดังนี้
    • 16 เมษายน: พบ test policies bug
    • 29 เมษายน: พบ inspectedWindow.reload() race condition bug
    • 1 พฤษภาคม: รายงานบั๊กต่อ Google
    • 4 พฤษภาคม: Google จัดประเภทเป็น P1/S1
    • 5 พฤษภาคม: พบและอัปเดตรายงานบั๊กเกี่ยวกับการแครชของเพจที่ถูก inspect
    • 6 พฤษภาคม: Google ขอรายงานบั๊กแยกสำหรับแต่ละส่วนของ chain
    • 8 กรกฎาคม: รายงานบั๊กถูกทำเครื่องหมายว่า fixed
    • 13 กรกฎาคม: ส่งต่อให้คณะกรรมการ Chrome VRP เพื่อพิจารณาเงินรางวัล
    • 17 กรกฎาคม: คณะกรรมการ VRP ตัดสินเงินรางวัล $20,000
    • 15 ตุลาคม: รายงานบั๊กทั้งหมดถูกเปิดเผยต่อสาธารณะ
  • รายงานบั๊กต้นฉบับที่เกี่ยวข้องดูได้ที่ crbug.com/338248595
  • PoC ของแต่ละส่วนช่องโหว่เผยแพร่ใน GitHub repository
  • บั๊ก inspectedWindow.reload ทำงานย้อนหลังได้ถึง Chrome v45
  • หากเผยแพร่ฟีเจอร์ที่ไม่มีเอกสารกำกับ ยังไม่สมบูรณ์ และไม่ปลอดภัยให้ผู้ใช้ทุกคน ความผิดพลาดเล็ก ๆ หลายอย่างอาจรวมกันจนกลายเป็นช่องโหว่ความรุนแรงสูงได้

1 ความคิดเห็น

 
GN⁺ 2024-10-18
ความคิดเห็นจาก Hacker News
  • มีการบอกว่า URL ของหน้าจะถูกแทนที่เป็น ${url} และถ้าใส่ไว้หลัง # ก็จะกลายเป็นคอมเมนต์เพื่อไม่ให้คำสั่งพัง แล้วในนโยบายนี้มีตรรกะตรวจสอบหรือไม่ว่า URL ต้องถูกส่งไปยังที่ใดที่หนึ่งใน AlternativeBrowserParameters?

  • เป็น นักเรียนมัธยมปลาย ที่สนใจการเขียนโปรแกรม การพัฒนาเว็บ และความปลอดภัยไซเบอร์ น่าประทับใจจริง ๆ

    • ทั้ง พรสวรรค์ด้านเทคนิค ความพากเพียร และความสามารถในการทำเอกสารกับการสื่อสารก็ยอดเยี่ยมมาก
      แถมยังมีจริยธรรมวิชาชีพที่ปฏิบัติตามกระบวนการเปิดเผยอย่างรับผิดชอบอีกด้วย ดูเป็นคนที่จะไปได้ไกลมากในอนาคต
  • เป็นบทความและผลงานที่ยอดเยี่ยมมาก และให้ความรู้สึกเหมือนได้ตามไปกับกระบวนการค้นพบที่ยิ่งตื่นเต้นขึ้นเรื่อย ๆ
    สมควรได้รับรางวัลตอบแทนอย่างมาก

  • กระบวนการเชื่อมโยงช่องโหว่ก็เรียบร้อยดี และตัวบทความก็ยอดเยี่ยม ชอบที่ค่อย ๆ แยกให้เห็นว่าโค้ดที่มีช่องโหว่ทำงานอย่างไร
    ทริกง่าย ๆ อย่าง “กด F12 เพื่อลองอีกครั้ง” เป็นอะไรที่เห็นทีไรก็อดทึ่งไม่ได้ แสบสันมากจริง ๆ

    • ฉันอยู่มิสซูรี ครั้งหนึ่งเคยกด F12 แล้วผู้ว่าการรัฐพยายามจะจับฉัน
  • ทำให้นึกถึงเมื่อก่อนที่เคยดีบักเชลล์ crosh ของ Chrome OS ด้วย API เดียวกัน เพื่อข้ามการป้องกันของ OS และถึงขั้นได้สิทธิ์ root บนอุปกรณ์นักพัฒนา CVE-2014-3172
    แต่ผู้เขียนบทความนี้ต้องข้ามอุปสรรคที่ยากกว่ามาก และเป็นงานที่ยอดเยี่ยมจริง ๆ

  • ดึกมากแล้วจนยากจะขุดลึกว่าอะไรพังใน การตรวจสอบ WebUI แต่ชอบที่เขาขุดต่อจนหาคำตอบได้จนสุด
    การตั้งข้อสงสัยและไม่ไว้ใจ toolchain ของสิ่งที่เราปล่อยออกไปถือเป็นท่าทีที่ค่อนข้างมาตรฐาน แต่ในขณะเดียวกันเรากลับเชื่อเครื่องมือพัฒนาที่สะดวกเหมือนเวทมนตร์จากบริษัทยักษ์ใหญ่อย่าง Google หรือ Microsoft มากเกินไป เพราะสุดท้ายแล้วเราก็แค่อยากเขียนและทดสอบโค้ดของตัวเอง มากกว่าจะกังวลว่ามีอะไรซ่อนอยู่ใน Chromium หรือ VSCode

  • เป็นบทความที่ยอดเยี่ยมติดอันดับต้น ๆ เท่าที่ฉันเคยอ่านมา
    เป็น งานแกะรอย ที่ฉลาดมากจริง ๆ

  • ความพยายามในการคุ้ยดูโค้ดของเบราว์เซอร์จนหาเรื่องนี้เจอได้นั้นน่าทึ่งมาก และบทความก็ทั้งน่าสนใจและละเอียดมาก

  • เป็นนักเรียนมัธยมปลายเหรอ ว้าว สุดยอดจริง ๆ

  • โปรเจกต์ Chromium ลบ chrome://net-internals ออกเพราะบอกว่ามันซับซ้อนเกินไป แต่กลับเพิ่ม chrome://policy ที่มีการรองรับการแก้ไข JSON แบบครึ่ง ๆ กลาง ๆ เข้ามา