Credits
Sick.Codes
Github: (https://github.com/sickcodes)
Twitter: (https://twitter.com/sickcodes)
John
Github: (https://github.com/johnjhacking)
Nick Sahler
Github: (https://github.com/nicksahler)
Twitter: (https://twitter.com/tensor_bodega)
With Collaboration from:
Harold Hunt
LinkedIn: (https://www.linkedin.com/in/huntharo)
Identification
Over the course of several months, John dealt with remediating a Server-Side Request Forgery vulnerability, several times. A researcher was able to bypass blocking mechanisms, therefore John decided to dig in further to get to the root cause of the vulnerability.
The researcher used the following payload to bypass restrictions:
http://0000.0000.0000.0000:<redactedport#>/<redacted>/<redacted>
Security researchers know that using multiple zeroes is a classic way to bypass localhost blocking when abusing SSRF vulnerabilities. Additionally, it was noted that other variations of 0’s can be used within the 127.0.0.1 and 0.0.0.0 payloads. However, the fundamental issue was a result of localhost skewing, which made it clear to John that the blocking mechanism wasn’t accounting for the variations.
After talking to other colleagues, John got in touch with Harold Hunt who helped him identify the package responsible for the IP blocking mechanism, private-ip.
The code logic was utilizing simple Regular Expression, therefore not accounting for variations of localhost, and other private-ip ranges, as predicted. John and Harold realized that it would be exceedingly difficult to come up with simple regex to cover all of the possibilities, therefore John contacted Nick Sahler and Sick.Codes for their Software Engineering expertise.
Impact
The npm private-ip package has an average of 14,000 downloads weekly - even though the package is roughly four years old. Using the out-of-date version can result in continuous SSRF bypass vulnerabilities, and threat actors can intentionally extract sensitive information and escalate their privileges further. Be advised, Enterprises utilizing this package as a means of preventing SSRF or related vulnerabilities, should upgrade to the latest version immediately.
Broken Logic
The old code was evaluated further, this was the prevention mechanism being used in the index.js:
/^(::f{4}:)?10\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})$/i.test(ip) ||
/^(::f{4}:)?192\\.168\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})$/i.test(ip) ||
/^(::f{4}:)?172\\.(1\[6-9\]|2\\d|30|31)\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})$/i.test(ip) ||
/^(::f{4}:)?127\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})$/i.test(ip) ||
/^(::f{4}:)?169\\.254\\.(\[0-9\]{1,3})\\.(\[0-9\]{1,3})$/i.test(ip) ||
/^f\[cd\]\[0-9a-f\]{2}:/i.test(ip) ||
/^fe80:/i.test(ip) ||
/^::1$/.test(ip) ||
/^::$/.test(ip)
There are already several issues within the regex. It’s not comprehensive enough to cover the localhost variations, nor many of the industry standard private IP ranges. Nick and Sick.Codes utilized netmask and rewrote the index.js:
var Netmask = require('netmask').Netmask
function netmaskCheck (params) {
let privateRanges = [
'0.0.0.0/8',
'10.0.0.0/8',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.0.0.0/24',
'192.0.0.0/29',
'192.0.0.8/32',
'192.0.0.9/32',
'192.0.0.10/32',
'192.0.0.170/32',
'192.0.0.171/32',
'192.0.2.0/24',
'192.31.196.0/24',
'192.52.193.0/24',
'192.88.99.0/24',
'192.168.0.0/16',
'192.175.48.0/24',
'198.18.0.0/15',
'198.51.100.0/24',
'203.0.113.0/24',
'240.0.0.0/4',
'255.255.255.255/32'
].map(b => new Netmask(b))
for (let r of privateRanges) {
if (r.contains(params)) return true
}
return false
}
Several other files were modified, but this key-function helped enable the private-ip package to account for the private-ip address ranges and variations, on the byte level. Hackers attempting to bypass SSRF vulnerabilities will now have an exceedingly difficult time because even payloads encoded into hexadecimal, etc, will be recognized as the IP address as if it were not encoded, triggering a conditional block.
Further Analysis and Proofs of Concept
The following Proofs of Concept are by SickCodes and Nick Sahler
Follow Sick.Codes on Twitter: @SickCodes
Follow Nick Sahler on Twitter: @tensor_bodega
Global Impact of CVE-2020-28360
As the time of discovery, there were:
- 12,120 direct weekly downloads of private-ip
- 355 publicly identified npm dependents of private-ip v1.0.5; packages that rely on private-ip. -- https://github.com/sickcodes/security/raw/master/etc/CVE-2020-28360-private-ip-dependents.txt
- 73 GitHub projects that depend on private-ip https://github.com/frenchbread/private-ip/network/dependents
- 153,374 combined weekly downloads of all dependents, with the largest being libp2p related. -- https://github.com/sickcodes/security/raw/master/etc/CVE-2020-28360-weekly-downloads.csv
PoC - Using patched private-ip 2.0.0 on tests from 1.0.5
Sick Codes used the ARIN IPv4 Address Space Registry: https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xhtml
|
|
Running patched private-ip 2.0.0 code against private-ip 1.0.5 test IPs:
- 17 passed
- 2 failed
The test ARIN reserved (private) IPs that failed are:
- 255.38.207.121
- 250.29.143.180
This indicates that since August 3rd 2016, 2 of the original test IP addresses have actually been reserved addresses.
PoC - Using unpatched private-ip 1.0.5 on new tests from 2.0.0
The new test includes the minimum and maximum of every ipv4 range that is ARIN reserved.
|
|
Using the 1.0.5 RegEx with 2.0.0 test.js
Running private-ip 1.0.5 code against the minimum and maximum of private IP ranges in 2.0.0 test.js:
- 37 passed
- 71 failed
This indicates that since August 3rd 2016, a large number of reserved IP ranges have been considered public IP addresses.
An application that relies on private-ip 1.0.5 and below to verify whether an incoming request is to localhost, or a private resource, may perform a request to an local IP address, or an IP address that is reserved, potentially resulting in an SSRF.
Since since August 3rd 2016 the following IP’s, and all of the IP addresses between them, have returned results for private-ip.
The following tests in the updated test.js indicate that these were incorrectly designated as public IPs:
0.0.0.0
0.0.0.1
0.0.0.255
0.0.0.7
0.0.255.255
0.1.255.255
0.15.255.255
0.255.255.254
0.255.255.255
0.63.255.255
100.127.255.254
100.127.255.255
100.64.0.0
100.64.0.1
192.0.0.0
192.0.0.1
192.0.0.10
192.0.0.11
192.0.0.170
192.0.0.171
192.0.0.254
192.0.0.255
192.0.0.6
192.0.0.7
192.0.0.8
192.0.0.9
192.0.2.0
192.0.2.1
192.0.2.254
192.0.2.255
192.175.48.0
192.175.48.1
192.175.48.254
192.175.48.255
192.31.196.0
192.31.196.1
192.31.196.254
192.31.196.255
192.52.193.0
192.52.193.1
192.52.193.254
192.52.193.255
192.88.99.0
192.88.99.1
192.88.99.254
192.88.99.255
198.18.0.0
198.18.0.1
198.19.255.254
198.19.255.255
198.51.100.0
198.51.100.1
198.51.100.254
198.51.100.255
203.0.113.0
203.0.113.1
203.0.113.254
203.0.113.255
240.0.0.0
240.0.0.1
255.0.0.0
255.192.0.0
255.240.0.0
255.254.0.0
255.255.0.0
255.255.255.0
255.255.255.248
255.255.255.254
255.255.255.255
0000.0000.0000.0000
All of the above IPs can result in SSRF in private-ip v1.0.5.
As this is a server-side package, it is difficult to ascertain the exact magnitude of use, as there are an incalculable number of server-side projects that use private-ip internally.
You can find Nick & I (Sick.Codes)’s pull request to fix the private-ip package here: https://github.com/frenchbread/private-ip/pull/2
References
https://www.npmjs.com/package/private-ip
https://johnjhacking.com/blog/cve-2020-28360
https://github.com/sickcodes/security/blob/master/advisories/SICK-2020-022.md
https://sick.codes/sick-2020-022
https://twitter.com/johnjhacking
https://www.linkedin.com/in/huntharo
https://twitter.com/sickcodes
https://twitter.com/tensor_bodega
CVE Links
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28360
https://nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-28360