1. 程式人生 > >openresty+lua在反向代理服務中的玩法

openresty+lua在反向代理服務中的玩法

0x01 起因

幾天前學弟給我介紹他用nginx搭建的反代,代理了谷歌和維基百科。

由此我想到了一些邪惡的東西:反代既然是所有流量走我的伺服器,那我是不是能夠在中途做些手腳,達到一些有趣的目的。 openresty是一款結合了nginx和lua的全功能web伺服器,我感覺其角色和tornado類似,既是一箇中間件,也結合了一個後端直譯器。所以,我們可以在nginx上用lua開發很多“有趣”的東西。

所以,這篇文章也是由此而來。

0x02 openresty的搭建

openresty是國人的一個開源專案,主頁在http://openresty.org/ ,其核心nginx版本相對比較高(1.7.10),搭配的一些第三方模組也很豐富。

首先在官網下載openresty原始碼,然後我還需要一個openresty中沒有的第三方庫:https://github.com/yaoweibin/ngx_http_substitutions_filter_module ,同樣下載下來。

編譯:

 

編譯選項中: —with-http_sub_module 附帶http_sub_module模組,這是nginx自帶的一個模組,用來替換返回的http資料包中內容。 --with-pcre-jit —with-ipv6 提供ipv6支援 —add-module=/root/requirements/ngx_http_substitutions_filter_module(此處為你下載的ngx_http_substitutions_filter_module目錄) 將剛才下載的http_substitutions_filter_module模組編譯進去。http_substitutions_filter_module模組是http_sub_module的加強版,它可以用正則替換,並可以多處替換。

編譯安裝的過程沒有什麼難點,很快就安裝好了,openresty和luajit都預設在/usr/local/openresty目錄下。nginx的二進位制檔案為 /usr/local/openresty/nginx/sbin/nginx。

然後像正常啟動nginx一樣啟動之。

0x03 反代目標網站

根據目標網站的不同,反代也是有難度之分的。

比如烏雲,我們可以很輕鬆地將其反代下來。因為烏雲主站有一個特點:所有連結都是相對地址。所以我甚至不需要修改頁面中任何內容即可完整反代。

一個簡單demo:http://wooyun.jjfly.party ,其配置檔案如下:

其中,location / 塊即為反代烏雲的配置塊。

proxy_pass 是將請求交給上游處理,而這裡的上游就是http://wooyun.com

proxy_cookie_domain是將所有cookie中的domain替換掉成自己的domain,達到能夠登陸的效果。

proxy_buffering off用來關閉記憶體緩衝區。

proxy_set_header是一個重要的配置項,利用這個項可以修改轉發時的HTTP頭。比如,烏雲在登入以後,修改資料的時候會驗證referer,如果referer來自wooyun.jjfly.party是會提示錯誤的。所以我在這裡用proxy_set_header將referer設定為wooyun.org域下的地址,從而繞過檢查。

這樣,做好了一個完美的“釣魚網站”:

我甚至可以正常登入、修改資訊:

但是並不是所有網站做反代都這樣簡單,比如google。谷歌是一個https的站點,所以通常也需要一個https的配置:

我申請了一個SSL證書,反代方法和烏雲類似。但不同的是,谷歌會檢查host,如果host不是谷歌自己的域名就會強制302跳轉到www.google.com。

於是我在這裡用proxy_set_header 將Host設定為www.google.com。

另外,谷歌與烏雲最大的不同是,其原始碼中連結均為絕對路徑,所以一旦使用者點選其中連結後又會跳轉回谷歌去。所以,我這裡使用了subs_filter模組將其中的字元竄“www.google.com”替換成“xdsec.mhz.pw”。

這是反代中經常會遇到的情況。

那麼,如果我並沒有條件購買SSL證書怎麼辦?其實我們在nginx配置中也是可以將https降成http的。比如http://qq.jjfly.party就是代理的https://mail.qq.com:

另外,在xui.qq.jjfly.party(登陸框的frame)中,我利用 subs_filter </head> "<script>alert('xxx');</script></head>"; ,在html的標籤前插入了一段javascript,通過這個方式,我可以簡單製作一個前端的資料擷取。(XSS) 開啟即會彈窗: 

在反代過程中,我們會常常和gzip打交道。熟悉http協議的同學應該知道,如果瀏覽器傳送的資料包頭含有Accept-Encoding: gzip,即告訴伺服器:“我可以接受gzip壓縮過的資料包”。這時後端就會將返回包壓縮後傳送,幷包含返回頭Content-Encoding: gzip,瀏覽器根據是否含有這個頭對返回資料包進行解壓顯示。

