Maxmind GeoLite2 설치와 활용
웹서비스를 운영하다 보면 정상적인 이용자의 유입이 아닌 Bot 또는 Crawling, Scraping 툴에 의해 서비스가 호출되는 것을 어렵지 않게 찾아 볼 수 있다. 물론 Google처럼 검색 목적의 Bot도 있지만, 좋지 않은 목적으로 또는 장난삼아 Script를 실행 하거나, 웹서비스 취약점을 찾기 위한 해킹 시도도 적지 않다.
웹서비스 운영자 입장에서는 1차적으로는 웹 애플리케이션의 취약점을 최소화 하는게 우선이나, 수많은 오픈소스 및 3rd-party lib 사용으로 인해 취약점 제로 애플리케이션을 개발한다는 것은 불가능에 가까우며, 상시적인 Patch를 진행하는 것이 최선의 방법이다. 2차적인 방법으로는 백신, 웹방화벽, IPS등 보안 관제를 통해 SQL Injection, Web Shell, Remote Code Execution 등 알려진 공격을 24/7/265 무중단으로 탐지 및 차단하여 외부의 칩입 가능성을 감소 시키는 것이다.
그러나 현존하는 보안툴의 탐지 방식은 대부분 시그니처 기반으로, 오탐 가능성으로 인해 차단보다는 탐지로 운용하여 상시 모니터링 관제가 필요하고, Credential Stuffing, Dictionary Attack등 기존 방식으로는 탐지가 불가능한 기법도 있기 때문에 웹서비스 운영자에게는 추가적인 수단이 필요하다.
이때 보조적으로 사용하는 수단이 특정 지역(?)에서의 접근을 차단하는 전통적인 방식(ACL)으로 대부분의 사이트에서 아직도 사용중이며 그중에서도 Maxmind의 GeoIP DB를 많이 참조한다. Maxmind는 오픈소스로 오래 전부터 사용되어 왔으며 많은 보안 솔루션에 탑재될 정도로 신뢰성이 있고 .Net, Java, Ruby, Python, Node.js 등 다양한 개발언어를 지원한다.
GeoIP 적용 사례는 ssh(터미널) 접근통제, Apache, NginX등 웹서버 Plugin 등 다양하지만 VPN이 널리 사용되고 있는 상황에서 IP만으로 ACL을 적용하는 것은 Risk가 있어 본 장에서는 애플리케이션 log(e.g. access-log 등) 분석을 통해 알람 서비스를 구현하는 손쉬운 방법을 소개하고자 한다.
2018년 이전에는 라이선스 없이 DB를 업데이트 할 수 있었으나 이후에는 라이선스가 있어야 DB 업데이트, API이용 등 정상적인 서비스를 이용할 수 있으며, 라이선스 키 발급은 무료이다.
"Figure 1.~2." 처럼 MaxMind 사이트(https://dev.maxmind.com)에 접속하여 회원 가입을 진행하며 이때 등록하는 Email 주소는 나중에 로그인할 때 계정(Account)으로 사용되므로 수신 가능한 Email 주소를 사용한다.
MaxMind(https://www.maxmind.com/en/account/login)에 접속하여 로그인 하면 "Figure 4." 계정 메인화면으로 이동하며, 다운로드 또는 업데이트한 GeoIP DB 현황을 참조할 수 있다.
신규 라이선스키 발급을 위해 "Manage License Keys"를 클릭하여 이동하면 "Figure 5."처럼 기존에 발급된 라이언스키를 조회할 수 있다. "Generate new license key" 버튼을 클릭하여 발급 화면으로 이동한다.
"Figure 6."은 라이선스키 발급을 위한 정보 입력 화면으로 최신 버전의 DB를 사용할 것이므로 "No"를 선택한다. License key description은 "Figure 5." 의 키 목록에 표시되는 값으로 식별이 용이한 단어를 기입한다.
키가 생성되면 "Figure 7." 처럼 License key가 표시되는데 키 값은 이 화면에서만 확인 가능하므로 복사해서 따로 저장해 두어야 한다. 만에 하나 키 값을 저장하지 못했다면 "Figure 5." 화면에서 키를 삭제하고 신규 키를 다시 생성한다.
Linux에서 geoiplookup RPM을 설치하여 간단한 명령어로 IP 정보를 조회할 수 있으나 이는 Maxmind 구버전으로 더이상 S/W 업데이트가 지원되지 않아 상용 서비스로 사용 하기에는 무리가 있어 Maxmind의 최신 S/W를 직접 설치한다. (참고로 최신 DB(.mmdb)를 구버전의 .dat로 변환하는 shell이 있기도 함)
공식 Github : https://github.com/maxmind/geoipupdate/releases
"Figure 8."에 보면 RPM 또는 Source 설치도 다운로드 가능하나 기존 패키지와의 충돌을 피하기 위해 컴파일된 실행파일(.tar.gz)을 선택한다. 서버에 GeoIP 설치를 위해 루트 경로(~/geoip/)와 DB파일이 저장될 저장소(~/geoip/geodb)를 "Figure 9." 처럼 생성하고 루트 경로에 geoipupdate 실행 파일을 다운로드 한다.
$ mkdir ./geoip ./geoip/geodb #GeoIP와 DB 경로를 생성한다
$ cd ./geoip/
$ wget https://github.com/maxmind/geoipupdate/releases/download/v4.10.0/geoipupdate_
4.10.0_linux_amd64.tar.gz
$ tar xvzf ./geoipupdate_4.10.0_linux_amd64.tar.gz
$ mv ./geoipupdate_4.10.0_linux_amd64 ./geoipupdate
"Figure 9."에서 geoipupdate 명령어의 구성파일 을 확인 할 수 있으며, 이중 GeoIP.conf 설정에 1.1.에서 신규 발급한 라이선스키를 반영한다.
EditionIDs에는 다운로드할 DB가 나열되며, GeoLite2-Country를 기본으로 좀 더 상세한 정보를 얻기 위한 GeoLite2-City와 통신사, 기업 등 IP주소 관리 기관을 식별하기 위해 ASN(Autonomous System Numbers)을 추가로 기입한다.
DatabaseDirectory는 다운로드 되는 DB의 저장 경로로 앞서 생성한 ./geoip/geodb 로 한다.
$ cd ./geoipupdate
$ ls -l
$ vi ./GeoIP.conf
설정파일을 저장한 후, 아래와 같이 geoipupdate 명령어를 실행하여 DB를 다운로드 하며 해당 명령어를 crontab에 Daily로 설정해 놓으면 최신화된 DB를 유지할 수 있다.
$ ./geoipupdate -f ./GeoIP.conf -v
geoiplookup 처럼 주어진 IP에 대한 지역 정보를 조회하는 명령어로 Maxmind 공식 사이트에서 다운로드 할 수 있다.
공식 Github : https://github.com/maxmind/mmdbinspect/releases
앞서 geoipupdate와 마찬가지로 실행파일을 다운로드 하며 "Figure 13."처럼 ./geoip 루트에 설치한다.
$ wget https://github.com/maxmind/mmdbinspect/releases/download/v0.1.1/mmdbinspect
_0.1.1_linux_amd64.tar.gz
$ tar xvzf ./mmdbinspect_0.1.1_linux_amd64.tar.gz
$ mv ./mmdbinspect_0.1.1_linux_amd64 ./mmdbinspect
"Figure 14."는 mmdbinspect로 www.kakao.com의 IP정보를 조회한 것으로 country가 한국인 것을 확인 할 수 있다.
$ ./mmdbinspect -db /app/util/geoip/geodb/GeoLite2-Country.mmdb 211.249.221.105
마찬가지로 www.kakao.com의 IP로 ASN을 조회하면 "Figure 15." 카카오 주식회사 정보를 확인할 수 있다.
$ ./mmdbinspect -db /app/util/geoip/geodb/GeoLite2-ASN.mmdb 211.249.221.105
1.4 에서 설치한 mmdbinspect의 결과는 JSON 규격으로 프롬프트 창에서 활용하기 쉽지 않으므로 jq 를 추가 설치하여 JSON 결과를 파싱해서 사용한다.
$ sudo yum install jq #jq RPM 설치
"Figure 16."은 jq를 이용하여 country 와 asn 값을 추출하는 예제를 보여준 것으로 Maxmind github 사이트에서 다양한 검색 방법을 참조할 수 있으므로 본장에서는 jq 사용법을 따로 다루진 않는다.
(https://github.com/maxmind/mmdbinspect#examples)
$ ./mmdbinspect -db /app/util/geoip/geodb/GeoLite2-Country.mmdb 211.249.221.105 \
> | jq -r '.[].Records[].Record | [.country.names.en][0]’
$ ./mmdbinspect -db /app/util/geoip/geodb/GeoLite2-ASN.mmdb 211.249.221.105 \
> | jq -r '.[].Records[].Record | [.autonomous_system_number][0]’
Apache, Nginx등 웹서버의 access log는 common과 combined 2가지를 포멧을 기본적으로 제공하며, 본장에서는 Client 정보를 조금더 상세하게 저장하는 combined을 기준으로 설명한다.
(운영중인 서버에서 IP주소가 Proxy서버 주소로 저장되거나 User-Agent가 없는 경우 설정을 통해서 수정이 가능하며, 공식 사이트에 설명이 되어 있으므로 본장에서는 다루지 않는다.)
10.0.1.1 - - [05/Sep/2022:19:06:36 +0900] "POST /api/store HTTP/1.1" 200 138 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
상기 예제는 combined 포멧으로 Apache에서는 아래와 같은 정보로 구성된다.
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
%h : 원격 호스트 이름(Proxy IP로 저장되는 경우 %{X-Forwarded-For}i 로 수정 필요)
%l : 클라이언트 식별자
%u : 인증 사용자명
%t : 시간
%r : 요청의 첫 번째 행의 값
%>s : 마지막 응답의 상태(HTTP 응답코드)
%b : 전송된 바이트 수 (헤더는 제외), 0바이트인 경우는 '-'이 표시된다.
%{Referer}i : 직전 방문 URL
%{User-Agent}i : User-Agent
상기 필드중 IP주소(%h)와 User-Agent(%{User-Agent}i)를 추출하여 특정 IP에서 웹서비스를 호출하는 빈도를 분석한다. User-Agent를 사용하는 이유는 Unique한 식별자는 아니지만 해당 정보에서 Http Client(브라우저, 단말기 정보)를 식별할 수 있고 IP와 결합하면 최소한 접속중인 단말기 수를 구분 할 수 있기 때문이다.
access log는 웹서버 설치시 별도의 수정을 하지 않았다면 1개의 파일에 계속 저장되어, 분석이 불가능할 정도로 대용량이 되어서는 곤란하므로 logrotate(Linux command가 안되는 경우 RPM 설치 해야함)를 이용하여 daily(또는 hourly)로 분리해야 한다.
logrotate를 사용하는 방법은 여러가지가 있으나 다른 설정과의 충돌을 최소화 하기 위해 access log를 위한 전용 conf 파일을 만들고 crontab에 등록해서 daily로 스케쥴링 하는 방법을 소개한다.
아래는 nginx의 access, error log를 daily로 rotation 시키는 예제이다.
#logrotate.conf 설정
/app/server/nginx/logs/*.log {
hourly #시간별로 생성
missingok
rotate 60
compress
dateext
delaycompress
notifempty
sharedscripts
postrotate
if [ -f /app/server/nginx/conf/nginx.pid ]; then
kill -USR1 `cat /app/server/nginx/conf/nginx. pid`
fi
endscript
}
#crontab 등록
1 0 * * * /usr/sbin/logrotate -f /app/server/nginx/conf/logrotate.conf > /dev/null 2>&1
2.2의 access log를 shell을 이용하여 주기적(e.g. 10분)으로 조회하여 임계치 이상의 트래픽이 유입되는 경우를 추출하여 알람에 활용할 수 있다. 기본적인 컨셉은 access log에서 IP와 User-Agent를 추출하여 unique 값만 추출하고, 추출된 값에서 IP별로 count(*) 하여 임계치 이상인 경우를 추출하는 것이다.
이때 임계치 설정은 지역에 따라 설정이 가능하고, 통신사의 경우 공유기 처럼 다수의 사설 IP가 존재하므로 임계치를 더 높게 설정하여 운영이 가능하며 국내 ASN은 인터넷진흥원에서 확인 할 수 있다.
국내 ASN : https://한국인터넷정보센터.한국/jsp/business/management/asList.jsp
아래는 bash shell script 예제로 웹서비스 성격 및 특성에 따라 응용하면, 보조적인 위협 탐지수단으로 활용이 가능할 것으로 생각 된다.
#!/bin/bash --login
#분석대상 일자
BASE_DT=`date +%Y%m%d`
#log파일 조회, grep 등 사용 가능
EXE_CMD="cat"
#임계치 이상 유입된 IP리스트
NOTI_ARR=""
#국내 통신사 ASN 예제
ASN_ARR="AS9318,AS9277,AS17846,AS9644,AS18302,AS23575,AS46004,AS38109,AS17597,AS17573,AS10164,AS9756,AS9971,AS9689,AS38103,AS38097,AS17849,AS17854,AS17857,AS17864,AS17871,AS18310,AS23578,AS38095,AS23563,AS9768,AS5051,AS3559,AS4040,AS3825,AS3757,AS4060,AS4766,AS23577,AS9760,AS45400,AS9964,AS10059,AS17858,AS9316,AS9950,AS17853,AS4753,AS3786,AS9688,AS17610,AS38092,AS17839,AS18313,AS10066,AS9845,AS18318,AS9698,AS9697,AS17577,AS38091,AS9770,AS38669,AS38120,AS17586,AS38121,AS17598,AS45365,AS38092,"
if [ -f "access.log-$BASE_DT" ]; then
EXE_CMD="$EXE_CMD access.log-$BASE_DT"
for IP in `$EXE_CMD | awk -F' - |\\"' '{print $1" "$7}' | sort | uniq | awk '{print $1}' | uniq -d -c | sort -nr | awk '{if ($1>100) print $2"^"$1}' | head -n20`; do
IP_ADDR=`echo $IP | awk -F'^' '{print $1}'`
IP_CNT=`echo $IP | awk -F'^' '{print $2}'`
IP_COUNTRY=`/app/util/geoip/mmdbinspect/mmdbinspect -db /app/util/geoip/geodb/
GeoLite2-Country.mmdb $IP_ADDR | jq -r '.[].Records[].Record | [.country.names.en][0]'`
MAX_CNT=100 #기본 임계치
if [ "$IP_COUNTRY" == "South Korea" ]; then #국내는 임계치 상향 적용
IP_ASN=`/app/util/geoip/mmdbinspect/mmdbinspect -db /app/util/geoip/geodb/GeoLite 2-ASN.mmdb $IP_ADDR | jq -r '.[].Records[].Record | [.autonomous_system_number][0]'`
IP_ASN="AS${IP_ASN},"
if [[ "$ASN_ARR" =~ "$IP_ASN" ]]; then
MAX_CNT=400 #통신사는 임계치 상향 적용
else
MAX_CNT=200
fi
fi
if [ $IP_CNT -ge $MAX_CNT ]; then
NOTI_ARR="$NOTI_ARR $IP_ADDR($IP_CNT)"
fi
done
fi
echo $NOTI_ARR
@@ 항상 느끼는 것이지만 글을 쓴다는 것은 매우 어려운 일인 것 같습니다. 반평생을 coding과 script 속에서 살던 사람에겐 더욱 그러한 것 같습니다. 몇번이고 수정했지만 틀리거나 부족한 부분이 있을 것이며 혹시 발견하시면 과감한 피드백 요청 드립니다. - 감사합니다. -