- ตรวจพบว่า เซิร์ฟเวอร์ Hetzner ส่วนตัวมีการใช้ CPU ผิดปกติ เมื่อตรวจสอบจึงพบว่า โปรแกรมขุดคริปโต Monero (
xmrig) กำลังทำงานอยู่
- สาเหตุมาจาก คอนเทนเนอร์เครื่องมือวิเคราะห์ Umami ถูกโจมตี โดยมี ช่องโหว่รันโค้ดจากระยะไกลของ Next.js (CVE-2025-66478) รวมอยู่ด้วย
- โชคดีที่คอนเทนเนอร์ดังกล่าวรันด้วย ผู้ใช้แบบ non-root และ ไม่มีการเมานต์โฮสต์หรือการยกระดับสิทธิ์ ทำให้การบุกรุกถูกจำกัดอยู่ภายในคอนเทนเนอร์
- ผู้โจมตีอาศัย การ deserialize ที่ไม่ปลอดภัยใน Next.js server components เพื่อรันเพย์โหลดอันตรายและติดตั้งตัวขุด
- กรณีนี้แสดงให้เห็นถึง ความสำคัญของการจัดการ dependency และการตั้งค่าความปลอดภัยของคอนเทนเนอร์ โดยผู้เขียนได้เสริมมาตรการ เช่น เปิดไฟร์วอลล์ เสริมความปลอดภัย SSH และอัปเดตอย่างสม่ำเสมอ
การถูกแฮ็กและการตอบสนองเบื้องต้น
- ได้รับ อีเมลแจ้งเตือนจาก Hetzner ว่าเซิร์ฟเวอร์มีการสแกนเครือข่ายภายนอก
- ระบุด้วยว่าหากไม่ดำเนินการภายใน 4 ชั่วโมง เซิร์ฟเวอร์อาจถูกระงับ
- เมื่อล็อกอินผ่าน SSH เพื่อตรวจสอบ พบว่าโปรเซสที่พาธ
/tmp/.XIN-unix/javae ใช้ CPU ถึง 819% พร้อมกับพบโปรเซส xmrig หลายตัว
- ยืนยันได้ว่ามี การขุดคริปโตต่อเนื่องราว 10 วัน
การสืบหาเส้นทางการบุกรุก
- โปรเซสอันตรายทั้งหมดรันด้วย ผู้ใช้ UID 1001 ซึ่งตรงกับ คอนเทนเนอร์ Umami
- พบ ไฟล์รันตัวขุด ในไดเรกทอรี
/app/node_modules/next/dist/server/lib/xmrig-6.24.0/
- คำสั่งที่ใช้รันมีที่อยู่พูลขุด
auto.c3pool.org:443 และคีย์ผู้ใช้รวมอยู่ด้วย
ช่องโหว่ของ Next.js และวิธีโจมตี
- สาเหตุมาจาก ช่องโหว่การ deserialize ของ React Server Components ใน Next.js (CVE-2025-66478)
- หากผู้โจมตีส่ง HTTP request ที่ถูกดัดแปลงมา ก็จะสามารถรันโค้ดตามอำเภอใจบนเซิร์ฟเวอร์ได้
- ส่งผลให้ ติดตั้งและรันตัวขุดคริปโต ได้
- ผู้เขียนเคยคิดว่า “ตัวเองไม่ได้ใช้ Next.js โดยตรง” แต่ภายหลังจึงทราบว่า Umami สร้างอยู่บน Next.js
การยืนยันการแยกตัวของคอนเทนเนอร์
- ยืนยันว่า
/tmp/.XIN-unix/javae ไม่มีอยู่บนไฟล์ซิสเต็มของโฮสต์
- สิ่งที่เห็นในผลลัพธ์
docker ps เป็นเพียงลักษณะการแสดงผลของโปรเซสคอนเทนเนอร์ และในความเป็นจริงยังคงถูกแยกออกจากกัน
- ผลจาก
docker inspect
- ผู้ใช้:
nextjs
Privileged: false
Mounts: ไม่มี
- ดังนั้นมัลแวร์จึง ไม่สามารถเข้าถึงโฮสต์ ลงทะเบียน cron สร้าง system service หรือฝัง rootkit ได้
การกู้คืนและการเสริมความปลอดภัย
- หยุดและลบ คอนเทนเนอร์ Umami ที่ติดเชื้อ แล้วการใช้ CPU ก็กลับสู่ปกติ
- เปิดใช้ไฟร์วอลล์ UFW เพื่ออนุญาตเฉพาะ SSH, HTTP และ HTTPS
- หลังรายงานผลการตรวจสอบให้ Hetzner ทราบ ทิกเก็ตก็ถูกปิดภายใน 1 ชั่วโมง
บทเรียนและจุดที่ควรปรับปรุง
- คำว่า “ฉันไม่ได้ใช้ X” ไม่ได้ครอบคลุม dependency ที่ซ่อนอยู่
- จำเป็นต้องตรวจสอบว่าเครื่องมือที่ใช้อยู่นั้นประกอบขึ้นจากเทคโนโลยีสแตกใดบ้าง
- เป็นหลักฐานยืนยัน ประสิทธิภาพของการแยกคอนเทนเนอร์
- การใช้ผู้ใช้ non-root โหมดไม่สิทธิพิเศษ และไม่ใช้วอลุ่มที่ไม่จำเป็น ช่วยป้องกันความเสียหายลุกลาม
- ความจำเป็นของ การป้องกันหลายชั้น (Defense in Depth)
- ไฟร์วอลล์, fail2ban, การมอนิเตอร์ และการอัปเดตสม่ำเสมอเป็นสิ่งจำเป็น
- เน้นย้ำความสำคัญของ การเขียน Dockerfile เอง และ การลดสิทธิ์ของคอนเทนเนอร์ให้น้อยที่สุด
มาตรการหลังเกิดเหตุ
- นำ Umami เวอร์ชันล่าสุดกลับมาดีพลอยใหม่ และ ตรวจสอบคอนเทนเนอร์ third-party ทั้งหมด
- ตรวจดูผู้ใช้ที่รัน การเมานต์ ช่วงเวลาอัปเดต และความจำเป็นในการใช้งาน
- เปลี่ยนไปใช้การยืนยันตัวตนด้วย SSH key, ปิดการล็อกอินด้วยรหัสผ่าน, และ ตั้งค่า fail2ban
- เสริมการมอนิเตอร์ผ่าน Grafana และ Node Exporter พร้อม ติดตั้งอัปเดตความปลอดภัยทันที
สรุป
- ช่องโหว่ของ Next.js ใน Umami ทำให้ถูกนำไปใช้ ขุด Monero นาน 10 วัน แต่
ด้วยการแยกคอนเทนเนอร์และการรันแบบ non-root จึงทำให้ความเสียหายถูกจำกัดไว้
- จากประสบการณ์ครั้งนี้ ผู้เขียนได้เรียนรู้ถึง ความสำคัญของการเข้าใจ dependency การตั้งค่าความปลอดภัย และการจัดการอัปเดต
- เหตุการณ์ได้รับการจัดการเสร็จภายในราว 2 ชั่วโมง และกลายเป็น กรณีตัวอย่างที่พิสูจน์ผลลัพธ์จริงของความปลอดภัยคอนเทนเนอร์
1 ความคิดเห็น
ความเห็นจาก Hacker News
เมื่อก่อนผมใช้ UFW แต่ตอนนี้แนะนำ firewalld มากกว่า
UFW พอนานไปจะจัดการยาก แต่ firewalld ใช้การตั้งค่าแบบ XML เลยเสถียรกว่ามาก
สามารถตั้งค่า SSH, HTTPS, พอร์ต 80 ฯลฯ ได้ด้วยคำสั่ง
firewall-cmdและควรใช้แบ็กเอนด์ nftablesสำหรับ Docker มักมีกรณีที่มันข้ามกฎไฟร์วอลล์แล้วเปิดพอร์ตเอง ดังนั้นตั้งค่า
StrictForwardPorts=yesใน/etc/firewalld/firewalld.confจะปลอดภัยกว่า8080:8080แต่ควร bind กับ private IP แบบ192.168.0.1:8080:8080ผมรัน Docker บน VM ที่ 10.0.10.11 และให้เข้าถึงได้ผ่าน WireGuard เท่านั้น โดยวาง Caddy เป็น reverse proxy ซึ่งสะดวกดี
มัลแวร์จะพยายามเชื่อมต่อไปยัง mining pool ภายนอกหรือเซิร์ฟเวอร์ C2 ดังนั้นต้องบล็อกไม่ให้ไบนารีที่ไม่ได้รับอนุญาตเข้าถึงเครือข่าย
การถอดสิทธิ์รันไฟล์ใน
/tmp,/var/tmp,/dev/shmก็มีประโยชน์เช่นกันnftables.confก็ง่ายและชัดเจนพอแล้วiptables ถูกเลิกใช้ไปแล้ว จึงไม่จำเป็นต้องมีเลเยอร์เพิ่มอย่าง firewalld เสมอไป
นี่น่าจะเป็นปัญหาที่เกี่ยวกับ “React2Shell CVE-2025-55182”
เป็นช่องโหว่ที่มีผลกระทบมานานกว่าหนึ่งปีแต่กลับแทบไม่ได้รับความสนใจ ซึ่งแปลกมาก
ถ้าคุณ deploy เว็บแอปด้วย Next.js ในช่วง 12 เดือนที่ผ่านมา ก็มีโอกาสสูงที่มันจะกลายเป็นส่วนหนึ่งของ botnet ไปแล้ว
น่าหงุดหงิดที่วงการยังให้คำแนะนำแค่ระดับ “ใช้ Docker”, “เปิดไฟร์วอลล์”
ช่วงนี้ผมเริ่มหมดศรัทธากับ ecosystem ฝั่งฟรอนต์เอนด์จนกำลังคิดจะย้ายสายอาชีพไปทาง C++
ตอนนี้ชุด Next.js, React, Tailwind, Postgres แทบจะกลายเป็นมาตรฐานมา 5 ปีแล้ว
ถ้าเทียบกับยุคปลาย 2000 ถึงต้น 2010 ที่เฟรมเวิร์กผุดขึ้นเต็มไปหมด ตอนนี้ถือว่าเสถียรกว่า
ถ้าไม่ชอบกระแสและความเปลี่ยนแปลง สายพัฒนา AI เปลี่ยนเร็วกว่าเยอะ
ฝั่งแบ็กเอนด์ใช้ technology stack ที่มั่นคง อย่าง .NET, Java, Go แล้วเลือกฟรอนต์เอนด์ได้ตามสะดวก
แบบนี้จะมี CVE น้อยลงและความล้าทางเทคโนโลยีก็ลดลงด้วย
GitHub discussion ที่เกี่ยวข้อง
ถ้าจำกัดการใช้ CPU ของคอนเทนเนอร์ Docker ด้วย
--cpus="0.5"ก็จะช่วยให้ บริการที่ทำงานผิดปกติหรือ miner ไม่ลากทั้งระบบลงไปด้วยการรันเซิร์ฟเวอร์โดยไม่มีไฟร์วอลล์ถือว่าเป็นตัวเลือกที่ กล้ามาก
ถ้าใช้ไฟร์วอลล์ภายนอกของ Hetzner ร่วมด้วย เวลาพลาดก็ยังมีชั้นป้องกันเพิ่มอีกชั้น
ผมอนุญาต SSH แค่จาก IP บ้าน และถ้าต้องใช้จากภายนอกก็จะเปิดชั่วคราวผ่านเว็บไซต์ของ Hetzner
สิ่งที่สำคัญจริง ๆ คือ อย่ารันซอฟต์แวร์ที่มีช่องโหว่ RCE
ที่ Docker ดูเหมือนผู้กอบกู้ จริง ๆ ก็แค่โชคดีเท่านั้น
อีกทางคือใช้ bastion host แล้วควบคุมทราฟฟิกเข้าออกผ่าน HTTP proxy แต่การตั้งค่าค่อนข้างซับซ้อน
แค่ใช้พอร์ตที่ไม่มาตรฐานก็ได้ผลอย่างน่าประหลาด
ไม่แน่ใจว่ามันได้ผลจริงแค่ไหน แต่ให้ความรู้สึกเหมือนร่มกันฝนทางใจมากกว่า
ผมเคยสงสัยว่าถ้ารันคอนเทนเนอร์ Docker ด้วย root มันจะโจมตีถึงโฮสต์ได้ไหม
ถ้าจะรันโค้ดที่เชื่อถือไม่ได้ ควรใช้ VM (KVM/QEMU) หรือเทคโนโลยีอย่าง gVisor(https://gvisor.dev/), Firecracker(https://firecracker-microvm.github.io/)
ควรเข้าใจ Docker ว่าเป็นสภาพแวดล้อมการรันที่แยกออกมา ไม่ใช่ sandbox
การตั้งค่าเริ่มต้นของ Docker ไม่มีการจำกัด RAM, CPU, หรือการใช้ดิสก์ จึงเสี่ยงต่อ การโจมตีแบบ DoS ด้วย
แถมคู่มือจำนวนมากยังแนะนำตัวเลือกอันตรายอย่าง
--privilegedหรือCAP_SYS_PTRACEและยังสตาร์ตคอนเทนเนอร์ใหม่เพื่อยกระดับเป็นสิทธิ์ root ได้ด้วย
Docker ยังไม่ได้เปิดเป็นค่าเริ่มต้น ดังนั้นถ้าไม่ตั้งค่าไว้ ความเสี่ยงในการ escape ก็สูง
การหนีออกจากคอนเทนเนอร์ส่วนใหญ่ในอดีตสามารถป้องกันได้ด้วย user namespace
ถ้าเป็นเซิร์ฟเวอร์ที่ไม่ได้เก็บข้อมูลอ่อนไหว การจัดการแค่คอนเทนเนอร์ก็มักเพียงพอ
ถ้าจะลด attack surface ก็ควร ไม่เปิดเผยบริการที่ไม่จำเป็นต้องเปิดสู่ภายนอก
ตัวอย่างเช่น เครื่องมือวิเคราะห์ควรให้เข้าถึงได้ผ่าน WireGuard หรือ SSH SOCKS proxy เท่านั้น
ผมก็เคยเจอ การติด Monero miner บนเซิร์ฟเวอร์ Hetzner เหมือนกัน
โชคดีที่เกิดขึ้นแค่ในคอนเทนเนอร์ LXC ของ Incus และเพราะ priority ของ CPU ต่ำเลยไม่ทันสังเกต
มีการเพิ่ม SSH key เข้าไปในบัญชี root และติดตั้ง remote management agent ไว้
สุดท้ายผมทิ้งคอนเทนเนอร์นั้นไป แต่ก็ได้บทเรียนบางอย่าง
ในบทความมี เนื้อหาหลอนของ AI ที่ไม่ได้ผ่านการตรวจทานโดยมนุษย์รวมอยู่ด้วย
มันย้ำหลายครั้งว่าเป็นช่องโหว่ที่เกี่ยวกับ Puppeteer ทั้งที่ไม่จริง
ช่วงนี้มีการติดตั้ง Monero miner โดยอาศัย ช่องโหว่ของ React 19 อยู่หลายแห่ง
ผมก็เจอปัญหาเดียวกัน
มันทำงานเหมือน bug bounty อัตโนมัติ ช่วยบอกว่ามีช่องโหว่อยู่
ถ้ามีการมอนิเตอร์เครือข่ายหรือโปรเซสที่ดี ก็ตรวจพบได้ง่าย
แต่ก็กลายเป็นโอกาสดีในการอัปเกรดระบบแบ็กอัปไปด้วย
พอเห็นกรณีแบบนี้ก็ยิ่งรู้สึกว่าตัดสินใจถูกแล้วที่ไม่ดูแล VPS เอง
ผมเคยลองมาก่อน แต่รู้สึกว่าตัวเองก็เป็นแค่ ผู้ดูแลระบบระดับทั่วไป จึงรักษาความปลอดภัยให้ดีได้ยาก
เพราะงั้นตอนนี้ให้ผู้เชี่ยวชาญดูแลและจ่ายเงินไปเลยสบายใจกว่ามาก