명령어 주입

C# Process API의 운영 체제 명령 및 인자 주입 위험과 안전한 프로세스 실행 방법을 설명합니다.

설명

신뢰할 수 없는 입력이 Process.Start 또는 ProcessStartInfo에 전달되면 두 가지 위험이 생길 수 있습니다.

  • 입력이 ProcessStartInfo.FileName이나 Process.Start의 첫 번째 인자를 제어하면 공격자가 의도하지 않은 실행 파일을 선택할 수 있습니다.
  • 입력이 ProcessStartInfo.Arguments나 Process.Start(string, string)의 두 번째 인자에 포함되면 원시 명령줄의 인자 경계 또는 옵션이 변경될 수 있습니다. 실행 파일이 cmd.exe, PowerShell, /bin/sh, /bin/bash 같은 명령 인터프리터라면 입력이 명령 구문으로 실행될 수 있습니다.

UseShellExecute = false는 운영 체제의 문서 연결 셸을 사용하지 않도록 설정할 뿐입니다. 이 값은 애플리케이션이 cmd.exe, PowerShell, sh 같은 명령 인터프리터를 직접 실행하는 것을 막지 않습니다.

ProcessStartInfo.ArgumentList와 .NET 10의 Process.Start(string, IEnumerable<string>) 오버로드는 각 인자의 경계를 보존하므로 원시 Arguments 문자열보다 안전합니다. 그러나 인자를 분리해도 대상 프로그램이 공격자 입력을 옵션으로 해석할 수 있으며, sh -c, cmd /c, PowerShell -Command 뒤의 값은 여전히 실행할 코드입니다.

잠재적 영향

  • 애플리케이션 계정 권한으로 임의 명령 또는 프로그램 실행
  • 파일 읽기·변조·삭제, 자격 증명 탈취, 내부 네트워크 접근
  • 공격자가 선택한 옵션에 따른 데이터 유출, 권한 오용 또는 서비스 중단

해결 방법

  1. 가능하면 프로세스를 생성하지 말고 파일, 압축, 네트워크, 데이터 변환 등을 위한 관리형 라이브러리 API를 사용합니다.
  2. 프로세스 실행이 필요하면 외부 입력을 실행 파일 경로로 사용하지 않습니다. 검증된 작업 식별자를 개발자가 관리하는 고정된 절대 경로에 매핑하고, 실행 파일은 신뢰할 수 있으며 비권한 사용자가 쓸 수 없는 위치에 둡니다.
  3. UseShellExecute를 false로 설정하고 명령 인터프리터를 호출하지 않습니다. 특히 신뢰할 수 없는 값을 -c, /c, -Command 또는 유사한 코드 실행 옵션 뒤에 전달하지 않습니다.
  4. 인자를 ProcessStartInfo.ArgumentList 또는 IEnumerable<string> 오버로드로 하나씩 전달합니다. 각 외부 값에는 대상 프로그램의 문법과 의미에 맞는 허용 목록 기반 검증 및 길이 제한을 적용합니다.
  5. 대상 프로그램이 지원하면 신뢰할 수 없는 피연산자 앞에 -- 같은 옵션 종료 표식을 사용합니다. 이 표식은 대상이 해당 규칙을 지원할 때만 유효합니다.
  6. 프로세스를 최소 권한 계정으로 실행하고 필요한 파일 시스템 및 네트워크 접근만 허용합니다. 모든 프로그램에 통용되는 명령줄 이스케이프 함수가 있다고 가정하지 않습니다.

예시

변경 전

csharp
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

public sealed class RunController : Controller
{
    public void Run([FromQuery] string command)
    {
        var processInfo = new ProcessStartInfo
        {
            FileName = "/bin/sh",
            UseShellExecute = false
        };
        processInfo.ArgumentList.Add("-c");
        processInfo.ArgumentList.Add(command);
        Process.Start(processInfo);
    }
}

ArgumentList가 인자 경계를 보존하더라도 -c 다음 값은 셸 프로그램이 해석할 코드입니다. UseShellExecute = false도 직접 실행한 /bin/sh의 해석을 비활성화하지 않습니다.

변경 후

csharp
using System.Diagnostics;

static class Printer
{
    public static void PrintUserValue(string userInput)
    {
        var processInfo = new ProcessStartInfo
        {
            FileName = "/usr/bin/printf",
            UseShellExecute = false
        };
        processInfo.ArgumentList.Add("%s\n");
        processInfo.ArgumentList.Add(userInput);
        Process.Start(processInfo);
    }
}

안전한 예시는 명령 인터프리터를 사용하지 않고 고정된 실행 파일과 고정된 형식 문자열을 선택합니다. 사용자 값은 별도의 데이터 인자로 전달됩니다. 실제 애플리케이션에서는 /usr/bin/printf의 존재와 소유권을 배포 환경에서 보장해야 하며, 단순 출력만 필요하다면 Console.WriteLine처럼 프로세스를 생성하지 않는 API가 더 적절합니다.

참조