当前位置: 首页 > 科技观察

SQL注入居然让我们的系统崩溃了

时间:2023-03-14 14:43:35 科技观察

本文转载自微信公众号《苏三说技术》,作者因热爱坚持ing。转载本文请联系苏三硕科技公众号。前言最近在整理安全漏洞相关的问题,准备分享到公司里。恰好在这段时间团队发现了一个sql注入漏洞:在一个公共的分页函数中,将排序字段作为入参,可以自定义前端页面。在分页sql的mybatismapper.xml中,使用orderby字段后面的$符号,动态接收计算出的排序参数,从而实现动态排序功能。但是,如果传入参数:id;select1--最终执行的sql会变成:select*fromuserorderbyid;select1--limit1,20--会注释掉后面的limit语句,导致分页条件失败返回所有数据。攻击者可以利用该漏洞一次性获取所有数据。动态排序这个功能的初衷是好的,但是有SQL注入的风险。好在这次发现问题及时,及时解决,没有造成任何损失。然而,几年前在老东家的时候,我就没那么幸运了。一个sql注入直接把我们的支付服务给挂了。1.还原事故现场。一天,运营小姐姐来找我,说很多用户付不起钱。此支付服务是一个旧系统。已经换了3个人,一直很稳定没出什么问题。话不多说,我开始定位问题。首先,我查看了服务器日志,发现有很多异常报告数据库连接过多。因为支付功能太重要了,为了保证支付功能的快速恢复,我们先让运维重启了支付服务的两个节点。5分钟后暂时恢复正常。我会继续查找原因。根据我当时的经验,一般是数据库连接过多,可能是忘记关闭连接造成的。但是仔细检查代码后,并没有发现问题。我们当时使用的数据库连接池会自动回收空闲连接,就排除了这种可能。一段时间后,另一个节点出现了数据库连接过多的问题。但是这个时候还没找到原因,只好要求运维重启服务。但是这次增加了最大数据库连接数。默认是100,我们当时设置的是500,后来调整为1000。。(其实现在大部分公司都把这个参数设置为1000)使用命令:setGLOBALmax_connections=500;可以及时生效,不需要重启mysql服务。这次争取到了更多的时间,让dba一起帮忙排查原因。使用显示进程列表;查看当前线程执行的命令:您还可以查看当前连接状态,以帮助识别有问题的查询语句。(需要注意的是,上图只是我给的例子,网上真实的结果不是这样的)idthreadid执行sql的用户账号执行sql的主机ip和数据库端口号dbdatabasename命令为executecommands,包括:Daemon,Query,Sleep等。Time执行sql的时间StateExecutionstatusinfo执行信息,可能包含sql信息。果不其然,发现了一个异常查询sql,快一个小时了还没有执行。dba把sql复制过来发给我。然后kill-9把执行时间很长的sql线程杀了。后来数据库连接过多的问题就没有再出现了。拿到sql仔细分析了一下,发现攻击者在很长的sql中注入了一条订单查询语句,肯定是高手写的,有些语法我还真没见过。但是可以确认是sql注入的。通过那个sql里面的信息,很快就找到了相关的代码。在查询数据时,使用了Statment,而不是PrepareStatement预编译机制。知道原因就好办了。将查询数据的地方改成prepareStatement预编译机制后,问题终于解决了。2、为什么会导致数据库连接过多?相信很多同学看到这里都会有一个疑问:为什么sql注入会导致数据库连接过多呢?下面我用一张图给大家解释一下:攻击者sql注入的参数是这样的:-1;锁表语句--.他们之中;先执行前面的查询语句。由于--后面的语句会被注释掉,所以接下来只会执行locktable语句来锁表。正常业务请求从数据库连接池中成功获取连接后,当需要对表进行操作时,会尝试获取表锁,但是直到超时才获取不到。注意这里可能积累了大量的数据库连接,没有及时返回。数据库连接池不够,没有空闲连接。新的业务请求无法从数据库连接池中获取连接,报数据库连接过多异常。SQL注入导致数据库连接过多。最根本的原因是长期表锁定。3、为什么预编译可以防止SQL注入?preparestatement预编译机制会在SQL语句执行前对其进行解析、编译和优化,参数位置由占位符?代替。实际运行时,传入的参数会被当成纯文本,不会被重新编译,也不会被当成SQL命令。这样,即使sql注入命令作为参数传入,如:id;select1--最终执行的sql会变成:select*fromuserorderby'id;select1--'limit1,20,这样就不会有sql注入问题了。4.预编译一定要安全吗?不知道大家在查询数据的时候有没有用过like语句。例如,如果您查询名称中包含单词“Su”的用户,您可以使用这样的查询:select*fromuserwherenamelike'%Su%';正常情况下是没有问题的。但是有些场景是需要输入条件的,比如:name是必填项,如果注入:%,最终执行的SQL会变成这样:select*fromuserwherenamelike'%%%';这种情况预编译机制正常通过,但是sql的执行结果不会返回包含%的用户,而是返回所有用户。必填的name字段变得无用,攻击者也可以获得user表中的所有数据。为什么会出现这个问题?%是mysql中的关键字。如果使用like'%%%',则like条件无效。如何解决?需要转义%:/%。转义后的SQL变为:select*fromuserwherenamelike'%/%%';仅返回包含%的用户。5、遇到一些特殊情况怎么办?如果使用mybatis作为java中的持久化框架,在mapper.xml文件中,如果使用#passvalue作为入参,就会使用预编译机制。一般我们是这样使用的:方式。更安全、更高效。但是有时候会出现一些特殊情况,比如:orderby${sortString}sortString字段的内容是在一个方法中动态计算出来的。在这种情况下,用#代替$是不可能的。这个程序会报错。使用$时,存在SQL注入的风险。那么遇到这种情况我们应该怎么办呢?自己写一个util工具过滤掉所有的注入关键词,动态计算时调用这个工具。如果数据源使用了阿里的druid,可以在filter中开启wall(防火墙),其中包含防止SQL注入的功能。但是有个问题,就是它默认不允许多条语句同时操作,同时也会拦截批量更新操作,这就需要我们自定义过滤器了。6.表信息泄露是怎么回事?有细心的同学可能会问一个问题:在上面锁表的例子中,攻击者是如何获取到表信息的呢?方法一:盲猜是指攻击者根据常识猜测可能性。现有表名。假设我们有这样一个查询条件:select*fromt_orderwhereid=${id};传入参数:-1;select*fromuser最后执行sqlinto:select*fromt_orderwhereid=-1;选择*来自用户;如果sql有数据返回,说明user表存在,被猜到。建议表名不要太简单,可以加一个合适的前缀,比如:t_user。这样可以增加盲猜的难度。方法二:通过系统表,其实mysql是有一些系统表的,我们可以找到我们自定义的数据库和表的信息。假设我们还是以这条sql为例:selectcode,namefromt_orderwhereid=${id};第一步是获取数据库和账户名。参数为:-1unionselectdatabase(),user()#最终执行sql变为:selectcode,namefromt_orderwhereid=-1unionselectdatabase(),user()#会返回当前数据库名:sue和账户名:root@本地主机。第二步是获取表名。把参数改成:-1unionselecttable_name,table_schemafrominformation_schema.tableswheretable_schema='sue'#最后执行sql变成:selectcode,namefromt_orderwhereid=-1unionselecttable_name,table_schemafrominformation_schema.tableswheretable_schema='sue'#会返回下面的全部数据库起诉表名。建议将构建环境程序访问的数据库账号和管理员账号分开,并且要控制权限,不能访问系统表。7、sql注入的危害有哪些?1、核心数据泄露。大多数攻击者的目的是赚钱。说白了,他们获取有价值的信息,然后卖钱,比如:用户账号、密码、手机号、身份证明信息、银行卡号、地址等敏感信息。他们可以注入这样的语句:-1;select*fromuser;--他们可以轻松获取用户表中的所有信息。因此,建议您对这些敏感信息进行加密存储,可以使用AES对称加密。2.攻击者多,删库跑路。有些攻击者不按常理出牌。SQL注入后,他们直接删除系统的表或数据库。他们可以注入这样的语句:-1;deletefromuser;--上面的语句会删除user表中的所有数据。-1;dropdatabasetest;--以上语句会删除整个test数据库的所有内容。一般情况下,我们需要控制在线账户的权限,只允许DML(datamanipulationlanguage)数据操作语言语句,包括:select、update、insert、delete等DDL(datadefinitionlanguage)数据库定义语言语句不允许,包括:create、alter、drop等。也不允许DCL(DataControlLanguage)数据库控制语言语句,包括:grant、deny、revoke等。DDL和DCL语句只能由管理员操作dba的帐户。顺便说一句:如果表或数据库被删除了,其实还有一个补救措施,就是从备份文件中恢复。只有少量的实时数据可能会丢失,因此必须有备份机制。3.对系统进行黑客攻击有些攻击者甚至可以直接挂掉我们的服务,老公司就是这样。他们可以注入这样的语句:-1;锁表语句;-长时间锁表后,可能会导致数据库连接耗尽。这时候我们就需要监控数据库线程了。如果某个SQL执行时间过长,需要邮件警告。另外,合理设置数据库连接的超时时间也能稍微缓解此类问题。从以上三个方面可以看出,sql注入问题的危害确实很大,一定要避免这类问题的发生,切不可心存侥幸。如果遇到一些不按常理出票的攻击者,一旦被攻击,可能损失惨重。8、如何防止sql注入?1.使用预编译机制。尽量使用预编译机制,少用字符串拼接传递参数,这是造成sql注入问题的根本原因。2.一些特殊字符必须转义。例如,在like语句中使用%作为参数时,必须对其进行转义。3.要捕获异常,需要捕获所有的异常。记住接口是直接返回异常信息的,因为有些异常信息中包含了sql信息,包括:库名,表名,字段名等,有了这些信息,攻击者就可以通过sql注入为所欲为地攻击你的数据库了。目前主流的做法是有一个专门的网关服务,统一对外暴露接口。当用户请求接口时,首先通过它,然后它把请求转发给业务服务。这样做的好处是可以统一封装返回数据的返回体,如果出现异常可以返回统一的异常信息,隐藏敏感信息。此外,它还可以做限流和权限控制。4.使用代码检测工具使用sqlMap等代码检测工具,可以检测SQL注入漏洞。5.要有监控,就要监控数据库sql的执行情况,如果有异常情况,会及时通过邮件或者短信提醒。6.数据库账号需要控制权限。为生产环境的数据库创建一个单独的账户,只分配DML相关的权限,不能访问系统表。切勿在程序中直接使用管理员帐户。7、代码审查建立代码审查机制,可以发现一些隐藏的问题,提高代码质量。8.当预编译参数不能使用时,使用其他方式处理,要么开启druid的过滤防火墙,要么编写代码逻辑过滤掉所有可能的注入关键字。