วิเคราะห์โค้ดโจมตีช่องโหว่ Drupalgeddon2 (CVE-2018-7600)

สรุปย่อ

หลังจากโครงการ Drupal ประกาศพบช่องโหว่ร้ายแรงรหัส CVE-2018-7600 หรือ SA-CORE-2018-002 ซึ่งเป็นช่องโหว่ประเภท Remote Code Execution (RCE) ที่มีผลกระทบโดยตรงกับ Drupal เวอร์ชั่น 7.x, 8.3.x, 8.4.x และ 8.5.x เมื่อวันที่ 28 มีนาคมที่ผ่านมานั้น ในตอนนี้โค้ดสำหรับโจมตีช่องโหว่ดังกล่าวก็ได้มีการถูกเผยแพร่ออกสู่สาธารณะและถูกนำมาใช้ในการโจมตีจริงแล้ว ทีมผู้เชี่ยวชาญด้านความปลอดภัยบนเว็บแอปพลิเคชันจาก บริษัท ไอ-ซีเคียว จำกัด จึงจะขอนำช่องโหว่และโค้ดสำหรับโจมตีช่องโหว่มาอธิบายเพื่อสร้างความตระหนักรู้ซึ่งจะนำไปสู่การควบคุมและจัดการความเสี่ยงที่จะถูกโจมตีโดยช่องโหว่นี้ครับ

From Drupal Security Advisories (SA-CORE-2018-002)

Drupal เป็นโครงการซอฟต์แวร์แบบโอเพนซอร์สในรูปแบบของตัวจัดการเนื้อหาบนเว็บไซต์ (Content Management System - CMS) ซึ่งได้รับความนิยมและถูกใช้งานมากมายทั่วโลก สำหรับช่องโหว่ล่าสุดคือ Drupalgeddon2 นั้น ที่มาที่แท้จริงยังคงเกิดจากปัญหายอดนิยมคือประเด็นของการตรวจสอบข้อมูลนำเข้า (input) ที่ถูกส่งมายัง Form API ซึ่งไม่สมบูรณ์มากพอ ทำให้เกิดช่องโหว่ด้านความปลอดภัยที่ผู้ประสงค์ร้ายสามารถรันโค้ดที่เป็นอันตรายได้

ทำความรู้จัก Form API

Form API เป็น API รูปแบบหนึ่งใน Drupal ซึ่งมีการใช้งานรูปแบบการเก็บข้อมูลที่เรียกว่า Renderable Arrays โดยรูปแบบการเก็บข้อมูลชนิดนี้นั้นเป็นส่วนเสริมที่สร้างขึ้นมาเพื่อใช้ในการแสดงโครงสร้างรวมไปถึงส่วนประกอบต่าง ๆ ของ Drupal

ที่มา - https://www.slideshare.net/zyxware/learn-drupal-8-render-pipeline

ข้อมูลใน Renderable Arrays นั้นจะถูกเก็บอยู่ในรูปแบบของอาเรย์ที่มี key และ value ซึ่งจะถูกเรียกใช้ในแต่ละครั้งที่มีการพยายามแสดงผล (render) จาก API ข้อมูลในส่วนของ key ภายในอาเรย์นั้นจะถูกระบุโดยมีการใช้เครื่องหมาย # (hash) นำหน้าค่าของ key เสมอ

Form API นั้นมีการใช้งานโดยทั่วไปใน Drupal ในรูปแบบของฟอร์มสำหรับกรอกข้อมูล แต่สำหรับ Drupal ซึ่งพึ่งมีการติดตั้งใหม่นั้น ฟอร์มสำหรับกรอกข้อมูลจุดหนึ่งที่มีการใช้งาน Form API และมักจะถูกใช้งานโดยผู้ใช้เว็บไซต์เสมอนั้นคือฟอร์มสำหรับกรอกข้อมูลเพื่อสมัครสมาชิกในการเข้าสู่ระบบของเว็บไซต์

ด้วยลักษณะของฟอร์มกรอกข้อมูลที่สามารถเข้าถึงสาธารณะ ช่องโหว่ Drupalgeddon2 จึงอาศัยฟอร์มกรอกข้อมูลและปัญหาของการไม่ตรวจสอบข้อมูลนำเข้าที่ผู้ใช้งานส่งเข้าไปในรูปแบบ Renderable Arrays เพื่อควบคุมและสั่งการให้เกิดการประมวลผลที่ส่งผลกระทบต่อความปลอดภัยได้

การโจมตีช่องโหว่