但gzip在反代中,會造成很大問題:subs_filter替換內容時,如果內容是壓縮過的,明顯就不能正常替換了。同時在日誌裡可以看到這樣的記錄:

http subs filter header ignored, this may be a compressed response. while reading response header from upstream

所以網上一般處理方式是,在向上層伺服器轉發資料包的時候,設定proxy_set_header Accept-Encoding ””,這樣後端伺服器就不會壓縮資料包了。

但有時候,做反代的時候會發現subs_filter的替換失效或部分失效了,我在做126.com反代的時候就遇到了這個問題。經過一段時間的研究發現,可能和快取有關係,快取中的資料包是gzip壓縮過的,所以就算髮送Accept-Encoding=””也不管用。 如下是http://126.jjfly.party 配置:

我設定了很多阻止其快取的方法,但實際上好像並沒有效果。

於是這裡我想到藉助lua,我想通過lua指令碼在資料包返回的時候解壓縮gzip資料,並代替subs_filter進行字串的替換。

0x04 藉助lua進行gzip解壓與返回包修改

openresty在編譯安裝的時候就加入了lua支援,所以無需再對nginx進行改造。但lua下對gzip進行解壓,需要藉助一個庫:lua-zlib(https://github.com/brimworks/lua-zlib) lua是一個和C語言結合緊密的指令碼語言,實際上lua-zlib就是一個C語言編寫的庫,我們現在需要做的就是將其編譯成一個動態連結庫zlib.so,讓lua來引用。

 

以上程式碼解釋一下。首先執行cmake來生成編譯配置檔案。LUA_INCLUDE_DIR指定luajit的include資料夾,LUA_LIBRARIES指定luajit的lib資料夾。USE_LUAJIT=ON和USE_LUA=OFF指定我們使用的是luajit而不是lua:

再執行make && make install即可:

這時候已經編譯好了zlib.so,拷貝到openresty的lib目錄下即可:

cp zlib.so /usr/local/openresty/lualib/zlib.so

然後回到nginx的配置檔案中,“body_filter_by_lua_file /usr/local/openresty/luasrc/repl.lua; ”這句話告訴nginx我需要把返回包的body交給repl.lua處理。 repl.lua指令碼:

 

思路是個簡單粗暴的方式:ngx.arg1是原始的body,我將之交給pcall(lua下的異常處理方式),利用zlib.inflate進行解壓。如果不出異常說明解壓成功了,就將結果覆蓋ngx.arg1,丟擲異常了說明body可能是沒壓縮的,就保持不變。 但實際操作中遇到幾個困難: 

資料包並不是一次全部交給repl.lua,而是被分成許多chunks。所以我判斷了一下,當資料包沒有接收完整的時候就先儲存在一個臨時檔案中,直到eof,我才將之解壓縮傳送給客戶端。

多使用者情況下,我需要區分臨時檔案屬於哪個使用者。所以我將臨時檔名儲存在ngx.shared中,根據IP+uri判斷(實際上也並不完美)。

lua生成的隨機數並不會自動播種,所以我需要根據系統時間來設定隨機數種子。

最後,解壓完成後我直接呼叫callback()函式在其中對資料包進行替換,實際上就是完成之前subs_filter做的那些操作。 這樣配置完成後,重啟nginx,用瀏覽器訪問將會發現一個問題:

提示是:ERR_CONTENT_DECODING_FAILED,但我用burpsuite發包會發現似乎一切正常:

其實這個問題我之前都說了,還是和gzip有關。我們看到上圖,返回包中含有Content-Encoding: gzip,當我們的瀏覽器檢視到此頭後,會認為資料包是gzip壓縮過的。

但實際上我們已經在lua中將其解壓縮了,所以返回的資料其實是沒壓縮過的。最終導致瀏覽器解壓出錯,造成ERR_CONTENT_DECODING_FAILED。

怎麼解決?

在nginx配置中將返回包頭中的Content-Encoding設定為空就好了:

header_filter_by_lua就是在修改返回頭的配置。後面可以直接編寫lua指令碼,將ngx.header["Content-Encoding"]=""。 這時就可以正常訪問了:

0x05 利用lua擷取資料

那麼,lua除了能夠解決上述的解壓縮問題以外,還有沒有什麼新玩法?

這時候,理應就想到就是資料包的截獲。釣魚網站的最終目的就是獲取使用者的資訊,我在前面說到了可以通過在前端插入javascript指令碼來擷取使用者的輸入。

但實際上這並不是最好的方案,最好的方法就是在後端擷取資料包。

於是我來使用lua完成這個任務。首先在nginx的server塊外面(主配置檔案中)加入配置項:

 

這兩項在ngx_lua_waf中也介紹過。init_by_lua_file是在nginx啟動的時候載入並執行的lua指令碼,access_by_lua_file是在一次HTTP請求開始前執行的lua指令碼。

init_by_lua_file一般是初始化一些全域性使用的函式,不多說了。說一下我寫的access_by_lua_file時呼叫的fish.lua:

 

當host在valid_host(釣魚站的host)中時,判斷如果請求是POST請求,就將資料包的body寫入/home/wwwroot/fish/ $ngx.var.host .txt 中。

這時,我訪問http://126.jjfly.party/admin/126.jjfly.party.txt 就可以看到實時釣魚的結果:

烏雲也一樣:http://wooyun.jjfly.party/admin/wooyun.jjfly.party.txt

QQ郵箱那個因為環境太複雜(有至少三個host需要反代),所以我寧願選擇在前端插入指令碼進行劫持。

除了記錄使用者輸入的賬號密碼,根據反代網站的型別不同還能擷取很多有趣的東西。

比如谷歌,我可以記錄訪客在谷歌中查詢的內容:

指令碼也很簡單:

 

可見,雖然你看到的流量是經過一個擁有正規的證書的https站點的。但實際上我在寫lua指令碼的時候根本不用在乎流量是否加密,因為openresty總會將一個明文的資料包交給我處理。

那麼:Youtube,我們可以記錄訪客看過哪些視訊;wikipedia,我們可以記錄使用者搜尋過哪些姿勢;1024,我們可以記錄哪些片子的點選率最高……(笑)

自從各大國外站點陸續從網際網路上消失以後,現在映象網站越來越多。但上面的案例也說明了,映象網站也並不一定都是正直的。

0x06 結合快取與redis提升反代效率

當然openresty絕不僅僅是擁有這樣一些簡單的功能。openresty出現的定義就是一個“全功能的 Web 應用伺服器”,所以php可以有的功能它都可以辦到。 簡單來說我們可以直接在openresty上用lua編寫一個完整的動態網站。 之前我們的反代配置,有一些無法避免的缺點:

  1. 對gzip的支援不好,要不就是不使用壓縮,要不就是需要解壓,效率較低
  2. 沒有使用快取,請求頻繁、併發量大的情況下nginx可能被上游伺服器封掉。
  3. 後端沒有進行負債均衡。

如果僅僅是釣魚的話,效率低是問題不大的,因為訪問量不會太大。但如果你想做一個使用量大的谷歌映象之類的網站,就必須要考慮這個問題了。

如何緩解這個問題?

比如,我們可以利用谷歌全球的IP進行負載均衡:

 

另外,利用proxy_cache進行快取,可以減少很多反代伺服器向上遊伺服器請求的次數,防止被封。

當然,除了使用檔案快取以外,openresty還可以使用一些效率更高的服務,比如redis。

openresty自帶了一個redis客戶端lua-resty-redis:https://github.com/openresty/lua-resty-redis (openresty還有個RedisNginxModule模組,這個是反代redis請求的,並不是redis客戶端) 不過,現今的openresty對於redis模組(包括所有依賴於socket的模組)的支援僅限於在rewrite_by_lua, access_by_lua, content_by_lua這三個context中,也就是說我們沒法將返回的資料包儲存於redis中,但我們可以將擷取到的資料儲存於redis中。

還是以谷歌為例,我將查詢結果按照IP來存入redis:

 

再將location /result 解析到如下lua指令碼中,讀取redis顯示結果:

 

最後效果如圖所示:

0x07 總結與引用

通過這篇文章,我簡單地講了openresty一些有意思的玩法。

說白了,就是藉助其能夠擷取資料包的能力,來做很多隻有hacker才想做的事情。除了文中說到的玩法(釣魚、使用者隱私探測),我還想到一些openresty可以做的大事:

蜜罐:利用lua自動擷取資料包中的0day並進行分析。

流量分析與漏洞自動化挖掘:將目標網站反代下來,正常瀏覽使用。lua在後端擷取資料包並交給各種自動化分析工具分析。

高階服務的負載均衡:nginx 1.9後代理模組被加入核心,那時候我們甚至可以用openresty作為shadowsocks的前端伺服器,作負載均衡。利用lua配置多使用者shadowsocks環境,讓shadowsocks多使用者不再侷限於埠與密碼,而變成一個host+username+password認證的形式。

當然openresty的能力絕不僅僅是如此,還是最開始說的,openresty是一個全功能web伺服器。

但作為一個hacker,我往往去先挖掘這裡面最有意思的一些內容,也就是我上面說的。

如果諸君有興趣深入研究,都可以和我一起探索。

本文參考資料:

我推薦一些nginx/lua的相關資料與我關注的lua專案:
來自 :https://www.tuicool.com/articles/EZZZFn3