본문 바로가기
CS/시큐어 코딩

[시큐어 코딩] OS 명령어 삽입(Injection)

by J-rain 2024. 5. 24.

 

 

운영체제 명령어 삽입

프로그램 내에서 사용자 입력이 윈도우의 cmd.exe나 유닉스의 sh, bash 같은 쉘 프로그램 실행을 위한 전달인자(argument)로 사용되는 경우, 그 사용자 입력을 애플리케이션에서 충분히 검증하지 않고 사용함에 따라 공격자가 의도하는 명령이 실행될 수 있는 취약점이다. 이런 취약점을 통해 공격자는 공격용 툴을 외부로부터 업로드하거나 업로드된 툴에 대해 실행권한을 부여할 수 있고, 또한 관리자 권한을 획득해 서버를 제어할 수도 있다.

 

 

발생 원인

PHP의 exec, ASP의 wscript, shell, JSP의 Runtime.exe()와 같은 운영체제 명령을 실행하는 함수의 인자값으로 검증되지 않은 사용자 입력값을 사용하는 경우 발생한다.

 

 

이전시간에 수행했던 SQL인젝션과 같은 환경에서 실습

 

 

공격기법

외부 입력값을 명령어의 일부로 사용하는지 체크

시큐어코딩테스트 > 명령어인젝션

 

요청되는 패킷을 Paros를 이용해 검사한다.

 

Paros를 사용하기 위하여 프록시 서버 편집 + Paros tool 옵션에서 8081 포트를 사용해야한다!!

프록시 서버 편집할때 ; <-loopback> 넣어줘야함

 

Paros에서 Trap request를 선택해주고

명령어 실행 요청에서는 data라는 파라미터의 값으로 실행을 원하는 명령어인 type이 직접적으로 전달되는 것을 확인할 수 있었다.

 

Paros에서 잡아놓은 요청의 파라미터 값을 data = notepad 로 수정하고 Continue 버튼을 클릭해 요청을 전달한 후 실행 결과를 확인한다.

 

 

이렇게 웹 서버에서 메모장이 실행되는 것을 확인할 수 있다.

 

이 화면을 처리하는 코드를 확인해 보면, 외부로부터 입력된 값 data를 검증하지 않고 시스템 쉘(윈도우에서는 cmd.exe)을 실행하여 외부 입력값을 명령어로 실행한다. 이 경우 공격자가 외부 입력값 data를 조작하여 전송하게 되면 시스템은 의도되지 않은 명령어을 실행하는 취약점을 가지게 된다.

 