อ้างอิงจากบทวิเคราะห์ช่องโหว่จาก CheckPoint และ Dofinity พร้อมโค้ดสำหรับโจมตีช่องโหว่ Drupalgeddon2 ซึ่งถูกเผยแพร่ออกมานั้น หนึ่งในตัวอย่างของการโจมตีช่องโหว่สามารถทำได้โดยผ่าน curl อ้างอิงจาก @IamSecurity ในรูปแบบดังนี้

$ curl -s -X 'POST' --data 'mail[%23post_render][]=exec&mail[%23children]=pwd&form_id=user_register_form' 'http://drupal.url/user/register?element_parents=account/mail/%23value&ajax_form=1'

จาก payload ของการโจมตีซึ่งใช้ curl ในการส่งนั้น HTTP request ซึ่งจะถูกส่งไปเว็บแอปพลิเคชันที่มีช่องโหว่จะปรากฎให้เห็นตามลักษณะด้านล่าง

POST /user/register?element_parents=account/mail/%23value&ajax_form=1 HTTP/1.1
Host: drupal.url
Content-Length: 76
Content-Type: application/x-www-form-urlencoded

mail[%23post_render][]=exec&mail[%23children]=pwd&form_id=user_register_form

สำหรับในตัวอย่างของการโจมตีที่พุ่งเป้าไปที่ Drupal เวอร์ชัน 8.5.0 นั้น การส่งข้อมูลในรูปแบบตามตัวอย่างด้านบนจะถูกประมวลผลโดยฟังก์ชัน uploadAjaxCallBack() ซึ่งอยู่ในไฟล์ core/modules/file/src/Element/ManagedFile.php ผ่านทางพารามิเตอร์ชื่อ element_parents โดยหลังจากที่รับค่าเข้ามาแล้ว ค่าดังกล่าวจะถูกแยกออกจากกันด้วยฟังก์ชัน explode() โดยใช้ / เป็น delimeter ก่อนจะถูกเก็บรวบรวมไว้ในตัวแปร $form_parents

เมื่อเราใช้ var_dump() กับ $form_parents เราจะเห็นลักษณะของการจัดเก็บข้อมูลแบบอาเรย์ตามลักษณะด้านล่าง

array(3) {
 [0]=>
 string(7) "account"
 [1]=>
 string(4) "mail"
 [2]=>
 string(6) "#value"
}

จากนั้นตัวแปร $form และ $form_parents ในฟังก์ชั่น uploadAjaxCallback() จะถูกส่งไปยังฟังก์ชั่น getValue() ในบรรทัดถัดมา เพื่อดึงค่าของ value ของ key ที่มีเครื่องหมาย # นำหน้าเป็นลำดับแรกมาเก็บไว้ในตัวแปร $form

เมื่อเราใช้ var_dump เพื่อแสดงค่าในลักษณะอาเรย์ที่ถูกเก็บในตัวแปร $form เมื่อผ่านฟังก์ชัน getValue() แล้วจะมีลักษณะด้านล่าง

array(2) {
 ["#post_render"]=>
 array(1) {
 [0]=>
 string(4) "exec"
 }
 ["#children"]=>
 string(3) "pwd"
}

ต่อมาค่าในลักษณะอาเรย์ที่ถูกเก็บอยู่ในตัวแปร $form จะถูกนำไปแสดงผล (render) โดยฟังก์ชัน renderRoot()

หากเราตามไปดูการทำงานของฟังก์ชัน renderRoot() ซึ่งอยู่ในไฟล์ core/lib/Drupal/Core/Render/Renderer.php ฟังก์ชัน renderRoot() ที่บรรทัด 129 จะมีการใช้ค่าในลักษณะอาเรย์ดังกล่าวโดยการส่งเป็นตัวแปรชื่อ $elements ไปยังฟังก์ชัน render()

เมื่อรับค่าเข้ามาแล้ว ฟังก์ชัน render() จะทำการส่งค่าต่อไปยังฟังก์ชัน doRender()

ภายในฟังก์ชัน doRender() นั้น เมื่อมีการรับค่าเข้ามาใส่ในตัวแปร $elements ฟังก์ชัน doRender() จะทำการตรวจสอบว่าภายในตัวแปร $elements มีการระบุค่า key ชื่อ #post_render ไว้หรือไม่ หากเงื่อนไขนี้เป็นจริง ($elements มี #post_render) ค่า value ซึ่งถูกอ้างอิงโดยใช้ key คือ #post_render จะถูกนำไปใช้ในฟังก์ชัน call_user_func() ที่ตัวแปร $callable เป็นอากิวเมนต์ที่ 1 และยังมีการส่งค่าใน key ชื่อ #children ไปเป็นอากิวเมนต์ที่ 2 ที่ตัวแปรต่อกันด้วย

