- ขอบเขตสิทธิ์ของ 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หรือไม่
- argument แรกคือ
- ใน 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
- ใน enum
การรันคำสั่งเชลล์ต่อผ่าน 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.comBrowser 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.scriptingchrome.tabsของ Manifest v2chrome.debuggerchrome.devtools.inspectedWindow
- เป้าหมายการตรวจสอบคือ
chrome.devtools.inspectedWindowซึ่งคาดว่าอาจถูก harden น้อยกว่า- ส่วนขยายที่ใช้ API
chrome.devtoolsต้องมีฟิลด์devtools_pageใน manifest - เมื่อผู้ใช้เปิด DevTools หน้านี้จะถูกโหลดเป็น iframe และสามารถใช้ API
chrome.devtoolsภายในนั้นได้
- ส่วนขยายที่ใช้ API
- รายงานบั๊กเดิมของ 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 ได้เช่นกัน หากรับ argumentinjectedScript- เมื่อเรียก
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ไม่ได้เปิด popupabout: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
- Windows:
- ปฏิสัมพันธ์จากผู้ใช้อยู่ในระดับแค่ทำให้เปิด DevTools
- “extension install error” ในภาพตัวอย่างเป็นกลไกเพื่อหลอกให้ผู้ใช้เปิด DevTools
- เมื่อ DevTools ถูกเปิด chain ที่นำไปสู่ sandbox escape ก็เริ่มทำงาน
การแก้ไขของ Google และการกำหนด CVE
- หลังได้รับรายงาน Google ยืนยันช่องโหว่อย่างรวดเร็วและจัดประเภทเป็น P1/S1
- P1/S1 หมายถึง high priority และ high severity
- ต่อมาในช่วงไม่กี่สัปดาห์ มีการแก้ไขหลักสามอย่างถูกนำเข้า
- เพิ่ม argument
loaderIdให้คำสั่งPage.reloadและ ตรวจloaderIDฝั่ง renderer- ทำให้คำสั่งมีผลกับ origin เดียวเท่านั้น และแม้จะไปถึง privileged page โดยไม่ตั้งใจ ก็ไม่ทำงาน
- ตรวจ URL ในฟังก์ชัน
inspectedWindow.reload()- ไม่ต้องพึ่งพาเฉพาะการเพิกถอนการเข้าถึง extension API อีกต่อไป
- ตรวจว่าเปิดใช้นโยบายทดสอบหรือไม่ใน WebUI handler
- ทำให้นโยบายทดสอบถูกบล็อกอย่างสมบูรณ์
- เพิ่ม argument
- ช่องโหว่ที่เกี่ยวกับ 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มีการบอกว่า URL ของหน้าจะถูกแทนที่เป็น
${url}และถ้าใส่ไว้หลัง#ก็จะกลายเป็นคอมเมนต์เพื่อไม่ให้คำสั่งพัง แล้วในนโยบายนี้มีตรรกะตรวจสอบหรือไม่ว่า URL ต้องถูกส่งไปยังที่ใดที่หนึ่งในAlternativeBrowserParameters?เป็น นักเรียนมัธยมปลาย ที่สนใจการเขียนโปรแกรม การพัฒนาเว็บ และความปลอดภัยไซเบอร์ น่าประทับใจจริง ๆ
แถมยังมีจริยธรรมวิชาชีพที่ปฏิบัติตามกระบวนการเปิดเผยอย่างรับผิดชอบอีกด้วย ดูเป็นคนที่จะไปได้ไกลมากในอนาคต
เป็นบทความและผลงานที่ยอดเยี่ยมมาก และให้ความรู้สึกเหมือนได้ตามไปกับกระบวนการค้นพบที่ยิ่งตื่นเต้นขึ้นเรื่อย ๆ
สมควรได้รับรางวัลตอบแทนอย่างมาก
กระบวนการเชื่อมโยงช่องโหว่ก็เรียบร้อยดี และตัวบทความก็ยอดเยี่ยม ชอบที่ค่อย ๆ แยกให้เห็นว่าโค้ดที่มีช่องโหว่ทำงานอย่างไร
ทริกง่าย ๆ อย่าง “กด F12 เพื่อลองอีกครั้ง” เป็นอะไรที่เห็นทีไรก็อดทึ่งไม่ได้ แสบสันมากจริง ๆ
ทำให้นึกถึงเมื่อก่อนที่เคยดีบักเชลล์
croshของ Chrome OS ด้วย API เดียวกัน เพื่อข้ามการป้องกันของ OS และถึงขั้นได้สิทธิ์ root บนอุปกรณ์นักพัฒนา CVE-2014-3172แต่ผู้เขียนบทความนี้ต้องข้ามอุปสรรคที่ยากกว่ามาก และเป็นงานที่ยอดเยี่ยมจริง ๆ
ดึกมากแล้วจนยากจะขุดลึกว่าอะไรพังใน การตรวจสอบ WebUI แต่ชอบที่เขาขุดต่อจนหาคำตอบได้จนสุด
การตั้งข้อสงสัยและไม่ไว้ใจ toolchain ของสิ่งที่เราปล่อยออกไปถือเป็นท่าทีที่ค่อนข้างมาตรฐาน แต่ในขณะเดียวกันเรากลับเชื่อเครื่องมือพัฒนาที่สะดวกเหมือนเวทมนตร์จากบริษัทยักษ์ใหญ่อย่าง Google หรือ Microsoft มากเกินไป เพราะสุดท้ายแล้วเราก็แค่อยากเขียนและทดสอบโค้ดของตัวเอง มากกว่าจะกังวลว่ามีอะไรซ่อนอยู่ใน Chromium หรือ VSCode
เป็นบทความที่ยอดเยี่ยมติดอันดับต้น ๆ เท่าที่ฉันเคยอ่านมา
เป็น งานแกะรอย ที่ฉลาดมากจริง ๆ
ความพยายามในการคุ้ยดูโค้ดของเบราว์เซอร์จนหาเรื่องนี้เจอได้นั้นน่าทึ่งมาก และบทความก็ทั้งน่าสนใจและละเอียดมาก
เป็นนักเรียนมัธยมปลายเหรอ ว้าว สุดยอดจริง ๆ
โปรเจกต์ Chromium ลบ
chrome://net-internalsออกเพราะบอกว่ามันซับซ้อนเกินไป แต่กลับเพิ่มchrome://policyที่มีการรองรับการแก้ไข JSON แบบครึ่ง ๆ กลาง ๆ เข้ามา