자정 무렵에 문득 스마트폰의 슬랙(Slack)앱에서 알림이 왔다. 서버의 메모리 점유율이 90%가 넘으면 스마트폰으로 메세지를 보내도록 디지털오션의 모니터링 기능에서 설정해 두었는데, 그 때문에 경고 메시지가 도착한 것이었다.
즉시 SSH로 접속해서 free -m 명령어로 메모리 사용현황을 확인해보니 혹시나 해서 잡아둔 스왑(swap)영역까지 거의 다 소진한 상태였다. top명령어로 현재 프로세스를 확인해 보니 평소 한두개 정도 활성화 되어 있던 아파치(apache2) 프로세스가 비오는날 개구리떼 마냥 수십개가 떠 있었다.
방문자가 폭증하여 생긴 현상이라면 반가운 일이었겠지만, 구글 애널리틱스의 실시간 데이터에서는 현재 접속자가 거의 없는 상태였다. 즉, 누군가가 고의로 동시에 접속해서 서버를 공격하고 있는 상황이었고 그래서 서버가 폭주하여 다른 방문자들이 방문을 못하고 있는 상황이었다.
아무튼 약간 번거롭긴 했지만 일단 syslog와 apache의 error.log를 다운받아서 로그분석을 해보기로 하였다. 누가 어떻게 공격하고 있는지 호기심이 생겼고 로그를 추적하는 것 또한 꽤나 흥미롭기도 했기 때문이다.
syslog를 분석해 보니 메모리 부족 관련 메시지 외에는 딱히 특이사항은 없었다. 하지만 아파치 에러로그 쪽에서는 아주 분석하기 좋도록 해킹에 관련된 상세한 정보들이 알차게 빼곡히 들어 있었다.
-상략-
[Thu May 04 23:55:46.108241 2017] [:error] [pid 8114] [client 64.79.85.205:32969] WordPress database error MySQL server has gone away for query SELECT option_value FROM wp_options WHERE option_name = ‘uninstall_plugins’ LIMIT 1 made by require(‘wp-blog-header.php’), require_once(‘wp-load.php’), require_once(‘wp-config.php’), require_once(‘wp-settings.php’), include_once(‘/plugins/advanced-ads/advanced-ads.php’), Advanced_Ads::get_instance, Advanced_Ads->__construct, Advanced_Ads_Plugin::get_instance, Advanced_Ads_Plugin->__construct, register_uninstall_hook, get_option
[Thu May 04 23:55:47.030685 2017] [:error] [pid 8049] [client 64.79.85.205:31018] WordPress database error MySQL server has gone away for query SELECT option_value FROM wp_options WHERE option_name = ‘_transient_jetpack_idc_allowed’ LIMIT 1 made by require(‘wp-blog-header.php’), require_once(‘wp-load.php’), require_once(‘wp-config.php’), require_once(‘wp-settings.php’), include_once(‘/plugins/jetpack/jetpack.php’), require_once(‘/plugins/jetpack/class.jetpack-idc.php’), Jetpack_IDC::init, Jetpack_IDC->__construct, Jetpack::check_identity_crisis, Jetpack::validate_sync_error_idc_option, get_transient, get_option
[Thu May 04 23:55:51.451153 2017] [:error] [pid 7934] [client 64.79.85.205:31009] WordPress database error MySQL server has gone away for query SELECT option_value FROM wp_options WHERE option_name = ‘wp_statistics_removal’ LIMIT 1 made by require(‘wp-blog-header.php’), require_once(‘wp-load.php’), require_once(‘wp-config.php’), require_once(‘wp-settings.php’), include_once(‘/plugins/wp-statistics/wp-statistics.php’), get_option
[Thu May 04 23:55:55.433766 2017] [:error] [pid 8115] [client 64.79.85.205:32970] WordPress database error MySQL server has gone away for query SELECT option_value FROM wp_options WHERE option_name = ‘advanced-ads-ab-module’ LIMIT 1 made by require(‘wp-blog-header.php’), require_once(‘wp-load.php’), require_once(‘wp-config.php’), require_once(‘wp-settings.php’), include_once(‘/plugins/advanced-ads/advanced-ads.php’), Advanced_Ads_ModuleLoader::loadModules, require_once(‘/plugins/advanced-ads/modules/ad-blocker/main.php’), Advanced_Ads_Ad_Blocker::get_instance, Advanced_Ads_Ad_Blocker->__construct, Advanced_Ads_Ad_Blocker->options, get_option
[Thu May 04 23:55:58.643707 2017] [:error] [pid 9938] [client 64.79.85.205:37683] PHP Warning: mysqli_query(): (HY000/2013): Lost connection to MySQL server during query in /var/www/html/wp/wp-includes/wp-db.php on line 1877
[Thu May 04 23:56:00.072306 2017] [mpm_prefork:error] [pid 2142] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting
-중략-
[Fri May 05 00:01:10.048041 2017] [core:warn] [pid 2142] AH00045: child process 18776 still did not exit, sending a SIGTERM
[Fri May 05 00:01:10.048056 2017] [core:warn] [pid 2142] AH00045: child process 7928 still did not exit, sending a SIGTERM
[Fri May 05 00:01:10.048070 2017] [core:warn] [pid 2142] AH00045: child process 7929 still did not exit, sending a SIGTERM
[Fri May 05 00:01:10.048165 2017] [core:notice] [pid 2142] AH00051: child pid 12025 exit signal Segmentation fault (11), possible coredump in /etc/apache2
[Fri May 05 00:01:10.048280 2017] [core:warn] [pid 2142] AH00045: child process 13832 still did not exit, sending a SIGTERM
[Fri May 05 00:01:10.048300 2017] [core:warn] [pid 2142] AH00045: child process 18955 still did not exit, sending a SIGTERM
-하략-
위와 같은 패턴의 로그가 수백개가 있었다. 해커의 IP는 64.79.85.205였는데 미국발IP주소였고 해킹 수법은 워드프레스의 취약점을 이용하려 했던 것 같다. 워드프레스 내의 각종 플러그인으로 뭔가를 시도한 흔적이 있었다. 구글에서 해당 에러메시지(WordPress database error Lost connection to MySQL server during query for query)를 검색해 보니 딱히 해커의 공격이 아니더라도 다양한 상황에서 발생하는 에러였긴 한데 아무튼 서버가 잠시 버벅거렸던 것 빼고는 별다른 피해는 없었다.
아무튼 예전 같았으면 그냥 sudo shutdown -r now 명령어로 서버를 재부팅 시키는 것으로 해결을 하였겠지만, 이번에는 호기심이 생겨서 다른 시도를 해보기로 했다.
일단 sudo service apache2 restart 명령어로 아파치 서버만 재시작 했을 경우에는 프로세스들이 떨어져 나가지를 않았다. 서비스를 재시작 하는 데에도 무척 오래 걸렸지만 재시작한 뒤에도 다시 수십개의 프로세스가 생성되었다.
결국 이리저리 검색해보다가 아파치 프로세스(혹은 쓰레드) 개수 자체를 제한하는 방향으로 연구해 보았다. 하지만 이 방법은 실방문자가 몰릴 때에는 대응하기 어려울 것 같아서 일단은 서버 기본값을 그대로 두기로 하고 대신에 IPTABLE를 이용해서 특정 공격자만 막아내는 방식으로 해결하기로 하였다.
그리하여 아래와 같이 명령어를 실행시켰다.
sudo iptables -A INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 16 -j DROP sudo apt-get install iptables-persistent
설명하자면 첫번째 줄은 80포트에서 한IP당 16개의 접속만 허용하고 그 이상은 그냥 씹어버리라는 뜻이다. 하지만 서버를 재부팅 하면 이 iptables 규칙이 날아가 버린다. 그래서 두번째 줄에서 iptables-persistent패키지를 설치해 준다. 현재의 세팅에 만족하면 그냥 Y만 누르면 되고 위에서 설정한 iptables 규칙들이 재부팅 된 뒤에도 계속 유지된다.
동시 접속을 16개까지 허용할 필요는 없었지만 다음번에 또 해커의 공격이 들어오면 어떻게 되는지 궁금하기도 하고 스트레스 테스트도 할 겸 해서 일단은 16으로 정해두었다. 만약 다음번 공격을 막아내지 못하면 곧바로 fail2ban에다가 워드프레스 관련 설정을 집어넣을까 생각중이다.
만약 iptables의 규칙을 변경할 경우에는 아래와 같이 해주면 된다.
sudo iptables -F sudo iptables -A INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 32 -j DROP sudo invoke-rc.d iptables-persistent save
iptables-persistent와 fail2ban을 같이 사용할 경우 재부팅시에 규칙들이 중복으로 설정되는 경우가 있기 때문에 일단 -F옵션으로 규칙들을 전부 싹다 삭제해준다. 그리고 그 다음에 변경된 규칙, 즉 32개의 동시접속을 허용하는 규칙을 설정해준다. 그리고 마지막 세번째 줄의 invoke-rc.d 명령어를 실행시켜서 세팅값을 저장하면 된다. 그리고 fail2ban은 재부팅하면 스스로 알아서 규칙을 추가하니 별도로 설정할 것은 없다.
아직까지 이곳은 서버 용량이 한계에 다다르기에는 엄청나게 여유가 많은 편이기는 하지만, 궁극적으로는 서버램을 1GB로 증설하고 웹서버도 nginx로 바꾸고 싶다는 생각을 하고 있다. 마치 지뢰찾기 게임을 해도 라이젠R7에서 하면 기분이 좋은 것처럼 말이다. 아무튼 이 글은 아름다운 5월의 어느 날 서버에 불청객이 다녀간 이야기다.
안녕하세요.
웹 어플리케이션을 개발하고 있는 1년차 개발자입니다.
글을 읽다가, 문득 궁금한 점이 생겨서 댓글 달아요~
“80포트에서 한IP당 16개의 접속만 허용” 하는 iptable을 설정하셨는 데
예를 들어,
대기업의 한 부서 인원 수가 30명인 곳에서 같은 네트워크를 사용하고 있으면 ip가 동일할텐 데
30명 중 16명만 접속할 수 있는거죠?
네. 맞습니다. 외부IP가 동일하다면 16명만 접속할 수 있습니다. 그리고 이리저리 테스트 해보니 한번 접속할 때마다 평균 4~5개씩 차지하더군요. 그러므로 실제로는 3~4명만 접속이 가능할 듯 싶습니다.