การนำโครงสร้างพื้นฐาน Go ไปใช้ในทางที่ผิด
(reverse.put.as)- checksum database ของ Go และ public module proxy สามารถรับแม้แต่รีโพซิทอรีที่ไม่มีโค้ด Go ได้ ทำให้เกิดเส้นทางสำหรับนำข้อมูลจาก Git repository ตามอำเภอใจไปเก็บบนโครงสร้างพื้นฐานของ Go และดาวน์โหลดกลับมาได้อีกครั้ง
- คำขอ
sum.golang.org/lookup/$module@$versionจะไปดึงจาก เซิร์ฟเวอร์ต้นทาง เมื่อพบเวอร์ชันของโมดูลที่ยังไม่ถูกบันทึก และในกระบวนการนี้ zip ของรีโพซิทอรีนั้นจะถูกให้บริการบนproxy.golang.orgด้วย - พบรีโพซิทอรี Ruby ของ Homebrew และ fork ของรีโพซิทอรี Rust อยู่ใน checksum database และการทดลองลงทะเบียนทั้งโมดูล Go ใหม่และรีโพซิทอรีที่ไม่มีไฟล์ Go เลยด้วย pseudo-version ก็สำเร็จ
- แม้ module zip จะมีข้อจำกัด สูงสุด 500 MiB ทั้งแบบบีบอัดและไม่บีบอัด แต่ก็ยังใหญ่พอสำหรับการหลบเลี่ยงข้อจำกัดการดาวน์โหลดบนเครื่องนักพัฒนาหรือ CI/CD การเก็บเพย์โหลด และการทำ C2
- จากพาธเฉพาะของ
sum.golang.orgราว 1.59 ล้านรายการ มีพาธ GitHub ราว 1.51 ล้านรายการ คิดเป็นประมาณ 95% สะท้อนทั้งการพึ่งพา GitHub ของระบบนิเวศ Go และความเป็นไปได้ในการนำ public proxy ไปใช้ในทางที่ผิด
รีโพซิทอรีที่ไม่ใช่ Go ที่พบใน Go checksum database
- ระหว่างตรวจดู checksum database ของ Go พบว่า
github.com/homebrew/homebrew-coreปรากฏในตารางmodulesจำนวนมากผิดปกติgithub.com/homebrew/homebrew-core: 39,438 รายการgithub.com/Homebrew/homebrew-core: 30,896 รายการgithub.com/concourse/concourse: 25,372 รายการgithub.com/openshift/release: 24,065 รายการgithub.com/cilium/cilium: 22,138 รายการ
- รีโพซิทอรี Homebrew เป็นที่รู้กันว่าใช้ Ruby และไม่พบ
go.modหรือไฟล์ซอร์ส Go ทั้งในรีโพซิทอรีและไฟล์ที่โคลนมา - ความแตกต่างของตัวพิมพ์เล็กพิมพ์ใหญ่สามารถอธิบายได้ด้วยกฎ case encoding ในเอกสารของ Go
- ตัวอักษรพิมพ์ใหญ่จะถูกเข้ารหัสเป็น
!ตามด้วยตัวพิมพ์เล็กที่สอดคล้องกัน ทำให้สามารถเก็บทั้งexample.com/Mและexample.com/mพร้อมกันได้แม้บนระบบไฟล์ที่ไม่แยกตัวพิมพ์เล็กพิมพ์ใหญ่
- ตัวอักษรพิมพ์ใหญ่จะถูกเข้ารหัสเป็น
github.com/Edu4rdSHL/rust-headless-chromeก็ปรากฏใน checksum database เช่นกัน ทั้งที่เป็น fork ของรีโพซิทอรี Rust ที่ไม่เกี่ยวกับ Go
วิธีที่ /lookup ดึงรีโพซิทอรีมา
- ตาม Go Modules Reference ในเอกสารโมดูล Go คำสั่ง Go จะดึงข้อมูลเรคคอร์ดจากเอ็นด์พอยต์
/lookupก่อนเมื่อทำการสอบถาม checksum database - หากเวอร์ชันของโมดูลยังไม่ได้ถูกบันทึกลงในล็อก checksum database จะพยายามดึงโมดูลนั้นจาก เซิร์ฟเวอร์ต้นทาง ก่อนตอบกลับ
- รูปแบบของเอ็นด์พอยต์คือ
$base/lookup/$module@$version- จะคืนหมายเลขเรคคอร์ดในล็อกสำหรับ
$versionของ$module, บรรทัดgo.sumและคำอธิบายต้นไม้ที่มีลายเซ็น
- จะคืนหมายเลขเรคคอร์ดในล็อกสำหรับ
- เมื่อตรวจสอบ pseudo-version ของ
github.com/homebrew/homebrew-coreก็ได้รับ checksum record และแฮชของgo.modกลับมา - หากรีโพซิทอรีไม่มี version tag ระบบจะใช้กฎ pseudo-version ของ Go
การทดลองลงทะเบียนโมดูล Go ใหม่
- ผู้เขียนสร้างโมดูล Go ใหม่
github.com/gdbinit/fluxmatterแล้วตรวจสอบการลงทะเบียนผ่านคำขอlookup - การสอบถาม
@latestตอบกลับด้วยข้อผิดพลาดว่าไม่ใช่ canonical versionbad request: version "latest" is not canonical
- การสอบถาม
@v0.0.0ตอบกลับด้วยข้อผิดพลาดว่าไม่รู้จัก revision ดังกล่าวnot found: ... invalid version: unknown revision v0.0.0
- แต่เมื่อลองซิงก์ checksum database อีกครั้งแล้วสอบถามใหม่ พบว่าโมดูลถูกลงทะเบียนไปแล้ว
github.com/gdbinit/fluxmatter|v0.0.0-20240524163826-a7e64ffd69f2|2024-05-24T16:40:51.203837Z
proxy.golang.org/github.com/gdbinit/fluxmatter/@latestส่งคืน pseudo-version และข้อมูลต้นทางจาก GitHub และยังสามารถดาวน์โหลดไฟล์ zip ของเวอร์ชันนั้นและตรวจสอบการบีบอัดได้- ในการ seed ครั้งแรก ไม่จำเป็นต้องระบุเวอร์ชันที่ถูกต้องอย่างแม่นยำ แค่มี lookup query ที่ใส่ค่าในรูปแบบเหมือนพาธโมดูลและเวอร์ชันก็เพียงพอ
รีโพซิทอรีที่ไม่มีโค้ด Go ก็ถูกนำขึ้น public proxy ได้
- การทดลองแบบเดียวกันกับรีโพซิทอรี
github.com/gdbinit/readmemที่ไม่มีโค้ด Go เลยก็สำเร็จ - คำขอ
lookupตอบกลับว่าไม่รู้จัก revisionv0.0.0แต่ใน checksum database กลับถูกลงทะเบียนเป็น pseudo-versiongithub.com/gdbinit/readmem|v0.0.0-20131006075740-407cb0a56933|2024-05-24T16:45:35.88456Z
@latestของproxy.golang.orgส่งคืน pseudo-version ของรีโพซิทอรีนั้นพร้อมข้อมูลต้นทางจาก Git- ภายใน zip ที่ดาวน์โหลดมาไม่มีไฟล์ Go แต่มี
Entitlements.plist,README, ไฟล์โปรเจกต์ Xcode,main.cเป็นต้น - แม้การทดลองนี้จะใช้ รีโพซิทอรี GitHub แต่หากเป็น VCS ที่ใช้งานได้ ก็อาจทำได้กับโฮสติงอื่นเช่นกัน
การพึ่งพา GitHub และข้อจำกัดด้านขนาด
- จำนวนพาธเฉพาะใน checksum database คือ 1,591,375 รายการ และในนั้น
github.com%มี 1,515,957 รายการ - ประมาณ 95% ของพาธเฉพาะถูกโฮสต์บน GitHub {p:95}
- ตัวเลขนี้เป็นสถิติดิบที่ยังไม่ได้ตัด fork หรือสิ่งที่ไม่ใช่โค้ด Go จริงออก
- module zip ของ Go อยู่ภายใต้ File path and size constraints
- ไฟล์ module zip มีขนาดสูงสุด 500 MiB
- ขนาดรวมของไฟล์เมื่อไม่บีบอัดก็สูงสุด 500 MiB
- ไฟล์
go.modมีขนาดสูงสุด 16 MiB - ไฟล์
LICENSEก็มีขนาดสูงสุด 16 MiB
- ข้อจำกัดเหล่านี้มีไว้เพื่อลดความเสี่ยงจาก การโจมตีแบบปฏิเสธการให้บริการ ต่อผู้ใช้, proxy และส่วนอื่นของระบบนิเวศโมดูล
- แต่ขนาด 500 MiB ก็ยังถือว่าใหญ่พอเมื่อมองในแง่ความเป็นไปได้ของการนำไปใช้ในทางที่ผิด
สถานการณ์การนำไปใช้ในทางที่ผิดที่เป็นไปได้
- public Go proxy อาจถูกใช้เพื่อ หลบเลี่ยงข้อจำกัดปลายทางของการดาวน์โหลด บนเครื่องนักพัฒนาหรือเซิร์ฟเวอร์ CI/CD
- สมมติว่าไม่มี private
GOPROXY - มัลแวร์สามารถอัปโหลดเพย์โหลดขึ้นรีโพซิทอรี แล้วค่อยดาวน์โหลดจาก proxy เมื่อจำเป็น
- ต่อให้ซอร์สต้นทางหายไป ก็อาจเหลือเพียงร่องรอยเล็กน้อยใน checksum database entry
- สมมติว่าไม่มี private
- การทำ DoS กับ
proxy.golang.orgอาจลงมือได้ไม่ง่าย- ผู้โจมตีสามารถร้องขอให้ proxy ดาวน์โหลดรีโพซิทอรี Git ตามอำเภอใจได้
- แนวทางที่เป็นไปได้คือรวบรวม URL ของ GitHub จำนวนมาก แล้วส่งคำขอจำนวนมากไปยัง API ของ
lookup - แม้จะไม่รู้รายละเอียดฝั่งเซิร์ฟเวอร์ แต่ก็อาจมีการจำกัดการประมวลผลพร้อมกัน เช่น work queue
- ฝั่ง GitHub เองก็อาจมีการป้องกันแบนด์วิดท์ทำงานอยู่
- ยังมีความเป็นไปได้ของ DoS ที่มุ่งเป้าไปยังพื้นที่เก็บข้อมูล แต่ยังเป็นเพียงการคาดการณ์
- สามารถสร้าง C2(command and control) บน
proxy.golang.orgได้ค่อนข้างง่าย- ใช้คำขอ
@latestเพื่อหาเวอร์ชันล่าสุดของโมดูลที่ต้องการ - เพย์โหลดอาจเป็นไฟล์ธรรมดา หรือซ่อนไว้ใน
go.modหรือไฟล์ซอร์ส Go ก็ได้ - หากไม่ต้องการใช้รีโพซิทอรีเดียว อาจใช้ module DGA ได้
- ใช้คำขอ
โฟลว์การดาวน์โหลดของ C2
- หาก implant ต้องการรับคำสั่ง ก็เพียงทำตามขั้นตอนต่อไปนี้
- ขอ
https://proxy.golang.org/module_path/@latest - ดึง pseudo-version หรือ version จากผลลัพธ์ JSON
- ดาวน์โหลด zip ผ่าน
https://proxy.golang.org/module_path/@v/version.zip - แตกไฟล์ zip แล้วพาร์สคำสั่งจากเนื้อหาภายใน
- ขอ
- โฟลว์นี้เรียบง่ายมากจนสามารถเขียนได้ด้วยโค้ด Go ไม่ถึง 300 บรรทัด
บทสรุปและคำถามที่ยังค้างอยู่
- checksum database และ proxy ของ Go สามารถดึงและเก็บโมดูลที่ยังไม่ถูกบันทึกจากเซิร์ฟเวอร์ต้นทางได้ตามกระบวนการที่มีการจัดทำเอกสารไว้
- สถานะปัจจุบันอาจยังไม่ดูเป็นปัญหาร้ายแรงของโครงสร้างพื้นฐาน Go แต่ก็ถูกนำไปใช้ในทางที่ผิดได้ง่ายและยังมีช่องให้ปรับปรุง
- อาจมีเหตุผลที่มีการบันทึกไว้ในเอกสารหรือไม่เปิดเผยว่าเหตุใดจึงอนุญาตให้รีโพซิทอรีที่ไม่ใช่โค้ด Go ขึ้นไปอยู่บน proxy และ checksum database ได้
- หากต้องการตรวจสอบว่ามีผู้ใดนำสิ่งนี้ไปใช้ในทางที่ผิดแล้วหรือไม่ จำเป็นต้องสำรวจรีโพซิทอรีเฉพาะราว 1.6 ล้านรายการ และ entry ราว 22 ล้านรายการ ตามฐานข้อมูลโลคัลล่าสุด
- ยังเป็นคำถามเปิดอยู่ว่าทำไมบางโปรเจกต์ที่ไม่ใช่ Go แต่ถูกต้องตามปกติจึงอยู่ใน database
1 ความคิดเห็น
ความเห็นจาก Hacker News
บริการออนไลน์ ที่ให้ผู้ใช้อัปโหลดข้อมูลและทำให้ข้อมูลนั้นมองเห็นได้แบบสาธารณะ สุดท้ายก็มักจะถูกนำไปใช้เป็น command-and-control, ละเมิดลิขสิทธิ์, และโฮสต์ CSAM
ยิ่งเป็นบริการที่บล็อกได้ยากเพราะมีการใช้งานสำคัญนอกเหนือจากการโฮสต์ไฟล์ก็ยิ่งเป็นแบบนั้น โดยเรื่องนี้เคยเกิดขึ้นแล้วกับ Twitter[1], Telegram[2], และโครงสร้างพื้นฐานคีย์ PGP[3] ส่วนเป้าชัด ๆ อย่าง GitHub ก็ไม่ต้องพูดถึง
[1] https://pentestlab.blog/2017/09/26/command-and-control-twitt...
[2] https://www.blazeinfosec.com/post/leveraging-telegram-as-a-c...
[3] https://torrentfreak.com/openpgp-keyservers-now-store-irremo...
ในกรณีของ Gmail มีการแจก credentials ให้ไปล็อกอิน แล้วให้อ่านไฟล์แนบที่อัปไว้ผ่าน IMAP
ผมเคยอยู่ทีม Google SAD-SRE (Spam, Abuse, Delivery)
จะเข้ารหัสไฟล์เป็น Base64 แล้วใส่ไว้ในสตริงของโค้ด Python ก็ยังได้
ผมเป็น Googler และนี่เป็นความเห็นส่วนตัว อีกทั้งก็ไม่ได้เชี่ยวชาญด้านนี้
หวังว่าทีม Go จะร่วมมือกับฝั่ง GCP และ Drive เพราะการโฮสต์ไฟล์อันตรายเป็นปัญหาที่ Google รับมืออยู่ตลอด
มันไม่ได้ต่างจาก endpoint อื่น ๆ ที่ Google อนุญาตให้คนใส่ข้อมูลตามอำเภอใจอยู่แล้วมากนัก
Google เก่งมากเรื่องมีทีมกลางมาดูแลโครงสร้างพื้นฐานและแชร์ให้ทั้งองค์กรใช้ ยกเว้นแอปแชตก็อีกเรื่อง แต่ถ้าเดาล้วน ๆ ทีม Go ก็น่าจะใช้ blob storage ภายใน และน่าจะมีทีมโครงสร้างพื้นฐานภายในที่จัดการการรับมือการใช้งานในทางที่ผิดและสแกนไฟล์แบบอัตโนมัติอยู่แล้ว
ใน PyPI ก็มี โปรเจกต์ที่ไม่ใช่ Python อยู่ไม่น้อย
Python จำเป็นต้องรองรับการแจกจ่าย wheel ซึ่งเป็นไบนารีที่คอมไพล์แล้ว เพราะผู้ใช้อาจคอมไพล์โค้ดไลบรารีเองไม่ได้
โค้ดแบบนั้นมักเขียนด้วย C แต่ก็ทำด้วย Golang[1] ได้เหมือนกัน และแม้จะหาตัวอย่างไม่เจอ ผมคิดว่าเคยเห็นมันถูกใช้แจกจ่ายแอปพลิเคชัน ไม่ใช่แค่ไลบรารีด้วย
การเขียนแอปด้วย C แล้วอัปขึ้น PyPI จากนั้นให้ผู้ใช้ติดตั้งด้วย
pip installก็ถือว่าเจ๋งดี[1] https://github.com/popatam/gopy_build_wheel_example
บน Linux ก็เหมือนกับว่า
lsเขียนด้วย Python ไปแล้ว ดังนั้นผมว่าคงไม่ควรเล่นเกมแบบนี้pipจะเริ่มบังคับว่าต้องอยู่ในสภาพแวดล้อมอย่าง venv/virtualenv/pipenv/pyenv ถึงจะดาวน์โหลดแพ็กเกจได้ การใช้งานลักษณะนี้น่าจะมีประโยชน์กว่านี้มากpip install cmakeและไบนารีปิดซอร์สก็ทำได้แบบpip install nvidia-cudnn-cu12นี่เป็นหนึ่งในเหตุผลที่ทำให้ผมเลิกใช้ Homebrew ได้หมดจด
อาจจะเป็นความคิดไร้เดียงสา แต่ผมไม่เข้าใจว่ามันต่างจากการอัปไฟล์ขึ้น GitHub repository อย่างไร
หรือความต่างมีแค่ว่า GitHub ต้องสร้างบัญชี? บน GitHub ก็เก็บข้อมูลอะไรก็ได้เหมือนกัน และก็ไม่ได้มีลิมิต 500MB ด้วย
ระบบโมดูลของ CUE กำลังออกมาอย่างเป็นทางการในที่สุด และ MVS ก็คล้ายกับของ Go แต่สร้างอยู่บน โครงสร้างพื้นฐาน OCI
ถ้าสนใจระบบจัดการ dependencies นี่คือลิงก์ที่น่าอ่าน
proposal: https://github.com/cue-lang/proposal/tree/main/designs/modul...
custom registry: https://cuelang.org/docs/tutorial/working-with-a-custom-modu...
road map: https://github.com/orgs/cue-lang/projects/10/views/8
ตั้งแต่ 0.9.0-alpha-5 เป็นต้นไป โมดูลถูกเปิดใช้เป็นค่าเริ่มต้น: https://github.com/cue-lang/cue/releases/tag/v0.9.0-alpha.5
ใน Go Sum โปรเจกต์ Trillian รองรับ transparency log อยู่เบื้องหลัง: https://github.com/google/trillian
CUE มีแผนจะอาศัยตัวเลือกของ OCI อย่าง attestation ด้วย
ลองเล่นกับไอเดียที่อาศัย golang proxy และ sumdb หรือพูดอีกอย่างคือใช้ในทางที่ผิด เพื่อสร้าง transparent log ฟรีสำหรับ checksum ของ URL ใดก็ได้ตามใจ
https://getsum.pub/
ถ้าต้องการแค่ transparent log แบบสาธารณะ public rekor instance ของโครงการ sigstore น่าจะเหมาะกว่ามาก
https://www.sigstore.dev/
https://docs.sigstore.dev/logging/overview/
อาจเป็นเพราะผมไม่เข้าใจก็ได้ แต่ผมไม่เห็นชัด ๆ ว่าปัญหาที่แท้จริงตรงนี้คืออะไร
การที่พร็อกซีแคชรีโพที่ไม่ใช่ Go อาจจะสิ้นเปลืองอยู่บ้าง แต่ถึงไม่ทำแบบนั้น ถ้าบังคับให้มันแคชรีโพ Go ได้ ก็ทำให้มันเก็บข้อมูลตามอำเภอใจได้อยู่ดีไม่ใช่หรือ?
ถ้าผมไม่ได้พลาดอะไรไป มันฟังดูเหมือนไม่ใช่เรื่องใหญ่อะไรเลย
ข่าวใหม่ตรงนี้น่าจะมีแค่ว่า public proxy ที่ไม่มีมาตรการความปลอดภัย รับสิ่งที่ให้มันพร็อกซีแล้วนำมาเผยแพร่ต่อแบบสาธารณะโดยไม่มีมาตรการความปลอดภัยเช่นกัน
ในบทความบอกว่าเครือข่ายที่มีการเฝ้าระวังบางแห่งอาจเชื่อถือ URL ของ golang proxy มากกว่า URL เว็บทั่วไป เลยอาจใช้หลบตัวกรองตามชื่อเสียงได้ แต่ก็มีวิธีแบบนี้อยู่แล้วหลายแบบ และวิธีนี้ก็ดูไม่ได้พิเศษอะไร
นอกเรื่องหน่อย แต่เห็นโดเมนแล้วมุกคำพ้องมันชวนรู้สึกไม่ค่อยดี เลยลองเข้าไปดู put.as และก็เป็นอย่างที่คาดไว้โดยรวม
เป็นประเด็นที่รู้จักกันอยู่แล้ว: https://github.com/golang/go/issues/31866
.modกับ.goไว้ที่รูทก็พอไม่ใช่หรือ?เขากับ Aaron เป็นคนสร้าง Athens และเท่าที่ผมรู้ Marwan เป็นคนเขียน implementation ของ Go download protocol ตัวแรกที่กลายมาเป็นพื้นฐานของ Athens
สิ่งที่น่าสนใจใน issue นี้คือ Athens ใช้คำสั่ง
go mod download -jsonที่ถูกพูดถึงก่อนหน้านี้ว่าเป็นการตรวจสอบล่วงหน้าสำหรับการยืนยันโมดูลอยู่แล้วโดยทั่วไป ถ้ารีโพผ่านเป็นโมดูลที่คำสั่ง Go module เข้าใจได้ Athens ก็จะให้บริการมัน
ถ้าจะพูดแบบตรงกว่าเดิม ก็ต้องสามารถสร้างเวอร์ชันโมดูล, pseudo-version และ
+incompatibleได้ และทั้งโมดูลนั้นกับ dependency ของมันต้องสร้าง checksum ที่ถูกต้องได้checksum ของโมดูลตอนนี้เกี่ยวข้องกับการรวม
.mod, ทุกไฟล์ และแต่ละ dependency แบบเรียกซ้ำเพราะงั้นอย่างที่ผู้เขียนบทความบอก แค่มีโปรแกรม Go พื้นฐานก็มีพื้นที่สำหรับไฟล์ตามอำเภอใจได้มากอยู่แล้วตามการออกแบบ
W3C ได้วางรากฐานไว้ให้ทุกอย่างบนเว็บสามารถถูกแคชแบบเข้มข้นได้ แต่การมี generic proxy cache น้อยขนาดนี้กลับดูแปลก
เป็นไปได้ไหมว่าผู้เผยแพร่ส่ง
Cache-Control: max-ageสั้น ๆ หรือVary: Cookieทั้งที่ไม่จำเป็นอยู่ตลอด?หรือ ISP ต้องจ่ายค่าทรานซิตมากกว่าค่า peering มากเกินไป?
ตัวอย่างเช่น ในไซต์ที่ไม่ใช้ HTTPS พร็อกซีของ ISP อาจแทรกโฆษณาได้
การดาวน์โหลดซอฟต์แวร์มักมีลายเซ็นและ checksum แต่เนื้อหาทั่วไปมักไม่มี