JDBC對SQL注射的防禦
SQL注射爲何會產生,對於我來說,則會將其總結爲一句話:“被動態拼接執行的SQL語句中包含了不可信任的數據。”
什麼是動態拼接?看看下面這條SQL語句:
select * from "+param_table+" where name='"+param_name+"'";
看到語句中的‘+’號了麼,這意味着param_table和param_name並不是寫死在語句中的,而我可以對其進行傳參從而達到我的某些目的。
那麼假如我有student表:
teacher表:
我想從中查詢hacker的信息
那麼將有如下代碼:
String param_table = "student";
String param_name = "hacker";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from "+param_table+" where name='"+param_name+"'");
while(rs.next()) {
out.println(rs.getString(1)+"/"+
rs.getString(2)+"/"+
rs.getString(3));
}
於是構成了這樣一條語句:
select * from student where name=’hacker’;
這樣就可以查詢到hacker的信息:
但是如果我將hacker修改爲hacker’ or 1=1#:
String param_name = “hacker’ or 1=1#”;
則student表中所有數據被dump出來:
接着也可以將student修改爲student union select * from teacher,於是連同teacher表的數據也被dump出來:
那該如何防護?這是重點,我以前挖SQL注入的時候,僅僅是給廠商提供了這樣的建議,但對於廠商來說可能只是極其模糊的概念:
現在我寫下實例,以便同時也加深自己對SQL注入的理解。
1.預編譯:
這裏用到PreparedStatement類進行預編譯,那麼將有如下代碼:
String param_table = "student";
String param_name = "hacker";
String stmt = "select * from ? where name= ?";
PreparedStatement ps = conn.prepareStatement(stmt);
ps.setString(1,param_table);
ps.setString(2,param_name);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
out.println(rs.getString(1)+"/"+
rs.getString(2)+"/"+
rs.getString(3));
接着運行卻出現了錯誤:
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''student' where name= 'hacker'' at line 1
最後經過調試發現param_table不能被綁定,並且發現字段名也不能被綁定,那麼可能會用拼接的方式進行預編譯再查詢,代碼如下:
String param_table = "student";
String param_name = "hacker";
PreparedStatement ps = conn.prepareStatement("select * from "+param_table+" where name=?");
ps.setString(1,param_name);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
out.println(rs.getString(1)+"/"+
rs.getString(2)+"/"+
rs.getString(3));
}
但是param_table=student這裏依舊產生了注入,如果修改爲:
String param_table = "student union select * from teacher";
則:
這個注入比較奇葩點,where子句被拼接到查詢teacher表的語句上:
那麼我只能把student寫死在語句中:
String param_name = "hacker";
String stmt = "select * from student where name=?";
PreparedStatement ps = conn.prepareStatement(stmt);
ps.setString(1,param_name);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
out.println(rs.getString(1)+"/"+
rs.getString(2)+"/"+
rs.getString(3));
}
此時再將param_name修改爲hacker’ or 1=1#:
則會將hacker’ or 1=1#當做表名來查詢,查不到這個表,當然無回顯了:
2.存儲過程:
有這樣一個對student表操作的存儲過程:
create procedure `getstudent`(in aname varchar(20),out uname varchar(20),out uage int(11),out usex varchar(10))
begin
select * from student where name=aname into uname,uage,usex;
end;
那麼我們可以用CallableStatement類來防止注入,代碼如下:
String param_name = "hacker’ or 1=1#";
CallableStatement cs = conn.prepareCall("{call getstudent(?,?,?,?)}");
cs.setString(1,param_name);
cs.registerOutParameter(2,Types.VARCHAR);
cs.registerOutParameter(3,Types.INTEGER);
cs.registerOutParameter(4,Types.VARCHAR);
cs.executeQuery();
out.println(cs.getString(2)+"/"+
cs.getInt(3)+"/"+
cs.getString(4));
可以看到SQL注入的語句已經不再起作用:
3.白名單驗證:
前面的預編譯和存儲過程不能對錶名進行操作,那麼這裏用白名單對錶名進行過濾,代碼如下:
String param_table = "student union select * from teacher";
String param_name = "hacker";
String stmt = "";
if(param_table.equals("student")) {
stmt = "select * from student where name=?";
}
else if(param_table.equals("teacher")) {
stmt = "select * from teacher where name=?";
}
else {
out.println("table name error!");
}
PreparedStatement ps = conn.prepareStatement(stmt);
ps.setString(1,param_name);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
out.println(rs.getString(1)+"/"+
rs.getString(2)+"/"+
rs.getString(3));
}
則會報錯:
4.對輸入進行編碼
這裏我使用十六進制對輸入進行編碼,方法的聲明及定義代碼如下:
public static String bytestoHex(byte[] byteArr) {
if(byteArr == null || byteArr.length < 1) return "";
StringBuilder sb = new StringBuilder();
for(byte t : byteArr) {
if((t & 0xF0) == 0) sb.append("0");
sb.append(Integer.toHexString(t & 0xFF));
}
return sb.toString().toUpperCase();
}
使用方法byte2HexStr對輸入param_name進行編碼,代碼:
String param_name = "hacker' or 1=1#";
Statement stmt = conn.createStatement();
String hex_param_name = bytestoHex(param_name.getBytes());
out.println("編碼後的param_name爲:"+bytestoHex(param_name.getBytes()));
ResultSet rs = stmt.executeQuery("select * from student where hex(name)='"+hex_param_name+"'");
while(rs.next()) {
out.println(rs.getString(1)+"/"+
rs.getString(2)+"/"+
rs.getString(3));
}
由於hacker’ or 1=1#被編碼爲6861636B657227206F7220313D3123並被作爲表名進行查詢,因此不會dump出其他信息: