天天看點

java開發常見漏洞_詳解Javaweb中常見漏洞的防禦

上一篇給大家介紹了SpringMVC中常見的用戶端資料輸入點,這一篇給大家講解下java中常見漏洞的防禦方法。

0x01.sql注入

下面我們就用利用SpringMVC自帶的資料庫操作類jdbcTemplate舉例。比如下面Dao中有如下的兩個函數。

函數save使用的是綁定變量的形式很好的防止了sql注入,而queryForInt_函數接收id參數直接對sql語句進行了拼接,測試時出現sql注入。

public static void save(String username,String password) {

jdbcTemplate.update("insert into test_table(user_name,password) values(?,?)",

new Object[]{username,password});

}

public static int queryForInt_(String id){

return jdbcTemplate.queryForInt("select count(0) from test_table where id = " + id);

}

public static void save(String username,String password) {

jdbcTemplate.update("insert into test_table(user_name,password) values(?,?)",

new Object[]{username,password});

}

public static int queryForInt_(String id){

return jdbcTemplate.queryForInt("select count(0) from test_table where id = " + id);

}

#為了友善僅僅貼出了DAO層代碼

是以,在java代碼的開發過程中,我們盡量避免使用拼接sql語句的形式去執行資料庫語句。如果需要使用拼接sql語句的形式進行資料庫查詢,那麼OWASP提供了一個防禦sql注入的Esapi包,這個包中的encodeForSQL方法能對sql注入進行很好的防禦。

接着我們就分析下這個encodeForSQL方法。

首先我們介紹這個方法的使用,使用時調用如下,不同的資料庫使用不到的方法。

//防止Oracle注入

ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam)

//防止mysql注入

ESAPI.encoder().encodeForSQL(new MySQLCodec(Mode.STANDARD),queryparam) //Mode.STANDARK為标準的防注入方式,mysql一般用使用的是這個方式

//防止DB2注入

ESAPI.encoder().encodeForSQL(new DB2Codec(),queryparam)

//防止Oracle注入的方法例子,為了友善僅僅給出sql語句的拼接部分

Codec ORACLE_CODEC = new OracleCodec();

String query ="SELECT user_id FROM user_data WHERE user_name = ‘"+ESAPI.encoder().encodeForSQL(ORACLE_CODEC,req.getParameter("userID"))+"’ and user_password = ‘"+ESAPI.encoder().encodeForSQL(ORACLE_CODEC,req.getParameter("pwd"))+"’";

//防止Oracle注入

ESAPI.encoder().encodeForSQL(new OracleCodec(),queryparam)

//防止mysql注入

ESAPI.encoder().encodeForSQL(new MySQLCodec(Mode.STANDARD),queryparam) //Mode.STANDARK為标準的防注入方式,mysql一般用使用的是這個方式

//防止DB2注入

ESAPI.encoder().encodeForSQL(new DB2Codec(),queryparam)

//防止Oracle注入的方法例子,為了友善僅僅給出sql語句的拼接部分

Codec ORACLE_CODEC = new OracleCodec();

String query ="SELECT user_id FROM user_data WHERE user_name = ‘"+ESAPI.encoder().encodeForSQL(ORACLE_CODEC,req.getParameter("userID"))+"’ and user_password = ‘"+ESAPI.encoder().encodeForSQL(ORACLE_CODEC,req.getParameter("pwd"))+"’";

下面我們就用mysql為例字分析encodeForSQL函數做了什麼防禦。具體函數過程就不跟蹤了,直接分析最後調用了哪個方法。根據代碼可知最後調用的是encodeCharacter方法。

public String encodeCharacter( char[] immune, Character c ) {

char ch = c.charValue();

// check for immune characters

if ( containsCharacter( ch, immune ) ) {

return ""+ch;

}

// check for alphanumeric characters

String hex = Codec.getHexForNonAlphanumeric( ch );

if ( hex == null ) {

return ""+ch;

}

switch( mode ) {

case ANSI: return encodeCharacterANSI( c );

case STANDARD: return encodeCharacterMySQL( c );

}

return null;

}

public String encodeCharacter( char[] immune, Character c ) {

char ch = c.charValue();

// check for immune characters

if ( containsCharacter( ch, immune ) ) {

return ""+ch;

}

// check for alphanumeric characters

String hex = Codec.getHexForNonAlphanumeric( ch );

if ( hex == null ) {

return ""+ch;

}

switch( mode ) {

case ANSI: return encodeCharacterANSI( c );

case STANDARD: return encodeCharacterMySQL( c );

}

return null;

}

上述方法中containsCharacter函數是不進行驗證的字元串白名單,Codec.getHexForNonAlphanumeric函數查找字元傳中是否有16進制,沒有傳回空值。

而encodeCharacterANSI和encodeCharacterMySQL才是防禦的重點,我們看一下這兩個函數的不同,如果選擇的我們選擇是Mode.ANSi模式,則字元串則進入下面的函數,可以看到這個函數對單撇号和雙撇号進行了轉義。

