JAVA安全之JAVA伺服器安全漫談

wyzsk發表於2020-08-19
作者: z_zz_zzz · 2016/06/08 10:50

0x00 前言


本文主要針對JAVA伺服器常見的危害較大的安全問題的成因與防護進行分析,主要為了交流和拋磚引玉。

0x01 任意檔案下載


示例

以下為任意檔案下載漏洞的示例。

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”檔案如下圖所示。

p1

由於在DownloadAction類中沒有對filename引數值進行檢查,因此產生了任意檔案下載漏洞。

使用DownloadAction下載web應用根目錄中的“WEB-INF/web.xml”檔案如下圖所示。

p2

原因分析

從上述示例可以看出,在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獲取當前檔案的真實路徑,判斷檔案是否在允許下載的目錄中,若發現檔案不在允許下載的目錄中,則拒絕下載。

0x02 惡意檔案上傳


當攻擊者利用惡意檔案上傳漏洞時,通常會向伺服器上傳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”進行上傳。

p3

使用fiddler抓包並攔截,將filename引數修改為“a.jsp#.jpg”後的HTTP請求資料如下。

p4

使用十六進位制形式檢視HTTP請求資料如下。

p5

將#對應的位元組修改為0x00併傳送HTTP請求資料。

p6

完成檔案上傳後,檢視儲存上傳檔案的目錄,可以看到檔案上傳成功,生成的檔案為“a.jsp”。

p7

原因分析

從上述示例中可以看出,在上傳檔案時產生了檔名截斷的問題。

使用以下程式碼測試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環境檔名截斷問題測試

在Windows 7,64位環境,使用JDK1.5執行上述程式碼生成檔案的結果如下。

p8

可以看到使用JDK1.5執行時,除0x00外,冒號“:”(ASCII碼十進位制為58)也會產生檔名截斷問題。

JDK1.6與JDK1.5執行結果相同。

p9

JDK1.7也與JDK1.5執行結果相同。

p10

JDK1.8與JDK1.5執行結果不同,僅有冒號會產生檔名截斷問題,0x00不會產生檔名截斷問題,可能是JDK1.8已修復該問題。

p11

使用Procmon檢視上述過程中java.exe程式執行的寫檔案操作。

JDK1.5、1.6、1.7的監控結果相同,監控結果如下。

JDK1.5~1.7,當檔名中包含0x00時,java.exe在執行寫檔案操作時,會將0x00及之後的字串丟棄,使用0x00之前的字串作為檔名寫檔案。

p12

JDK1.5~1.7,當檔名包含冒號時,java.exe在執行寫檔案操作時,不會將冒號及之後的字串丟棄。

p13

JDK1.8的監控結果如下。

JDK1.8,當檔名中包含0x00時,java.exe不會執行寫檔案的操作。

p14

與JDK1.5~1.7一樣,JDK1.8當檔名包含冒號時,java.exe在執行寫檔案操作時,不會將冒號及之後的字串丟棄。截圖略。

雖然java.exe在寫檔案時不會將冒號及之後的字串丟棄,但在Windows環境下仍然出現了檔名截斷的問題。

在Windows中執行“echo 1>abc:123”命令,可以看到生成的檔名為“abc”,冒號及之後的字串被丟棄,造成了檔名截斷。這是Windows特性導致的,與JAVA無關。

Linux環境檔名截斷問題測試

在Linux RedHat 6.4環境,使用JDK1.6執行上述程式碼生成檔案的結果如下。

p15

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環境執行getCanonicalPath方法的結果

在Windows 7,64位環境,使用JDK1.5~1.7執行上述程式碼使用getCanonicalPath方法獲取檔案實際路徑的結果相同,結果如下。

JDK1.5執行getCanonicalPath方法的結果。

p16

JDK1.6執行getCanonicalPath方法的結果。

p17

JDK1.7執行getCanonicalPath方法的結果。

p18

可以看到JDK1.5~1.7使用getCanonicalPath方法獲取檔案實際路徑時,當檔名中包含0x00時,獲取到的檔案實際路徑中0x00及之後的字串已被丟棄。

在Windows 7,64位環境,使用JDK1.8執行getCanonicalPath方法的結果如下。

p19

可以看到JDK1.8使用getCanonicalPath方法獲取檔案實際路徑時,當檔名中包含0x00時,會出現java.io.IOException異常,異常資訊為“Invalid file path”。

Linux環境執行getCanonicalPath方法的結果

在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);
        }
    }
}   

執行結果如下。

p20

當檔名為“a.jsp[特定字元]a.jpg”形式時,無論[特定字元]是否為0x00,使用String物件的endsWith方法對檔名進行檢測,均認為是以“.jpg”結尾。

針對0x00進行檢測

當檔名中包含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目錄。

p21

檔案上傳目錄“C:/Users/Public”中有以下檔案。

p22

訪問文字檔案正常。

p23

訪問圖片正常。

p24

訪問音訊檔案正常。

p25

訪問jsp檔案只返回檔案本身的內容,不會被編譯成class檔案並執行。

p26

使用fiddler檢視訪問記錄,瀏覽器快取機制正常。

p27

修改web應用目錄許可權

將檔案上傳目錄移出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的許可權設定較複雜且速度較慢,使用上述的防護方法時會比較麻煩。

0x03 SQL隱碼攻擊


PreparedStatement與Statement

眾所周知,在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語句對應的資料包如下。

p28

db.executeStatement("aaa", "bbb");對應的資料包如下,可以看到查詢語句未使用oracle繫結變數方式,使用正常查詢條件查詢到了資料。

p29

db.executeStatement("aaa", "' or '2'='2");對應的資料包如下,可以看到查詢語句未使用oracle繫結變數方式,利用SQL隱碼攻擊查詢到了資料。

p30

db.executePreparedStatement("aaa", "bbb");對應的資料包如下,可以看到查詢語句使用了oracle繫結變數方式,使用正常查詢條件查詢到了資料。

p31

db.executePreparedStatement("aaa", "' or '2'='2");對應的資料包如下,可以看到查詢語句使用了oracle繫結變數方式,SQL隱碼攻擊未生效,無法查詢到對應資料。

p32

在JAVA中使用PreparedStatement訪問oracle資料庫時,除了能防止SQL隱碼攻擊外,還能使oracle伺服器降低硬解析率,降低系統開銷,減少記憶體碎片,提高執行效率。

剛才執行的sql語句在oracle的v$sql檢視中產生的資料如下。

p33

ibatis

當使用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語句時產生的資料包如下。

p34

執行test_wrong語句時產生的資料包如下。

p35

like

在使用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產生的資料包如下。

p36

執行語句時test_like_wrong產生的資料包如下。

p37

in

在使用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語句時產生的資料包如下。

p38

執行test_in_wrong語句時產生的資料包如下。

p39

在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語句時產生的資料包如下。

p40

使用正確的查詢條件執行test_in_right語句時產生的資料包如下。

p41

上述全部語句執行時在oracle的v$sql檢視中產生的資料如下。

p42

0x04 其他問題


錯誤頁

在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時,需要判斷當前使用者是否已登入且具有相應許可權,防止出現越權使用。

0x05 後記


以上為本人的一點總結,難免存在錯誤之處,大牛請輕噴。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章