How to Bypass Golang SSL Verification

July 15, 2024 Michael Pasternak

Safe Encrypted Connection on Internet

Golang applications that use HTTPS requests have a built-in SSL verification feature enabled by default. In our work, we often encounter an application that uses Golang HTTPS requests, and we have to examine the requests in plain text to find security flaws and bugs.

Usually, we lack the application’s source code, and without debug symbols, it becomes much more complex to change the binary to allow us to intercept the HTTP requests.

In this blog post, we will explore the Golang core net/http library more deeply to understand how to remove the SSL verification manually or using a short Python script.

TL;DR

We will get into Golang SSL verification and explore simple patching methods to bypass it.

How Did It All Start?

We begin with a Golang application, which examines HTTPS requests in plain text.

We tried using a tool like “Burp Suite” (or any other preferred proxy tool) by setting the HTTPS_PROXY environment variable. However, we encountered an error when trying this method:

Trying to proxy an app

Figure 1: Trying to  proxy an app

We pondered the situation and considered adding the Burp certificate to our computer’s CA store, assuming it would resolve the “unknown authority” certificate error.

However, adding the burp suite cert into the computer CA didn’t work because Golang does not rely on the computer’s CA store and verifies every certificate itself.

We thought about performing MITM (man in the middle) attacks on the Golang apps and concluded that it would be difficult because of the self-verification.

Usually, in network libraries and HTTP handling, the programmer can disable SSL verification by changing the config or adding flags in the HTTP handler. We thought that might be the case here, too.

To disable SSL verification, we found a parameter in the config called “InsecureSkipVerify” with the default value set to false. To disable SSL Verification, you can add the code below (code snippet 1) to the app. However, in our case, we worked on a compiled app and needed to modify it on the disk since it had already been built.

Method 1:

 

http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
 _, err := http.Get("https://golang.org/")


Method 2:

 

tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}
    _, err := client.Get("https://golang.org/")

Although the “InsecureSkipVerify” flag is precisely what we need, we faced a challenge because our application was pre-compiled, and we couldn’t access the source code. It cannot be recompiled with the flag enabled, so we needed a different approach to tackle this problem.

Deep Into Golang Source Code

Our next objective was to find where in the program binary the flag “InsecureSkipVerify” was being used and patch it.

Although we could attempt to understand the application’s binary format and assembly code, this was unnecessary. Instead, we referred to the net/http source code.

By searching through the Golang codebase for the “InsecureSkipVerify” flag, we discovered it was used in the file “crypto/tls/handshake_client.go.”

 

func (c *Conn) verifyServerCertificate(certificates [][]byte) error {
	activeHandles := make([]*activeCert, len(certificates))
	certs := make([]*x509.Certificate, len(certificates))
	for i, asn1Data := range certificates {
		cert, err := globalCertCache.newCert(asn1Data)
		if err != nil {
			c.sendAlert(alertBadCertificate)
			return errors.New("tls: failed to parse certificate from server: " + err.Error())
		}
		activeHandles[i] = cert
		certs[i] = cert.cert
	}

	if !c.config.InsecureSkipVerify {
		opts := x509.VerifyOptions{
			Roots:         c.config.RootCAs,
			CurrentTime:   c.config.time(),
			DNSName:       c.config.ServerName,
			Intermediates: x509.NewCertPool(),
		}

		for _, cert := range certs[1:] {
			opts.Intermediates.AddCert(cert)
		}
		var err error
		c.verifiedChains, err = certs[0].Verify(opts)
		if err != nil {
			c.sendAlert(alertBadCertificate)
			return &CertificateVerificationError{UnverifiedCertificates: certs, Err: err}
		}
	}

In this file, we found a function called “verifyServerCertificate” (code snippet 3). After examining it, it became evident that if the flag is set to true, the server certificate verification is bypassed. Therefore, we can bypass the check certificate part by merely patching the “if” statement or the assembly opcode.

Demo App

To demonstrate what we found, we wrote a simple Golang code that creates a GET request to ipinfo.io and prints the output.

 

package main

import (
   "io/ioutil"
   "log"
   "net/http"
)

func main() {
   resp, err := http.Get("https://ipinfo.io/")
   if err != nil {
      log.Fatalln(err)
   }
   
	//We Read the response body on the line below.
   body, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Fatalln(err)
   }
   
   //Convert the body to type string
   sb := string(body)
   log.Printf(sb)
}


