窥探SQL预编译内幕
Author : kj021320
TEAM : I.S.T.O
Author_blog: http://blog.csdn.net/kj021320
前言套话:
本来文章打算昨天就写出来,环境没搭好… 迟来的祝福~Mickey 生日快乐!
首先感谢PT007竭力帮我搭环境,可惜最后还是没能用上,还有ISTO团队的幕后成员,AMXSA,SUMMER等…还有我几位好朋友axis,suddy,larry 春节提前快乐!哈!
现在部分大企业 都采用DOTNET或者J2EE,迟点应该都是RubyOnRails,之后的技术越来越成熟,在SQL操作无疑都使用绑定变量,有些人叫预编译,也有些人叫同构SQL,参数追加…
像dotnet j2ee 上这样的使用几乎成了习惯,规范!
早期的ADODB中也一样有参数绑定的方式,只是少人使用Adodb.Command。
PHP后期也推出了MYSQL扩展了mysqli_prepare函数,而对Oracle OCI早早就有提供了
那么到底他们是如何进行操作,到底还会不会存在SQL注射的呢?
首先从数据库技术角度去看这个,几乎是百利而无一害的!(排除某些特殊情况)
OK先来思想教育一下~~
每个数据库中 都有自己的SQL引擎,为什么PostGreSQL MSSQL SYBASE可以执行多语句,而MYSQL ORACLE DB2就不行呢?就是因为他们引擎都有自己的实现方式,各有各的特点优势。
首先把SQL语句读入引擎,然后语法分析,有些是解析 有些是编译(预编译就是这里来)
解析我就不多说了!例如JET的,SQLite的…
那么编译呢?SQL引擎会把整个 语句的结构取出来,然后如果发现有参数的地方就会拿变量代替!整个结构编译为 该数据库能识别的执行指令,存储在SQL缓存池里面
例子
select * from ISTOMEMBER where membername=’kj021320’
这样的语句就好比 一般的C语言语句
if(ISTOMember.membername==”kj021320”)printf(“*”);
以上C代码非常不灵活,如果我换一个判断把kj021320改为amxsa,那你得从新编译生成EXE
现在用预编译,以变量的方式使用
select * from ISTOMEMBERN where membername=?
转为我们熟悉的C
if(ISTOMember.membername==name)println(“*”);
现在应该很好理解了吧?用预编译之后每次再使用同一个语句,只需要换一下条件就OK了,就是上述C语言代码里面的name变量。所以免去了语法分析,优化,编译,这些操作,使则数据库执行非常快…
那么到底我们提交SQL语句中,驱动是做了哪些手脚呢?
现在我来揭晓这个迷!本来打算DB2 PostGreSQL Informix那些数据库都拉上来分析的!后来因为机器环境问题,没下文了!……..
我对MYSQL进行分析,第一个拿它开刀是因为他的驱动包开源,而且MYSQL在它们当中比较小
现在我先稿写一个简单调用… 然后跟踪进去
import java.io.*;
import java.sql.*;
public class SQLtrack {
public static void main(String[] args) throws Exception{
Class.forName("com.mysql.jdbc.Driver");
Connection con=DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "kj021320", "I.S.T.O");
PreparedStatement ps=null ;
ps=con.prepareStatement("select * from user where username=?");
ps.setString(1, "hello kj"); //加入断点 跟踪进去
ps.executeQuery();
ps.close();
con.close();
}
}
package com.mysql.jdbc;
public class PreparedStatement extends com.mysql.jdbc.Statement implements
java.sql.PreparedStatement {
public void setString(int parameterIndex, String x) throws SQLException {
// if the passed string is null, then set this column to null
if (x == null) {
setNull(parameterIndex, Types.CHAR);
} else {
checkClosed();
int stringLength = x.length();
if (this.connection.isNoBackslashEscapesSet()) {
// Scan for any nasty chars
boolean needsHexEscape = false;
for (int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch (c) {
case 0: /* Must be escaped for 'mysql' */
needsHexEscape = true;
break;
case '\n': /* Must be escaped for logs */
needsHexEscape = true;
break;
case '\r':
needsHexEscape = true;
break;
case '\\':
needsHexEscape = true;
break;
case '\'':
needsHexEscape = true;
break;
case '"': /* Better safe than sorry */
needsHexEscape = true;
break;
case '\032': /* This gives problems on Win32 */
needsHexEscape = true;
break;
}
if (needsHexEscape) {
break; // no need to scan more
}
}
if (!needsHexEscape) {
byte[] parameterAsBytes = null;
StringBuffer quotedString = new StringBuffer(x.length() + 2);
quotedString.append('\'');
quotedString.append(x);
quotedString.append('\'');
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(quotedString.toString(),
this.charConverter, this.charEncoding,
this.connection.getServerCharacterEncoding(),
this.connection.parserKnowsUnicode());
} else {
// Send with platform character encoding
parameterAsBytes = quotedString.toString().getBytes();
}
setInternal(parameterIndex, parameterAsBytes);
} else {
byte[] parameterAsBytes = null;
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(x,
this.charConverter, this.charEncoding,
this.connection.getServerCharacterEncoding(),
this.connection.parserKnowsUnicode());
} else {
// Send with platform character encoding
parameterAsBytes = x.getBytes();
}
setBytes(parameterIndex, parameterAsBytes);
}
return;
}
StringBuffer buf = new StringBuffer((int) (x.length() * 1.1));
buf.append('\'');
//
// Note: buf.append(char) is _faster_ than
// appending in blocks, because the block
// append requires a System.arraycopy()….
// go figure…
//
for (int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch (c) {
case 0: /* Must be escaped for 'mysql' */
buf.append('\\');
buf.append('0');
break;
case '\n': /* Must be escaped for logs */
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '"': /* Better safe than sorry */
if (this.usingAnsiMode) {
buf.append('\\');
}
buf.append('"');
break;
case '\032': /* This gives problems on Win32 */
buf.append('\\');
buf.append('Z');
break;
default:
buf.append(c);
}
}
buf.append('\'');
String parameterAsString = buf.toString();
byte[] parameterAsBytes = null;
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(parameterAsString,
this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode());
} else {
// Send with platform character encoding
parameterAsBytes = parameterAsString.getBytes();
}
setInternal(parameterIndex, parameterAsBytes);
}
}
}
以上 有颜色的代码块就是 进行替换的操作,很明显MYSQL没有SQL缓存池,每提交1条语句数据库服务器就得从新编译,然后执行!哈!所以MYSQL慢哈!…这个 预编译的类有点欺骗的感觉…总的来说!替换交给数据库!大家注意到在这个for语句前后 都有这个东西吗?
buf.append('\''); 就是如果你是字符串 他会帮你加入这个',
那么如果是PreparedStatement的 setInt方法呢?
这个不用说了吧!直接类型转换为数值!如果有注射那就给卡住了
OK~ 继续往下一个数据库的分析 MSSQL
再写了一个差不多的方法贴出来
import java.sql.*;
public class SQLtrack {
public static void main(String[] args) throws Exception{
Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
Connection con=DriverManager.getConnection("jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseName=master", "isto-team", "kj021320");
PreparedStatement ps=null ;
ps=con.prepareStatement("select * from sysobjects where name=?");
ps.setString(1, "aaa");
ps.executeQuery();
ps.close();
con.close();
}
}
这里MSSQL JDBC驱动是没有开源的,所以我就不跟踪代码了!换一个方式!我们跟踪数据库的SQL,MSSQL提供了profiler工具可以直接跟踪 这样省事了!哈!如果变量为字符串的,MSSQL-JDBC驱动还会像MYSQL那样负责把escape字符转换掉的。那到底MSSQL驱动会转换为什么样的预编译语句呢?看下图
我把语句复制出来了给看不到图片的朋友…
Exec sp_executesql N’select * from sysobjects where name=@P1’,N’@P1 nvarchar(4000)’,N’aaa’
好了!到了最后的ORACLE了!这个分析起来比较复杂
看下面代码
import java.sql.*;
public class SQLtrack {
public static void main(String[] args) throws Exception{
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection con=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:orcl","isto-team","kj021320");
PreparedStatement ps=con.prepareStatement("select * from all_tables where table_name=?");
ps.setString(1, "table_name");
ps.executeQuery();
ps.close();
con.close();
}
}
//再写一个测试类同样的方式 , 有颜色的地方就是需要跟踪进去的
/*
这里一个小插曲
ORACLE的JDBC驱动class12.jar 是编译好的class文件!一般看不到原代码的
需要用jad反编译,然后需要修改好些部分才可以从新编译运行
经过我一翻修改终于 能跑起来…待会我会提供整个驱动java文件下载,大家有兴趣可以分析一下人家是怎么实现SOCKET连接数据库提交SQL的
*/
//言归正转 现在来继续跟踪下面的函数调用
private PreparedStatement privatePrepareStatement(String s, String s1, int i, int j)throws SQLException{
if(s1 == null && s == null || s == "")DBError.throwSqlException(104);
checkPhyiscalStatus();
if(closed)DBError.throwSqlException(8);
Object obj = null;
if(logicalHandle && m_opc.isStatementCacheInitialized()){
obj = m_opc_oc.privatePrepareStatement(s, s1, i, j);
} else {
int k = 0;//OracleStatement.DEFAULT_RSET_TYPE;
if(i != -1 || j != -1)k = ResultSetUtil.getRsetTypeCode(i, j);
if(statementCache != null)
if(s1 != null)obj = (OraclePreparedStatement)statementCache.searchExplicitCache(s1);
else obj = (OraclePreparedStatement)statementCache.searchImplicitCache(s, 1, k);
if((statementCache == null || s1 == null) && obj == null)
if(i != -1 || j != -1)obj = new OraclePreparedStatement(this, s, default_batch, default_row_prefetch, i, j);
else obj = new OraclePreparedStatement(this, s, default_batch, default_row_prefetch);
}
return ((PreparedStatement) (obj));
}
//着色的地方是 需要跟踪进去的地方,继续
public OraclePreparedStatement(OracleConnection oracleconnection, String s, int i, int j, int k, int l)
throws SQLException
{
super(oracleconnection, i, j, k, l);
check_bind_types = true;
has_ref_cursors = false;
m_batchStyle = 0;
super.statementType = 1;
super.need_to_parse = true;
has_ref_cursors = false;
prepare_for_new_result(true);
super.sql_query = s;
super.m_originalSql = s;
super.clear_params = true;
m_binds = null;
m_scrollRsetTypeSolved = false;
premature_batch_count = 0;
super.binds_in = super.connection.db_access.createDBDataSet(oracleconnection, this, i, 1);
parseSqlKind();
if(oracleconnection.db_access.getVersionNumber() >= 8000)
{
min_binary_stream_size = 2000;
min_ascii_stream_size = 4000;
} else
{
min_binary_stream_size = 255;
min_ascii_stream_size = 2000;
}
}
//一大陀, 郁闷啊!下面还有好多调用!呢!我就不贴出来了!哈!
//不然会浪费收藏本文章人事的硬盘
//直接给出最终调用吧!
oracle.jdbc.driver.OracleSql
这个类 大家有兴趣可以分析一下~,最后他在内部把 ? 替换为单个单个的变量
上述语句 他替换为 ORACLE独有的变量绑定
替换前
select * from all_tables where table_name=?
替换后
select * from all_tables where table_name=:1
不过这里有个败北的就是不知道它怎么对这个 :1 的变量进行追加参数,以下是我跟踪ORACLE SQL缓存池的语句
select a.address address,s.hash_value hash_value,s.piece piece,s.sql_text sql_text,u.username
parsing_user_id,c.username parsing_schema_id from v$sqlarea a,v$sqltext_with_newlines
s,dba_users u,dba_users c where a.address=s.address and
a.hash_value=s.hash_value and a.parsing_user_id=u.user_id and
a.parsing_schema_id=c.user_id and exists (select 'x' from v$sqltext_with_newlines
x where x.address=a.address and x.hash_value=a.hash_value and upper(x.sql_text) like
'%TABLE_NAME%')order by 1,2,3
执行之后可以看到我的预编译语句结构,但是同样也没有发现追加进去的参数写到哪里!
汗!看来我的技术还没有修炼到家,闭关去…
哦对了~ ORACLE-JDBC驱动的源代码 可编译的 我会上传到
http://blog.csdn.net/I_S_T_O/ 下载资源区
全文完