-
-
[原创]扫描器程序卡住的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:
1python
-
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参数就可以减少这个等重传的时间。
1python
-
c
'import requests;requests.get("http://1.1.2.3", timeout=(1, 100))'
# 1s的connect超时设置
怎么实现的connect超时控制?
connect、read等系统调用是没有参数可以控制超时时间的,那connect超时控制是怎么实现的呢?
在Modules/socketmodule.c可以找到connect函数的实现
12345678910111213141516171.
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系统调用就会返回
流程如下:
12*
设置socket为非阻塞模式后,调用connect系统调用
*
使用poll系统调用来判断是否超时
实际上这是一种很通用的对connect做超时控制的方式,在其他tcp客户端中也可以这么实现超时控制。
什么是read超时?
客户端需要调用read系统调用来读取服务端发送的数据,如果服务端一直不发送数据,读数据时就会卡住。
比如我们用nc命令开启一个服务端只负责监听建立链接,不发送数据.
1nc
-
l
8081
客户端请求nc开启的服务,代码如下,3s后会出现读超时
12import
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 sleepdef dealWithClient(newSocket,destAddr):
recvData = newSocket.recv(1024)
newSocket.send(b"""HTTP/1.1 100 OK\n""")while True:
1234567891011# 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官方,很快被修复了。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]CVE-2020-8558-跨主机访问127.0.0.1 8302
- "容器逃逸失败"案例分析 8449
- [原创]扫描器程序卡住的bug复盘 841
- [原创]Python栈溢出CVE-2021-3177分析 14276