ถ้าใช้ iTerm2 แม้แต่ `cat readme.txt` ก็อาจไม่ปลอดภัย
(blog.calif.io)- SSH integration ใช้ escape sequence ของเทอร์มินัลเพื่อสื่อสารกับเชลล์ระยะไกล และด้วยโครงสร้างนี้เอง เอาต์พุตเทอร์มินัลทั่วไปก็อาจถูกตีความเหมือนโปรโตคอล conductor ได้
- ปัญหาหลักคือ ความล้มเหลวของการเชื่อถือ โดยแม้จะไม่ใช่ conductor ระยะไกลตัวจริง แต่ ไฟล์อันตราย, แบนเนอร์, MOTD, การตอบสนองจากเซิร์ฟเวอร์ ก็สามารถปลอมตัวเป็น conductor ได้ผ่าน
DCS 2000pและOSC 135ที่ถูกปลอมขึ้น - เพียงรัน
cat readme.txtถ้ามีการเรนเดอร์ transcript ของ conductor ปลอม iTerm2 ก็จะเดินกระบวนการgetshell·pythonversion·run(...)ด้วยตัวเอง และฝั่งโจมตีเพียงแค่ปลอมการตอบกลับเท่านั้น - เอ็กซ์พลอยต์อาศัยความสับสนที่คำสั่ง base64 ซึ่งถูกเขียนลง PTY จะตกลงไปเป็น อินพุตข้อความธรรมดาของเชลล์โลคัล เมื่อไม่มี SSH conductor ตัวจริง และสามารถถูกรันได้เมื่อชังก์สุดท้ายถูกตีความเป็นพาธ
ace/c+aliFIo - การแก้ไขถูกใส่ไว้ในคอมมิตวันที่ 31 มีนาคม
a9e745993c2e2cbb30b884a16617cd5495899f86แล้ว แต่ ณ เวลาที่เผยแพร่ยัง ไม่ถูกรวมใน stable release ทำให้เกิดช่วงว่างของการป้องกันก่อนที่แพตช์จะกระจายถึงผู้ใช้
เบื้องหลัง SSH integration ของ iTerm2
- iTerm2 SSH integration เป็นฟีเจอร์ที่ช่วยให้เข้าใจเซสชันระยะไกลได้ลึกขึ้น โดยมีโครงสร้างการทำงานผ่านการอัปโหลดสคริปต์ช่วยขนาดเล็กชื่อ conductor ไปยังเชลล์ระยะไกล
- เริ่ม SSH integration ผ่าน
it2ssh - ส่ง conductor ซึ่งเป็นสคริปต์บูตสแตรประยะไกลผ่านเซสชัน SSH เดิม
- สคริปต์ระยะไกลนี้ทำหน้าที่เป็นอีกฝั่งของโปรโตคอล iTerm2
- เริ่ม SSH integration ผ่าน
- iTerm2 และ conductor ระยะไกลไม่ได้สื่อสารกันแบบบริการเครือข่ายทั่วไป แต่ใช้วิธีส่ง escape sequence ผ่าน terminal I/O
- ตรวจจับ login shell
- ตรวจสอบการมีอยู่ของ Python
- เปลี่ยนไดเรกทอรี
- อัปโหลดไฟล์
- รันคำสั่ง
วิธีการทำงานของ PTY
- เทอร์มินัลอีมูเลเตอร์สมัยใหม่คือซอฟต์แวร์เวอร์ชันของฮาร์ดแวร์เทอร์มินัลในอดีต โดยรับผิดชอบการแสดงผลหน้าจอ การรับอินพุตคีย์บอร์ด และการตีความลำดับควบคุมของเทอร์มินัล
- เชลล์และโปรแกรมบรรทัดคำสั่งยังคงคาดหวังอุปกรณ์ที่ดูเหมือนเทอร์มินัลจริงอยู่ ดังนั้นระบบปฏิบัติการจึงจัดเตรียม PTY ไว้
- PTY คือ pseudoterminal ที่อยู่ระหว่างเทอร์มินัลอีมูเลเตอร์กับโปรเซสที่ทำงานอยู่เบื้องหน้า
- ในเซสชัน SSH ทั่วไป iTerm2 จะเขียนไบต์ลง PTY จากนั้นโปรเซสเบื้องหน้าอย่าง
sshจะส่งต่อไปยังเครื่องระยะไกล และ conductor ระยะไกลจะอ่านจาก stdin - เมื่อ iTerm2 ต้องการส่งคำสั่งไปยัง conductor ระยะไกล ในฝั่งโลคัลมันก็คือการ เขียนไบต์ลง PTY นั่นเอง
โปรโตคอล conductor
- ช่องทางขนส่งของโปรโตคอล SSH integration ใช้ terminal escape sequence
- องค์ประกอบหลักมีอยู่สองอย่าง
DCS 2000pใช้สำหรับ hook SSH conductorOSC 135ใช้สำหรับข้อความ conductor แบบ pre-framer
- ในระดับซอร์สโค้ด
DCS 2000pจะทำให้ iTerm2 สร้าง conductor parser ขึ้นมา และจากนั้น parser จะจัดการข้อความOSC 135ต่อbegin <id>- command output lines
end <id> <status> runhook
- conductor ระยะไกลที่ทำงานปกติสามารถสื่อสารกับ iTerm2 ได้ด้วย เอาต์พุตเทอร์มินัลเพียงอย่างเดียว
ช่องโหว่หลัก
- แก่นของช่องโหว่นี้คือ ความล้มเหลวของการเชื่อถือ เพราะแม้จะไม่ใช่เซสชัน conductor ที่เชื่อถือได้จริง iTerm2 ก็ยังรับเอาต์พุตเทอร์มินัลมาเป็นโปรโตคอล SSH conductor
- ผลลัพธ์คือเอาต์พุตเทอร์มินัลที่ไม่น่าเชื่อถือสามารถ ปลอมเป็น conductor ระยะไกล ได้
- ไฟล์อันตราย
- การตอบสนองจากเซิร์ฟเวอร์
- แบนเนอร์
- MOTD
- อินพุตโจมตีสามารถพิมพ์ hook
DCS 2000pปลอมและการตอบกลับOSC 135ปลอมได้ และในกรณีนั้น iTerm2 จะทำงานราวกับว่ากำลังมีการแลกเปลี่ยน SSH integration จริงอยู่
วิธีการทำงานของเอ็กซ์พลอยต์
- ไฟล์เอ็กซ์พลอยต์มีลักษณะเป็น transcript ของ conductor ปลอม ฝังอยู่ภายใน
- เมื่อผู้ใช้รัน
cat readme.txtiTerm2 จะเรนเดอร์ไฟล์ แต่ในไฟล์นั้นไม่ใช่แค่ข้อความธรรมดา ทว่าแฝงองค์ประกอบต่อไปนี้ไว้- บรรทัด
DCS 2000pปลอม ที่บอกว่ามีเซสชัน conductor ปลอม - ข้อความ
OSC 135ปลอม ที่ตอบสนองต่อคำขอของ iTerm2
- บรรทัด
- เมื่อ hook ถูกยอมรับ iTerm2 จะเริ่มเวิร์กโฟลว์ conductor ปกติ โดยในซอร์สต้นทาง
Conductor.start()จะส่งgetshell()ทันที และถ้าสำเร็จก็จะส่งpythonversion()ต่อ - ผู้โจมตีไม่จำเป็นต้องฉีดคำขอเหล่านี้เอง เพราะ iTerm2 จะออกคำขอด้วยตัวเอง และเอาต์พุตอันตรายเพียงแค่ปลอมเป็นคำตอบ
ลำดับการเดินของ state machine
- ข้อความ
OSC 135ปลอมถูกจัดเรียงให้น้อยที่สุดแต่มีลำดับถูกต้อง- เริ่ม command body สำหรับ
getshell - ส่งคืนบรรทัดที่ดูเหมือนผลลัพธ์การตรวจจับเชลล์
- ปิดคำสั่งนั้นด้วยสถานะสำเร็จ
- เริ่ม command body สำหรับ
pythonversion - ปิดคำสั่งนั้นด้วยสถานะล้มเหลว
unhook
- เริ่ม command body สำหรับ
- เพียงแค่ลำดับนี้ก็ทำให้ iTerm2 เข้าสู่เส้นทาง fallback ตามปกติ และหลังจากนั้นจะตัดสินว่าเวิร์กโฟลว์ SSH integration เสร็จสมบูรณ์พอแล้วก่อนเดินไปขั้นถัดไป
- ขั้นถัดไปคือการประกอบและส่งคำสั่ง
run(...)
บทบาทของ sshargs
- hook
DCS 2000pที่ถูกปลอมมีหลายฟิลด์ และหนึ่งในนั้นคือsshargsที่ผู้โจมตีควบคุมได้ - ค่านี้จะถูกใช้เป็น วัตถุดิบของคำสั่ง ในภายหลัง เมื่อ iTerm2 สร้างคำขอ
run ...สำหรับ conductor - เอ็กซ์พลอยต์เลือก
sshargsเพื่อให้เมื่อ iTerm2 เข้ารหัสข้อมูลต่อไปนี้เป็น base64run <padding><magic-bytes>
- ชังก์ 128 ไบต์สุดท้ายจะออกมาเป็น
ace/c+aliFIo - สตริงนี้ไม่ได้ถูกเลือกแบบสุ่ม แต่ถูกกำหนดให้ตรงกับสองเงื่อนไขพร้อมกัน
- เป็นเอาต์พุตที่ใช้ได้ของเส้นทางการเข้ารหัสของ conductor
- เป็นชื่อพาธสัมพัทธ์ที่ใช้ได้
ความสับสนของ PTY ที่ทำให้เอ็กซ์พลอยต์เกิดขึ้นได้
- ในเซสชัน SSH integration ปกติ iTerm2 จะเขียนคำสั่ง conductor ที่เข้ารหัสด้วย base64 ลง PTY และ
sshจะส่งต่อให้ conductor ระยะไกล - แต่ในสถานการณ์เอ็กซ์พลอยต์ iTerm2 ก็ยังเขียนคำสั่งลง PTY แบบเดิม เพียงแต่ไม่มี SSH conductor จริงอยู่ ทำให้ เชลล์โลคัลรับมันไปเป็นอินพุตข้อความธรรมดา
- ในเซสชันที่ถูกบันทึกไว้จะเห็นลักษณะดังนี้
getshellปรากฏในรูปแบบ base64pythonversionปรากฏในรูปแบบ base64- ต่อด้วย payload
run ...แบบเข้ารหัส base64 ที่ยาว - ชังก์สุดท้ายคือ
ace/c+aliFIo
- ชังก์ก่อนหน้านั้นจะล้มเหลวเพราะเป็นคำสั่งที่ไม่มีความหมาย ส่วนชังก์สุดท้ายจะทำงานได้หากพาธดังกล่าวมีอยู่จริงในเครื่องและสามารถรันได้
ขั้นตอนการทำซ้ำ
- PoC แบบอาศัยไฟล์ต้นฉบับสามารถทำซ้ำได้ด้วย
genpoc.pypython3 genpoc.pyunzip poc.zipcat readme.txt
- ขั้นตอนนี้จะสร้างไฟล์สองรายการ
- สคริปต์ช่วยที่รันได้ชื่อ
ace/c+aliFIo readme.txtที่มีลำดับDCS 2000pและOSC 135อันตรายฝังอยู่
- สคริปต์ช่วยที่รันได้ชื่อ
- ไฟล์แรกทำหน้าที่ชักนำให้ iTerm2 สื่อสารกับ conductor ปลอม ส่วนไฟล์ที่สองให้เป้าหมายที่เชลล์จะรันจริงเมื่อชังก์สุดท้ายมาถึง
- เพื่อให้เอ็กซ์พลอยต์สำเร็จ ต้องรัน
cat readme.txtจากไดเรกทอรีที่มีace/c+aliFIoอยู่ เพื่อให้ชังก์สุดท้ายที่ผู้โจมตีจัดรูปไว้ถูกตีความเป็นพาธที่รันได้จริง
ไทม์ไลน์การเปิดเผยและแพตช์
- รายงานบั๊กต่อ iTerm2 เมื่อวันที่ 30 มีนาคม
- แก้ไขเสร็จในคอมมิตวันที่ 31 มีนาคม
a9e745993c2e2cbb30b884a16617cd5495899f86 - ณ เวลาที่เขียน การแก้ไขยัง ไม่ถูกรวมใน stable release
- หลังจากมีคอมมิตแพตช์แล้ว มีการพยายามสร้างเอ็กซ์พลอยต์ขึ้นใหม่ตั้งแต่ต้นโดยอาศัยเฉพาะแพตช์นั้น
- พรอมป์ตของกระบวนการนั้นอยู่ใน
prompts.md - ผลลัพธ์คือ
genpoc2.py - ทำงานคล้ายกับ
genpoc.pyมาก
- พรอมป์ตของกระบวนการนั้นอยู่ใน
คำถามต่อจังหวะการเปิดเผย
- มีการเปิดเผยข้อมูลก่อนที่การแก้ไขจะไปถึง stable release ทำให้เกิดช่องทางที่ทำให้ ผู้ใช้ส่วนใหญ่ยังไม่ได้รับการปกป้องอย่างแท้จริง แต่กลับรู้ช่องโหว่นี้แล้ว
- ความขัดแย้งเรื่องจังหวะการเปิดเผยแบบนี้จำเป็นต้องมี เหตุผลรองรับที่ชัดเจน
- ระยะเวลา 2 สัปดาห์นั้นสั้นเกินกว่าจะคาดหวังการกระจายแพตช์อย่างมีนัยสำคัญ และก็สั้นเกินกว่าจะใช้เป็นเหตุผลว่าจำเป็นต้องเปิดเผยก่อนเวลาเพื่อบีบให้เกิดการตอบสนอง
- ผลคือช่องโหว่กลายเป็นที่รับรู้ในวงกว้าง แต่เวอร์ชันแก้ไขกลับยังไม่ถูกส่งถึงผู้ใช้ที่ต้องการจริง เกิดเป็น ช่วงว่างของการเปิดเผย
- ทางเลือกที่ดีกว่าอาจเป็นการรอจนกว่าเวอร์ชันแก้ไขจะถึงมือผู้ใช้จริง หรืออย่างน้อยก็ชี้แจงให้ชัดว่าทำไมการเปิดเผยก่อนเวลาจึงจำเป็น แต่ในกรณีนี้ไม่มีข้อใดเกิดขึ้น
1 ความคิดเห็น
ความเห็นจาก Hacker News
สงสัยว่าทำไมถึงเปิดเผยตอนนี้ ทั้งที่ แพตช์ สำหรับเวอร์ชันเสถียรยังไม่ออก และเพิ่งรายงานไปทาง upstream ได้แค่ 18 วัน เท่านั้น อีกทั้งโพสต์บล็อกก็ละเอียดกว่าคอมมิตที่เปิดเผยมาก จนรู้สึกว่าเพิ่มความเป็นไปได้ในการนำไปโจมตีจริง ผู้เขียนยืนยันว่าต่อให้ดูแค่คอมมิต upstream ก็ใช้ LLM สร้าง exploit ได้ แต่ก็ยังมองว่าโพสต์นี้ทำให้ช่องโหว่ถูกมองเห็นชัดขึ้น
งานนี้เจ๋งดี แต่ไม่ได้ถึงกับน่าตกใจมาก เพราะนี่เป็นปัญหาที่เกิดซ้ำ ๆ ใน แอปเทอร์มินัลที่มีฟีเจอร์มากมาย และในช่วง 15 ปีที่ผ่านมาก็มีการเปิดเผยช่องโหว่คล้ายกันหลายครั้ง เครื่องมืออย่าง less หรือ vim ก็ไม่ใช่ข้อยกเว้น และปัญหาแบบนี้หลายกรณีก็ใกล้เคียง บั๊กเชิงตรรกะ มากกว่าปัญหา memory safety ดังนั้นต่อให้เขียนใหม่ด้วย Rust ก็ไม่ได้แก้อัตโนมัติ ในอีกมุมหนึ่งเราก็อยากให้เครื่องมือระดับ OS เรียบง่ายและคาดเดาได้ แต่ในอีกมุมก็อยากได้สีสวย แอนิเมชัน และการปรับแต่งแบบไม่สิ้นสุด ตอนนี้ยังมี AI agent เข้ามาอีก จนกลายเป็นยุคที่ไฟล์ข้อความอันตรายอาจมีแค่ประโยคอย่าง "ignore previous instructions" ก็พอแล้ว
ทำให้นึกถึงสมัย PDP-10 มีเพื่อนร่วมงานคนหนึ่งพบว่าถ้ากด backspace รัว ๆ ตัวจัดการเทอร์มินัลจะลบไปถึงตัวอักษรด้านหน้าบัฟเฟอร์ และถ้าใช้ อักขระ escape สำหรับลบทั้งบรรทัดต่อไป ระบบปฏิบัติการก็ล่มไปเลย
เมื่อ 6 ปีก่อนก็เคยมีประเด็นความปลอดภัยของ iTerm2 ที่แทบเหมือนกัน
ผมเป็นผู้สร้าง iTerm2 ปัญหานี้สามารถใช้เป็นส่วนหนึ่งของ exploit chain ได้ แต่การพูดเหมือนว่ามันอันตรายรุนแรงแบบเดี่ยว ๆ ตามชื่อเรื่องนั้นผมมองว่า เกินจริง ตอนนี้ผมกำลังพาครอบครัวไปเที่ยว และพอกลับไปจะออก เวอร์ชันแก้ไข
ไม่ได้แปลกใจที่เกิดบั๊กละเอียดอ่อนใน ระบบซับซ้อน ที่ใช้ bootstrap script, remote conductor agent, escape sequence ฯลฯ เมื่อเอาองค์ประกอบต่าง ๆ มาใช้ในแบบที่ไม่ได้ตั้งใจแต่แรก ปัญหาแบบนี้ก็เกิดได้ง่าย สิ่งที่ผมเข้าใจคือถ้ามีโค้ดพิเศษแทรกอยู่ใน เอาต์พุตที่ไม่น่าเชื่อถือ อย่างไฟล์ข้อความหรือ server banner ที่พิมพ์ออกจอ แล้วระบบประมวลผลมันโดยไม่ตรวจสอบแหล่งที่มา ก็จะเกิดปัญหา
รู้สึกเหมือนเคยเห็นเรื่องนี้มาก่อน เคยมีกรณีที่ SSH integration ของ iTerm2 เป็นสาเหตุของ CVE และก็นึกถึง CVE-2025-22275 ด้วย ก่อนหน้านี้ก็มีหลายเคสแล้ว และประเด็นเก่าที่มีคนพูดถึงในเธรดนี้เป็นฝั่ง tmux integration ดูแล้วฟีเจอร์ integration แบบนี้น่าจะใส่มาแบบไม่รุกเกินไปจะดีกว่า
ชื่อเรื่องเร้าอารมณ์เกินไป ปัญหาไม่ใช่ตัวแมว แต่เป็น SSH integration ของ iTerm และโครงสร้าง ช่องควบคุม ที่ไม่ถูกแยกจาก data stream ดูอันตราย ฟีเจอร์นี้ถ้าไม่ใช้และใช้ SSH ปกติอย่างเดียวก็น่าจะปลอดภัยเป็นส่วนใหญ่
เทอร์มินัลอีมูเลเตอร์สมัยก่อนถึงขั้นยอมให้ รีไบน์คีย์บอร์ด ผ่าน escape code ได้ เพราะงั้นการบอกว่าอย่า
catไฟล์ที่ไม่น่าเชื่อถือ แต่ให้เปิดด้วยเครื่องมืออย่างlessแทน จึงแทบจะเป็น ความรู้พื้นฐานการใช้คำในบทความไม่แม่นนัก ย่อหน้าที่สองอ่านเหมือนบอกว่า "ถ้าใช้ iTerm2 ก็ไม่ปลอดภัย" แต่ที่จริงควรจะหมายถึงว่าอาจมีปัญหาเมื่อใช้ฟีเจอร์ Shell Integration แบบเลือกเปิดนี้เท่านั้น ถ้าฟีเจอร์นี้ปิดไว้เป็นค่าเริ่มต้น ขอบเขตผลกระทบก็น่าจะจำกัด ถ้าผมเข้าใจผิดก็ยินดีให้แก้ไข