Usually, programmers choose to strip their apps from debug symbols to remove unnecessary information about their apps, such as strings and function names.

As you can see in the code (Code snippet 3), we didn’t configure any “special” flags or settings and relied on Golang’s default configuration.

For the sake of this blog, we won’t strip the binary, making reverse engineering the app easier.

Reversing the App

Before we examine the assembly code, let’s look at the source code of “verifyServerCertificate.

verifyServerCertificate

Figure 2: Source code of verifyServerCertificate

As you can see in Figure 2, we can divide the code into three sections by their flow:

Red. Checks if the server is in the cache

Green. Checks if the InsecureSkipVerify is set

Blue. Checks public key verifications and Peer Certificate

The second part is of interest because of the usage of the flag InsecureSkipVerify.

Let’s search the function by name because we didn’t remove the debug symbols from the binary:

IDA function search

Figure 3: IDA function search

Let’s see how the “graph view” (Figure 4) looks:

IDA graph view

Figure 4: IDA graph view

As we examine the IDA graph (Figure 4) and look at the loops inside the binary (light blue arrows), we can see that it looks similar to the source code (Figure 2). Now, we can divide the source code into three sections.

We can that entering the second section is affected by the flag “InsecureSkipVerify.”

Figure 4 shows the margin area between the red and green sections, where the OPCODEs are responsible for checking whether we are following the flow or jumping straight to the blue section.

IDA graph view

Figure 5: IDA graph view of the if statement

From our analysis, we can deduce that the “if” statement involves the two OP-CODEs, cmp and jnz.

Patching the Program

Based on our findings, it can be assumed that the jnz opcode controls whether or not the program enters the second section of the code. We wanted to reverse the condition to “bypass” the if statement and then jump straight into the third (Figure 5).

X86 opcodes

Figure 6: X86 opcodes

Referring to the opcode table, we determined that only one byte needs to be changed, specifically from 85 to 84. This alteration will transform the opcode from jnz to jz, which represents “jump if zero.”

Bytes before the patch

Figure 7: Bytes before the patch

To patch the program, we can right-click and select “edit.” Then, we can modify the byte at position 85 to become 84.

Bytes after the patch

Figure 8: Bytes after the patch

Afterward, right-click again and choose “Apply changes.” Consequently, the program changes from jnz to jz.

Graph view after the patch

Figure 9: Graph view after the patch

To apply the patches to the input program, navigate to the toolbar, click on “Edit,” select “Patch program,” and click “Apply patches to input file…” When prompted, you can choose to preserve an original copy of the program.

IDA patching menu

Figure 10: IDA patching menu

That’s it! Your program no longer has SSL verification. Let’s verify this.

Pathed Program Output

Figure 11: Pathed program output

Now, let’s examine our Burp Suite proxy.

Burp view of the proxied HTTPS request

Figure 12: Burp view of the proxied HTTPS request

We are victorious! We can observe the request successfully passing through.

Patching by Python Script

For convenience, we created a Python script that searches for the cmp and jnz instructions and replaces them with jz instructions. Here is the code of the script:

 

#!/usr/bin/env python3
import subprocess
import argparse

