使用 HTTPS 请求的 Golang 应用程序默认启用了内置的 SSL 验证功能。在工作中,我们经常会遇到使用 Golang HTTPS 请求的应用程序,我们需要以纯文本形式检查请求以查找安全漏洞和错误。
通常,我们缺少应用程序的源代码,并且没有调试符号,更改二进制文件以允许我们拦截 HTTP 请求变得更加复杂。
在这篇博文中,我们将更深入地探讨 Golang 核心 net/http 库,以了解如何手动或使用简短的 Python 脚本删除 SSL 验证。
开始
我们从一个 Golang 应用程序开始,它以纯文本形式检查 HTTPS 请求。
我们尝试使用“Burp Suite”之类的工具(或任何其他首选代理工具)来设置 HTTPS_PROXY 环境变量。但是,我们在尝试此方法时遇到了错误:
图 1:尝试代理应用程序
我们仔细考虑了这种情况,并考虑将 Burp 证书添加到我们计算机的 CA 存储中,假设它可以解决“未知颁发机构”证书错误。
但是,将 burp suite 证书添加到计算机 CA 中不起作用,因为 Golang 不依赖于计算机的 CA 存储并自行验证每个证书。
我们考虑对 Golang 应用程序执行 MITM(中间人)攻击,并得出结论,由于自我验证,这会很困难。
通常,在网络库和 HTTP 处理中,程序员可以通过更改配置或在 HTTP 处理程序中添加标志来禁用 SSL 验证。我们认为这里也可能是这种情况。
要禁用 SSL 验证,我们在配置中找到了一个名为“ InsecureSkipVerify ”的参数,其默认值设置为 false。要禁用 SSL 验证,您可以将以下代码(代码片段 1)添加到应用程序。但是,在我们的例子中,我们使用的是已编译的应用程序,并且需要在磁盘上对其进行修改,因为它已经构建好了。
方法 1:
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
_, err := http.Get("https://golang.org/")
方法 2:
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
尽管“ InsecureSkipVerify ”标志正是我们所需要的,但我们仍面临挑战,因为我们的应用程序是预编译的,我们无法访问源代码。启用该标志后无法重新编译,因此我们需要采用不同的方法来解决这个问题。
深入 Golang 源代码
我们的下一个目标是找到程序二进制文件中使用标志“ InsecureSkipVerify”的位置并对其进行修补。
虽然我们可以尝试理解应用程序的二进制格式和汇编代码,但这是没有必要的。相反,我们参考了net/http源代码。
通过在 Golang 代码库中搜索“InsecureSkipVerify”标志,我们发现它在文件“ 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}
}
}
在这个文件中,我们发现了一个名为“verifyServerCertificate”的函数(代码片段3)。检查后发现,如果将标志设置为true,则可以绕过服务器证书验证。因此,我们只需修补“if”语句或汇编操作码即可绕过检查证书部分。
演示应用程序
为了演示我们的发现,我们编写了一个简单的 Golang 代码,向 ipinfo.io 创建一个 GET 请求并打印输出。
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)
}
通常,程序员选择从调试符号中剥离他们的应用程序,以删除有关其应用程序的不必要的信息,例如字符串和函数名称。
正如您在代码中看到的(代码片段 3),我们没有配置任何“特殊”标志或设置,并且依赖于 Golang 的默认配置。
为了本博客的目的,我们不会剥离二进制文件,以便更容易地对应用程序进行逆向工程。
逆向应用程序
在检查汇编代码之前,我们先来看一下“ verifyServerCertificate ”的源代码。
图2:verifyServerCertificate的源代码
如图 2 所示,我们可以根据代码流程将代码分为三个部分:
红色。检查服务器是否在缓存中
绿色。检查是否设置了InsecureSkipVerify
蓝色。检查公钥验证和对等证书
第二部分很有趣,因为使用了标志InsecureSkipVerify**。
让我们按名称搜索该函数,因为我们没有从二进制文件中删除调试符号:
图3:IDA函数搜索
让我们看看“图形视图”(图 4)是什么样子的:
图 4:IDA 图形视图
当我们检查 IDA 图(图 4)并查看二进制文件内的循环(浅蓝色箭头)时,我们可以看到它看起来与源代码(图 2)类似。现在,我们可以将源代码分为三个部分。
我们可以看到进入第二部分受到标志“ InsecureSkipVerify ”的影响。
图 4 显示了红色和绿色部分之间的边缘区域,其中 OPCODE 负责检查我们是否遵循流程还是直接跳转到蓝色部分。
图 5:if 语句的 IDA 图形视图
通过分析,我们可以推断出“if”语句涉及两个OP-CODE,cmp和jnz。
修补程序
根据我们的发现,可以假设jnz操作码控制程序是否进入代码的第二部分。我们希望反转条件以“绕过” if 语句,然后直接跳转到第三部分(图 5)。
图 6:X86 操作码
参照操作码表,我们确定只需要更改一个字节,具体来说就是从85更改为84。此更改将操作码从jnz更改为jz,表示“若为零则跳转”。
图 7:补丁前的字节数
为了修补该程序,我们可以右键单击并选择“编辑”。然后,我们可以将位置 85 的字节修改为 84。
图 8:补丁后的字节数
然后,再次右键单击并选择“应用更改”。因此,程序从jnz更改为jz。
图 9:补丁后的图表视图
要将补丁应用到输入程序,请导航到工具栏,单击“编辑”,选择“补丁程序”,然后单击“将补丁应用到输入文件...”出现提示时,您可以选择保留程序的原始副本。
图 10:IDA 修补菜单
就是这样!你的程序不再具有 SSL 验证。让我们验证一下。
图 11:Pathed 程序输出
现在,让我们检查我们的 Burp Suite 代理。
图 12:代理 HTTPS 请求的 Burp 视图
我们胜利了!我们可以观察到请求已成功通过。
通过 Python 脚本进行修补
为了方便起见,我们创建了一个 Python 脚本,用于搜索 cmp 和 jnz 指令并将其替换为 jz 指令。以下是该脚本的代码:
#!/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()
了解源代码以修补漏洞
我们的目标是从预编译的 Golang 中删除 SSL 验证,以便我们可以检查代码中的漏洞。我们通过了解 Golang 中 net/http 库的源代码和流程并对其进行分析来更好地了解我们需要修补二进制文件的位置。这不需要您精通低级编程;它只需要常识。
我们还可以将此方法应用于其他应用程序/语言中的绕过方法,并更轻松地修补其他二进制文件以获得乐趣。