nginx โดน heap overflow ย้อนหลัง 15 ปี — แต่เช็คแค่เวอร์ชันไม่พอ
บั๊กตัวนี้ไม่ได้ดูที่เวอร์ชันอย่างเดียวแล้วจบ ต้องไปดู config ว่าเข้าเงื่อนไขไหม และเงื่อนไขจริงกว้างกว่าที่ข่าวส่วนใหญ่เขียนไว้พอสมควร ถ้าเช็คด้วยเงื่อนไขแคบๆ จะได้ใบผ่านปลอมกลับมา
พอดีเห็นข่าว CVE-2026-42533 ของ nginx เมื่อวันก่อน เลยไปไล่เช็คเครื่องที่ผมดูแลอยู่ ตอนแรกก็เช็คตามที่ข่าวเขียนไว้ ผลออกมาว่าผ่านหมด สบายใจไปได้พักหนึ่ง
แต่พอไปตามอ่านของนักวิจัยที่เป็นคนรายงานบั๊กตัวจริง ปรากฏว่าเงื่อนไขที่จะโดนมันกว้างกว่าที่ข่าวสรุปไว้เยอะมากครับ ถ้าผมเชื่อรอบแรกแล้วปิดเรื่องไปเลย ก็จะเป็นการให้ใบผ่านตัวเองแบบผิดๆ เลยต้องกลับไปเช็คใหม่ทั้งหมด
เห็นว่าเรื่องนี้คนน่าจะพลาดกันเยอะ เลยเอามาเล่าให้ฟัง พร้อมวิธีเช็คที่ผมใช้จริง
บั๊กมันคืออะไร
nginx เวลาประกอบสตริงจากตัวแปรใน config มันทำงานสองรอบ รอบแรกวัดว่าผลลัพธ์ยาวเท่าไหร่แล้วจองหน่วยความจำตามนั้น รอบสองค่อยเขียนข้อมูลลงไปจริง
ปัญหาคือทั้งสองรอบใช้ค่า capture จาก regex ตัวเดียวกัน แล้วถ้ามี regex ตัวอื่นมาทำงานคั่นกลางระหว่างสองรอบนั้น มันจะไปทับค่า capture เดิมทิ้ง
ผลคือ buffer ถูกจองตามขนาดของ capture ตัวหนึ่ง แต่ถูกเขียนด้วยข้อมูลของอีกตัวที่คนยิงกำหนดความยาวได้เอง ทั้งความยาวและเนื้อหาที่ล้นออกมามาจาก request ตรงๆ
ระดับความเสียหายอย่างต่ำคือ worker process ตายแล้ว restart = DoS ส่วนที่หนักกว่านั้น ทาง F5 บอกว่าถ้า ASLR ปิดอยู่หรือ bypass ได้ก็มีโอกาสถึงขั้นรันโค้ดจากระยะไกล ส่วนนักวิจัยที่รายงานเถียงว่าตัวบั๊กมันแถม information leak ที่ใช้ bypass ASLR ได้ในตัว แปลว่าไม่ได้จบแค่ DoS ตรงนี้ยังไม่มีใครยืนยันได้เพราะเขายังไม่ปล่อย PoC
เวอร์ชันไหนโดน
ช่วงที่โดนคือตั้งแต่ 0.9.6 ถึง 1.31.2 ซึ่งย้อนกลับไปถึงปี 2011 ตอนที่ map เริ่มรองรับ regex แปลว่าถ้าใครไม่ได้อัปเดตมานาน ก็อยู่ในช่วงนี้แทบทั้งหมด แก้แล้วใน 1.31.3 (mainline) กับ 1.30.4 (stable) และ NGINX Plus 37.0.3.1
อยู่ในช่วงนี้ ไม่ได้แปลว่าโดนอัตโนมัติ และไม่อยู่ในช่วงก็ไม่ได้แปลว่ารอดถ้ายังไม่อัป ตัวตัดสินจริงคือ config ของเครื่องนั้นเข้าเงื่อนไขไหม
กับดัก: เช็คตามข่าวแล้วจะได้ใบผ่านปลอม
ข่าวส่วนใหญ่สรุปเงื่อนไขไว้ประมาณว่า "map ที่ใช้ regex แล้วเอาตัวแปรของ map ไปต่อสตริงร่วมกับ capture $1 จาก regex ก่อนหน้า"
อ่านแบบนี้คนส่วนใหญ่ก็จะไป grep หาบรรทัดที่มีทั้ง $1 และตัวแปร map อยู่ด้วยกัน ซึ่งผมก็ทำแบบนั้นรอบแรกครับ แล้วมันไม่เจอ
capture กับตัวแปร map ไม่จำเป็นต้องอยู่บรรทัดเดียวกัน หรือ directive เดียวกันด้วยซ้ำ — อยู่คนละ directive ใน location block เดียวกันก็ทริกเกอร์ได้แล้ว การ grep หาบรรทัดที่มีทั้งคู่ เลยตอบว่า "ไม่เจอ" ทั้งที่ config เข้าเงื่อนไขเต็มๆ
ตัวอย่างที่โดน ทั้งที่ไม่มีบรรทัดไหนมีทั้งสองอย่างพร้อมกันเลย:
location ~ "^/api/(.+)$" {
proxy_set_header X-Path "$1"; # capture ถูกวัดขนาดในรอบแรก
proxy_set_header X-Check "$my_map_var"; # regex ของ map มาทับ capture state
proxy_pass http://backend;
}
แล้วมันไม่ได้จำกัดอยู่แค่ map กับ proxy_set_header ด้วย นักวิจัยไล่เจอว่าแตะได้ถึง 13 จุดในโค้ด ครอบคลุม directive ที่ใช้กันทั่วไปเกือบหมด
ฝั่ง script engine หลัก: return, add_header, proxy_redirect, proxy_method, auth_basic_user_file, proxy_pass, fastcgi_pass, root/alias, access_log, rewrite, set และของฝั่ง stream อีกชุด
ฝั่งโมดูล: proxy_set_header, proxy_set_body, fastcgi_param, scgi_param, uwsgi_param, grpc_set_header, index
และอีกจุดที่สำคัญมาก คือ named capture ก็โดนเหมือนกัน แบบ (?<name>...) ไม่ได้เขียนลงที่เดียวกับ $1 แต่ไปเข้าอีกทาง สรุปคือถ้าใครไป grep หาแต่ $1 ถึง $9 อย่างเดียว ก็ยังตกอีกเส้นทางหนึ่งอยู่ดี
วิธีเช็คเครื่องตัวเอง
ข้อแรกที่ต้องย้ำก่อนเลย อย่าไล่ดูแค่ /etc/nginx ครับ
เครื่องที่รัน control panel พวก DirectAdmin/cPanel หรือใครที่มี include เยอะๆ config จริงที่ nginx โหลดกระจายอยู่คนละที่กับที่เราคิด เครื่องที่ผมเช็คนี่ไฟล์ที่โหลดจริง 95 ไฟล์ อยู่นอก /etc/nginx ไปตั้ง 76 ไฟล์ ถ้าไล่แต่ในนั้นก็พลาดของจริงไปเกือบหมด
ใช้ nginx -T ครับ มันคลี่ include ให้ครบทุกชั้น:
nginx -T > /tmp/nf.txt
wc -l /tmp/nf.txt
จากนั้นคำถามที่ตัดสินทุกอย่างคือ ใน config มีการอ้างถึง capture อยู่หรือเปล่า ถ้าไม่มีการอ้างถึง capture เลยแม้แต่ที่เดียว nginx ก็ไม่สร้าง opcode ที่บั๊กตัวนี้ต้องใช้ ต่อให้มี map เป็นสิบตัวก็ยิงไม่เข้า:
# อ้างถึง capture แบบตัวเลข
grep -cE '[$][1-9]' /tmp/nf.txt
# แบบใส่วงเล็บปีกกา
grep -cE '[$]\{[1-9]\}' /tmp/nf.txt
# named capture — อย่าลืมอันนี้เด็ดขาด
grep -cE '\(\?P?<[A-Za-z_]' /tmp/nf.txt
ถ้าสามอันนี้ออกมา 0 หมด แปลว่ายังไม่เข้าเงื่อนไข ต่อไปดูว่ามีอะไรที่ผลิต capture ได้บ้าง:
grep -cE '^\s*location\s+~\*?\s+.*\(' /tmp/nf.txt
grep -cE '^\s*server_name\s+.*~' /tmp/nf.txt
grep -cE '^\s*rewrite\s+.*\(' /tmp/nf.txt
grep -cE '^\s*if\s*\(.*~.*\(' /tmp/nf.txt
server_name ที่เป็น regex อย่าข้ามนะครับ คนทำโฮสติ้งชอบเขียนแบบ ~^(?<sub>.+)\.example\.com$ เพื่อรับ wildcard subdomain ซึ่งอันนั้นคือ named capture เต็มๆ
แล้วดูว่า map ตัวไหนถูกเอาไปใช้ตรงไหน:
grep -E '^\s*map\s+' /tmp/nf.txt | sed -E 's/\{.*//' | awk '{print $NF}' | sort -u
ได้ชื่อตัวแปรมาแล้วก็ไล่ดูว่ามันถูกใช้ใน block เดียวกับที่มี capture อ้างอยู่หรือเปล่า ย้ำอีกทีว่าดูทั้ง block ไม่ใช่ดูทีละบรรทัด
อ่านผลยังไง
ยังยิงไม่เข้า
ไม่มี $1 ไม่มี named capture ที่ไหนเลย = บั๊กเข้าไม่ถึง ต่อให้มี regex map อยู่ก็ตาม แต่นี่คือรอดเพราะ config ไม่ใช่รอดเพราะแพตช์ วันไหนแก้ config เพิ่ม capture เข้าไปก็ต้องกลับมาเช็คใหม่
ถือว่าเข้าเงื่อนไข
ถ้ามีทั้ง capture ที่ถูกอ้างถึง และ regex map ทำงานใน request เดียวกัน โดยที่ capture ถูกอ้างก่อน = ต้องรีบจัดการ อย่ามานั่งเถียงว่าลำดับมันจะตรงเป๊ะไหม
ของผมออกมาทางซ้าย คือ location ที่มีวงเล็บก็มีอยู่ แต่เป็นพวก location ~ /(\.htaccess|\.env|\.git) { deny all; } ซึ่งข้างในมีแต่ deny all; ไม่มีการเอา capture ไปต่อสตริงเลย วงเล็บสร้าง capture ก็จริง แต่ไม่มีใครอ้างถึงมัน เลยไม่เกิด buffer ที่วัดแล้วเขียน
แก้ยังไง
อัปเป็น 1.31.3 หรือ 1.30.4 ครับ สรุปคือนี่เป็นทางเดียวที่ปิดจบได้จริง
ส่วนวิธีบรรเทาที่ F5 แนะนำว่าให้เปลี่ยน regex map ไปใช้ named capture แทน ตรงนี้ผมว่าต้องระวังครับ เพราะนักวิจัยคนที่รายงานบั๊กเขาชี้ว่า named capture มันเป็นอีกเส้นทางที่ไปถึง overflow เดียวกันได้ และเป็นเส้นทางที่ advisory ของ F5 ไม่ได้พูดถึง สรุปคือมันลดความเสี่ยงลงได้แต่ไม่ได้ปิดสนิท อย่าเอาไปใช้แทนการอัปเดตแล้วนอนหลับสบาย
บางคนติดที่ control panel ยังไม่ปล่อยเวอร์ชันใหม่ให้ build ตาม ระหว่างรอก็ไล่เช็ค config ตามข้างบนไว้ก่อน ถ้าไม่มี capture ถูกอ้างถึงเลยก็พอหายใจได้ แต่ต้องล็อกไว้ในใจว่า ห้ามมีใครไปเพิ่ม capture เข้า config ระหว่างนี้ โดยไม่เช็คซ้ำ
เรื่องที่ต้องระวังต่ออีกอย่างคือพวก template ของ control panel ที่ยังไม่มีใครเปิดใช้ ของที่ผมเจอคือ template สำหรับ WordPress FastCGI cache กับ dokuwiki มี $1 อยู่ในนั้น แต่ตอนนี้ยังไม่มีโดเมนไหนเปิดใช้ เลยไม่โผล่ใน config ที่โหลดจริง วันไหนมีคนเปิด FastCGI cache ให้ลูกค้าสักราย capture ก็โผล่ขึ้นมาทันที ตรงนี้ต้องจำไว้
สรุปเป็นเช็คลิสต์
- เช็คเวอร์ชันก่อน
nginx -v— อยู่ในช่วง 0.9.6 ถึง 1.31.2 คือต้องอ่านต่อ - ใช้
nginx -Tเท่านั้น อย่าไล่แค่/etc/nginxเพราะ config จริงกระจายอยู่นอกนั้นเยอะ - คำถามตัดสินคือมีการอ้างถึง capture ไหม — เช็คทั้ง
$1,${1}และ named capture(?<n>) - อย่า grep หาบรรทัดที่มี capture กับ map อยู่ด้วยกัน มันคนละ directive ก็โดนได้ ต้องดูทั้ง block
- ไม่ได้มีแต่ map กับ proxy_set_header — มี 13 จุด รวม return, add_header, rewrite, set, fastcgi_param, index
- อัปเป็น 1.31.3 หรือ 1.30.4 คือทางเดียวที่จบจริง วิธีเปลี่ยนไป named capture ปิดไม่สนิท
- ไม่มี capture วันนี้ ไม่ได้แปลว่าไม่มีตลอดไป — แก้ config ทีไรกลับมาเช็คใหม่
ส่วนตัวผมได้บทเรียนจากรอบนี้ว่าเวลามี CVE ใหญ่ๆ ออกมา อย่าเช็คตามที่ข่าวสรุปอย่างเดียว ให้ตามไปอ่านของคนที่รายงานบั๊กจริงด้วย เพราะข่าวมันย่อให้อ่านง่ายแล้วรายละเอียดที่หายไปดันเป็นตัวที่ทำให้เราเช็คผิด รอบนี้ถ้าผมหยุดที่รอบแรกก็พลาดไปแล้ว
ใครจะเอาคำสั่งข้างบนไปทำเป็น script ไล่เช็คหลายเครื่องทีเดียวก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย