-
CTF docker 세팅Tips 2020. 3. 14. 16:11
서버에 pwnable 문제를 올려 CTF를 진행할 때 참고하면 좋을 듯 하다.
먼저 pwnable 문제는 무조건 도커를 사용해 분리시켜둬야한다. 그래서 일단 docker와 docker-compose를 설치해준다.
docker 설치
curl -fsSL https://get.docker.com/ | sudo sh
docker-compose 설치
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
위 두개를 간단하게 설명하면 문제 하나하나 도커가 존재하는데, 그 도커들을 한번에 관리할 수 있게 도와주는 툴이 docker-compose이다.
도커는 아래와 같이 구성할 계획이다.
- 도커 내부에 문제파일, 플래그를 넣는다.
- 도커 내부에서 특정 포트에 문제 바이너리를 연결해 nc로 접근 가능하도록 한다.
- 도커 내부 포트와 외부 서버 포트를 포워딩한다.
도커 내부에서 특정 포트에 바이너리를 연결해 주는 것은 여러 방법이 있겠지만, 여기서는 xinetd, socat을 이용한 방법을 설명할 것이다. 먼저 흔히 사용하는 xinetd를 이용한 방법이다.
xinetd를 이용하려면 먼저 xinetd를 설치하고 서비스에 등록해야한다. 서비스를 등록하려면 먼저 /etc/xinetd.d/에 서비스 파일을 생성해야 한다. 이는 아래와 같이 작성하면 된다.
service prob1 { disable = no flags = REUSE socket_type = stream protocol = tcp user = pwn wait = no server = /home/pwn/prob type = UNLISTED port = 8080 }
여기서 user는 사용자, server는 바이너리, port는 열어둘 포트를 의미한다. 이는 적절하게 변경하면 된다.
다음으로 도커를 생성해보자. 도커를 만들기 위해서는 Dockerfile을 작성해야 한다. Dockerfile의 문법은 대략 아래와 같다.
FROM ubuntu:16.04 # 32bit #RUN dpkg --add-architecture i386 RUN apt-get update # test #RUN apt-get install -y openssh-server gdb # 32bit #RUN apt-get install -y libc6:i386 libncurses5:i386 libstdc++6:i386 RUN apt-get install -y xinetd netcat RUN useradd -d /home/pwn pwn -s /bin/bash RUN mkdir /home/pwn RUN chown -R root:pwn /home/pwn RUN chmod 750 /home/pwn ADD ./prob/prob /home/pwn/prob ADD ./prob/flag /flag RUN chown root:pwn /flag RUN chmod 440 /flag ADD ./settings/prob.xinetd /etc/xinetd.d/prob ADD ./settings/start.sh /start.sh
주석은 #으로 처리한다.
가장 위의 FROM은 도커에서 사용할 이미지인데, 여기서는 ubuntu:16.04를 사용하였다.
그다음 RUN이라는 명령어는 컨테이너를 생성할 때 실행할 명령어를 나타낸다. 이를 이용해 필요한 프로그램들을 설치해주고 유저를 생성해 권한을 잘 분리해준다.
ADD는 호스트(서버)에 있는 파일을 컨테이너로 복사하는 명령어이다. ADD (host path) (container path) 형식이다. 이를 이용해 먼저 작성해둔 xinetd 서비스 파일을 컨테이너 내부 /etc/xinetd.d/로 복사한다. 또한 루트에 start.sh를 복사하는데, 이는 컨테이너가 생성된 직후 실행할 명령어들을 작성해둔 스크립트이다.
#!/bin/bash /etc/init.d/xinetd restart /bin/bash sleep infinity
이렇게 작성해두었다. 여기까지 문제 폴더의 구조는 아래와 같다.
. ├── Dockerfile ├── prob │ ├── flag │ └── prob └── settings ├── prob.xinetd └── start.sh
이제 여기에 도커를 실행하는 스크립트를 작성할 것이다.
run_docker.sh:
#!/bin/bash NAME="name" sudo docker kill $NAME sudo docker rm $NAME sudo docker build --tag $NAME:1.0 ./ PORT="-p 30001:8080" OPTION="-idt" DEV_OPTION="--cap-add=SYS_PTRACE --security-opt seccomp=unconfined" sudo docker run $OPTION $PORT --name $NAME $NAME:1.0 /start.sh sudo docker attach $NAME
먼저 이전에 존재하는 컨테이너를 중단/제거하고 다시 현재 디렉토리에 있는 도커파일을 이용해 도커를 빌드한다. 이후 이 컨테이너의 내부포트 8080을 외부포트 30001로 포워딩해주며 실행한다. 이때 /start.sh를 실행한다.
이렇게 기본적으로 도커를 이용해 문제 하나를 만들었다. 하지만 문제가 많아지면 조금 복잡해지기 때문에 docker-compose를 이용할 것이다.
각 문제별로 Dockerfile, 세팅 파일, 문제 바이너리, 플래그 등을 준비해두고 그 상위 폴더에 docker-compose.yml이라는 파일을 생성한다.
이는 아래와 같은 문법으로 작성하면 된다.
version: '3' services: prob1: build: context: ./pwnable/prob1/ dockerfile: ./Dockerfile ports: - "30001:8080" command: - /start.sh prob2: build: context: ./pwnable/prob2/ dockerfile: ./Dockerfile ports: - "30002:8080" command: - /start.sh prob3: build: context: ./pwnable/prob3/ dockerfile: ./Dockerfile ports: - "30003:8080" command: - /start.sh
이는 앞서 만든 run_docker.sh를 대신한다. 이렇게 작성하면 prob1은 ./pwnable.prob1/Dockerfile을 이용해 빌드하고, 포트 30001:8080으로 포워딩해 컨테이너를 실행한다.
이 docker-compose.yml 파일이 있는 디렉토리에서 sudo docker-compose up -d 를 하면 실행이 되고 sudo docker-compose down을 하면 종료된다. 이후 도커파일이나 무언가 변경되었을 때 sudo docker-compose up -d --build로 옵션을 주면 재빌드한다.
마지막으로 xinetd에서 timeout을 줄 방법을 찾아보다가 그냥 socat을 이용하면 자동으로 timeout을 주는것 같아 아래에 방법을 적어둔다.
socat을 이용할 때는 xinetd와 달리 socat을 먼저 설치해주고 명령어 하나만 실행해주면 된다.
socat TCP-LISTEN:8080,reuseaddr,fork EXEC:'su pwn -c /home/pwn/prob'
이를 start.sh에 넣어주면 된다. (xinetd와 socat 둘 중 하나만 사용)