如何設(shè)計(jì)實(shí)現(xiàn)一個(gè)輕量的開放API網(wǎng)關(guān)之重放攻擊及防御
文章地址: https://blog.piaoruiqing.com/blog/2019/08/11/開放api網(wǎng)關(guān)實(shí)踐之重放攻擊及防御/
前言
上一篇文章(開放API網(wǎng)關(guān)實(shí)踐(一)中的接口設(shè)計(jì)提到timestamp和nonce兩個(gè)參數(shù)的作用是用來防重放. 本文就重放攻擊及其防御進(jìn)行探討. 先拋出兩個(gè)問題:
- 什么是重放攻擊
- 如何防御重放攻擊
什么是重放攻擊(Replay Attacks)
什么是重放, 先舉個(gè)例子:
打開瀏覽器的調(diào)試工具并訪問一個(gè)網(wǎng)站, 在網(wǎng)絡(luò)工具中找到一個(gè)請(qǐng)求并右鍵選擇Replay. 如圖:

上述的重放操作是接口調(diào)試中比較常用的手段, 這種操作可以讓我們跳過認(rèn)證信息的生成過程, 直接重復(fù)發(fā)起多次有效的請(qǐng)求.
而重放攻擊是一種黑客常用的攻擊手段, 又稱重播攻擊、回放攻擊, 是指攻擊者發(fā)送目的主機(jī)已接收過的數(shù)據(jù), 以達(dá)到欺騙系統(tǒng)的目的, 主要用于身份認(rèn)證過程, 破壞認(rèn)證的正確性.
舉個(gè)易懂的例子:
- 服務(wù)端提供了打款接口, 用戶A向服務(wù)端請(qǐng)求發(fā)起一次打款5元的操作(附帶了簽名并進(jìn)行了加密), 服務(wù)端接收到了數(shù)據(jù)并正確打款給用戶B.
- 但這個(gè)請(qǐng)求被黑客攔截到(可能就是用戶B干的 ( ̄▽ ̄)"), 黑客將請(qǐng)求原封不動(dòng)地向服務(wù)器發(fā)送, 服務(wù)器多次錯(cuò)誤地打款給用戶B. (當(dāng)然, 這些都是是建立在服務(wù)端的付款沒做冪等等防范措施、安全級(jí)別較低的前提下)
- 盡管A發(fā)起的請(qǐng)求有簽名和加密, 但B無需破解這個(gè)數(shù)據(jù), 只是將
同樣的數(shù)據(jù)重復(fù)發(fā)給服務(wù)器就能達(dá)到欺騙的目的.

模擬重放攻擊
實(shí)驗(yàn)器材
| 序號(hào) | 名稱 | 數(shù)量 | 備注 |
|---|---|---|---|
| 1 | 服務(wù)器 | 2 | Mac: 10.33.30.101 - 真實(shí)服務(wù)器<br />Windows: 10.33.30.100 - 偽造服務(wù)器 |
| 2 | 域名 | 1 | replay-test.piaoruiqing.com (10.33.30.101) |
| 3 |
DNS服務(wù)器 |
1 | 用來模擬DNS劫持 |
實(shí)驗(yàn)步驟
- 啟動(dòng)服務(wù)器, 請(qǐng)求接口并收到響應(yīng)數(shù)據(jù).
- 劫持DNS(在路由器中修改DNS服務(wù)器地址模擬劫持), 并攔截請(qǐng)求數(shù)據(jù).
- 向服務(wù)器重復(fù)發(fā)送攔截到的數(shù)據(jù)(重放攻擊).
過程記錄
準(zhǔn)備工作
DNS配置, 將域名replay-test.piaoruiqing.com指向內(nèi)網(wǎng)中服務(wù)器的IP. 并啟動(dòng)服務(wù)器.

正常請(qǐng)求
使用postman發(fā)起一個(gè)正常的請(qǐng)求, 其中簽名已在Pre-request-script中生成.

通過DNS劫持來攔截?cái)?shù)據(jù)
修改內(nèi)網(wǎng)的dnsmasq配置, 將域名replay-test.piaoruiqing.com指向偽造的服務(wù)器10.33.30.100.


此時(shí)向replay-test.piaoruiqing.com發(fā)起的請(qǐng)求便會(huì)被發(fā)送到偽造的服務(wù)器上(10.33.30.100), 手動(dòng)將請(qǐng)求的數(shù)據(jù)保存下來. 由于請(qǐng)求帶有簽名, 且攻擊者并沒有拿到私鑰, 故無法篡改請(qǐng)求, 但可以進(jìn)行重放攻擊. 如圖, 偽造服務(wù)器已成功接收到請(qǐng)求數(shù)據(jù):

[版權(quán)聲明]
本文發(fā)布于樸瑞卿的博客, 允許非商業(yè)用途轉(zhuǎn)載, 但轉(zhuǎn)載必須保留原作者樸瑞卿 及鏈接:blog.piaoruiqing.com. 如有授權(quán)方面的協(xié)商或合作, 請(qǐng)聯(lián)系郵箱: piaoruiqing@gmail.com.
重放請(qǐng)求
使用上一步保存下來的數(shù)據(jù), 直接向真實(shí)服務(wù)器發(fā)送請(qǐng)求(帶有簽名數(shù)據(jù)). 如圖:

