본문 바로가기

WEB개발/Sample

Java sample code

 

1. 외부 명령 실행(ProcessBuilder / 쉘 실행)을 악용한 커맨드 인젝션(Command Injection) 및 시스템 파괴 공격을 사전에 차단하기 위한 방어 코드

 

- Path Manipulation(경로 조작)

- OS Command Injection (명령 주입 공격)


import java.util.*;
import java.util.regex.Pattern;

/**
 * 확장된 금지 명령어 검사기 (Java)
 * - ProcessBuilder 인자 리스트 또는 쉘 커맨드를 사전에 검사해서 금지 토큰/명령을 발견하면 예외를 던집니다.
 * - 금지 바이너리 목록은 실무에서 흔히 위험한 셸/인터프리터/관리 유틸을 포함하도록 확장되어 있습니다.
 *
 * 주의: 금지목록만으로는 항상 완전한 방어가 되지 않습니다. 가능하면 화이트리스트(허용 목록) 사용을 권장합니다.
 */
public final class ForbiddenCommandChecker {

    // 확장된 금지된 실행파일(기본 이름만 검사). 필요시 절대경로도 처리하도록 확장 가능.
    private static final Set FORBIDDEN_BINARAMES = Set.of(
        // 쉘/인터프리터
        "sh", "bash", "dash", "ksh", "zsh", "mksh", "ash", "fish",

        // 일반적인 시스템/관리 유틸 (위험한 파일/디스크/유저 관련)
        "rm", "rmdir", "mv", "cp", "dd", "mkfs", "fdisk", "parted", "mount", "umount",
        "chmod", "chown", "chgrp", "ln", "lns", "useradd", "userdel", "usermod", "groupadd", "groupdel",
        "passwd", "chpasswd", "pwck", "pwconv",

        // 프로세스/서비스 제어
        "kill", "killall", "pkill", "nice", "renice", "systemctl", "service", "init", "shutdown",
        "reboot", "halt", "poweroff", "telinit", "runlevel",

        // 네트워크/리모트 유틸
        "ssh", "scp", "sftp", "telnet", "nc", "netcat", "ncat", "socat", "curl", "wget", "ftp", "tftp",
        "ifconfig", "ip", "iptables", "nft", "route", "arp", "ss", "netstat",

        // 스크립트 언어 실행기
        "python", "python3", "perl", "ruby", "php", "node", "nodejs", "lua", "groovy",

        // 빌드/패키지/관리
        "apt", "apt-get", "yum", "dnf", "zypper", "pacman", "rpm", "dpkg", "pip", "pip3", "gem", "npm", "mvn", "gradle", "ant",

        // 파일 전송/편집 도구
        "vi", "vim", "nano", "ed", "sed", "awk", "rsync",

        // 기타 유해 가능성 높은 유틸
        "crontab", "at", "batch", "systemd-run", "screen", "tmux"
    );

    // 금지 토큰(정확 매칭)
    private static final Set FORBIDDEN_TOKENS = Set.of(
        ";", "&&", "|", "`", "$(", ">", ">>", "<", "2>&1", ";&", ";&"
    );

    // 위험한 패턴(정규식) - 쉘 인젝션이나 치명적 옵션 등을 잡아냄
    private static final List FORBIDDEN_PATTERNS = List.of(
        // 명령줄에 ; 또는 && 또는 | 등이 들어가는 경우
        Pattern.compile(".*(;|\\&\\&|\\|).*", Pattern.DOTALL),
        // backtick 또는 $() 서브쉘
        Pattern.compile(".*(`|\\$\\().*", Pattern.DOTALL),
        // rm -rf 형태 (공백/탭 허용), 대소문자 무시
        Pattern.compile("(?i).*\\brm\\b\\s+-\\s*[-rfv]+.*", Pattern.DOTALL),
        // 민감 경로 접근 시도 (/etc, /root, /boot 등)
        Pattern.compile(".*(/etc|/root|/boot|/proc|/sys|/dev).*", Pattern.DOTALL)
    );