ดังนั้นในกรณีที่มีการส่งข้อมูลเข้ามาตามที่ระบุไว้ในโค้ดสำหรับโจมตีช่องโหว่ ผลลัพธ์ของการ var_dump ตัวแปร $callable และ $elements['#children'] จะมีลักษณะดังนี้

// var_dump($callable)
string(4) "exec"

//var_dump($elements['#children']
string(3) "pwd"

ดังนั้นในบรรทัดที่ 505 ซึ่งอยู่ภายใต้ฟังก์ชัน doRender() ฟังก์ชัน call_user_func() จะถูกเรียกโดยมีอากิวเมนต์ที่แท้จริงในลักษณะนี้

call_user_func('exec', 'pwd', $elements);

ซึ่งทำให้ได้ผลลัพธ์เป็นการรันฟังก์ชัน exec() ในภาษา PHP เพื่อรันคำสั่ง pwd พร้อมด้วย $elements ตามตัวอย่างการโจมตีที่สำเร็จซึ่งทำให้เกิดการรันโค้ดที่เป็นอันตรายได้จากระยะไกล (Remote Code Execution - RCE) ด้านล่าง

Attack Vector อื่นๆ

เมื่อตรวจสอบลักษณะการทำงานของฟังก์ชัน doRender() ซึ่งเกิดการรันโค้ดที่เป็นอันตรายแล้วโดยใช้ #post_render เป็น key ในการรันคำสั่งแล้ว ยังมีเงื่อนไขอีกหลายกรณีที่สามารถเปลี่ยนค่าของ key เป็นค่าอื่นแต่ได้ผลลัพธ์ในการรันโค้ดที่เป็นอันตรายได้ตามตัวอย่างด้านล่าง

ค่า key "#access_callback" ที่เรียกไปยัง call_user_func() เช่นเดียวกับ #post_render

ค่า key "#pre_render" ที่เรียกไปยัง call_user_func() เช่นเดียวกับ #post_render

ค่า key "#lazy_builder" ที่เรียกไปยัง call_user_func_array() เช่นเดียวกับ #post_render

รุ่นของซอฟต์แวร์ที่ได้รับผลกระทบ

  • Drupal 8.x ก่อนหน้ารุ่น 8.5.1
  • Drupal 7.x ก่อนหน้ารุ่น 7.58
  • Drupal 8.3.x ก่อนหน้ารุ่น 8.3.9
  • Drupal 8.4.x ก่อนหน้ารุ่น 8.4.6

คำแนะนำในการตอบสนองต่อช่องโหว่

  • สำหรับ Drupal ในรุ่น 8.x ให้ทำการอัปเดตเวอร์ชันของ Drupal ไปยังเวอร์ชัน 8.5.1 ที่มีการแพตช์ช่องโหว่ดังกล่าวแล้วทันที อย่างไรก็ตามในกรณีที่ไม่สามารถอัปเดตได้ ทีมนักพัฒนาของ Drupal ได้แนะนำให้ทำการแพตช์เฉพาะเพื่อป้องกันการโจมตีช่องโหว่ดังกล่าวไว้ก่อน
  • สำหรับ Drupal ในรุ่น 7.x ให้ทำการอัปเดตเวอร์ชันของ Drupal ไปยังเวอร์ชัน 7.58 ที่มีการแพตช์ช่องโหว่ดังกล่าวแล้วทันที อย่างไรก็ตามในกรณีที่ไม่สามารถอัปเดตได้ ทีมนักพัฒนาของ Drupal ได้แนะนำให้ทำการแพตช์เฉพาะเพื่อป้องกันการโจมตีช่องโหว่ดังกล่าวไว้ก่อน
  • สำหรับ Drupal ในรุ่น 8.3.x และรุ่น 8.4.x ซึ่งปัจจุบันไม่ได้รับการซัพพอร์ตแพตช์ใหม่ๆ แล้ว ทีมนักพัฒนา Drupal ได้ทำการออกเวอร์ชันใหม่สำหรับ 2 รุ่นนี้เป็นกรณีพิเศษโดยสำหรับรุ่น 8.3.x ให้ทำการอัปเดตเป็น 8.3.9 หรือใช้แพตช์เฉพาะ ส่วนสำหรับรุ่น 8.4.x ให้ทำการอัปเดตเป็น 8.4.6 หรือใช้แพตช์เฉพาะ

วิเคราะห์และเรียบเรียงโดย Kitwipat Towattana