private String encodeCharacterANSI( Character c ) {

if ( c == '\'' )

return "\'\'";

if ( c == '\"' )

return "";

return ""+c;

}

private String encodeCharacterANSI( Character c ) {

if ( c == '\'' )

return "\'\'";

if ( c == '\"' )

return "";

return ""+c;

}

如果選擇的是Mode.STANDARD模式,則字元串則進入下面的函數,可以看到這個函數對單撇号和雙撇号、百分号、反斜線等更多的符号進行了轉換,是以使用時推薦使用标準模式。

private String encodeCharacterMySQL( Character c ) {

char ch = c.charValue();

if ( ch == 0x00 ) return "\\0";

if ( ch == 0x08 ) return "\\b";

if ( ch == 0x09 ) return "\\t";

if ( ch == 0x0a ) return "\\n";

if ( ch == 0x0d ) return "\\r";

if ( ch == 0x1a ) return "\\Z";

if ( ch == 0x22 ) return "\\\"";

if ( ch == 0x25 ) return "\\%";

if ( ch == 0x27 ) return "\\'";

if ( ch == 0x5c ) return "\\\\";

if ( ch == 0x5f ) return "\\_";

return "\\" + c;

}

private String encodeCharacterMySQL( Character c ) {

char ch = c.charValue();

if ( ch == 0x00 ) return "\\0";

if ( ch == 0x08 ) return "\\b";

if ( ch == 0x09 ) return "\\t";

if ( ch == 0x0a ) return "\\n";

if ( ch == 0x0d ) return "\\r";

if ( ch == 0x1a ) return "\\Z";

if ( ch == 0x22 ) return "\\\"";

if ( ch == 0x25 ) return "\\%";

if ( ch == 0x27 ) return "\\'";

if ( ch == 0x5c ) return "\\\\";

if ( ch == 0x5f ) return "\\_";

return "\\" + c;

}

我們介紹了利用綁定變量和利用esapi兩種方式對sql注入進行防禦,我的建議是盡量使用綁定變量的是形式進行防注入,安全性能都比較好。

0x02:跨站腳本攻擊

關于跨站腳本攻擊的防禦,我們分析esapi的防禦方式。

esapi的防禦方式是根據輸出點的不同在不同的輸出點進行相應的編碼。我們看一下使用方法:

xss輸出點在html網頁中

ESAPI.encoder().encodeForHTML(String input)

xss輸出點在html屬性中

ESAPI.encoder().encodeForHTMLAttribute(String input)

xss輸出點在JavaScript代碼中

ESAPI.encoder().encodeForJavaScript(String input)

xss輸出點在CSS代碼中

ESAPI.encoder().encodeForCSS(String input)

xss輸出點在VBScript代碼中

ESAPI.encoder().encodeForVBScript(String input)

xss輸出點在XPath中

ESAPI.encoder().encodeForXPath(String input)

xss輸出點在XML中

ESAPI.encoder().encodeForXML(String input)

xss輸出點在XML屬性中

ESAPI.encoder().encodeForXMLAttribute(String input)

直接對url進行URL編碼

ESAPI.encoder().encodeForURL(String input)

xss輸出點在html網頁中

ESAPI.encoder().encodeForHTML(String input)

xss輸出點在html屬性中

ESAPI.encoder().encodeForHTMLAttribute(String input)

xss輸出點在JavaScript代碼中

ESAPI.encoder().encodeForJavaScript(String input)

xss輸出點在CSS代碼中

ESAPI.encoder().encodeForCSS(String input)

xss輸出點在VBScript代碼中

ESAPI.encoder().encodeForVBScript(String input)

xss輸出點在XPath中

ESAPI.encoder().encodeForXPath(String input)

xss輸出點在XML中

ESAPI.encoder().encodeForXML(String input)

xss輸出點在XML屬性中

ESAPI.encoder().encodeForXMLAttribute(String input)

直接對url進行URL編碼

ESAPI.encoder().encodeForURL(String input)

如果java輸出在html頁面,使用如下示例的方法即可。

String username = ESAPI.encoder().encodeForHTML(req.getParameter("name"))

String username = ESAPI.encoder().encodeForHTML(req.getParameter("name"))

接下來我們就研究這個方法的具體實作。

public String encodeCharacter( char[] immune, Character c ) {

// check for immune characters

if ( containsCharacter(c, immune ) ) {

return ""+c;

}

// check for alphanumeric characters

String hex = Codec.getHexForNonAlphanumeric(c);

if ( hex == null ) {

return ""+c;

}

// check for illegal characters

//ascii碼中的非數字,字元的編碼,一般為非列印字元,即不能轉換的為uncoide的ascii字元,直接替換成\ufffd,顯示的為?

if ( ( c <= 0x1f && c != '\t' && c != '\n' && c != '\r' ) || ( c >= 0x7f && c <= 0x9f ) )

{

hex = REPLACEMENT_HEX;  // Let's entity encode this instead of returning it

c = REPLACEMENT_CHAR;

}

// check if there's a defined entity

//#惡意字元以實體的形式輸出

String entityName = (String) characterToEntityMap.get(c);

if (entityName != null) {

return "&" + entityName + ";";

}

// return the hex entity as suggested in the spec,#如果是16進制就轉換為html16進制實體字元輸出

return "" + hex + ";";

}

public String encodeCharacter( char[] immune, Character c ) {

// check for immune characters

if ( containsCharacter(c, immune ) ) {

return ""+c;

}

// check for alphanumeric characters

String hex = Codec.getHexForNonAlphanumeric(c);

if ( hex == null ) {

return ""+c;

}

// check for illegal characters

//ascii碼中的非數字,字元的編碼,一般為非列印字元,即不能轉換的為uncoide的ascii字元,直接替換成\ufffd,顯示的為?

if ( ( c <= 0x1f && c != '\t' && c != '\n' && c != '\r' ) || ( c >= 0x7f && c <= 0x9f ) )

{

hex = REPLACEMENT_HEX; // Let's entity encode this instead of returning it

c = REPLACEMENT_CHAR;

}

// check if there's a defined entity

//#惡意字元以實體的形式輸出

String entityName = (String) characterToEntityMap.get(c);

if (entityName != null) {

return "&" + entityName + ";";

}

// return the hex entity as suggested in the spec,#如果是16進制就轉換為html16進制實體字元輸出

return "" + hex + ";";

}

containsCharacter函數一般定義不需要編碼的字元,如果我們不想編碼那個字元就可以利用這個函數定義。

Codec.getHexForNonAlphanumeric函數判斷是否是數字和字母,如果僅僅是字元和字母就直接傳回,不在編碼。

剩下的代碼是對非數字和字母的字元進行實體編碼或者html十六進制字元編碼。

如果java輸出在js代碼頁面,使用如下示例的方法即可。

String username = ESAPI.encoder().encodeForJavaScript(req.getParameter("name"))

String username = ESAPI.encoder().encodeForJavaScript(req.getParameter("name"))

我們研究一下encodeForJavaScript方法:

public String encodeCharacter( char[] immune, Character c ) {

// check for immune characters

if ( containsCharacter(c, immune ) ) {

return ""+c;

}

// check for alphanumeric characters

String hex = Codec.getHexForNonAlphanumeric(c);

if ( hex == null ) {

return ""+c;

}

// Do not use these shortcuts as they can be used to break out of a context

// if ( ch == 0x00 ) return "\\0";

// if ( ch == 0x08 ) return "\\b";

// if ( ch == 0x09 ) return "\\t";

// if ( ch == 0x0a ) return "\\n";

// if ( ch == 0x0b ) return "\\v";

// if ( ch == 0x0c ) return "\\f";

// if ( ch == 0x0d ) return "\\r";

// if ( ch == 0x22 ) return "\\\"";

// if ( ch == 0x27 ) return "\\'";

// if ( ch == 0x5c ) return "\\\\";

// encode up to 256 with \\xHH,編碼成js十六進制的形式

String temp = Integer.toHexString(c);

if ( c 

String pad = "00".substring(temp.length() );

return "\\x" + pad + temp.toUpperCase();

}

// otherwise encode with \\uHHHH,編碼成jsunicode編碼格式

String pad = "0000".substring(temp.length() );

return "\\u" + pad + temp.toUpperCase();

}

public String encodeCharacter( char[] immune, Character c ) {

// check for immune characters

if ( containsCharacter(c, immune ) ) {

return ""+c;

}

// check for alphanumeric characters

String hex = Codec.getHexForNonAlphanumeric(c);

if ( hex == null ) {

return ""+c;

}

// Do not use these shortcuts as they can be used to break out of a context

// if ( ch == 0x00 ) return "\\0";

// if ( ch == 0x08 ) return "\\b";

// if ( ch == 0x09 ) return "\\t";

// if ( ch == 0x0a ) return "\\n";

// if ( ch == 0x0b ) return "\\v";

// if ( ch == 0x0c ) return "\\f";

// if ( ch == 0x0d ) return "\\r";

// if ( ch == 0x22 ) return "\\\"";

// if ( ch == 0x27 ) return "\\'";

// if ( ch == 0x5c ) return "\\\\";

// encode up to 256 with \\xHH,編碼成js十六進制的形式

String temp = Integer.toHexString(c);

if ( c < 256 ) {

String pad = "00".substring(temp.length() );

return "\\x" + pad + temp.toUpperCase();

}

// otherwise encode with \\uHHHH,編碼成jsunicode編碼格式

String pad = "0000".substring(temp.length() );

return "\\u" + pad + temp.toUpperCase();

}

惡意字元在js中的編碼大家可以看到使用的是js的十六進制編碼或者jsunicode編碼進行的編碼。

其實上面的方法大都是對字元進行html實體編碼,html十六進制編碼,js十六進制編碼,jsunicode的編碼和url編碼來防止惡意标簽的執行。如果感興趣可以看一下其他的編碼方法,原理大緻相同就不在一一介紹。