|
) U! c* ^6 B* q0 L6 x# O) L1 f
前言最近我在整理安全漏洞相关问题,准备在公司做一次分享恰好,这段时间团队发现了一个sql注入漏洞:在一个公共的分页功能中,排序字段作为入参,前端页面可以自定义在分页sql的mybatismapper.xml。
7 o% A/ K& c) g3 F 中,order by字段后面使用$符号动态接收计算后的排序参数,这样可以实现动态排序的功能但是,如果入参传入:id; select 1 --' x7 |; M3 L$ U4 Z
最终执行的sql会变成:select * from user order by id; select 1 -- limit 1,20
8 ~2 |2 {! k+ l/ x 。 & }8 ?/ O2 o7 X: T. L; x
--会把后面的limit语句注释掉,导致分页条件失效,返回了所有数据攻击者可以通过这个漏洞一次性获取所有数据动态排序这个功能原本的想法是好的,但是却有sql注入的风险值得庆幸的是,这次我们及时发现了问题,并且及时解决了,没有造成什么损失。 # H! l `" U6 Y
但是,几年前在老东家的时候,就没那么幸运了一次sql注入直接把我们支付服务搞挂了1. 还原事故现场有一天运营小姐姐跑过来跟我说,有很多用户支付不了这个支付服务是一个老系统,转手了3个人了,一直很稳定没有出过啥问题。 & c& W* Q6 F% C: U
我二话不说开始定位问题了,先看服务器日志,发现了很多报数据库连接过多的异常因为支付功能太重要了,当时为了保证支付功能快速恢复,先找运维把支付服务2个节点重启了5分钟后暂时恢复了正常我再继续定位原因,据我当时的经验判断一般出现数据库连接过多,可能是因为。 9 l% G. ?- |: B+ _
连接忘了关闭导致但是仔细排查代码没有发现问题,我们当时用的数据库连接池,它会自动回收空闲连接的,排除了这种可能过了会儿,又有一个节点出现了数据库连接过多的问题但此时,还没查到原因,逼于无奈,只能让运维再重启服务,不过这次把数据库。
7 Z! X" f# b$ Z8 q' F1 V% _; {- r 最大连接数调大了,默认是100,我们当时设置的500,后面调成了1000(其实现在大部分公司会将这个参数设置成1000)使用命令:setGLOBAL max_connections=500;* Y8 P4 E9 L/ m+ I
能及时生效,不需要重启mysql服务。
: R8 c! ^$ D. ~% D! j: L1 K 这次给我争取了更多的时间,找dba帮忙一起排查原因。使用show processlist;命令查看当前线程执行情况:
4 E$ ~/ S9 |( j 还可以查看当前的连接状态帮助识别出有问题的查询语句(需要特别说明的是上图只是我给的一个例子,线上真实的结果不是这样的)id 线程idUser 执行sql的账号Host 执行sql的数据库的ip和端号db 数据库名称 8 U" n2 M& s0 Y3 |' a9 D
Command 执行命令,包括:Daemon、Query、Sleep等Time 执行sql所消耗的时间State 执行状态info 执行信息,里面可能包含sql信息果然,发现了一条不寻常的查询sql,执行了差不多1个小时还没有执行完。
; p; I j; S2 V* C6 r3 { dba把那条sql复制出来,发给我了然后kill -9 杀掉了那条执行耗时非常长的sql线程后面,数据库连接过多的问题就没再出现了我拿到那条sql仔细分析了一下,发现一条订单查询语句被攻击者注入了很长的一段sql,肯定是高手写的,有些语法我都没见过。 $ s. u0 C4 G3 A/ t1 {
但可以确认无误,被人sql注入了通过那条sql中的信息,我很快找到了相关代码,查询数据时入参竟然用的Statment,而非PrepareStatement预编译机制知道原因就好处理了,将查询数据的地方改成。 ' r( ~7 a) I" Q" @
preparestatement预编译机制后问题得以最终解决2.为什么会导致数据库连接过多?我相信很多同学看到这里,都会有一个疑问:sql注入为何会导致数据库连接过多?我下面用一张图,给大家解释一下: - W+ `# [1 p; y7 \7 J- f
攻击者sql注入了类似这样的参数:-1;锁表语句--其中;前面的查询语句先执行了由于--后面的语句会被注释,接下来只会执行锁表语句,把表锁住正常业务请求从数据库连接池成功获取连接后,需要操作表的时候,尝试获取表锁,但一直获取不到,直到超时。
, t* m0 F) B# A 注意,这里可能会累计大量的数据库连接被占用,没有及时归还数据库连接池不够用,没有空闲连接新的业务请求从数据库连接池获取不到连接,报数据库连接过多异常sql注入导致数据库连接过多问题,最根本的原因是长时间锁表。 5 k, @! ?+ }+ @! G; a% t$ _) h# ?* e9 x
3.预编译为什么能防sql注入?preparestatement预编译机制会在sql语句执行前,对其进行语法分析、编译和优化,其中参数位置使用占位符?代替了当真正运行时,传过来的参数会被看作是一个纯文本,不会重新编译,不会被当做sql指令。 , D; I8 m3 P' }$ x9 A1 l, j
这样,即使入参传入sql注入指令如:id; select 1 --+ R5 ^4 j A2 _0 T
最终执行的sql会变成:select * from user order by id; select 1 -- limit 1,20
! P4 H' ^# V1 T* t
' Z4 B( c. u: x6 \4 A n9 T 这样就不会出现sql注入问题了4.预编译就一定安全?不知道你在查询数据时有没有用过like语句,比如:查询名字中带有“苏”字的用户,就可能会用类似这样的语句查询:select * from user where name like %苏%;/ l8 Q. B8 l) `7 U( o9 @7 y
。 , e( h/ Q. D, `: S, M$ Q: `$ L
正常情况下是没有问题的但有些场景下要求传入的条件是必填的,比如:name是必填的,如果注入了:%,最后执行的sql会变成这样的:select * from user where name like %%%;& O+ E% ~0 \( h( j0 |; I9 [" y5 d
。 ! K5 u2 h" e3 I! |. n& i
这种情况预编译机制是正常通过的,但sql的执行结果不会返回包含%的用户,而是返回了所有用户name字段必填变得没啥用了,攻击者同样可以获取用户表所有数据为什么会出现这个问题呢?%在mysql中是关键字,如果使用。
6 H/ q4 }$ r; d3 ^4 ]2 t like %%%,该like条件会失效如何解决呢?需要对%进行转义:\%转义后的sql变成:select * from user where name like %\%%;
: s, Y) ^! X7 U! @) S 只会返回包含%的用户5.有些特殊的场景怎么办? % t6 I7 o- M9 C+ Z
在java中如果使用mybatis作为持久化框架,在mapper.xml文件中,如果入参使用#传值,会使用预编译机制一般我们是这样用的:
6 e V# x( ^7 u/ k! E! z0 @5 x select * fromuser9 M- ~$ A7 m5 X( v* ^2 F
5 G" {- F/ i. j! F+ N name = #{name}
4 y$ E; W' p2 { h Q' J
. `! q. p& B8 ]( u# X( `9 R j$ }8 S& N2 D( Z
。 4 j3 W4 v6 ^0 f4 _7 s
绝大多数情况下,鼓励大家使用#这种方式传参,更安全,效率更高但是有时有些特殊情况,比如:- K, M$ k6 T C$ i. k9 b. G1 ~
order by ${sortString}
. g+ J/ v6 V3 r' ?! ^3 c3 s! C- n3 A& j. W0 k5 z
sortString字段的内容是一个方法中动态计算出来的,这种情况是没法用。
( l' i6 P: g7 x" H7 c) i1 w #,代替$的,这样程序会报错使用$的情况就有sql注入的风险那么这种情况该怎办呢?自己写个util工具过滤掉所有的注入关键字,动态计算时调用该工具如果数据源用的阿里的druid的话,可以开启filter中的wall(防火墙),它包含了防止sql注入的功能。
u3 J$ m8 l3 z% p 但是有个问题,就是它默认不允许多语句同时操作,对批量更新操作也会拦截,这就需要我们自定义filter了6.表信息是如何泄露的?有些细心的同学,可能会提出一个问题:在上面锁表的例子中,攻击者是如何拿到表信息的?
# v* A G P% D9 H: c 方法1:盲猜就是攻击者根据常识猜测可能存在的表名称假设我们有这样的查询条件:select * from t_order where id = ${id};
: {1 r0 U) v4 o& h& \+ z 传入参数:-1;select * from user。 # K$ O) b4 u# e2 c: |
最终执行sql变成:select * from t_order where id = -1; select * from user;6 n+ @' U* j& M# Q
如果该sql有数据返回,说明user表存在,被猜中了建议表名不要起得过于简单,可以带上适当的前缀,比如:t_user。
" n, c j8 n" [4 g8 {" N3 i' f6 { 这样可以增加盲猜的难度方法2:通过系统表其实mysql有些系统表,可以查到我们自定义的数据库和表的信息假设我们还是以这条sql为例:select code,name from t_order where id = ${id};6 C& H+ c3 c. ^9 q( f8 L1 L
。 6 v0 y* H% I" ?8 @7 _4 y! H- t! V t
第一步,获取数据库和账号名传参为:-1 union select database(),user()#最终执行sql变成:select code,name from t_order where id = -1 union select database(),user()#$ b- f3 G1 X3 }# U$ s9 ^$ z
。
; e2 c' C2 Z; t/ ~$ z4 }4 p( z( l; ~ 会返回当前 数据库名称:sue 和 账号名称:root@localhost。
" |6 {+ Y0 `$ Q5 R1 ~ 第二步,获取表名传参改成:-1 union select table_name,table_schema from information_schema.tables where table_schema=sue#。 4 W8 K! y8 r; B! o' U" ^2 F0 E
最终执行sql变成:select code,name from t_order where id = -1 union select table_name,table_schema from information_schema.tables where table_schema=sue#
( J2 o6 Z" Y7 z/ W # U) i* P& Z0 D4 H* {) V) ?$ T
会返回数据库sue下面所有表名。
; d( R6 |% b- A& ?! U: x 建议在生成环境程序访问的数据库账号,要跟管理员账号分开,一定要控制权限,不能访问系统表7.sql注入到底有哪些危害?1. 核心数据泄露大部分攻击者的目的是为了赚钱,说白了就是获取到有价值的信息拿出去卖钱,比如:用户账号、密码、手机号、身份证信息、银行卡号、地址等敏感信息。 $ D+ V! V; h ~" i k
他们可以注入类似这样的语句:-1; select * from user; --
& b+ S" H' J3 s" x: ^- @ 就能轻松把用户表中所有信息都获取到所以,建议大家对这些敏感信息加密存储,可以使用AES对称加密2. 删库跑路也不乏有些攻击者不按常理出牌,sql注入后直接把系统的表或者数据库都删了。 , [% b; H4 S! z( R- @
他们可以注入类似这样的语句:-1; delete from user; --
* ?2 Q$ u6 ^. r$ o 以上语句会删掉user表中所有数据-1; drop database test; --
/ N$ c1 i+ R! d 以上语句会把整个test数据库所有内容都删掉。 * F' s6 G6 L# r4 B
正常情况下,我们需要控制线上账号的权限,只允许DML(data manipulation language)数据操纵语言语句,包括:select、update、insert、delete等不允许DDL(data definition language)数据库定义语言语句,包含:create、alter、drop等。 ! l4 s; B6 J% I5 L
也不允许DCL(Data Control Language)数据库控制语言语句,包含:grant,deny,revoke等DDL和DCL语句只有dba的管理员账号才能操作顺便提一句:如果被删表或删库了,其实还有补救措施,就是从备份文件中恢复,可能只会丢失少量实时的数据,所以一定有备份机制。
, \# T2 f8 [# [/ a6 m* U# U 3. 把系统搞挂有些攻击者甚至可以直接把我们的服务搞挂了,在老东家的时候就是这种情况他们可以注入类似这样的语句:-1;锁表语句;--
( k# h- Y' f# b3 ?+ q 把表长时间锁住后,可能会导致数据库连接耗尽这时,我们需要对数据库线程做监控,如果某条sql执行时间太长,要邮件预警。
/ n0 M1 W! D% J" ?/ L+ c 此外,合理设置数据库连接的超时时间,也能稍微缓解一下这类问题从上面三个方面,能看出sql注入问题的危害真的挺大的,我们一定要避免该类问题的发生,不要存着侥幸的心理如果遇到一些不按常理出票的攻击者,一旦被攻击了,你可能会损失惨重。 . p6 V V) h1 }4 p G
8. 如何防止sql注入?1. 使用预编译机制尽量用预编译机制,少用字符串拼接的方式传参,它是sql注入问题的根源2. 要对特殊字符转义有些特殊字符,比如:%作为like语句中的参数时,要对其进行转义处理。
" K* @! _; `8 ?" v 3. 要捕获异常需要对所有的异常情况进行捕获,切记接口直接返回异常信息,因为有些异常信息中包含了sql信息,包括:库名,表名,字段名等攻击者拿着这些信息,就能通过sql注入随心所欲的攻击你的数据库了目前比较主流的做法是,有个专门的网关服务,它统一暴露对外接口。 # M# Q* r8 k! p7 I8 A5 ]. _7 h
用户请求接口时先经过它,再由它将请求转发给业务服务这样做的好处是:能统一封装返回数据的返回体,并且如果出现异常,能返回统一的异常信息,隐藏敏感信息此外还能做限流和权限控制4. 使用代码检测工具使用sqlMap等代码检测工具,它能检测sql注入漏洞。
4 _( C6 h8 ?6 i* U5 W" y 5. 要有监控需要对数据库sql的执行情况进行监控,有异常情况,及时邮件或短信提醒6. 数据库账号需控制权限对生产环境的数据库建立单独的账号,只分配DML相关权限,且不能访问系统表切勿在程序中直接使用管理员账号。 : g1 t6 g$ O9 f: O% n
7. 代码review建立代码review机制,能找出部分隐藏的问题,提升代码质量8. 使用其他手段处理对于不能使用预编译传参时,要么开启druid的filter防火墙,要么自己写代码逻辑过滤掉所有可能的注入关键字。
7 r+ r9 x& h. n r8 J: K 最后说一句(求关注,别白嫖我)如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力。传统美德不能丢,记得点个赞同喔 $ b- H7 [- H" L( d. m
+ H4 }2 o" Y# x3 A/ p/ g
2 r: h- U1 u$ {, j# t+ c) }
5 O# C! D9 ^) Q8 U7 N2 J5 ?
, o x! \7 k3 z: o* d |