python3 SSLCertVerificationError 研究

Please Call me 小强 / 2024-09-27 / 原文

WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)'))': /simple/frida-tools/
WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)'))': /simple/frida-tools/
WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)'))': /simple/frida-tools/
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)'))': /simple/frida-tools/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)'))': /simple/frida-tools/
Could not fetch URL https://pypi.org/simple/frida-tools/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/frida-tools/ (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)'))) - skippin

python3 ssl验证出错,因为开启了https代理, (启动了charles工具)

我知道怎么解决这个问题, 就是关闭代理工具。但是我想探探它怎么验证的因为浏览器可以

1.  写了一个nodejs代码做测试, 这个是http, 直接请求, charles并没有抓到包

const axios = require('axios');

async function requestBaidu() {
    try {
        const response = await axios.get('http://www.baidu.com');
        console.log('Status Code:', response.status);
        console.log('Response Headers:', response.headers);
        console.log('Response Data:', response.data.substring(0, 100)); // 输出前100个字符
    } catch (error) {
        console.error('Error requesting Baidu:', error);
    }
}

requestBaidu();

我发现了在命令行设置: export HTTP_PROXY="http://127.0.0.1:8888",  再次运行就可以抓到了。

2. 继续探路, 用python试试

import requests

# 发起请求
response = requests.get('http://www.baidu.com')

# 打印响应内容
print(response.text)

执行结果非常好,Charles能抓包到,

通过1和2,说明了python3默认是走代理的!

3.  将1和2的代码,全部改成https

在设置了HTTP(S)_PROXY的终端分别执行js和python脚本:  js可以正常访问,能抓包。  python不能正常访问,能抓包

在没有设置HTTP(S)_PROXY的终端分别执行js和python脚本:  js可以正常访问,没抓到包。  python不能正常访问,能抓包

通过这些测试发现了一点东西,python默认走系统代码,并且验证比nodejs严格。   nodejs默认不走代理,如果设置了环境变量代理,相当于和浏览器一样了。

4.  这个结论比较有趣, 不过至少可以证明任何程序,走不走代理是app应用自己说了算! 下面的python代码,证明了这个结论:

import http.client

# 创建连接
connection = http.client.HTTPConnection('baidu.com')

# 发送 GET 请求
connection.request('GET', '/')  # 你可以根据需要更改请求路径

# 获取响应
response = connection.getresponse()

# 输出状态和内容
print(f'Status: {response.status}, Reason: {response.reason}')
print(response.read().decode())

# 关闭连接
connection.close()

使用最原始的请求, 不管有没有设置http_proxy, 它都不会走代理。对它进行改造

我通过断点跟踪,发现requests库默认环境变量和系统代理。 优先获取代理环境变量,然后在获取系统代理。 

def getproxies():
  return getproxies_environment() or getproxies_macosx_sysconf()

nodejs axios 仅仅只是获取代理环境变量.

5.  那么现在就只有一个问题: 都走代理的情况下, 为啥nodejs可以,python不行

开启逆向之旅: 

错误信息的关键: self-signed certificate

hopper载入: 

/usr/local/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so

 没有搜索到,那么查看这个so的依赖:发现了

 

/usr/local/opt/openssl@3/lib/libssl.3.dylib

 

 

 hopper 载入它:

 还是没有, 我确定它是openssl报出来的, 那么就全局查找吧, 发现了:

 居然在libcrypto.3.dylib里面, 简直不敢相信, 好吧,继续跟踪它:

 继续跟踪,看下它的触发条件,发现没有任何地方引用它...  那么就在网上拉去openssl源码吧, 它是开源的:

https://openssl-library.org/source/index.html

 错误编号:X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN

python里有一个验证选项,如果为true,那么

:param verify: (optional) Either a boolean, in which case it controls whether we verify
the server's TLS certificate, or a string, in which case it must be a path
to a CA bundle to use. Defaults to ``True``.
 
 
@verify_mode.setter
def verify_mode(self, value: ssl.VerifyMode) -> None:
self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback)
 
这个_ctx.set_verify方法 实际调用的是:openssl库里的 SSL_CTX_set_verify方法
mode模式有这么几个值, 默认是SSL_VERIFY_PEER, 这就是
# define SSL_VERIFY_NONE 0x00
# define SSL_VERIFY_PEER 0x01
# define SSL_VERIFY_FAIL_IF_NO_PEER_CERT 0x02
# define SSL_VERIFY_CLIENT_ONCE 0x04
# define SSL_VERIFY_POST_HANDSHAKE 0x08
 
如果不指定道verify参数,那么就是CERT_REQUIRED.    

 

python3 也就是会默认使用VERIFY_PEER 模式,这个模式就是会让openssl库进行证书检查!   看了nodejs相关源码, https默认的模式是VERIFY_NONE, 所以不会进行任何报错!

 

解决方案,证书验证的时候

1.  requests.get("httpsUrl", verify=False)

2. 控制台 export REQUESTS_CA_BUNDLE=/path/charles-ssl-proxying-certificate.pem

或者export CURL_CA_BUNDLE=/path/charles-ssl-proxying-certificate.pem

3. requests.get('https://example.com', cert=('path/to/client.crt', 'path/to/client.key'), verify=cert_path)
 
总之请求域名要和证书能匹配上否则一律报错。  
到这里,还是心有不甘心, 为啥需要我手动指定证书, 我明明已经安装到钥匙串里,并且已经信任证书了。 有时间继续!断点调试