Filters and Bypasses - Rare IPv4 Formats for SSRF

5 minute read Published:

A short description of the many ways there are to write down an IP address, along with an online IP address encoder that will create alternative representations for any IP you enter. Useful for filter bypasses when testing for SSRF.
Table of Contents

Many applications these days do network requests on behalf of their users. For example, it is often possible to define webhooks, where users can specify URLs to which a server shall send HTTP requests when certain events occur. Other applications may allow uploading images by URL or they may generate PDFs from HTML rendered server-side, which could contain URLs (see here). In all these examples, users provide input to specify the destinations of server-side network requests.

Filters are required to prevent access to internal systems. Attackers may otherwise abuse the feature to access resources that are supposed to be inaccessible. They may attempt to access services local to the server (127.0.0.1), other services within internal networks (e.g., 10.0.0.1) or instance metadata of cloud hosts on link-local addresses (e.g., EC2 instance metadata at 169.254.169.254). Thus, the server must validate the input to ensure only an expected range of IP addresses will ever be contacted.

IP validations can sometimes be bypassed if rare, exotic representations of IP addresses are supplied as input. Not all parsers will properly support rare IP formats. If one parser is used during IP address validation and another when making the actual network request, then if the two disagree it might be that the validator does not block a string that is parsed as a dangerous IP later on.

Such problems appear in popular software from time to time (e.g., GitLab). There are also plenty of CVEs for IP parsing libraries describing failures to parse exotic representations correctly (see, e.g., CVE-2021-2941 for Node’s netmask package, CVE-2021-29921 for Python’s ipaddress package, CVE-2021-29922 for Rust’s parser or CVE-2021-29923 for Go’s net parsers). Lots of reasons to thoroughly test IP address validators if their purpose is to keep people out of your private network ranges. But what are those rare IP formats?

A good starting point is RFC 3986, which describes the syntax of a Uniform Resource Identifier (URI). Besides defining the format everyone is used to, section 7 (security considerations) contains some notes on exotic formats. Find them here. You may also find the man page of inet_aton interesting.

Below, you find a discussion of the various IPv4 address formats I’m aware of. If you know all of it and just want to get list of them for testing then go to the online IP encoder instead. It will generate a number of alternative formats for any IPv4 you enter.

Starting point: Dotted-decimal form

Lets start with defining the normal IPv4 format, just for completeness. It is a sequence of four decimal numbers called octets, separated by dots. Octets are 8-bit numbers. That is, they must be in the range of 0 and 255. An example is 127.0.0.1. This is what everybody knows and is used to.

Hexadecimal and octal octets

Octets don’t have to be decimal numbers. You can also write them in hexadecimal and octal form. This is achieved in the following ways:

  • Hexadecimal: prefix the octet with 0x or 0X
  • Octal: prefix the octet with 0

Use the input box below to try it out. Type in your IP and it will give you the hexadecimal and octal equivalents of it. Use them with a tool such as ping or curl to convince yourself that are in fact all the same.

IP Calculator
In dotted-decimal format, e.g., 127.0.0.1

You can even mix all representations in a single IP address. For example, you can write 8.8.8.8 as 0x8.0X8.010.8, where the first two octets are hexadecimal, the third one is octal and the fourth is decimal. Curl and many other programs are perfectly able to parse that:

user@notebook:~$ curl -v https://0x8.0X8.010.8 2>&1 | grep 'Connected to'
* Connected to 8.8.8.8 (8.8.8.8) port 443 (#0)

If you like, you can insert as many zeros as you want in between the prefixes and the hexadecimal/octal numbers. For example:

  • write the octal octet 010 as 0010, 00010, …
  • write the hexadecimal octet 0x8 as 0x08, 0x008, …

To see that in action, ping 8.8.8.8 in the following way:

user@notebook:~$ ping 0x0000000000000008.0X8.00000000000000000000010.8
PING 0x0000000000000008.0X8.00000000000000000000010.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: icmp_seq=0 ttl=112 time=15.293 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=112 time=14.347 ms

Less than three dots

You don’t need exactly four octets to write an IPv4 address. Rather, you can use at most four numbers. If you want to use fewer, you have to combine the octets from right to left to a single, bigger number. Best explained using an example.

Say you want to write the IP address 1.2.3.4 with two dots and three numbers. This is what you have to do: Leave the first two octets 1.2 untouched. Now combine the last two octets 3.4 to one number. Written as 8-bit binary numbers, octet 3 is 00000011 and octet 4 is 00000100 Combined to a 16-bit number, you get 0000001100000100, which is 772 in decimal form. So you can write the IP 1.2.3.4 also as 1.2.772.

This actually works all the way until only one 32-bit number is left. So you can write 1.2.3.4 as 1.2.772, 1.131844 or just 16909060.

Again, use the following input box to type your IP and see all the different representations below:

IP Calculator
In dotted-decimal format, e.g., 127.0.0.1

Combining the two variations

You probably expect it at this point, but it shall be mentioned explicitly for completeness: of course you can also use hexadecimal or octal numbers when you use less than four numbers. For example, to write 8.8.8.8, you can use three numbers all in different formats: 0x8.010.2056. Convince yourself by pinging it:

user@notebook:~$ ping 0x8.010.2056
PING 0x8.010.2056 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: icmp_seq=0 ttl=112 time=12.784 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=112 time=21.280 ms