    private ForbiddenCommandChecker() {}

    public static class ForbiddenCommandException extends SecurityException {
        public ForbiddenCommandException(String message) {
            super(message);
        }
    }

    /**
     * ProcessBuilder 같은 인자 리스트를 검사한다.
     * - 첫번째 인자(실행 파일) 이름이 금지 목록에 있는지 검사
     * - 각 인자에 금지 토큰/패턴이 포함되어 있는지 검사
     */
    public static void checkArgs(List args) {
        if (args == null || args.isEmpty()) return;

        String first = args.get(0);
        String baseName = extractBaseName(first);

        if (isForbiddenBinary(baseName)) {
            throw new ForbiddenCommandException("금지된 실행파일 사용 시도: " + baseName);
        }

        for (String arg : args) {
            if (arg == null) continue;
            if (isForbiddenToken(arg)) {
                throw new ForbiddenCommandException("금지된 토큰 발견(인자): " + arg);
            }
            if (matchesForbiddenPattern(arg)) {
                throw new ForbiddenCommandException("금지된 패턴 발견(인자): " + arg);
            }
            // 추가 검증 포인트: 만약 인자가 경로라면 canonical 검사 등을 여기에 추가 가능
        }
    }

    /**
     * 쉘 전체 커맨드 문자열을 검사한다. (쉘을 쓰는 경우에만)
     */
    public static void checkCommandLine(String cmdLine) {
        if (cmdLine == null || cmdLine.isBlank()) return;

        for (String token : FORBIDDEN_TOKENS) {
            if (cmdLine.contains(token)) {
                throw new ForbiddenCommandException("금지된 토큰이 명령행에 포함됨: " + token);
            }
        }

        if (matchesForbiddenPattern(cmdLine)) {
            throw new ForbiddenCommandException("금지된 패턴이 명령행에서 발견됨: " + cmdLine);
        }

        String[] parts = cmdLine.split("\\s+");
        if (parts.length > 0) {
            String bn = extractBaseName(parts[0]);
            if (isForbiddenBinary(bn)) {
                throw new ForbiddenCommandException("금지된 실행파일 사용 시도: " + bn);
            }
        }
    }

    // 헬퍼
    private static boolean isForbiddenBinary(String baseName) {
        if (baseName == null) return false;
        return FORBIDDEN_BINARAMES.contains(baseName.toLowerCase());
    }

    private static boolean isForbiddenToken(String s) {
        return FORBIDDEN_TOKENS.contains(s);
    }

    private static boolean matchesForbiddenPattern(String s) {
        for (Pattern p : FORBIDDEN_PATTERNS) {
            if (p.matcher(s).matches()) return true;
        }
        return false;
    }

    private static String extractBaseName(String pathOrName) {
        if (pathOrName == null) return null;
        int i = Math.max(pathOrName.lastIndexOf('/'), pathOrName.lastIndexOf('\\'));
        return (i >= 0 && i + 1 < pathOrName.length()) ? pathOrName.substring(i + 1) : pathOrName;
    }

    // 간단한 테스트용 main
    public static void main(String[] args) {
        try {
            checkArgs(Arrays.asList("ls", "-l", "/tmp"));
            System.out.println("Args OK");
        } catch (ForbiddenCommandException e) {
            System.err.println("검사 실패: " + e.getMessage());
        }

        try {
            checkArgs(Arrays.asList("/bin/bash", "-c", "echo hi"));
            System.out.println("Should not print");
        } catch (ForbiddenCommandException e) {
            System.err.println("금지검출(예상): " + e.getMessage());
        }

        try {
            checkCommandLine("ls -l; rm -rf /");
            System.out.println("Should not print");
        } catch (ForbiddenCommandException e) {
            System.err.println("쉘 검사 실패(예상): " + e.getMessage());
        }
    }
}