From the public account: New World Grocery store

Write it up front

“Flowers are the same from year to year, and people are different from year to year.” Nothing is permanent and a lot of things will be in the past. For example, I used to call myself “the author” in the article, but if you think about it carefully, there is still a certain distance. After some careful consideration, I plan to change the self-proclaimed “Lao Xu” in the article.

As for self-claim, Lao Xu will not go too far, but let’s return to the main idea of this article.

What is the SSRF

SSRF Stands for Server Side Request Forgery. When the attacker fails to obtain the server permission, he sends a constructed request to the Intranet where the server resides by using the server vulnerability. The access control of Intranet resources must be well known.

If the above statement is difficult to understand, then Lao Xu directly give a practical example. Many writing platforms support uploading images through urls. If the URL verification is not strict, malicious attackers can access Intranet resources.

As a Chinese saying goes, “a thousand miles’ dam is broken by a thousand miles’ nest”, we programmers should not ignore any vulnerability that may cause risks, and it is likely that such vulnerability will become a stepping stone for others’ performance. In order not to become a stepping stone, hsu will take a look at the SSRF offensive and defensive rounds with you.

Turn 1: Ever-changing Intranet addresses

Why the word “kaleidoscope”? Lao Xu will not answer, please be patient to read down. Here, Mr. Xu uses 182.61.200.7 (an IP address at www.baidu.com) to review the different representations of IPv4.

Note ⚠️ : in the dot mix system, each part can be written in a different base (10, 8, and hexadecimal only) divided by dots.

The above are just different representations of IPv4. There are also three different representations of IPv6 addresses. And these three representations can be written differently. The following uses the IPv6 loopback address 0:0:0:0:0:0:1 as an example.

Note ⚠️ : the leading 0 of each X in the decimal notation can be omitted, so I can partially omit and partially omit to write a different representation of an IPv6 address. The 0-bit compressed representation and the embedded IPv4 address representation can also represent an IPv6 address in different ways.

Speaking so much, Lao Xu has been unable to count how many different ways an IP can be written, trouble math good calculation.

Intranet IP. You think that’s the end of it? Of course not! I don’t know if you’ve heard of xip. IO. Xip can help you do custom DNS resolution, and can resolve to any IP address (including Intranet).

We can use the domain name resolution provided by XIP to access Intranet IP addresses by domain name.

Access to Intranet IP will continue here! As anyone who has done Basic validation knows, you can access resources at http://user:passwd@hostname/. An attacker might be able to bypass some of the less rigorous logic if he wrote it differently, as shown below.

About the Intranet address, Old Xu emptied all knowledge reserves summed up the above content, so old Xu said that the ever-changing Intranet address is not too much!

At this point, Lao Xu just wants to ask how you can identify and deny access to photos uploaded by malicious attackers using Intranet addresses with different manifestations. There will not really be regular expressions to complete the above filtering, if you have, please tell me to let my younger brother learn.

Now that we have a basic understanding of the various Intranet addresses, the problem is how to convert them into an IP that we can judge. The above Intranet addresses can be classified into three types: 1. They are IP addresses themselves but in different forms. 2. A domain name pointing to the Intranet IP address. An address containing Basic authentication information and an Intranet IP address. According to the three characteristics, you can perform the following steps to identify the Intranet address and reject the access before sending a request.

  1. Resolve HostName in the address.
  2. Initiate DNS resolution and obtain the IP address.
  3. Check whether the IP address is an Intranet address.

In the preceding steps, do not ignore the IPv6 loopback address and the unique IPv6 local address. The following is the logic for Xu to determine whether the IP address is an Intranet IP address.

// IsLocalIP Check whether it is an Intranet IP address
func IsLocalIP(ip net.IP) bool {
	if ip == nil {
		return false
	}
	// Check whether it is a loopback address (127.0.0.1 for ipv4). Ipv6 is: : 1
	if ip.IsLoopback() {
		return true
	}
	// Check whether ipv4 is an Intranet
	ifip4 := ip.To4(); ip4 ! =nil {
		return ip4[0] = =10 || / / 10.0.0.0/8
			(ip4[0] = =172 && ip4[1] > =16 && ip4[1] < =31) | |/ / along / 12
			(ip4[0] = =192 && ip4[1] = =168) / / 192.168.0.0/16
	}
	// Check whether ipv6 is an Intranet
	ifip16 := ip.To16(); ip16 ! =nil {
		/ / reference https://tools.ietf.org/html/rfc4193#section-3
		/ / reference https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
		// Check the unique ipv6 local address
		return 0xfd == ip16[0]}// Not IP directly returns false
	return false
}