事實(shí)上, 簽名、加密等手段并不能防御重放攻擊, 因?yàn)楣粽邤r截到的數(shù)據(jù)已是正確的請(qǐng)求數(shù)據(jù), 即使無法破解其內(nèi)容, 也可以重放向服務(wù)器發(fā)送原數(shù)據(jù)以達(dá)到欺騙的目的.
如何防御重放攻擊
加隨機(jī)數(shù): 該方法優(yōu)點(diǎn)是認(rèn)證雙方不需要時(shí)間同步,雙方記住使用過的隨機(jī)數(shù), 如發(fā)現(xiàn)報(bào)文中有以前使用過的隨機(jī)數(shù), 就認(rèn)為是重放攻擊. 缺點(diǎn)是需要額外保存使用過的隨機(jī)數(shù), 若記錄的時(shí)間段較長, 則保存和查詢的開銷較大.加時(shí)間戳: 該方法優(yōu)點(diǎn)是不用額外保存其他信息. 缺點(diǎn)是認(rèn)證雙方需要準(zhǔn)確的時(shí)間同步, 同步越好, 受攻擊的可能性就越小. 但當(dāng)系統(tǒng)很龐大, 跨越的區(qū)域較廣時(shí), 要做到精確的時(shí)間同步并不是很容易.加流水號(hào): 就是雙方在報(bào)文中添加一個(gè)逐步遞增的整數(shù), 只要接收到一個(gè)不連續(xù)的流水號(hào)報(bào)文(太大或太小), 就認(rèn)定有重放威脅. 該方法優(yōu)點(diǎn)是不需要時(shí)間同步, 保存的信息量比隨機(jī)數(shù)方式小. 缺點(diǎn)是一旦攻擊者對(duì)報(bào)文解密成功, 就可以獲得流水號(hào), 從而每次將流水號(hào)遞增欺騙認(rèn)證端.
在實(shí)際使用中, 常將1和2結(jié)合使用, 時(shí)間戳有效期內(nèi)判斷隨機(jī)數(shù)是否已存在, 有效期外則直接丟棄.
重放攻擊防御實(shí)踐
我們采取時(shí)間戳+隨機(jī)數(shù)的方式來實(shí)現(xiàn)一個(gè)簡單的重放攻擊攔截器. 時(shí)間戳和隨機(jī)數(shù)互補(bǔ), 既能在時(shí)間有效范圍內(nèi)通過校驗(yàn)緩存中的隨機(jī)數(shù)是否存在來分辨是否為重放請(qǐng)求, 也能在緩存失效后(緩存有效時(shí)間和時(shí)間范圍一致)通過時(shí)間戳來校驗(yàn)該請(qǐng)求是否為重放. 如圖:

代碼如下:
@Resource
private ReactiveStringRedisTemplate reactiveStringRedisTemplate;
private ReactiveValueOperations<String, String> reactiveValueOperations;
@PostConstruct
public void postConstruct() {
reactiveValueOperations = reactiveStringRedisTemplate.opsForValue();
}
@Override
protected Mono<Void> doFilter(ServerWebExchange exchange, WebFilterChain chain) {
// 此處的`ATTRIBUTE_OPEN_API_REQUEST_BODY`是前面過濾器存入的
OpenApiRequest<String> body
= exchange.getRequiredAttribute(ATTRIBUTE_OPEN_API_REQUEST_BODY);
if (!ObjectUtils.allNotNull(body, body.getTimestamp(), body.getNonce())) {
return fail(exchange);
}
Long gmt = System.currentTimeMillis();
// (一)
if (gmt + effectiveTimeRange < body.getTimestamp() ||
gmt - effectiveTimeRange > body.getTimestamp()) {
return fail(exchange);
}
// (二)
return reactiveValueOperations.setIfAbsent(MessageFormat.format(
KEY_REPLAY_NONCE, body.getAppId(), body.getNonce()),
String.valueOf(System.currentTimeMillis()),
Duration.ofMillis(effectiveTimeRange * 2L))
.log(LOGGER, Level.FINE, true)
.flatMap(approved -> approved ?
chain.filter(exchange) : fail(FORBIDDEN, exchange)
);
-
(一): 請(qǐng)求時(shí)間超出時(shí)間范圍的將被拒絕. -
(二): 緩存過期時(shí)間等于有效時(shí)間的跨度, 若緩存中已存在該隨機(jī)數(shù), 則拒絕.
結(jié)語
重放攻擊防御的關(guān)鍵點(diǎn):
- 記錄請(qǐng)求標(biāo)識(shí)并緩存, 接受請(qǐng)求時(shí)校驗(yàn), 拒絕重放, 即將
nonce存入緩存, 拒絕相同的nonce - 隨機(jī)數(shù)的方式可能造成過多的緩存, 故需要配合時(shí)間戳進(jìn)行過濾, 時(shí)間戳不在有效范圍內(nèi)的一律拒絕.
重放攻擊是一種常用且有效的攻擊手段, 其危害不可忽視, 盡管可以通過業(yè)務(wù)層面來保障數(shù)據(jù)的正確性, 但依舊會(huì)給系統(tǒng)造成不必要開銷, 在網(wǎng)關(guān)層過濾掉重放請(qǐng)求是一個(gè)不錯(cuò)的選擇.
系列文章:
- 開放API網(wǎng)關(guān)實(shí)踐(一) ——設(shè)計(jì)一個(gè)API網(wǎng)關(guān)
- 開放API網(wǎng)關(guān)實(shí)踐(二) —— 重放攻擊及防御
- 開放API網(wǎng)關(guān)實(shí)踐(三) —— 限流
歡迎關(guān)注公眾號(hào): 代碼如詩
[版權(quán)聲明]
本文發(fā)布于樸瑞卿的博客, 允許非商業(yè)用途轉(zhuǎn)載, 但轉(zhuǎn)載必須保留原作者樸瑞卿 及鏈接:blog.piaoruiqing.com. 如有授權(quán)方面的協(xié)商或合作, 請(qǐng)聯(lián)系郵箱: piaoruiqing@gmail.com.