1 คะแนน โดย GN⁺ 2023-07-04 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Nginx คือเว็บเซิร์ฟเวอร์อเนกประสงค์ที่ครองส่วนแบ่งตลาดอย่างโดดเด่นมาตั้งแต่ปี 2004
  • การตั้งค่า Nginx ที่ไม่ถูกต้องอาจนำไปสู่ช่องโหว่ด้านความปลอดภัยและการเปิดเผยข้อมูล
  • แนะนำ NavGix เครื่องมืออัตโนมัติสำหรับตรวจจับช่องโหว่ใน Nginx
  • ช่องโหว่นี้ทำให้สามารถเข้าถึงไฟล์และไดเรกทอรีนอกขอบเขตที่ตั้งใจไว้ได้
  • กรณีศึกษาของ Bitwarden และ HPC Toolkit ของ Google แสดงให้เห็นถึงความร้ายแรงของช่องโหว่นี้

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

 
GN⁺ 2023-07-04
ความคิดเห็นจาก Hacker News
  • เผื่อไว้เป็นข้อมูล gixy (เครื่องมือตรวจสอบคอนฟิก nginx) สามารถตรวจจับปัญหานี้ได้: https://github.com/yandex/gixy/blob/master/docs/en/plugins/a...
    และ NixOS จะรัน gixy อัตโนมัติกับคอนฟิกที่สร้างผ่านระบบของตัวเอง และถ้ามีปัญหาก็จะปฏิเสธการ build ระบบ

    • ถ้าเว็บเซิร์ฟเวอร์ต้องให้ผู้ใช้ใช้เครื่องมือเสริมเพื่อหลีกเลี่ยงกับดักแบบนี้ บางทีคงต้องกลับมาทบทวน ค่าเริ่มต้น ใหม่
    • ไม่เคยรู้จัก gixy มาก่อน แต่พอลองรันบนโฮมเซิร์ฟเวอร์ก็เจอช่องโหว่หนึ่งจุด กำลังใช้ $uri อยู่ใน 301 redirect
    • เคยลองใช้ Nix อยู่ครั้งหนึ่ง และจนถึงตอนนี้ก็ดูค่อนข้างดี
      แต่สงสัยว่ามี ตัวค้นหาออปชัน ที่ดีกว่านี้ไหม การค้นจากออปชันทั้งหมดหลายพันรายการไม่ค่อยเวิร์กนัก อยากดูแค่แพ็กเกจอย่าง ssh แล้วเห็นเฉพาะออปชันที่เกี่ยวข้อง แต่ผลลัพธ์ที่ไม่เกี่ยวข้องปนมาเยอะเกินไป
    • NixOS ไม่ได้รัน Gixy อีกต่อไปแล้ว ดู https://github.com/NixOS/nixpkgs/pull/209075
  • อาจเป็นคำถามโง่ ๆ แต่มีเหตุผลดีอะไรไหมที่ nginx ควรยอมให้ใช้ .. ใน URL path เพื่อไต่ขึ้นไปยังไดเรกทอรีแม่? มันดูเหมือนพฤติกรรมที่รอให้เกิดปัญหาชัด ๆ
    แก้ไข: ตอนแรกยังงงนิดหน่อยว่าในช่องโหว่ต้นฉบับเกิดอะไรขึ้น ดูเหมือนว่า http://localhost/foo../secretfile.txt จะถูกตีความเป็นอะไรอย่าง /var/www/foo/../secretfile.txt แต่ถ้าเป็นเซิร์ฟเวอร์ที่ไม่มีช่องโหว่ ทำไม http://localhost/foo/../secretfile.txt ถึงไม่ถูกตีความแบบเดียวกันด้วยล่ะ? ไม่เข้าใจว่าทำไม .. ใน path ถึงทำงานเฉพาะบางกรณี

    • นี่เป็นประเด็นที่รู้กันมานานมากแล้วใน nginx และเป็น attack vector ที่พบบ่อยใน CTF: https://book.hacktricks.xyz/network-services-pentesting/pent...
    • ปัญหาคือ URL ไม่ใช่ path จริง URL เป็นที่อยู่เชิงนามธรรมของทรัพยากร ซึ่งอาจเป็นไดเรกทอรี ไฟล์ ไฟล์ปฏิบัติการ สตรีม หรืออย่างอื่นก็ได้
      ในกรณีนี้ nginx ตีความ URL ส่วนหนึ่งเป็นไดเรกทอรี (http://localhost/foo) เพราะวิธีที่คอนฟิกแมปมันเข้ากับไฟล์ซิสเต็มภายในเครื่อง เมื่อมันดูเหมือนชี้ไปที่ไดเรกทอรี ตอน nginx สร้าง full path ของทรัพยากรที่ร้องขอ มันจึงกลายเป็น "${mapped_path}/../secretfile.txt" และถึงแม้จากมุมมองของ URL จะไม่สมเหตุสมผล แต่มันกลับใช้ได้บนไฟล์ซิสเต็มภายในเครื่อง URL ไม่ใช่องค์ประกอบ path จริง ๆ แต่เป็นแค่สตริง ดังนั้นตำแหน่งของ slash จึงไม่ได้สำคัญโดยเนื้อแท้
      นี่เป็นปัญหาที่พบได้บ่อยมากในเว็บเซิร์ฟเวอร์มาตั้งแต่ยุคแรกของเว็บ การแมป URL เข้ากับ file path โดยตรงได้รับความนิยมเพราะเริ่มต้นจากการเป็นไฟล์เซิร์ฟเวอร์แบบง่ายที่มี index แต่หลังจากนั้นไม่นานก็กลายเป็นสภาพแวดล้อมแบบผสมที่ URL ไม่ได้เป็น path อีกต่อไป แต่เป็นตัวระบุแอปพลิเคชัน เช่น ระบุแอปด้วยส่วนหนึ่งของ path แล้วใช้ส่วนที่เหลือเป็นพารามิเตอร์หรืออาร์กิวเมนต์
      และโดยทั่วไปแล้ว การเคารพ . หรือ .. ของ URL กับอ็อบเจ็กต์ในไฟล์ซิสเต็มมักไม่สมเหตุสมผล แอปของฉันจะทำความสะอาด request path เพื่อให้แมปได้ถูกต้อง เบราว์เซอร์ปฏิบัติต่อ URL เหมือน path เวลาสร้าง relative link ดังนั้นจึงต้องระวังด้วยว่าเมื่อไรและอย่างไรถึงจะใช้ / ปิดท้าย เพราะฝั่งเซิร์ฟเวอร์อาจหมายถึงทรัพยากรคนละตัว
    • สำหรับกรณีใช้งานที่ “ปกติ” ไม่จำเป็นเลย น่าจะเหมาะกว่าถ้าทำให้เป็น พฤติกรรมที่ต้องเปิดใช้อย่างชัดเจน ผ่านแฟล็กใน location เช่น allow_parent_traversal on;
    • เดาว่า NginX คงตรวจ "/foo/bar/.." แล้วบล็อกไว้หรือทำ normalization ให้เป็น "/foo/" แต่ "/foo/bar.." เป็นชื่อไฟล์ที่ถูกต้องสมบูรณ์ เลยอาจหลุดรอดจากการตรวจแบบนั้นไปได้
    • การที่ .. จะทำงานเมื่อไรใน path ขึ้นอยู่กับ สิทธิ์ของไฟล์ ทั้งหมด
      ในกรณีนี้ ถ้าสมมติว่ามีสิทธิ์อ่านไฟล์จากไดเรกทอรี web index (../index.html) ย้อนขึ้นไปจนถึง root directory (/) ก็แปลว่าคุณมีสิทธิ์ไล่ขึ้นไปถึง root ได้ และเมื่อถึงตรงนั้นก็จะอ่านไฟล์ที่ world-readable ทุกไฟล์ที่เข้าถึงได้จาก root เช่น /etc/passwd ได้ด้วย
      ลองนึกภาพส้อมสามง่าม โดยเว็บเซิร์ฟเวอร์อยู่ที่ง่ามขวาสุด ถ้าส่วนด้ามที่ง่ามทั้งหลายมาบรรจบกันคือไฟล์ซิสเต็ม เมื่อเว็บเซิร์ฟเวอร์มีสิทธิ์เข้าถึงไฟล์และไดเรกทอรีจากง่ามขวาสุดไปจนถึงด้าม พอไปถึงด้ามแล้วก็สามารถเข้าถึงไฟล์ในง่ามอื่นต่อได้เหมือนกัน
  • ไม่เข้าใจว่าทำไมเรื่องนี้ถึงไม่ถูกมองว่าเป็นช่องโหว่ของ nginx พฤติกรรมนี้ไร้เหตุผลโดยสิ้นเชิง ดูไม่มีประโยชน์อะไร และเอาไปโจมตีได้ทันที

    • ทำแบบนั้นเพราะ ความเร็ว การแทนที่ข้อความแบบตรง ๆ เร็วกว่าการตรวจว่าพาธลงท้ายด้วย slash อย่างถูกต้องมาก
      ต้องจำไว้ด้วยว่า Nginx ได้รับความนิยมก็เพราะ benchmark ที่ชี้ว่ามัน “web scale” กว่า Apache2
  • มีคนเสนอแนวคิดแบบนี้ขึ้นมา: ใส่ ตัวเลือกคล้าย Linux capability ในตัวแยกวิเคราะห์ชื่อไฟล์ของ Linux kernel เพื่อเอาตัวเลือก .. ออก
    ในเว็บแอปมีวิธีเลี่ยงสารพัดแบบเพื่อยัดจุดสองจุดเข้าไปมาตั้งแต่สมัยใช้โทรศัพท์โมเด็มแล้ว เหมือนที่ Linux kernel เคยจัดการกับบั๊กใน user space อีกหลายประเภท บางทีถึงเวลาหาวิธีปิดปัญหานี้ทีเดียวให้จบได้แล้ว

    • มี RESOLVE_BENEATH ใน https://man7.org/linux/man-pages/man2/openat2.2.html
      FreeBSD ก็มีความสามารถนี้ใน openat(2) ปกติผ่าน O_RESOLVE_BENEATH
    • ถ้าทำแบบนั้น ของจำนวนมากจะพังเกินไป ไม่ใช่อะไรที่คนมีสติจะทำกัน
      แค่รัน nginx ด้วยผู้ใช้แยกต่างหากที่สิทธิ์จำกัดมาก ๆ หรือรันใน Docker แล้วอัปเดตเป็นประจำ ปกติก็แก้ ปัญหาด้านความปลอดภัย 90% ได้แล้ว
    • /some/../path ควรถูกห้ามเกือบ 100% ไม่มีกรณีใช้งานที่สมเหตุสมผลนอกจาก “มีใครสักคนเขียนโค้ดได้น่าเกลียด”
      ../some/path อย่างน้อยก็ยังมีความหมายอยู่บ้างเป็นครั้งคราว
      แต่ก็คงไม่ได้มีประโยชน์อย่างที่คิด เพราะหลายแอปตีความ .. เองก่อนส่งต่อให้ระบบปฏิบัติการ
    • แทบไม่ต่างกัน โค้ดมักจะ normalize path ก่อนจะไปแตะ API ของไฟล์ซิสเต็ม
    • ฝั่ง kernel มีระบบอีกชุดหนึ่งอยู่แล้ว คือ ระบบสิทธิ์การเข้าถึง ที่เราพึ่งพา
      ถ้าคุณเสิร์ฟไฟล์จากโฟลเดอร์ออกเว็บ เว็บเฟรมเวิร์กก็ควรจัดการไม่ให้ออกไปนอกโฟลเดอร์รากที่เปิดเผยต่อสาธารณะ ถ้าคุณทำเองก็ต้องคิดเผื่อสถานการณ์สารพัดรวมถึงเรื่องนี้ด้วย
  • บอกว่า “Google VRP Team ให้รางวัล 500 ดอลลาร์สำหรับการพบบั๊กนี้ โดยมองว่าผลกระทบต่อแอปพลิเคชันไม่ร้ายแรงพอจะได้รางวัลสูงกว่านี้” งั้นเหรอ อีเมลบัญชี GCP กับคีย์ส่วนตัวที่ไม่เปิดเผยถูกเปิดออกมา ยังได้แค่ 500 ดอลลาร์เองเหรอ? อะไรกันเนี่ย สมเป็น Google จริง ๆ

  • อย่างน้อยสิ่งที่รั่วไหลออกมายัง ถูกเข้ารหัส อยู่ ซึ่งถือว่าโชคดี แม้แต่บริษัทที่เชี่ยวชาญด้านนี้ก็ยังหนีการรั่วไหลไม่พ้น พูดตามตรง นี่ก็คือสถานการณ์ที่ดีที่สุดเท่าที่หวังได้แล้ว

  • ชื่อเรื่องถูกแก้ไปค่อนข้างเยอะ ชื่อเดิมคือ Hunting for Nginx Alias Traversals in the wild
    ชื่อที่ส่งเข้า HN เน้นช่องโหว่ของ Bitwarden แต่ในบทความก็พูดถึงกรณีของ Google ด้วย

    • โอเค เปลี่ยนชื่อกลับแล้ว ชื่อที่ส่งมาคือ “Leaking Bitwarden's Vault with a Nginx vulnerability”
  • ถ้าต้องการแค่ เสิร์ฟไฟล์สแตติก อย่างปลอดภัยและเสถียรโดยใช้ทรัพยากรให้น้อยที่สุด ทุกวันนี้ตัวเลือกที่ดีที่สุดคืออะไร? ถ้าเป็นเมื่อก่อนผมคงเลือก Nginx แต่ในมุมความปลอดภัยก็สงสัยว่าเครื่องมือที่ทำหน้าที่แคบกว่าและตั้งค่าได้น้อยกว่าน่าจะดีกว่าไหม

    • ผมใช้ https://static-web-server.net/
      มันข้ามแพลตฟอร์ม เขียนด้วย Rust ตั้งค่าง่าย และมีค่าเริ่มต้นที่ปลอดภัย ยังมีอิมเมจคอนเทนเนอร์แบบ hardened กับโมดูล NixOS แบบ hardened ด้วย
      ผมไม่แนะนำ Caddy เพราะอิมเมจ Docker ทางการรันเป็น root โดยปริยาย [1] และก็ไม่ได้ให้ไฟล์ยูนิต systemd ที่ sandbox อย่างเหมาะสมมาด้วย [2]
      [1]: https://github.com/caddyserver/caddy-docker/issues/104
      [2]: https://github.com/caddyserver/dist/blob/master/init/caddy.s...
      แก้ไข: ปรับถ้อยคำ
    • ขอโปรโมตหน่อยว่า Caddy ทำงานนี้ได้ดีมาก มี HTTPS อัตโนมัติ เขียนด้วย Go จึงไม่ต้องกังวลเรื่องบั๊กความปลอดภัยของหน่วยความจำ และมีโมดูล file_server ที่แข็งแรง
    • ผมใช้ Caddy มาหลายปีแล้ว มีใบรับรอง SSL อัตโนมัติ เสิร์ฟไฟล์ได้ เป็นรีเวิร์สพร็อกซีก็ได้ และตั้งค่าง่าย ชัดเจนมาก เป็นไบนารีเดี่ยวของ Go เลยติดตั้งก็ง่าย และมีไฟล์คอนฟิกแค่ไฟล์เดียว
    • Caddy ค่อนข้างตั้งค่าง่ายสำหรับการเสิร์ฟไฟล์สแตติก
    • Merecat: https://github.com/troglobit/merecat/
  • อาจเป็นคำถามงี่เง่า แต่ทำไม Bitwarden ถึงยอมให้มี คำขอที่ไม่ต้องยืนยันตัวตน ไปยัง /attachments ตั้งแต่แรก? ต่อให้มีบั๊กใน Nginx ถ้า URL นั้นต้องยืนยันตัวตนก่อน คำขอก็น่าจะล้มเหลวไม่ใช่เหรอ?

    • นี่เป็นเอ็กซ์พลอยต์ที่เล่นงานการตั้งค่าเว็บเซิร์ฟเวอร์ ดังนั้นโค้ดยืนยันตัวตนของ Bitwarden หรือโค้ดของ Bitwarden เองจะไม่ถูกรันเลย การที่โปรเจกต์ใช้ระบบยืนยันตัวตนของตัวเองแทนที่จะพึ่ง Nginx หรือโมดูลก็ไม่ได้แปลกหรือผิดอะไร
      ถึงอย่างนั้น Bitwarden ก็ยังต้องรับผิดชอบ เพราะปล่อยการตั้งค่าที่เสี่ยงผ่าน Docker ออกมา และดูเหมือน Bitwarden ก็ยอมรับเรื่องนี้แล้วและแก้ไขในภายหลัง
  • ขออภัยหากเป็นคำถามงี่เง่า แบบนี้ไม่ถูกป้องกันด้วย สิทธิ์ความเป็นเจ้าของไดเรกทอรีและไฟล์ ที่เหมาะสมหรือ?
    ถ้า nginx ไม่ได้รันด้วย root แล้ว nginx จะอ่านไฟล์อื่นนอกเหนือจากไฟล์ที่ถูกกำหนดให้ผู้ใช้ nginx โดยชัดเจนได้อย่างไร?

    • แน่นอนว่าป้องกันได้ รันแอปด้วยผู้ใช้หนึ่ง และรัน nginx ด้วยอีกผู้ใช้หนึ่ง จากนั้นตั้ง go-rwx ให้ไฟล์ทั้งหมดของแอป ตั้งกรุ๊ปของไฟล์ “static” เป็น www-data แล้วให้แค่ g+r เว็บเซิร์ฟเวอร์ก็จะเข้าถึงไฟล์แอปไม่ได้
      พูดตรงตัวเลยว่านี่คือ การโฮสต์แอป 101 และคนก็ทำกันแบบนี้มาตั้งแต่ 20 ปีก่อนแล้ว
    • อ๋อ ความมหัศจรรย์ของ 022 umask นี่เอง โดยส่วนตัวแล้วฉันแนะนำให้ตั้งค่าไม่ให้ผู้ใช้อื่นอ่านไฟล์ได้เสมอ ถ้าทำกับทุกไฟล์ยาก อย่างน้อยก็ควรทำกับไดเรกทอรีสำคัญ เช่น ใต้ /home
      อาจต้องจัดการเรื่องการเป็นสมาชิกกลุ่มเพิ่มอีกหน่อย แต่คุ้มค่าพอแน่นอน
    • ไม่รู้คนอื่นเป็นยังไง แต่ตอนนี้ฉันไม่ค่อยติดตั้ง nginx แบบจริงจังสำหรับใช้งานส่วนตัวแล้ว แค่รันอิมเมจ Docker ขึ้นมา และก็ไม่ได้ตรวจสอบจริง ๆ ด้วยว่ามันรันเป็น root หรือเปล่า
      บางทีฉันอาจกำลังทำพลาดครั้งใหญ่จริง ๆ ก็ได้ โอ๊ย
    • umask ทั่วไปคือ 022 ดังนั้นส่วนใหญ่ nginx worker จะอ่านได้แต่เขียนไม่ได้ และไม่จำเป็นต้องถูกกำหนดให้ผู้ใช้อย่าง www-data โดยชัดเจนด้วย ถ้าแอปพลิเคชันสร้างข้อมูลที่อ่อนไหว ก็ควรใช้ 077 umask อยู่แล้ว
    • ใช่
      น่าเสียดายที่ nginx และเว็บเซิร์ฟเวอร์อื่น ๆ มักจำเป็นต้องรันด้วย root ในเว็บแอปพลิเคชันทั่วไป เพราะต้อง listen ที่พอร์ต 80 หรือ 443 พอร์ตที่ต่ำกว่า 1024 นั้นมีแต่ root เท่านั้นที่เปิดได้
      มีคำอธิบายละเอียดกว่านี้อยู่ที่นี่: https://unix.stackexchange.com/questions/134301/why-does-ngi...