Copy the code

The following figure shows the result of following the preceding steps to check whether the request is an Intranet request.

Summary: There are various TYPES of urls. You can use DNS to obtain a standard IP address to determine whether the URL is an Intranet resource.

Round 2: URL redirect

If a malicious attacker were only using different ways of writing IP, we would be safe, but this battle of swords and shields has just begun.

If an attacker accesses Intranet resources through URL redirection during a request, the defense policy in Turn 1 can be completely bypassed. The attack flow is as follows.

As shown in the figure, an attacker can obtain Intranet resources through URL redirect. Before introducing how to defend against URL jump attack, Old Xu and you readers first review the HTTP redirection status code – 3xx.

According to Wikipedia, there are nine 3XX redirection codes ranging from 300 to 308. Xu took a look at go’s source code and found that the official HTTP. Client only supports the following redirection codes.

301: The requested resource has been permanently moved to a new location; The response is cacheable; The redirect request must be a GET request.

302: Request client to perform temporary redirection; The response is cacheable only if specified in cache-Control or Expires; The redirect request must be a GET request.

303: This code is used when the response to a POST (or PUT/DELETE) request can be found in another URI. This code exists primarily to allow the output of a POST request activated by the script to be redirected to a new resource; 303 Responses are not cached. The redirect request must be a GET request.

307: temporary redirection; The request method cannot be changed. If the original request is POST, the redirected request is ALSO POST.

308: Permanent redirection; The request method cannot be changed. If the original request is POST, the redirected request is ALSO POST.

This concludes the review of the 3XX status code, and we continue the discussion of SSRF’s offensive and defensive rounds. Since URL hops on the server may bring risks, we can completely avoid such risks by simply disabling URL hops. However, we can’t do this, because it’s risky to do this in the same way as it is to harm a normal request. So how do you protect against such attacks?

You can customize the CheckRedirect from http.Client if you have a business need for redirects. Let’s look at the definition of CheckRedirect.

CheckRedirect func(req *Request, via []*Request) error
Copy the code

As a special note, REQ is the request to be made and contains the response to the previous request, and VIA is the request already made. Knowing these conditions makes it easy to defend against URL jump attacks.

  1. Reject directly based on the response to the previous request307and308(Such jumps can be POST requests, which are extremely risky).
  2. Resolve the requested IP address and check whether it is an Intranet IP address.

Following the above steps, you can define http.client as follows.

client := &http.Client{
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		// Jump more than 10 times, also refuse to continue the jump
		if len(via) >= 10 {
			return fmt.Errorf("redirect too much")
		}
		statusCode := req.Response.StatusCode
		if statusCode == 307 || statusCode == 308 {
			// Reject jump access
			return fmt.Errorf("unsupport redirect method")}/ / determine IP
		ips, err := net.LookupIP(req.URL.Host)
		iferr ! =nil {
			return err
		}
		for _, ip := range ips {
			if IsLocalIP(ip) {
				return fmt.Errorf("have local ip")
			}
			fmt.Printf("%s -> %s is localip? : %v\n", req.URL, ip.String(), IsLocalIP(ip))
		}
		return nil}},Copy the code

The self-defined CheckRedirect can defend against URL redirect attacks, but this method is inefficient because multiple DNS resolution is performed. This section describes more effective defense measures combined with other attack modes.

Summary: You can customize the CheckRedirect of http.Client to defend against URL redirect attacks.

Round 3: DNS Rebinding

As is known to all, to initiate an HTTP request, you must first request the DNS service to obtain the IP address corresponding to the domain name. If the attacker has DNS services under control, DNS rebinding can be used to bypass the previous defense policy.

The specific process is shown in the figure below.

