本文主要針對JAVA服務器常見的危害較大的安全問題的成因與防護進行分析,主要為了交流和拋磚引玉。
以下為任意文件下載漏洞的示例。
DownloadAction為用于下載文件的servlet。
#!html
<servlet>
<description></description>
<display-name>DownloadAction</display-name>
<servlet-name>DownloadAction</servlet-name>
<servlet-class>download.DownloadAction</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DownloadAction</servlet-name>
<url-pattern>/DownloadAction</url-pattern>
</servlet-mapping>
在對應的download.DownloadAction類中,將HTTP請求中的filename參數作為待下載的文件名,從web應用根目錄的download目錄讀取文件內容并返回,代碼如下。
#!java
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String rootPath = this.getServletContext().getRealPath("/");
String filename = request.getParameter("filename");
if (filename == null)
filename = "";
filename = filename.trim();
InputStream inStream = null;
byte[] b = new byte[1024];
int len = 0;
try {
if (filename == null) {
return;
}
// 讀到流中
// 本行代碼未對文件名參數進行過濾,存在任意文件下載漏洞
inStream = new FileInputStream(rootPath + "/download/" + filename);
// 設置輸出的格式
response.reset();
response.setContentType("application/x-msdownload");
response.addHeader("Content-Disposition", "attachment; filename=\""
+ filename + "\"");
// 循環取出流中的數據
while ((len = inStream.read(b)) > 0) {
response.getOutputStream().write(b, 0, len);
}
response.getOutputStream().close();
inStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
使用DownloadAction下載web應用根目錄中的“download/test.txt”文件如下圖所示。
由于在DownloadAction類中沒有對filename參數值進行檢查,因此產生了任意文件下載漏洞。
使用DownloadAction下載web應用根目錄中的“WEB-INF/web.xml”文件如下圖所示。
從上述示例可以看出,在JAVA web程序的下載文件相關的代碼中,若不對HTTP請求中的待下載文件名進行檢查,則有可能產生任意文件下載漏洞。
java.io.File對象有兩個方法可以用于獲取文件對象的路徑,getAbsolutePath與getCanonicalPath。
查看JDK 1.6 API中上述兩個方法的說明。
getAbsolutePath
返回此抽象路徑名的絕對路徑名字符串。
如果此抽象路徑名已經是絕對路徑名,則返回該路徑名字符串,這與 getPath() 方法一樣。如果此抽象路徑名是空抽象路徑名,則返回當前用戶目錄的路徑名字符串,該目錄由系統屬性 user.dir 指定。否則,使用與系統有關的方式解析此路徑名。在 UNIX 系統上,根據當前用戶目錄解析相對路徑名,可使該路徑名成為絕對路徑名。在 Microsoft Windows 系統上,根據路徑名指定的當前驅動器目錄(如果有)解析相對路徑名,可使該路徑名成為絕對路徑名;否則,可以根據當前用戶目錄解析它。
getCanonicalPath
返回此抽象路徑名的規范路徑名字符串。
規范路徑名是絕對路徑名,并且是惟一的。規范路徑名的準確定義與系統有關。如有必要,此方法首先將路徑名轉換為絕對路徑名,這與調用 getAbsolutePath() 方法的效果一樣,然后用與系統相關的方式將它映射到其惟一路徑名。這通常涉及到從路徑名中移除多余的名稱(比如 "." 和 "..")、解析符號連接(對于 UNIX 平臺),以及將驅動器號轉換為標準大小寫形式(對于 Microsoft Windows 平臺)。
每個表示現存文件或目錄的路徑名都有一個惟一的規范形式。每個表示不存在文件或目錄的路徑名也有一個惟一的規范形式。不存在文件或目錄路徑名的規范形式可能不同于創建文件或目錄之后同一路徑名的規范形式。同樣,現存文件或目錄路徑名的規范形式可能不同于刪除文件或目錄之后同一路徑名的規范形式。
使用以下代碼在Windows環境測試上述兩個方法。
#!java
public static void main(String[] args) {
getFilePath("C:/Windows/System32/calc.exe");
getFilePath("C:/Windows/System32/drivers/etc/../../notepad.exe");
}
private static void getFilePath(String filename) {
File f = new File(filename);
try {
System.out.println("getAbsolutePath: " + filename + " " + f.getAbsolutePath());
System.out.println("getCanonicalPath: " + filename + " " + f.getCanonicalPath());
} catch (Exception e) {
e.printStackTrace();
}
}
輸出結果如下。
#!bash
getAbsolutePath: C:/Windows/System32/calc.exe C:\Windows\System32\calc.exe
getCanonicalPath: C:/Windows/System32/calc.exe C:\Windows\System32\calc.exe
getAbsolutePath: C:/Windows/System32/drivers/etc/../../notepad.exe C:\Windows\System32\drivers\etc\..\..\notepad.exe
getCanonicalPath: **C:/Windows/System32/drivers/etc/../../notepad.exe C:\Windows\System32\notepad.exe**
使用以下代碼在Linux環境測試上述兩個方法。
#!java
public static void main(String[] args) {
getFilePath("/etc/hosts");
getFilePath("/etc/rc.d/init.d/../../hosts");
}
private static void getFilePath(String filename) {
File f = new File(filename);
try {
System.out.println("getAbsolutePath: " + filename + " " + f.getAbsolutePath());
System.out.println("getCanonicalPath: " + filename + " " + f.getCanonicalPath());
} catch (Exception e) {
e.printStackTrace();
}
}
輸出結果如下。
#!bash
getAbsolutePath: /etc/hosts /etc/hosts
getCanonicalPath: /etc/hosts /etc/hosts
getAbsolutePath: /etc/rc.d/init.d/../../hosts /etc/rc.d/init.d/../../hosts
getCanonicalPath: **/etc/rc.d/init.d/../../hosts /etc/hosts**
可以看出,當File對象的文件路徑中包含特殊字符時,JAVA能夠按照操作系統的規范對其進行相應的處理。在Windows與Linux環境中,..均代表上一級目錄,因此使用..能夠訪問上一級目錄,導致任意文件讀取漏洞產生。
可在處理下載的代碼中對HTTP請求中的待下載文件參數進行過濾,防止出現..等特殊字符,但可能需要處理多種編碼方式。
也可在生成File對象后,使用getCanonicalPath獲取當前文件的真實路徑,判斷文件是否在允許下載的目錄中,若發現文件不在允許下載的目錄中,則拒絕下載。
當攻擊者利用惡意文件上傳漏洞時,通常會向服務器上傳jsp木馬并訪問,可以直接控制服務器。
以下為惡意文件上傳的示例。
upload目錄中的upload.jsp為處理文件上傳的jsp文件,內容如下。
#!html
<form name="form1" action="<%=request.getContextPath()%>/strutsUploadFileAction_signle.action"
method="post" enctype="multipart/form-data"><input type="file" name="file4upload"
size="30"> <br> <input type="submit"
value="submit_signle" name="submit">
</form>
strutsUploadFileAction_signle為處理文件上傳的struts的action,內容如下。
#!html
<action name="strutsUploadFileAction_signle" method="upload_signle" class="strutsUploadFile">
<result name="success">upload/success.jsp</result>
<result name="fail">upload/fail.jsp</result>
</action>
strutsUploadFile為處理文件上傳的Spring的bean,內容如下。
#!html
<bean id="strutsUploadFile" class="strutsTest.StrutsUploadFileAction">
</bean>
strutsTest.StrutsUploadFileAction為處理文件上傳的JAVA類,在其中會檢查上傳的文件名是否以“.jpg”結尾,代碼如下。
#!java
// 注意,并不是指前端jsp上傳過來的文件本身,而是文件上傳過來存放在臨時文件夾下面的文件
private File file4upload;
// 提交過來的file的名字
private String file4uploadFileName;
// 提交過來的file的MIME類型
private String file4uploadContentType;
public String upload_signle() throws Exception {
return uploadCommon(file4upload, file4uploadFileName);
}
private String uploadCommon(File file, String fileName) throws Exception {
boolean success = false;
try {
String newFileName = "";
String webPath = ServletActionContext.getServletContext()
.getRealPath("/");
String allowedType = ".jpg";
String fileName_new = fileName.toLowerCase();
// 本行代碼有判斷文件類型是否為".jpg",但存在文件名截斷問題
if(fileName_new.length() - fileName_new.lastIndexOf(allowedType) != allowedType.length()) {
file.delete();
ActionContext.getContext().put("reason", "file type is not: " + allowedType);
return "fail";
}
newFileName = webPath + "uploadDir/" + fileName;
File dest = new File(newFileName);
if (dest.exists())
dest.delete();
success = file.renameTo(dest);
} catch (Exception e) {
success = false;
e.printStackTrace();
throw e;
}
return success ? "success" : "fail";
}
打開upload.jsp,選擇文件“a.jpg”進行上傳。
使用fiddler抓包并攔截,將filename參數修改為“a.jsp#.jpg”后的HTTP請求數據如下。
使用十六進制形式查看HTTP請求數據如下。
將#對應的字節修改為0x00并發送HTTP請求數據。
完成文件上傳后,查看保存上傳文件的目錄,可以看到文件上傳成功,生成的文件為“a.jsp”。
從上述示例中可以看出,在上傳文件時產生了文件名截斷的問題。
使用以下代碼測試JAVA寫文件的文件名截斷問題,使用0x00至0xff間的字符作為文件名生成文件。
#!java
public static void main(String[] args) {
String java_version = System.getProperty("java.version");
new File(java_version).mkdirs();
String filename = "a.jsp#a.jpg";
for(int i=0; i<=0xff; i++) {
String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i);
File f = new File(filename_replace);
try {
f.createNewFile();
} catch (Exception e) {
System.out.println("error: " + i);
e.printStackTrace();
}
}
}
在Windows 7,64位環境,使用JDK1.5執行上述代碼生成文件的結果如下。
可以看到使用JDK1.5執行時,除0x00外,冒號“:”(ASCII碼十進制為58)也會產生文件名截斷問題。
JDK1.6與JDK1.5執行結果相同。
JDK1.7也與JDK1.5執行結果相同。
JDK1.8與JDK1.5執行結果不同,僅有冒號會產生文件名截斷問題,0x00不會產生文件名截斷問題,可能是JDK1.8已修復該問題。
使用Procmon查看上述過程中java.exe進程執行的寫文件操作。
JDK1.5、1.6、1.7的監控結果相同,監控結果如下。
JDK1.5~1.7,當文件名中包含0x00時,java.exe在執行寫文件操作時,會將0x00及之后的字符串丟棄,使用0x00之前的字符串作為文件名寫文件。
JDK1.5~1.7,當文件名包含冒號時,java.exe在執行寫文件操作時,不會將冒號及之后的字符串丟棄。
JDK1.8的監控結果如下。
JDK1.8,當文件名中包含0x00時,java.exe不會執行寫文件的操作。
與JDK1.5~1.7一樣,JDK1.8當文件名包含冒號時,java.exe在執行寫文件操作時,不會將冒號及之后的字符串丟棄。截圖略。
雖然java.exe在寫文件時不會將冒號及之后的字符串丟棄,但在Windows環境下仍然出現了文件名截斷的問題。
在Windows中執行“echo 1>abc:123”命令,可以看到生成的文件名為“abc”,冒號及之后的字符串被丟棄,造成了文件名截斷。這是Windows特性導致的,與JAVA無關。
在Linux RedHat 6.4環境,使用JDK1.6執行上述代碼生成文件的結果如下。
JDK1.6,文件名中包含0x00時同樣出現了文件名截斷問題(文件名中包含ASCII碼為92的反斜杠“\”時,生成的文件會產生在子目錄中,但不會導致文件類型的變化)。
綜上所述,JDK1.5-1.7存在0x00導致的文件名截斷問題,與操作系統無關。冒號在Windows環境會導致文件名截斷問題,與JAVA無關。
使用File對象的getCanonicalPath方法獲取JAVA在文件名中包含0x00至0xff的字符時,生成文件時的實際文件路徑,代碼如下。
#!java
public static void main(String[] args) {
String java_version = System.getProperty("java.version");
String filename = "a.jsp#a.jpg";
for(int i=0; i<=0xff; i++) {
String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i);
File f = new File(filename_replace);
try {
System.out.println("getCanonicalPath " + f.getCanonicalPath());
} catch (Exception e) {
System.out.println("error: " + i);
e.printStackTrace();
}
}
}
在Windows 7,64位環境,使用JDK1.5~1.7執行上述代碼使用getCanonicalPath方法獲取文件實際路徑的結果相同,結果如下。
JDK1.5執行getCanonicalPath方法的結果。
JDK1.6執行getCanonicalPath方法的結果。
JDK1.7執行getCanonicalPath方法的結果。
可以看到JDK1.5~1.7使用getCanonicalPath方法獲取文件實際路徑時,當文件名中包含0x00時,獲取到的文件實際路徑中0x00及之后的字符串已被丟棄。
在Windows 7,64位環境,使用JDK1.8執行getCanonicalPath方法的結果如下。
可以看到JDK1.8使用getCanonicalPath方法獲取文件實際路徑時,當文件名中包含0x00時,會出現java.io.IOException異常,異常信息為“Invalid file path”。
在Linux RedHat 6.4環境,使用JDK1.6執行上述代碼的結果與Windows環境相同,截圖略。
以下的防護方法可以根據實際需求進行組合,相互之間沒有沖突。
使用String對象的endsWith方法無法判斷出文件生成時的實際文件名,使用以下代碼進行證明。
#!java
public static void main(String[] args) {
String java_version = System.getProperty("java.version");
String filename = "a.jsp#a.jpg";
for(int i=0; i<=0xff; i++) {
String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i);
if(filename_replace.endsWith(".jpg")) {
System.out.println("yes: " + filename_replace);
}
}
}
執行結果如下。
當文件名為“a.jsp[特定字符]a.jpg”形式時,無論[特定字符]是否為0x00,使用String對象的endsWith方法對文件名進行檢測,均認為是以“.jpg”結尾。
當文件名中包含0x00時,使用String對象的indexOf(0)方法執行結果非-1,可以檢測到0x00的存在。但需考慮不同編碼情況下0x00的形式。
使用File對象的getCanonicalPath方法獲取上傳文件的實際文件名,若檢測到文件名的后綴不是允許的類型(0x00截斷,小于JDK1.8),或出現java.io.IOException異常(0x00截斷,JDK1.8),或包含冒號(Windows環境中需處理),則說明需要拒絕本次文件上傳。
上述的防護思路是防止攻擊者將jsp文件上傳至服務器中,本防護思路是防止攻擊者上傳的jsp文件被編譯為class文件。
當JAVA中間件收到訪問web應用目錄中的jsp文件請求時,會將對應的jsp文件編譯為class文件并執行。若將保存上傳文件的目錄修改為非web應用目錄,當JAVA中間件收到訪問上傳文件的請求時,即使被訪問的文件為jsp文件,JAVA中間件也不會將jsp文件編譯為成class文件并執行,可以防止攻擊者利用上傳jsp木馬控制服務器。
將保存上傳文件的目錄修改為非web應用目錄的操作很簡單,將處理文件上傳代碼中保存文件的目錄修改為非web應用目錄即可。進行該修改后,還可以使用共享目錄解決多實例應用上傳文件的問題。
將保存上傳文件的目錄修改為非web應用目錄后,會導致無法使用原有方式訪問上傳的文件(例如文件上傳目錄原本為web應用目錄中的upload目錄,可直接使用http://[IP]:[PORT]/xxx/upload/xxx進行訪問。將upload目錄移動到非web應用目錄后,無法再使用原有URL訪問上傳的文件)。可通過以下兩種方法解決。
使用Servlet/action/.do請求訪問上傳文件,可參考前文中的download.DownloadAction類。本方法的影響面較大,不推薦使用。
除上述方法外,還可使用filter攔截HTTP請求處理,當HTTP請求訪問文件上傳目錄中的文件時,讀取對應的文件內容并返回(例如原本上傳目錄為web應用目錄中的upload目錄,可直接使用http://[IP]:[PORT]/xxx/upload/xxx進行訪問。將upload目錄移動到非web應用目錄后,對HTTP請求處理進行攔截,當請求以“/xxx/upload”開頭時,從文件上傳目錄中讀取對應的文件內容并返回)。本方法可使用原本的URL訪問上傳文件,影響面較小,推薦使用。示例代碼如下。
在web.xml中使用filter攔截HTTP請求處理。
#!html
<filter>
<filter-name>testFilter</filter-name>
<filter-class>test.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>testFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
對應的test.TestFilter類代碼如下。
#!java
private static String IF_MODIFIED_SINCE = "If-Modified-Since";
private static String LAST_MODIFIED = "Last-Modified";
private static String startFlag = "/testDownload/upload/";
private static String storePath = "C:/Users/Public";
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 獲取瀏覽器訪問的URL,形式如/test/upload/xxx.jpg
String requestUrl = httpRequest.getRequestURI();
System.out.println("requestUrl: " + requestUrl);
if (requestUrl != null) {
// 判斷是否訪問upload目錄的文件,若是則從對應的存儲目錄讀取并返回
if (requestUrl.startsWith(startFlag)) {
try {
returnFileContent(requestUrl, (HttpServletRequest) request,
(HttpServletResponse) response);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return;
}
}
chain.doFilter(request, response);
return;
}
// 當訪問web應用特定目錄下的文件時,重定向到實際存儲這些文件的目錄
private void returnFileContent(String url, HttpServletRequest request,
HttpServletResponse response) throws Exception {
java.io.InputStream in = null;
java.io.OutputStream outStream = null;
try {
response.setHeader("Content-Type", "text/plain");// 若不返回text/plain類型,瀏覽器無法正常識別文件類型
String filePath = url.substring(startFlag.length() - 1);// 獲取被訪問的文件的URL
String filePath_decode = URLDecoder.decode(filePath, "UTF-8");// 經過url解碼之后的文件URL
// 生成最終訪問的文件路徑
// StorePath形式如C:/xxx/xxx,filePath_decode開頭有/
String targetfile = storePath + filePath_decode;
System.out.println("targetfile: " + targetfile);
File f = new File(targetfile);
if (!f.exists() || f.isDirectory()) {
System.out.println("文件不存在: " + targetfile);
response.sendError(HttpServletResponse.SC_NOT_FOUND);// 返回錯誤信息,顯示統一錯誤頁面
return;
}
// 判斷上送的HTTP頭是否有If-Modified-Since字段
String modified = request.getHeader(IF_MODIFIED_SINCE);
//獲取文件的修改時間
String modified_file = getFileModifiedTime(f);
if (modified != null) {
// 上送的HTTP頭有If-Modified-Since字段,判斷與對應文件的修改時間是否相同
if(modified.equals(modified_file)) {
//上送的文件時間與文件實際修改時間相同,不需返回文件內容
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);//返回304狀態
outStream = response.getOutputStream();
outStream.close();
outStream.flush();
outStream = null;
return;
}
}
// 文件無緩存,或文件有修改,需要在返回的HTTP頭中添加文件修改時間
response.setHeader(LAST_MODIFIED, modified_file);
// 讀取文件內容
in = new FileInputStream(f);
outStream = response.getOutputStream();
byte[] buf = new byte[1024];
int bytes = 0;
while ((bytes = in.read(buf)) != -1)
outStream.write(buf, 0, bytes);
in.close();
outStream.close();
outStream.flush();
outStream = null;
} catch (Throwable ex) {
ex.printStackTrace();
} finally {
if (in != null) {
in.close();
in = null;
}
if (outStream != null) {
outStream.close();
outStream = null;
}
}
}
// 獲取指定文件的修改時間
private String getFileModifiedTime(File file) {
SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
return sdf.format(file.lastModified());
}
上述示例代碼中,保存上傳文件的目錄為“C:/Users/Public”,當HTTP請求以“/testDownload/upload/”開頭時,說明需要訪問上傳文件。
上述修改方法接管了JAVA中間件對原本上傳目錄的靜態資源的訪問請求,導致瀏覽器的緩存機制不可用。為了保證瀏覽器的緩存機制可用,上述代碼中進行了專門處理。當HTTP請求頭中不包含“If-Modified-Since”參數時,或“If-Modified-Since”對應的文件修改時間小于實際文件修改時間時,將文件的內容返回給瀏覽器,并在返回的HTTP頭中加入“Last-Modified”參數返回文件修改時間,使瀏覽器對該文件進行緩存。當HTTP請求頭的“If-Modified-Since”對應的文件修改時間等于實際文件修改時間時,不返回文件內容,將返回的HTTP碼設為304,告知瀏覽器訪問的文件無修改,可使用緩存。
以下為上述代碼的測試結果。
web應用的目錄中無upload目錄。
文件上傳目錄“C:/Users/Public”中有以下文件。
訪問文本文件正常。
訪問圖片正常。
訪問音頻文件正常。
訪問jsp文件只返回文件本身的內容,不會被編譯成class文件并執行。
使用fiddler查看訪問記錄,瀏覽器緩存機制正常。
將文件上傳目錄移出web應用目錄后,JAVA中間件在運行過程中,web應用目錄及其中的文件一般不會被修改。可在JAVA中間件啟動后,將web應用目錄設為JAVA中間件不可寫;當需要進行版本更新或維護時,停止JAVA中間件后,將web應用目錄設為JAVA中間件可寫。通過上述限制,可嚴格地防止web應用目錄被上傳jsp木馬等惡意文件。
可將JAVA中間件使用a用戶啟動,將web應用的目錄對應用戶設為b用戶,JAVA中間件啟動后,將web應用的目錄設為a用戶只讀。需要進行版本更新或維護時,停止JAVA中間件后,將web應用的目錄設為a用戶可讀寫。對于某些JAVA中間件在運行過程中可能需要進行寫操作的文件或目錄,可單獨設置權限。可將對web應用的權限修改操作在JAVA中間件啟停腳本中調用,減少操作復雜度。
Windows的權限設置較復雜且速度較慢,使用上述的防護方法時會比較麻煩。
眾所周知,在JAVA中使用PreparedStatement替代Statement可以防止SQL注入。
在oracle數據庫中進行以下測試。
首先創建測試用的數據庫表并插入數據。
#!sql
create table test_user
(
username varchar2(100),
pwd varchar2(100)
);
Insert into TEST_USER
(USERNAME, PWD)
Values
('aaa', 'bbb');
COMMIT;
使用以下JAVA代碼進行測試。
#!java
private Connection conn = null;
public dbtest2(String url, String username, String password)
throws ClassNotFoundException, SQLException {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
conn = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public void closeDb() throws SQLException {
conn.close();
}
public void executeStatement(String username, String pwd)
throws SQLException {
String sql = "SELECT * FROM TEST_USER where username='" + username
+ "' and pwd='" + pwd + "'";
System.out.println("executeStatement-sql: " + sql);
java.sql.Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
showResultSet(rs);
stmt.close();
}
public void executePreparedStatement(String username, String pwd)
throws SQLException {
java.sql.PreparedStatement stmt = conn
.prepareStatement("SELECT * FROM TEST_USER where username=? and pwd=?");
stmt.setString(1, username);
stmt.setString(2, pwd);
ResultSet rs = stmt.executeQuery();
showResultSet(rs);
stmt.close();
}
public void showResultSet(ResultSet rs) throws SQLException {
ResultSetMetaData meta = rs.getMetaData();
StringBuffer sb = new StringBuffer();
int colCount = meta.getColumnCount();
for (int i = 1; i <= colCount; i++) {
sb.append(meta.getColumnName(i)).append("[")
.append(meta.getColumnTypeName(i)).append("]").append("\t");
}
while (rs.next()) {
sb.append("\r\n");
for (int i = 1; i <= colCount; i++) {
sb.append(rs.getString(i)).append("\t");
}
}
// 關閉ResultSet
rs.close();
System.out.println(sb.toString());
}
public static void main(String[] args) throws SQLException {
try {
dbtest2 db = new dbtest2(
"jdbc:oracle:thin:@192.xxx.xxx.xxx:1521:xxx",
"xxx", "xxx");
db.executeStatement("aaa", "bbb");
db.executeStatement("aaa", "' or '2'='2");
db.executePreparedStatement("aaa", "bbb");
db.executePreparedStatement("aaa", "' or '2'='2");
db.closeDb();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
執行結果如下。
#!bash
1 db.executeStatement("aaa", "bbb");對應的結果
executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='bbb'
USERNAME[VARCHAR2] PWD[VARCHAR2]
aaa bbb
2 db.executeStatement("aaa", "' or '2'='2");對應的結果
executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='' or '2'='2'
USERNAME[VARCHAR2] PWD[VARCHAR2]
aaa bbb
3 db.executePreparedStatement("aaa", "bbb");對應的結果
USERNAME[VARCHAR2] PWD[VARCHAR2]
aaa bbb
4 db.executePreparedStatement("aaa", "' or '2'='2");對應的結果
USERNAME[VARCHAR2] PWD[VARCHAR2]
可以看到使用Statement時,將查詢參數設為“username='aaa' and pwd='bbb'”使用正常的查詢條件能查詢到對應的數據。將查詢參數設為“username='aaa' and pwd='' or '2'='2'”能夠利用SQL注入查詢到對應的數據。
使用PreparedStatement時,使用正常的查詢條件同樣能查詢到對應的數據,使用能使Statement產生SQL注入的查詢條件無法再查詢到數據。
使用Wireshark對剛才的數據庫操作抓包并查看網絡數據。
查找select語句對應的數據包如下。
db.executeStatement("aaa", "bbb");對應的數據包如下,可以看到查詢語句未使用oracle綁定變量方式,使用正常查詢條件查詢到了數據。
db.executeStatement("aaa", "' or '2'='2");對應的數據包如下,可以看到查詢語句未使用oracle綁定變量方式,利用SQL注入查詢到了數據。
db.executePreparedStatement("aaa", "bbb");對應的數據包如下,可以看到查詢語句使用了oracle綁定變量方式,使用正常查詢條件查詢到了數據。
db.executePreparedStatement("aaa", "' or '2'='2");對應的數據包如下,可以看到查詢語句使用了oracle綁定變量方式,SQL注入未生效,無法查詢到對應數據。
在JAVA中使用PreparedStatement訪問oracle數據庫時,除了能防止SQL注入外,還能使oracle服務器降低硬解析率,降低系統開銷,減少內存碎片,提高執行效率。
剛才執行的sql語句在oracle的v$sql視圖中產生的數據如下。
當使用ibatis作為持久化框架時,也需要考慮SQL注入的問題。使用ibatis產生SQL注入主要是由于使用不規范。
$
與#
在ibatis中使用#時,與使用PreparedStatement的效果相同,不會產生SQL注入;在ibatis中使用$時,與使用Statement的效果相同,會產生SQL注入。
繼續使用剛才的數據庫表TEST_USER進行測試,再插入一條數據如下。
#!sql
Insert into TEST_USER
(USERNAME, PWD)
Values
('123', '456');
COMMIT;
將log4j中的數據庫相關日志級別設為DEBUG。
#!sql
log4j.logger.com.ibatis=DEBUG
log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG
log4j.logger.com.ibatis.common.jdbc.ScriptRunner=DEBUG
log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
首先使用#與$測試執行判斷條件為“=”的sql語句時的情況。
在ibatis對應的xml文件中配置了語句test_right與test_wrong如下。
#!html
<select id="test_right" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
select * from test_user where username = #username#
</select>
<select id="test_wrong" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
select * from test_user where username = '$username$'
</select>
在JAVA代碼中執行上述語句如下。
#!java
HashMap hs = new HashMap();
hs.put("username", "' or '1'='1");
List<Object> list1 = queryListSql("test_right",hs);
logger.info("test-list1: " + list1);
List<Object> list2 = queryListSql("test_wrong",hs);
logger.info("test-list2: " + list2);
log4j中執行test_right語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username = ?
[DEBUG] Executing Statement: select * from test_user where username = ?
[DEBUG] Parameters: [' or '1'='1]
[DEBUG] Types: [java language=".lang.String"][/java]
[DEBUG] ResultSet
[INFO ] test-list1: []
log4j中執行test_wrong語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username = '' or '1'='1'
[DEBUG] Executing Statement: select * from test_user where username = '' or '1'='1'
[DEBUG] Parameters: []
[DEBUG] Types: []
[DEBUG] ResultSet
[DEBUG] Header: [USERNAME, PWD]
[DEBUG] Result: [aaa, bbb]
[DEBUG] Result: [123, 456]
[INFO ] test-list2: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]
可以看到使用#可以防止SQL注入,使用$會產生SQL注入。
執行test_right語句時產生的數據包如下。
執行test_wrong語句時產生的數據包如下。
在使用ibatis執行判斷條件為“like”的操作時,較容易誤用$導致產生SQL注入問題。
當需要使用like時,應用使用“xxx like '%' || #xxx# || '%'”,而不應使用“xxx like '%$xxx$%'”(以oracle數據庫為例)。
使用以下代碼進行驗證測試。
在ibatis對應的xml文件中配置了語句test_like_right與test_like_wrong如下。
#!html
<select id="test_like_right" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
select * from test_user where username like '%' || #username# || '%'
</select>
<select id="test_like_wrong" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
select * from test_user where username like '$username$'
</select>
在JAVA代碼中執行上述語句如下。
#!java
HashMap hs = new HashMap();
hs.put("username", "' or '1'='1");
List<Object> list3 = queryListSql("test_like_right",hs);
logger.info("test-list3: " + list3);
List<Object> list4 = queryListSql("test_like_wrong",hs);
logger.info("test-list4: " + list4);
log4j中執行test_like_right語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username like '%' || ? || '%'
[DEBUG] Executing Statement: select * from test_user where username like '%' || ? || '%'
[DEBUG] Parameters: [' or '1'='1]
[DEBUG] Types: [java language=".lang.String"][/java]
[DEBUG] ResultSet
[INFO ] test-list3: []
log4j中執行test_like_wrong語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username like '' or '1'='1'
[DEBUG] Executing Statement: select * from test_user where username like '' or '1'='1'
[DEBUG] Parameters: []
[DEBUG] Types: []
[DEBUG] ResultSet
[DEBUG] Header: [USERNAME, PWD]
[DEBUG] Result: [aaa, bbb]
[DEBUG] Result: [123, 456]
[INFO ] [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]
執行語句時test_like_right產生的數據包如下。
執行語句時test_like_wrong產生的數據包如下。
在使用ibatis處理判斷條件為“in”的操作時,同樣容易誤用$導致SQL注入問題。
當需要使用in時,可使用以下方法。
java代碼。
#!java
String[] xxx_list = new String[] {"xx1","xx2"};
HashMap hs = new HashMap();
hs.put("xxx", xxx_list);
//hs為sql語句查詢參數
xml中的語句配置。
#!html
<select id="" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
...
<dynamic prepend=" and ">
<isNotEmpty prepend=" and " property="xxx">
(xxx in
<iterate open="(" close=")" conjunction="," property="xxx">#xxx[]#</iterate>
)
</isNotEmpty>
</dynamic>
...
</select>
當需要使用in時,不應使用“in ('$xxx$')”。
在ibatis對應的xml文件中配置了語句test_in_right與test_in_wrong如下。
#!html
<select id="test_in_right" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
select * from test_user where username in
<iterate open="(" close=")" conjunction="," property="username">#username[]#</iterate>
</select>
<select id="test_in_wrong" resultClass="java.util.HashMap"
parameterClass="java.util.HashMap">
select * from test_user where username in ('$username$')
</select>
在JAVA代碼中執行上述語句如下。
#!java
String[] username_list = new String[] {"') or ('1'='1"};
hs.put("username", username_list);
List<Object> list5 = queryListSql("test_in_right",hs);
logger.info("test-list5: " + list5);
HashMap hs = new HashMap();
hs.put("username", "') or ('1'='1");
List<Object> list6 = queryListSql("test_in_wrong",hs);
logger.info("test-list6: " + list6);
log4j中執行test_in_right語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username in (?)
[DEBUG] Executing Statement: select * from test_user where username in (?)
[DEBUG] Parameters: [') or ('1'='1]
[DEBUG] Types: [java language=".lang.String"][/java]
[DEBUG] ResultSet
[INFO ] test-list5: []
log4j中執行test_in_wrong語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username in ('') or ('1'='1')
[DEBUG] Executing Statement: select * from test_user where username in ('') or ('1'='1')
[DEBUG] Parameters: []
[DEBUG] Types: []
[DEBUG] ResultSet
[DEBUG] Header: [USERNAME, PWD]
[DEBUG] Result: [aaa, bbb]
[DEBUG] Result: [123, 456]
[INFO ] test-list6: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]
執行test_in_right語句時產生的數據包如下。
執行test_in_wrong語句時產生的數據包如下。
在ibatis中在執行包含like或in的語句時,使用#也是能正常查詢到數據的。
在JAVA代碼中使用正確的查詢條件執行test_like_right與test_in_right語句如下。
#!java
HashMap hs = new HashMap();
hs.put("username", "aaa");
List<Object> list7 = queryListSql("test_like_right",hs);
logger.info("test-list7: " + list7);
String[] username_list2 = new String[] {"aaa","123"};
hs.put("username", username_list2);
List<Object> list8 = queryListSql("test_in_right",hs);
logger.info("test-list8: " + list8);
log4j中使用正確的查詢條件執行test_like_right語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username like '%' || ? || '%'
[DEBUG] Executing Statement: select * from test_user where username like '%' || ? || '%'
[DEBUG] Parameters: [aaa]
[DEBUG] Types: [java language=".lang.String"][/java]
[DEBUG] ResultSet
[DEBUG] Header: [USERNAME, PWD]
[DEBUG] Result: [aaa, bbb]
[INFO ] test-list7: [{PWD=bbb, USERNAME=aaa}]
log4j中使用正確的查詢條件執行test_in_right語句時的相關日志如下。
#!bash
[DEBUG] Preparing Statement: select * from test_user where username in (?,?)
[DEBUG] Executing Statement: select * from test_user where username in (?,?)
[DEBUG] Parameters: [aaa, 123]
[DEBUG] Types: [java 1="java.lang.String" language=".lang.String,"][/java]
[DEBUG] ResultSet
[DEBUG] Header: [USERNAME, PWD]
[DEBUG] Result: [aaa, bbb]
[DEBUG] Result: [123, 456]
[INFO ] test-list8: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}]
使用正確的查詢條件執行test_like_right語句時產生的數據包如下。
使用正確的查詢條件執行test_in_right語句時產生的數據包如下。
上述全部語句執行時在oracle的v$sql視圖中產生的數據如下。
在web.xml中定義error-page,防止當出現錯誤時暴露服務器信息。
示例如下。
#!html
<error-page>
<error-code>404</error-code>
<location>xxx.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>xxx.jsp</location>
</error-page>
當用戶訪問jsp或Servlet/action/.do時,需要判斷當前用戶是否已登錄且具有相應權限,防止出現越權使用。
以上為本人的一點總結,難免存在錯誤之處,大牛請輕噴。