一个脚本绕过了Go语言SSL验证

admin 2024年7月18日15:06:31评论58 views字数 6359阅读21分11秒阅读模式
摘要

使用 HTTPS 请求的 Golang 应用程序默认启用内置的 SSL 验证功能。在工作中, 经常遇到使用Golang HTTPS请求的应用程序,我们必须检查纯文本的请求,以发现安全缺陷和错误。在本文中,将更深入地探索 Golang核心 net/http 库, 以了解如何手动或使用简短的Python脚本删除 SSL 验证。

技术预研

首先尝试通过设置 HTTPS_PROXY 环境变量来使用“Burp Suite”(或任何其他首选代理工具)等工具。然而,在尝试这个方法时遇到了错误:

一个脚本绕过了Go语言SSL验证

针对这种情况, 先考虑将 Burp证书添加到本地计算机的 CA 存储中,假设它可以解决“未知权威”证书错误。但是,将 burp suite 证书添加到计算机 CA 中不起作用,因为 Golang 不依赖计算机的 CA 存储并自行验证每个证书。后来又考虑过对 Golang 应用程序进行 MITM(中间人)攻击,并得出结论,由于自我验证,这会很困难。

通常,在网络库和 HTTP 处理中,程序员可以通过更改配置或在 HTTP 处理程序中添加标志来禁用 SSL 验证。这里假设也可能是这种情况。

为了禁用 SSL 验证,在配置中找到了一个名为“InsecureSkipVerify”的参数,其默认值设置为 false。要禁用 SSL 验证,可以将一些预设的代码片段添加到应用程序中。然而,对于一个已经编译过的二进制程序, 需要通过一些其它方式在磁盘中进行修改。

下面是两种禁用SSL验证的代码片段:

方法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”的函数。经过检查,很明显,如果该标志设置为 true,则服务器证书验证将被绕过。因此,只需修补“if”语句或汇编操作码即可绕过检查证书部分。

Demo程序开发

为了演示这种方法是否凑效,下面编写了一个简单的 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)
}

通常,程序员选择从调试符号中剥离应用程序,以删除有关应用程序的不必要的信息,例如字符串和函数名称。这里没有配置任何“特殊”标志或设置,而是依赖于 Golang 的默认配置。而接下来的操作, 也不会剥离二进制文件, 这样会让后面的逆向工程更容易一些。

二进制逆向分析

在检查汇编代码之前,先看一下“verifyServerCertificate”的源代码。

一个脚本绕过了Go语言SSL验证

可以按流程将代码分为三个部分:

  • 红色部分: 检查服务器是否在缓存中。
  • 绿色部分: 检查是否设置了 InsacuseSkipVerify
  • 蓝色部分: 检查公钥验证和对等证书

其中,第二个部分比较显眼, 因为使用了标志 InsacuseSkipVerify。

因为没有从二进制文件中删除调试符号, 所以这里可以先按名称搜索函数:

一个脚本绕过了Go语言SSL验证

看看“图表视图”的样子:

一个脚本绕过了Go语言SSL验证

查看上图二进制文件内的循环(浅蓝色箭头)时,可以看到它看起来与源代码类似。现在,可以将源代码分为三个部分。可以看到, 进入第二部分受到“InsacuseSkipVerify”标志的影响。在红色和绿色之间的边缘区域, OPCODE 负责检查是遵循流程还是直接跳到蓝色部分, 如图:

一个脚本绕过了Go语言SSL验证

经过分析可以推断出“if”语句涉及两个操作码:cmp 和 jnz。

编写补丁程序

根据上面的分析发现,可以假设 jnz 操作码控制程序是否进入代码的第二部分。尝试反转条件以“绕过”if 语句,然后直接跳到第三个, 整理了一个操作码表:

一个脚本绕过了Go语言SSL验证

通过上面的操作码表格, 可以确定只需要更改一个字节,将 85 更改为 84。这一更改会将操作码从 jnz 转换为 jz,表示“如果为零则跳转”。下图是打补丁之前的字节:

一个脚本绕过了Go语言SSL验证

要修补程序,可以右键单击并选择“编辑”。然后将位置85处的字节修改为84。下图是修改之后的字节:

一个脚本绕过了Go语言SSL验证

然后,再次右键单击并选择“应用更改”。因此,程序从 jnz 更改为 jz。如下图:

一个脚本绕过了Go语言SSL验证

要将补丁应用到输入程序,导航到工具栏,单击“编辑”,选择“补丁程序”,然后单击“将补丁应用到输入文件...”出现提示时,便可以选择保留程序的原始副本。如图:

一个脚本绕过了Go语言SSL验证

现在,该程序不再具有SSL验证, 下面验证一下:

一个脚本绕过了Go语言SSL验证

下面检查一下Burp Suite代理,如图:

一个脚本绕过了Go语言SSL验证

成功了, 可以看到请求成功通过。

用Python编写补丁脚本

为了方便,这里创建了一个 Python 脚本来搜索 cmp 和 jnz 指令并将其替换为 jz 指令。脚本代码如下:

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

supported_versions_to_bytes = {
        '11': [b"x00x0Fx85xB3x04x00x00", b"x00x0Fx84xB3x04x00x00"],
        '12': [b"x00x00x0Fx85x43x05x00x00", b"x00x00x0Fx84x43x05x00x00"],
        '13': [b"x00x00x0Fx85x32x05x00x00", b"x00x00x0Fx84x32x05x00x00"],
        '14': [b"x00x00x0Fx85x48x05x00x00", b"x00x00x0Fx84x48x05x00x00"],
        '15': [b"x00x00x0Fx85x3Ax06x00x00", b"x00x00x0Fx84x3Ax06x00x00"],
        '16': [b"x00x00x0Fx85x5Ax06x00x00", b"x00x00x0Fx84x5Ax06x00x00"],
        '17': [b"x00x00x0Fx85x7Fx01x00x00", b"x00x00x0Fx84x7Fx01x00x00"],
        '18': [b"x00x00x0Fx85x7Cx01x00x00", b"x00x00x0fx84x7Cx01x00x00"],
        '19': [b"x00x00x0Fx85x7Bx01x00x00", b"x00x00x0fx84x7Bx01x00x00"],
        '20': [b"x00x00x0Fx85x84x01x00x00", b"x00x00x0Fx84x84x01x00x00"],
        '21': [b"x00x00x0Fx85x82x01x00x00", b"x00x00x0Fx84x82x01x00x00"]
}


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()

参考链接:

https://www.cyberark.com/resources/threat-research-blog/how-to-bypass-golang-ssl-verification

原文始发于微信公众号(二进制空间安全):一个脚本绕过了Go语言SSL验证

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月18日15:06:31
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   一个脚本绕过了Go语言SSL验证https://cn-sec.com/archives/2966398.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息