When verifying whether the resource is valid, the server performs the first DNS resolution and obtains a non-intranet IP address with a TTL of 0. Check the resolved IP addresses and find that non-intranet IP addresses can be requested later. Because the TTL of the attacker’s DNS Server is set to 0, DNS resolution needs to be performed again when the attacker formally initiates a request. In this case, the DNS Server returns the Intranet address. The attacker can obtain Intranet resources because the DNS Server has entered the resource request phase and no defense measures are taken.

As an extra mention, Lao Xu looked at some source code of DNS resolution in Go and found that Go does not cache DNS results, so there is a risk of DNS rebinding even if the TTL is not 0.

DNS resolution in the process of making the request gives the attacker an opportunity. If we can control this process, we can avoid the risk of DNS rebinding. HTTP request control can be achieved by customizing HTTP.Transport, and there are two options for customizing HTTP.

Solution a:

dialer := &net.Dialer{}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
	host, port, err := net.SplitHostPort(addr)
	// Resolve host and port
	iferr ! =nil {
		return nil, err
	}
	// DNS resolves domain names
	ips, err := net.LookupIP(host)
	iferr ! =nil {
		return nil, err
	}
	// make requests to all serial IP addresses
	for _, ip := range ips {
		fmt.Printf("%v -> %v is localip? : %v\n", addr, ip.String(), IsLocalIP(ip))
		if IsLocalIP(ip) {
			continue
		}
		// Access can continue for non-intranet IP addresses
		// Splice the address
		addr := net.JoinHostPort(ip.String(), port)
		// Addr contains only IP and port information
		con, err := dialer.DialContext(ctx, network, addr)
		if err == nil {
			return con, nil
		}
		fmt.Println(err)
	}

	return nil, fmt.Errorf("connect failed")}// Use this client request to avoid DNS rebinding risk
client := &http.Client{
	Transport: transport,
}
Copy the code

Transport.DialContext is used to create unencrypted TCP connections. You can customize this function to avoid DNS rebinding risks. In addition, if the address passed to dialer.DialContext is a conventional IP address, the parseIPZone function in the NET package can be directly resolved successfully. Otherwise, the DNS resolution request will continue to be initiated.

Scheme 2:

dialer := &net.Dialer{}
dialer.Control = func(network, address string, c syscall.RawConn) error {
    // Address is already in IP :port format
	host, _, err := net.SplitHostPort(address)
	iferr ! =nil {
		return err
	}
	fmt.Printf("%v is localip? : %v\n", address, IsLocalIP(net.ParseIP(host)))
	return nil
}
transport := http.DefaultTransport.(*http.Transport).Clone()
// Create a TCP connection using an implementation of the official library
transport.DialContext = dialer.DialContext
// Use this client request to avoid DNS rebinding risk
client := &http.Client{
	Transport: transport,
}
Copy the code

Dialer. Control is called after a network connection is created and before the actual dial is made, and is only available if go is 1.11 or greater. The call location is in the (*netFD).dial method in sock_posix.go.

The two defense schemes can not only defend against DNS rebinding attacks, but also defend against other attacks. In fact, Xu recommended plan two, once and for all!

Summary:

  1. An attacker can use its own DNS service to perform DNS rebinding attacks.
  2. Through customizationhttp.TransportYou can defend against DNS rebinding attacks.

Personal experience

1. Don’t send detailed error messages! Do not send detailed error messages! Do not send detailed error messages!

If it is for development and debugging, enter the error information in the log file. This is emphasized not only to guard against SSRF attacks, but also to avoid the leakage of sensitive information. For example, if a DB operation fails, the error message is sent directly, and the error message may contain SQL statements.

As an added bonus, Xu’s company is very strict about desensitizing some of the information entered into the log file.

2. Limit the request port.

Before I close, the SSRF vulnerability is not specific to HTTP. This article focuses only on HTTP because go detects the protocol type when making requests through http.Client, which is much weaker in some P*P languages. Although http.Client detects protocol types, an attacker can constantly change ports to detect Intranet ports.

Finally, I sincerely hope that this article can be of some help to all readers.

Note:

  1. When writing this article, the author used go version: GO 1.15.2
  2. Full example used in the article: github.com/Isites/go-c…