Redis被攻擊


記一次Redis被攻擊的事件

 

最近幾個月非常忙,所以很少有時間寫博客,這幾天終於閑了一些,於是就在整理平時的一些筆記。恰好這幾天Redis服務器發生了問題,就記錄一下。

我司有兩款分別是2B和2C的App,類似於阿里旺旺的賣家版和買家版,里面有一個聊天的功能模塊。雙方可以通過這個功能聊天。內部通訊使用了環信,只是將本地賬號和環信賬號進行了關聯。其他的信息,比如用戶基本信息,好友關系,群組關系等存在Redis中,為防止Redis出現問題導致數據丟失(盡管配置了持久化),同時使用消息隊列將數據寫入SQLServer中進行了冗余。這是一種Redis的典型使用場景,從速度和效率上滿足要求。

線上環境一直運行正常,但是在上周日(一個本該休息的日子),領導打電話過來說線上環境的用戶登錄不了,無法聊天,沒有群相關信息。我想估計是Redis出現了問題,讓領導不要着急,先讓運維看看服務是否還在運行,不行的話,把Redis重啟一下,因為之前設置過持久化,數據應該不會丟失。於是繼續陪夫人吃飯,看電影。

到了晚上,還是打電話過來說有問題,沒辦法只有來公司一趟。打開機器用RedisClient連Linux環境上的Redis,發現里面的數據全沒了,只有幾個新注冊的孤零零的用戶在里面,老用戶全沒了,我當時驚呆了,以為是運維那邊沒搞好,是重啟的整個服務器而不是重啟的Redis,可能是Redis沒有及時保存把數據給弄丟了。看到現象之后跟領導電話告知目前的現象和建議的解決方案,在授權下,重新把用戶相關數據從SQLServer同步到了Redis中,關鍵的數據還好沒丟失,然后讓測試簡單測試了一下,一切正常就沒有太在意。而且由於Redis是安裝在Linux上的,是由運維同事維護的,出問題了我這邊也查不了,於是就回去了。

但是事情遠沒有那么簡單,星期一的時候,領導又打電話過來說聊天又不能用了,比較急說要趕緊處理,我說好,於是不緊不慢的收拾好出門去公司。心里非常不爽,前段時間加班太猛,周一周二全公司開發都調休放假的。就我一個開發的來到公司之后,打開遠程又發現Redis里面的數據全沒了。於是又從SQLServer把數據同步到了Redis里,然后檢查代碼,把除了和聊天相關之外的其他邏輯,比如Redis定時同步服務相關的可能會影響到的地方都暫停了。因為前段時間做了一次重構,擔心是代碼導致數據丟失,搞完了之后就回去了。

然而好景不長,消停了一天沒出問題。今天早上過來上班,領導走過來語重心長的說,聊天又登不上了,上去一看,Redis里面的數據又沒了。正好運維的同事也在,也就一起找下存在的漏洞和可能的原因。

原因

前幾天無意在微博上看到了烏雲平台發的一條漏洞信息

Redis

然后意識到是不是Redis沒有設置密碼訪問,導致產生了這個漏洞被利用和攻擊了。於是自查,發現了如下內容:

RedisCraket

 

 

里邊多了一個名為crackit的字符串,並且db0這個庫是沒有使用了的,現有的數據和邏輯都在db1上,剛打開的時候,db1是空的,上圖是我重新同步過數據之后的結構。

對比烏雲報的漏洞的最后一部分:

 

wuyuncrack

這簡直就是一模一樣啊。並且運維那邊發現redis的持久化文件被寫到了authorized_keys文件中:

linux

確認被攻擊之后,於是開始着手修復。之前在開發的時候,其實是有考慮給Redis加訪問密碼的,不知道后來太忙了,竟然給忘了,也想着公司比較小,應該不會被黑之類的,沒想到這次就撞上了。

解決方法

解決方法當然是給Redis加密碼,然后在訪問的時候,設置密碼訪問。

最初,訪問Redis的客戶端我們使用的是ServiceStack.Redis,我之前也寫過幾篇Redis相關的文章。 由於考慮到授權的原因,使用的是2.0版本的dll,在其API中,從簽名看,沒有地方可以設置密碼:

ServiceStack.RedisClient

這個地方只能設置主機名稱和端口號。

於是在使用中,比如下面這個刪除群組的操作,RedisHelper類是對ServiceStack.Redis的一個封裝類,只需要設置主機和端口號:

public static bool DeleteGroup(string groupId)
{
    lock (lockDeleteGroup)
    {
        bool result = true;
        using (RedisHelper redis = new RedisHelper(HOST, PORT))
        {
            var group = redis.GetWhereObj<GroupTable>(GROUPTABLE, x => x.GroupId == groupId);
            group.Is_Deleted = true;
            result = redis.Update<GroupTable>(GROUPTABLE, x => x.GroupId == groupId, group);
        }
        return result;
    }
}

后面為了安全考慮,需要給Redis設置個密碼,於是下載了最新版本的4.0的ServiceStack.Redis,這個是商業化版本的,使用中發現,是有限制的,在其下載頁面最下角也有說明:

Redis Limit

每小時只能請求6000次,這顯然不能滿足要求。除了以上限制之外,在使用過程中也出現過一些相當詭異的問題,比如通過Id查找的時候,其只能在1k條范圍內進行查詢等等,當然,也有可能是因為使用不正確,考慮到以上原因,加之之前的數據組織和結構設計不合理,於是決定重構。

重構的時候,就直接換了另一個C#客戶端,StackExchange.Redis

private static ConnectionMultiplexer _redis;
private static IDatabase _db;
private static IServer _server;
private static bool needSave = false;
private void Init(string host, int port, string pwd, int database)
{
    var options = ConfigurationOptions.Parse(host + ":" + port);
    options.SyncTimeout = int.MaxValue;
    options.AllowAdmin = true;
    if (!string.IsNullOrEmpty(pwd))
    {
        options.Password = pwd;
    }
    if (_redis == null)
        _redis = ConnectionMultiplexer.Connect(options);
    if (_server == null)
        _server = _redis.GetServer(host + ":" + port);
    if (_db == null)
        _db = _redis.GetDatabase(database);
    needSave = false;
}

這里面,可以直接對options對象設置Password屬性。於是對該對象進行了包裝,后面使用Redis可以這樣,在構造函數里邊傳入PWD即可,比如下面判斷用戶是否存在的接口:

public static bool HasShopUser(string userName)
{
    bool hasUser = false;
    ShopUserEntity userEntity;
    userEntity = null;
    using (RedisHelper redis = new RedisHelper(HOST, PORT, PWD))
    {
        userEntity = redis.GetShopUserInfo(userName);
    }

    if (userEntity != null)
    {
        hasUser = true;
    }
    return hasUser;
}

在替換Redis客戶端訪問類的時候,順便對之前Redis里面的數據結構進行了一次重構,經過這次重構,速度提升很明顯。於是,於是就直接弄到正式環境然后就把設置密碼這個事情給忘記了。

當然,部署有Redis的Linux服務器也按照漏洞建議做了登陸限制和修復。

總結

其實這是一個很低級的錯誤,訪問Redis沒有設置密碼(當然也可能是Redis所在的Linux服務器本身沒有對登錄做限制),也感謝有烏雲這么好的一個平台,能夠及時發現系統的問題和漏洞,避免出現更大的損失。作為一個碼農其實不應該抱有這樣的僥幸心理,就像墨菲定律說的那樣“會出錯的,終將會出錯“ 。最后,希望這篇文章能給大家一個提醒和一些幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM