首页
社区
课程
招聘
[原创]扫描器程序卡住的bug复盘
发表于: 2021-5-15 14:57 840

[原创]扫描器程序卡住的bug复盘

2021-5-15 14:57
840

问题背景

之前线上的漏洞扫描遇到一个奇怪的问题:requests.get即使设置了timeout,仍然卡住。

 

看lijiejie大佬 requests.get 异常hang住 也碰到过这个问题。

 

所以,我想要探究以下问题:

  • requests库中timeout参数的具体含义是什么?
  • 为什么requests.get时timeout参数"失效"?

分析过程

requests库中timeout参数的具体含义是什么?

  • requests库中timeout参数是什么?

    根据官网文档所说,timeout可以表示(connect超时时间,read超时时间)

  • 什么是connect超时?

    客户端需要connect系统调用来和服务端做tcp三次握手,当服务地址在互联网上不存在时,connect系统调用耗时就会比较长。

    比如请求1.1.2.3 过一段时间后会返回一个connect tiemout:

    1
    python -c 'import requests;requests.get("http://1.1.2.3")'

    在上面请求1.1.2.3这个不存在的ip时,客户端发出的 syn 包没有任何响应,于是客户端会重传syn包

    • 重传次数在 /proc/sys/net/ipv4/tcp_syn_retries 可以配置
    • 重传间隔时间并不是固定的,在Linux系统上测试结果是 [1,3,7,15,31]s,似乎就是 2^(n+1)-1

    如果重试完后仍然没有收到ack包,就会出现connect timeout

    而request的timeout参数就可以减少这个等重传的时间。

    1
    python -c 'import requests;requests.get("http://1.1.2.3", timeout=(1, 100))' # 1s的connect超时设置
  • 怎么实现的connect超时控制?

    connect、read等系统调用是没有参数可以控制超时时间的,那connect超时控制是怎么实现的呢?

    在Modules/socketmodule.c可以找到connect函数的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    1. socket设置成非阻塞模式
     
    ...
    sock_call_ex(...,_PyTime_t timeout) {
      ...
      interval = timeout;
      ...
      res = internal_select(s, writing, interval, connect);
      ...
    }
     
    static int internal_select(PySocketSockObject *s, int writing, _PyTime_t interval,
                int connect){
      ...
      ms = _PyTime_AsMilliseconds(interval, _PyTime_ROUND_CEILING);
      ...
      n = poll(&pollfd, 1, (int)ms);  2. poll系统调用,如果超时,poll系统调用就会返回

    流程如下:

    1
    2
    * 设置socket为非阻塞模式后,调用connect系统调用
    * 使用poll系统调用来判断是否超时

    实际上这是一种很通用的对connect做超时控制的方式,在其他tcp客户端中也可以这么实现超时控制。

  • 什么是read超时?

    客户端需要调用read系统调用来读取服务端发送的数据,如果服务端一直不发送数据,读数据时就会卡住。

    比如我们用nc命令开启一个服务端只负责监听建立链接,不发送数据.

    1
    nc -l 8081

    客户端请求nc开启的服务,代码如下,3s后会出现读超时

    1
    2
    import requests
    requests.get("http://127.0.0.1:8081", timeout=(1,3)) # read超时时间设置成3s

为什么requests.get时timeout参数"失效"?

requests.get在 dns解析、connect、read 这些阶段都有可能耗时比较久。下面分别说一下timeout在这三个阶段中是否生效。

 

文档中只说了timeout控制connect、read两个阶段,说明dns解析耗时很久时timeout是管不了的。
我自己实验,也得出相同的结论:dns解析时间即使超过timeout,也不会抛出异常。

 

connect阶段在上面已经分析过,timeout是可以控制这一阶段最多花费多长时间的。

 

Python中的read超时不是一个全局的时间,它只是在每一次读socket时不能超过这个时间。而一次响应的读取可能有多次read操作。这儿可能和其他的http客户端(比如curl)等超时时间含义不同。

 

如果服务端能够让客户端read非常多次,且每一次时间都不超过read timeout值,这个时候客户端会卡住。

 

所以,在下面两种情况下是会造成read timeout参数“失效”的:

  • 响应中content-length是一个特别大的数,服务端缓慢的每次响应1字节
  • 服务端返回的响应码是100,同时服务端持续不断地返回响应头,也会导致客户端持续不断的read

    比如下面的服务端持续不断地返回响应头,会导致客户端卡住。
    ```

    coding:utf-8

    from socket import
    from multiprocessing import

    from time import sleep

    def dealWithClient(newSocket,destAddr):
    recvData = newSocket.recv(1024)
    newSocket.send(b"""HTTP/1.1 100 OK\n""")

    while True:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # recvData = newSocket.recv(1024)
    newSocket.send(b"""x:a\n""")
     
    if len(recvData)>0:
        # print('recv[%s]:%s'%(str(destAddr), recvData))
        pass
    else:
        print('[%s]close'%str(destAddr))
        sleep(10)
        print('over')
        break

    newSocket.close()

def main():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
localAddr = ('', 8085)
serSocket.bind(localAddr)
serSocket.listen(5)
 
try:
    while True:
        newSocket,destAddr = serSocket.accept()
 
        client = Process(target=dealWithClient, args=(newSocket,destAddr))
        client.start()
 
        newSocket.close()
finally:
    serSocket.close()

if name == 'main':
main()
```

 

更多的讨论可以见提交的bug urllib http client possible infinite loop on a 100 Continue response

总结

请求在 dns解析、connect、read 这些阶段都有可能耗时很久,其中:

  • dns解析阶段 不受timeout参数控制
  • connect阶段 受timeout参数控制
  • read阶段 timeout不是全局的,如果服务端让客户端有很多次read操作,就有可能让客户端卡住

阻塞时的connect系统调用是有默认的最大时间限制,这个和系统配置有关;可以用"非阻塞connect+select/poll"来实现connect的超时控制。

 

在排查这个case原因时,发现这里存在潜在的dos攻击问题,也上报给Python官方,很快被修复了。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//