TestController.java

        @RequestMapping(value="/test/command_test.do", method = RequestMethod.POST)
	@ResponseBody
	public String testCommandInjection(HttpServletRequest request, HttpSession session){
		StringBuffer buffer=new StringBuffer();	
		String data=request.getParameter("data");
			
	    if ( data != null  && data.equals("type")) {
	    		data=data+" "+
	    	            request.getSession().getServletContext().getRealPath("/")+
	    	            "file1.txt"; 
	    		System.out.println(data);
	    }
    	
		Process process;
		String osName = System.getProperty("os.name");
		String[] cmd;

		if(osName.toLowerCase().startsWith("window")) {
		    cmd = new String[] { "cmd.exe","/c",data };
		    for( String s : cmd)
		       System.out.print(s+" ");
		} else {
		    cmd = new String[] { "/bin/sh",data };
		}

		try {
			process = Runtime.getRuntime().exec(cmd);
			InputStream in = process.getInputStream(); 
			Scanner s = new Scanner(in,"EUC-KR");
			buffer.append("실행결과: <br/>");
			while(s.hasNextLine() == true) {
			    buffer.append(s.nextLine()+"<br/>");
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			buffer.append("실행오류발생");
			e.printStackTrace();
		} 
			return buffer.toString();

	}

 

위 코드를 확인해보면
String data = request.getParameter("data");
cmd = new String[] { "cmd.exe", "/c",data };
를 통해 외부에서 입력된 파라미터 data에 대한 검증없이 시스템에서 명령이 실행되도록 처리되어있다.

 

 

방어기법

사전 허가된 명령어만 제한적으로 실행되도록 허용되는 명령어 목록을 하드코딩

 

명령어 삽입 취약점을 제거하기 위해서는 가능하면 웹 인터페이스를 통해 서버 내부의 시스템 명령어를 실행하지 않도록 프로그램을 작성하는 것이 좋다. 하지만 꼭 외부 입력값을 사용해 시스템 내부 명령이 실행되어야 하는 경우에는 입력값에 대한 충분한 검증을 수행한 뒤 사용하거나, 프로그램에 미리 허용할 명령어 목록을 하드코딩해 허용범위 안의 명령어만 제한적으로 실행될 수 있도록 한다.

 

윈도우의 cmd.exe는 &를, 유닉스 시스템의 sh는 ; 또는 | (파이프), &&, | | 와 같은 메타 문자를 사용해 여러 개의 명령을 하나의 명령어 라인에 실행할 수 있으므로 입력값 중 이러한 문자들이 입력되지 않도록 필터링 해야한다.

 

시스템에서 실행될 수 있는 명령어 또는 명령어에 사용될 수 있는 파라미터 값을 화이트리스트로 작성해, 외부에서 명령어 실행이 요청될 때 허용된 값들만 제한적으로 사용될 수 있도록 코드를 수정한다.

                String[] allowCommand = {"type","dir"};
		
		int index = TestUtil.getInt(data);
		
		if(index < 0 || index > 1)
		{
			buffer.append("잘못된 요청입니다.");
			return buffer.toString();
		}else
		{
			data = allowCommand[index];
		}
이와같이 TestController에 추가한다.

 

 

전체 TestController.java

        //Command 인젝션
	@RequestMapping(value="/test/command_test.do", method = RequestMethod.POST)
	@ResponseBody
	public String testCommandInjection(HttpServletRequest request, HttpSession session){
		StringBuffer buffer=new StringBuffer();	
		String data=request.getParameter("data");
		
		String[] allowCommand = {"type","dir"};
		
		int index = TestUtil.getInt(data);
		
		if(index<0 || index >1)
		{
			buffer.append("잘못된 요청입니다.");
			return buffer.toString();
		}else
		{
			data = allowCommand[index];
		}
			
	    if ( data != null  && data.equals("type")) {
	    		data=data+" "+
	    	            request.getSession().getServletContext().getRealPath("/")+
	    	            "file1.txt"; 
	    		System.out.println(data);
	    }
    	
		Process process;
		String osName = System.getProperty("os.name");
		String[] cmd;

		if(osName.toLowerCase().startsWith("window")) {
		    cmd = new String[] { "cmd.exe","/c",data };
		    for( String s : cmd)
		       System.out.print(s+" ");
		} else {
		    cmd = new String[] { "/bin/sh",data };
		}

		try {
			process = Runtime.getRuntime().exec(cmd);
			InputStream in = process.getInputStream(); 
			Scanner s = new Scanner(in,"EUC-KR");
			buffer.append("실행결과: <br/>");
			while(s.hasNextLine() == true) {
			    buffer.append(s.nextLine()+"<br/>");
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			buffer.append("실행오류발생");
			e.printStackTrace();
		} 
			return buffer.toString();

	}

 

이후 다시 재시작 해보면

정상적인 명령어 메세지를 날려보았지만 잘못된 요청이라고 돌아왔다.

Paros에서 어떻게 날라가는지 확인해보자

아직도 data=type으로 텍스트로 주고있는것을 볼 수 있다.
그 이유를 찾아보니

int index = TestUtil.getInt(data); 에서 getInt 함수가 Int형으로 바꿔주거나 예외처리하는 것을 볼 수 있었다.
따라서 type, dir, notepad 명령어가 실행이 안되는 것!
public class TestUtil {

	public static  int getInt(String data){
		int i=-1;
		try {
		    i= Integer.parseInt(data);
		}catch(NumberFormatException e){
			return i;
		}
		return i;
	}
...
이 과정을 통해 외부 데이터값은 허용된 명령어 목록의 인덱스값으로 사용될 수 있도록 숫자값으로 전달받아 처리하도록한다. 여기서는 0,1값만 유효하다 (각각 위에서 작성한 type, dir 값으로 수행)

 

 

data=0 으로 보냈을때 정상적으로 작동하는 것을 볼 수 있다.

 

data=1 또한 정상적으로 작동하는 것을 볼 수 있다.

 

 

 

 

 

참고: 해킹 방어를 위한 JAVA 시큐어코딩(실무에 바로 적용하는)(개정판 4판) 김영숙

댓글