supported_versions_to_bytes = {
        '11': [b"\x00\x0F\x85\xB3\x04\x00\x00", b"\x00\x0F\x84\xB3\x04\x00\x00"],
        '12': [b"\x00\x00\x0F\x85\x43\x05\x00\x00", b"\x00\x00\x0F\x84\x43\x05\x00\x00"],
        '13': [b"\x00\x00\x0F\x85\x32\x05\x00\x00", b"\x00\x00\x0F\x84\x32\x05\x00\x00"],
        '14': [b"\x00\x00\x0F\x85\x48\x05\x00\x00", b"\x00\x00\x0F\x84\x48\x05\x00\x00"],
        '15': [b"\x00\x00\x0F\x85\x3A\x06\x00\x00", b"\x00\x00\x0F\x84\x3A\x06\x00\x00"],
        '16': [b"\x00\x00\x0F\x85\x5A\x06\x00\x00", b"\x00\x00\x0F\x84\x5A\x06\x00\x00"],
        '17': [b"\x00\x00\x0F\x85\x7F\x01\x00\x00", b"\x00\x00\x0F\x84\x7F\x01\x00\x00"],
        '18': [b"\x00\x00\x0F\x85\x7C\x01\x00\x00", b"\x00\x00\x0f\x84\x7C\x01\x00\x00"],
        '19': [b"\x00\x00\x0F\x85\x7B\x01\x00\x00", b"\x00\x00\x0f\x84\x7B\x01\x00\x00"],
        '20': [b"\x00\x00\x0F\x85\x84\x01\x00\x00", b"\x00\x00\x0F\x84\x84\x01\x00\x00"],
        '21': [b"\x00\x00\x0F\x85\x82\x01\x00\x00", b"\x00\x00\x0F\x84\x82\x01\x00\x00"]
}


def replace_file_bytes(file_path, old_bytes, new_bytes):
    with open(file_path, 'rb') as f:
        data = f.read()
        position = data.find(old_bytes)
    
    if(-1 == position):
        raise Exception("cannot find bytes, maybe the program is already patched?")

    with open(file_path, 'rb+') as file:
        file.seek(position)
        existing_bytes = file.read(len(old_bytes))
            
        if existing_bytes == old_bytes:
            file.seek(position)
            file.write(new_bytes)
			
def run_command(command):
    result = subprocess.run(command, shell=True, capture_output=True, text=True)
    return result.stdout.strip()
			
def get_go_bin_version(filename):
    output = run_command(f"strings {filename} | grep '^go1' | head -n 1")
    if "" == output:
        output = run_command(f"strings {filename} | grep 'Go cmd/compile'  | head -n 1 | cut -d' ' -f 3")
        if "" == output:
            output = run_command(f"strings {filename} | grep -Eo 'go[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n 1")
    return output

def get_args():
    parser = argparse.ArgumentParser(description='Get a filename and patches it ssl verification check')
    parser.add_argument("-f", "--filename", help='File to patch', required=True)
    parser.add_argument("-v", "--version", help='Input version of Golang app')
    parser.add_argument("-g", "--get-version", help='tries to get the app Golang version', action='store_true')
    return parser.parse_args()

def main():
    args = get_args()
    version = get_go_bin_version(args.filename).split('.')[1]
    if args.get_version:
        print("Assuming that the Golang version is: %s" % version)
        return
    if args.version:
        version = args.version
    old_bytes = supported_versions_to_bytes[version][0]
    new_bytes = supported_versions_to_bytes[version][1]
    replace_file_bytes(args.filename, old_bytes, new_bytes)

if "__main__" == __name__:
    main()

Understanding Source Code to Patch Vulnerabilities

Our goal was to remove the SSL verification from the pre-compiled Golang so that we could check for vulnerabilities in the code. We did this by understanding the source code and flow of the net/http library in Golang and analyzing it to better understand where we needed to patch the binary. This doesn’t require you to be proficient with low-level programming; it only requires common sense.

We could also apply this method to bypasses in other apps/languages and get more comfortable patching other binaries for fun.

 

Michael Pasternak is a cyber research team leader at CyberArk Labs.

Previous Article
Identity Crisis: The Curious Case of a Delinea Local Privilege Escalation Vulnerability
Identity Crisis: The Curious Case of a Delinea Local Privilege Escalation Vulnerability

During a recent customer engagement, the CyberArk Red Team discovered and exploited an Elevation of Privile...

Next Article
What ‘Passwordless’ Really Means for Privileged Access Management
What ‘Passwordless’ Really Means for Privileged Access Management

Privileged access management (PAM) programs aim to secure the highest-risk access in an organization, inclu...