운영체제 명령어 주입

C/C++ 운영체제 명령어 주입

설명

system(), popen(), _popen()은 문자열을 명령 처리기에 전달합니다. C++에서 <cstdlib>의 std::system()을 호출하는 경우도 같습니다. 신뢰할 수 없는 입력이 이 문자열에 포함되면 셸 메타문자, 명령 구분자, 리디렉션 또는 추가 인자가 코드로 해석될 수 있습니다. wordexp()도 셸의 단어 확장을 수행하며, WRDE_NOCMD를 지정하지 않으면 명령 치환을 실행할 수 있습니다.

셸 문자를 이스케이프하는 것만으로는 모든 대상 셸과 실행 프로그램의 문법을 안전하게 처리할 수 없습니다. 셸 명령 자체가 차단되더라도 대상 프로그램이 신뢰할 수 없는 값을 옵션으로 해석하는 인자 주입이 남을 수 있습니다. shell_escape()나 sanitize_command_arg() 같은 함수 이름만으로 안전성을 판단해서도 안 됩니다.

잠재적 영향

  • 애플리케이션 권한으로 임의의 운영체제 명령이 실행될 수 있습니다.
  • 파일 삭제·변조, 민감정보 유출, 내부 시스템 접근 또는 서비스 중단으로 이어질 수 있습니다.
  • 높은 권한으로 실행되는 프로세스에서는 시스템 전체가 침해될 수 있습니다.

해결 방법

가능하면 외부 명령 호출을 제거하고 해당 기능을 제공하는 라이브러리 API를 사용합니다. 외부 프로세스가 꼭 필요하다면 다음 원칙을 함께 적용합니다.

  1. 실행 파일 경로를 코드에서 고정하고 execve(), 적절한 posix_spawn() 함수 또는 플랫폼의 프로세스 API를 사용합니다.
  2. 명령 문자열을 조립하지 말고 실행 파일과 인자를 분리합니다.
  3. 각 인자를 대상 프로그램의 데이터 형식에 맞는 허용 목록으로 검증하고, 선행 -나 -- 처리 등 옵션 주입 가능성도 확인합니다.
  4. 필요한 환경 변수만 신뢰할 수 있는 값으로 구성하고 오류를 처리합니다.
  5. wordexp()가 불가피하면 WRDE_NOCMD를 포함하고 반환값을 확인합니다. 이 플래그는 명령 치환을 요청한 입력을 WRDE_CMDSUB 오류로 거부하지만, 다른 단어 확장까지 일반적으로 안전하게 만드는 정제 함수는 아닙니다.

예시

C

변경 전

c
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        return 2;
    }

    return system(argv[1]);
}

변경 후

c
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

static int valid_label(const char *value) {
    size_t length = strlen(value);
    if (length == 0 || length > 64) {
        return 0;
    }

    for (size_t i = 0; i < length; ++i) {
        unsigned char ch = (unsigned char)value[i];
        if (!isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
            return 0;
        }
    }
    return 1;
}

int main(int argc, char **argv) {
    if (argc != 2 || !valid_label(argv[1])) {
        return 2;
    }

    char *const args[] = {"printf", "Archive: %s\n", argv[1], NULL};
    execv("/usr/bin/printf", args);
    perror("execv");
    return 1;
}

설명:

  • 변경 전: argv[1] 전체가 셸이 해석하는 명령 문자열로 전달됩니다. 따옴표를 추가해도 모든 셸 구문을 안전하게 만들지는 못합니다.
  • 변경 후: 실행 파일과 형식 문자열을 고정하고 사용자 값을 별도 인자로 전달합니다. 허용 목록 검증은 이 값에 허용되는 업무 형식도 제한합니다.

wordexp()가 필요한 경우에는 명령 치환을 명시적으로 금지하고 모든 오류를 처리합니다.

c
#include <wordexp.h>

int expand_without_commands(const char *input, wordexp_t *words) {
    int result = wordexp(input, words, WRDE_NOCMD | WRDE_UNDEF);
    if (result != 0) {
        if (result == WRDE_NOSPACE) {
            wordfree(words);
        }
        return -1;
    }
    return 0;
}

성공하면 호출자가 결과를 사용한 뒤 wordfree()로 해제합니다. WRDE_NOSPACE 오류에서도 부분 할당된 메모리를 해제해야 합니다.

C++

변경 전

cpp
#include <cstdlib>

int main(int argc, char **argv) {
    if (argc != 2) {
        return 2;
    }

    return std::system(argv[1]);
}

변경 후

cpp
#include <cctype>
#include <cstdio>
#include <cstring>
#include <unistd.h>

static bool valid_label(const char *value) {
    std::size_t length = std::strlen(value);
    if (length == 0 || length > 64) {
        return false;
    }

    for (std::size_t i = 0; i < length; ++i) {
        unsigned char ch = static_cast<unsigned char>(value[i]);
        if (!std::isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
            return false;
        }
    }
    return true;
}

int main(int argc, char **argv) {
    if (argc != 2 || !valid_label(argv[1])) {
        return 2;
    }

    char program[] = "/usr/bin/printf";
    char format[] = "Archive: %s\n";
    char *const args[] = {program, format, argv[1], nullptr};
    execv(program, args);
    std::perror("execv");
    return 1;
}

std::system() 대신 고정된 실행 파일을 직접 호출하고 사용자 값을 별도 인자로 전달합니다. 이 예시는 C++23과 POSIX.1-2024 환경에서 유효하며, 허용 목록은 사용자 값의 업무 형식을 함께 제한합니다.

참조