모르는 누군가의 흔적
안녕하세요, 다람쥐입니다.
개인 서버에 이상한 접속 로그가 보입니다.
$ grep " 404 " /var/log/nginx/access.log | awk '{print $1, $4, $7}'
8.xxx.xxx.167 [14/Feb/2025:01:24:11 /dns-query
...
193.xxx.xxx.176 [14/Feb/2025:04:34:41 /nginx/.env
193.xxx.xxx.176 [14/Feb/2025:04:34:41 /public/.env
193.xxx.xxx.176 [14/Feb/2025:04:34:42 /site/.env
193.xxx.xxx.176 [14/Feb/2025:04:34:42 /xampp/.env
193.xxx.xxx.176 [14/Feb/2025:04:34:42 /.docker/laravel/app/.env
193.xxx.xxx.176 [14/Feb/2025:04:34:42 /laravel/.env.local
193.xxx.xxx.176 [14/Feb/2025:04:34:42 /laravel/.env.production
193.xxx.xxx.176 [14/Feb/2025:04:34:43 /laravel/.env.staging
193.xxx.xxx.176 [14/Feb/2025:04:34:43 /laravel/core/.env.local
193.xxx.xxx.176 [14/Feb/2025:04:34:43 /laravel/core/.env.production
193.xxx.xxx.176 [14/Feb/2025:04:34:43 /laravel/core/.env.staging
...
* grep : 줄 당 해당 문구만 찾아 출력하는 프로그램
* awk : 라인 단위의 레코드를 공백 문자(스페이스/탭)로 필드로 구분하는 프로그램
Nginx 접속 로그에 404 상태 코드로 응답한 목록만 추출했어요.
누가 봐도 수상해 보이죠~? 😅
서버 정보를 훑어가다니, 오랜만에 피가 끓어오르는군요.
새해 첫 블로그가 분노의 글이 될 줄은 꿈에도 몰랐네요.
이러한 공격 형태를 프로빙 어택(Probing Attack), 프로빙 봇(Probing Bots)이라고 부르기도 합니다.
(경우에 따라 스캐닝 - Scanning - 이라고도 합니다.)
공개된 네트워크망에 돌아다니며, 단순한 사전 형태로 취약점을 찾고 민감한 데이터를 긁어갑니다.
우선 아이피만 추출해 봤습니다.
uniq 프로그램에 입력해, 중복을 제거해 봅시다.
마지막으로 nl 프로그램에 입력해, 총개수를 찾아봅시다.
$ grep " 404 " /var/log/nginx/access.log | awk '{print $1}' | uniq | nl
1 66.xxx.xxx.168
2 45.xxx.xxx.66
3 78.xxx.xx.156
...
68 193.xxx.xxx.51
69 34.xxx.xxx.96
너무 많네요...
언제 다 아이피를 조사하죠.
몇몇 개는 같은 CIDR 블록을 쓰는 것 같은데...
또 다 그런 건 아니에요.
치사하게 아이피를 바꿔서 털어갑니다.
애플리케이션보다는 웹 서버인 Nginx에서 무언가를 할 수 있지 않을까, 구글링을 했습니다.
설정이 간단하고 정상 응답을 건드리지 않으면은 좋을 것 같아요.
대략 두 가지 방법이 나왔습니다.
- Nginx : 정규식 기반 사전식 요청 필터링
- fail2ban : 필터 규칙 설정 기반 IP 차단 도구
1. Nginx : 정규식 기반 사전식 요청 필터링
첫 번째 방식으로 Nginx를 설정해 봅시다.
저희도 나름(?)의 사전을 만들어 봅시다.
우선 단어를 필터링해봅시다.
대충 눈대중으로만 봐도 env, php 가 많이 나오는 걸 확인했어요.
그래도 어떤 단어가 얼마나 나오는지 자세히 체크해 보죠.
$ grep " 404 " /var/log/nginx/access.log | awk '{print $7}' | tr -cs '[:alnum:]' '\n' | sort | uniq -c | sort -nr | nl
1 112 phpunit
2 85 php
3 62 stdin
4 62 eval
5 60 Util
6 60 PHP
7 55 env
8 48 vendor
9 48 src
10 14 dns
11 12 laravel
12 11 lib
13 11 index
14 10 phpinfo
15 10 page
16 10 5D
17 9 query
18 8 public
19 8 config
20 7 api
21 7 0
22 6 ico
23 6 favicon
24 5 v1
25 5 true
26 5 application
27 5 60
28 5 5Boffset
29 5 5Blimit
30 4 ws
31 4 vars
32 4 profiler
33 4 new
34 4 local
35 4 core
36 4 app
37 4 ADd
38 3 www
39 3 txt
40 3 staging
41 3 production
42 3 owa
43 3 md5
44 3 info
45 3 git
46 3 docker
47 3 1
48 2 zend
49 2 yii
50 2 xampp
51 2 x5Cthink
52 2 x5Capp
53 2 world
54 2 webui
55 2 web
56 2 user
57 2 url
58 2 tmp
59 2 tests
60 2 testing
61 2 test
62 2 s
63 2 robots
64 2 prepend
65 2 password
66 2 lara
67 2 lang
68 2 js
69 2 invokefunction
70 2 input
71 2 index1
72 2 include
73 2 hello
74 2 geoserver
75 2 function
76 2 func
77 2 file
78 2 ec
79 2 call
80 2 backup
81 2 auto
82 2 array
83 2 allow
84 2 V2
85 2 LICENSE
86 2 Hello
87 2 3dphp
88 2 3d1
89 1 yaml
90 1 x22hi
91 1 x22
92 1 x
93 1 wp
94 1 workspace
95 1 well
96 1 vscode
97 1 usr
98 1 upl
99 1 t4
100 1 systembc
101 1 site
102 1 security
103 1 secret
104 1 routes
105 1 refs
106 1 r3ABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ
107 1 portal
108 1 phpstorm
109 1 pearcmd
110 1 pdown
111 1 panel
112 1 node
113 1 nginx
114 1 modules
115 1 main
116 1 mailer
117 1 mail
118 1 known
119 1 json
120 1 html
121 1 geoip
122 1 gateway
123 1 form
124 1 echo
125 1 eVYBAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ
126 1 eP8BAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ
127 1 drupal
128 1 dev
129 1 demo
130 1 cron
131 1 crm
132 1 create
133 1 containers
134 1 conf
135 1 cms
136 1 blog
137 1 awstats
138 1 aws
139 1 auth
140 1 apps
141 1 admin
142 1 actuator
143 1 YBAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ
144 1 XDEBUG
145 1 UMBAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ
146 1 START
147 1 SESSION
148 1 HEAD
149 1 C
* tr : 입력된 문자를 변환하거나 삭제해주는 프로그램
* -c : 지정된 문자 집합에 미포함한 모든 문자를 처리 대상으로 설정
* -s : 연속된 중복 문자를 하나로 축소 (여기선 개행이 여러개 있어도 하나로 축소)
* '[:alnum:]' : POSIX 문자 클래스 중 하나로, 알파벳(a-z, A-Z)과 숫자(0-9)를 포함
* ex. /.env{\n}/admin -> {\n}{\n}env{\n}{\n}admin : 알파벳과 숫자를 제외한 모든 문자를 개행으로 치환 -> {\n}env{\n}admin : {\n} 개행은 하나로 축소
휴~ 많기도 합니다.
가장 많이 나온 단어는 phpunit, php, phpinfo 등 'php' 관련 단어가 제일 많습니다.
참 많이도 호출합니다.
나머지는 후술 할 fail2ban 으로 막기를 기대하고
요청이 많이 오는 단어와 민감한 데이터의 위험성이 있는 'env' 단어를 추가해 봅시다.
나머지 단어는 AI에게 로그 기반으로 추가해 달라고 했습니다. 😁
$ sudo vim ~~/nginx/default
server {
server_name xxx.xxx.xxx
...
location ~* (php|laravel|stdin|eval|env|vendor|test|git|svn|bak|old|backup|xampp|.well-known|security|dns) {
deny all;
add_header X-To-Bot "Fxxk you" always;
return 403;
}
...
}
인심써서 헤더에 아름다운 메시지도 첨가해주죠.
중간에 사전에 해당하는 URL 접근을 막았습니다.
Nginx 설정을 변경했으니, 데몬을 리로드 합니다.
$ service nginx reload
실제로 막아졌는지 확인해 볼까요?
$ curl -I https://xxx.yyy.zzz/phpunit/hello.txt
HTTP/1.1 403 Forbidden
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 15 Feb 2025 06:46:17 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
X-To-Bot: F**k you
아름다운 403 Forbidden 코드와
"F***k you" 헤더가 반겨줍니다.
2. fail2ban : 필터 규칙 설정 기반 IP 차단 도구
2번은 Nginx 외에 별도 프로그램을 데몬으로 실행하는 방법이에요.
Nginx의 rate-limit 모듈도 고려했지만...
QoS 목적의 Rate Limit 기능과는 거리가 멀어서, 보안 소프트웨어(공격자만 패는)를 원했습니다.
그리하여 여러 블로거가 추천하는 오픈 소스 소프트웨어인 fail2ban을 도입해 봤어요.
fail2ban 설치 방법은 깃허브 위키에 친절히 나와있습니다.
AWS EC2 인스턴스는 우분투이므로 apt 패키지를 이용해 보죠.
$ apt update && apt upgrade
$ apt install fail2ban
fail2ban 동작 방법
fail2ban은 시스템 로그 파일, 저널을 스캔해요.
과도하게 요청하면, 미리 설정한 규칙으로 차단해요.
규칙은 실패 정규식(failregex)과 필터(filter) 규칙을 설정할 수 있어요.
/etc/fail2ban 하위의 .conf 설정 파일을 불러와요.
우리가 따로 conf 파일을 건드릴 필요는 없어요.
.local 파일로 새로 생성하면 덮어씌워집니다.
예를 들면, jail.conf를 직접 수정하는 대신, jail.local로 설정하면 됩니다.
fail2ban 필터 정의 / 규칙 설정
$ sudo vim /etc/fail2ban/filter.d/nginx-404.conf
[Definition]
failregex = ^<HOST>.*"(GET|POST).*" 404 .*$
ignoreregex =
$ sudo vim /etc/fail2ban/jail.local
[nginx-404]
enabled = true
port = http,https
filter = nginx-404 # fiter.d 디렉토리에 정의한 필터명
logpath = /var/log/nginx/access.log # 모니터링할 로그 파일
bantime = 3600 # IP가 차단 시간 (초단위, 기본 600초/10분)
findtime = 300 # 공격 시도를 감지할 시간 간격 (초단위, 기본 600초/10분)
maxretry = 5 # 최대 허용할 실패 횟수
# fail2ban 재시작
$ sudo service fail2ban restart
filter.d 디렉터리에 로그를 탐지할 규칙을 작성했어요.
Nginx 접근 로그에서 정규식으로 404 상태 코드를 찾도록 설정했습니다.
jail.local 설정파일에 정의한 필터명을 입력하면 돼요.
모니터링할 로그 파일과 차단 시간, 시간 간격, 실패 횟수를 설정합니다.
저는 괘씸(?)하니깐 5분 내에 5번 404 오류를 반복하면, 무려 1시간 동안 쫓아냈어요.
아무래도 웹 API 서버다 보니, 클라이언트가 잘못 설계하지 않는 한 404 오류가 발생할 확률은 무척 낮습니다.
실패 횟수 5회로 민감률을 높여서, 적은 반복 수의 프로빙 어택도 막고자 했습니다.
운영 / 개발 환경에서의 404 오류가 잦다면, 조금씩 조절해 나가려고 합니다.
fail2ban 역모를 꾸민 IP를 추출하기
그물에 걸린 어떤 IP가 차단되었는지 어떻게 확인할 수 있을까요? (참고로 jail은 교도소란 뜻...)
# sudo fail2ban-client status <JAIL_NAME>
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 5
| |- Total failed: 6
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
따끈따끈한 감옥이라 아직 아무도 없군요.
제가 먼저 들어가 보겠습니다.
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 0
| |- Total failed: 11
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: xxx.xxx.xxx.xxx
fail2ban 수동으로 차단된 IP 해제하기
자, 제가 들어갔는데... 제가 못 나오면 안 되겠죠?
fail2ban-client 프로그램에서 임의로 설정할 수 있답니다.
# sudo fail2ban-client set <JAIL_NAME> unbanip <IP_ADDRESS>
$ sudo fail2ban-client set nginx-404 unbanip xxx.xxx.xxx.xxx
1
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 0
| |- Total failed: 11
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 1
`- Banned IP list:
Banned IP list에서 없어졌습니다!
만약 동료 개발자가 감옥에 갇혔다면, 구제해 주시길 바랍니다.
fail2ban 화이트리스트 설정하기
만약 사내 네트워크망 등 같은 네트워크는 규칙에 걸리지 않게 하고 싶거나
운영자, 동료끼리는 적용하고 싶지 않을 수 있습니다.
화이트 리스트는 어떻게 설정할까요?
jail.local 파일에서 ignoreip 속성을 추가합니다.
[DEFAULT]
ignoreip = xxx.xxx.xxx.xxx xxx.xxx.0.0/16
$ sudo systemctl restart fail2ban
jail.conf 파일에서 직접 수정해도 되지만
안전하게 재정의가 되는 jail.local에서 설정하는 걸 권장한다고 합니다.
띄어쓰기를 구분으로 특정 IP 주소 / 특정 서브넷을 포함시킬 수 있습니다.
전역 설정이 아닌 특정 Jail(nginx-404)에만 화이트리스트를 적용하려면,
해당 Jail 섹션에 위 ignoreip 설정을 추가하면 됩니다.
마무리
프로빙 어택이 꽤 흔한 것으로 알고 있습니다.
제 주변에서도 아무에게도 알려준 적이 없었는데도...
어디서 알고 오는지 접근하는 케이스가 있다고 하더라고요.
(은연중에 흘렸을 수도 있고, 저희의 잘못이 아닌 외부에서 유출당한 걸 수도 있고요.)
중요한 데이터는 하나도 털리지는 않았지만,
찜찜한 분들에게 도움이 되었기를 바랍니다.
새벽에 깔끔하게 작업하고
상쾌하게 하루를 맞이했습니다.
대략 12시간 후에 로그를 확인해보니...
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 11
| |- Total failed: 111
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 10
`- Banned IP list:
밴 수가 늘었습니다.
fail2ban 로그도 확인해볼까요?
$ tail /var/log/fail2ban.log -n 100
2025-02-15 00:42:41,180 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 00:42:41
2025-02-15 00:52:01,457 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 00:52:01
2025-02-15 01:06:12,759 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 01:06:12
...
2025-02-15 05:27:19,539 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 05:27:19
2025-02-15 05:27:39,005 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:27:38
2025-02-15 05:27:58,566 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:27:58
2025-02-15 05:29:08,883 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:29:08
2025-02-15 05:29:09,024 fail2ban.actions [711591]: NOTICE [sshd] Ban xxx.xxx.xxx.xxx
2025-02-15 05:29:37,275 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:29:37
2025-02-15 05:31:16,303 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:31:15
2025-02-15 05:32:56,731 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:32:56
2025-02-15 05:34:36,546 fail2ban.filter [711591]: INFO [sshd] Found xxx.xxx.xxx.xxx - 2025-02-15 05:34:36
2025-02-15 05:36:31,609 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 05:36:31
2025-02-15 05:38:16,126 fail2ban.actions [711591]: NOTICE [nginx-404] Unban xxx.xxx.xxx.xxx
2025-02-15 05:39:08,398 fail2ban.actions [711591]: NOTICE [sshd] Unban xxx.xxx.xxx.xxx
2025-02-15 06:05:16,900 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 06:05:16
2025-02-15 06:05:21,290 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 06:05:21
2025-02-15 06:20:09,337 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 06:20:09
2025-02-15 06:26:15,427 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 06:26:15
2025-02-15 06:44:25,699 fail2ban.filter [711591]: INFO [nginx-404] Found xxx.xxx.xxx.xxx - 2025-02-15 06:44:25
그새 정말 많은 아이피가 왔다갔네요.
밴 1시간은 너무 짧은가 싶기도 하고요.
그래서 1시간동안 찾아서 1년(!)까지 밴을 시켰습니다. 후후.
bantime = 31536000 # 1 year
findtime = 86400 # 1 day
아무튼 달밤의 체조 재밌었습니다.
이튿날 밤.
한 마리 잡았습니다.
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 22
| |- Total failed: 39
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 78.xxx.xxx.159
$ grep "78.xxx.xxx.159" /var/log/nginx/access.log
78.xxx.xxx.159 - - [15/Feb/2025:03:44:09 +0000] "GET /.env HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:03:44:10 +0000] "POST / HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:05:04:30 +0000] "GET /.env HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:05:04:31 +0000] "POST / HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:07:48:37 +0000] "GET /.env HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:07:48:38 +0000] "POST / HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:10:38:59 +0000] "GET /.env HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:10:39:00 +0000] "POST / HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:12:04:42 +0000] "GET /.env HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
78.xxx.xxx.159 - - [15/Feb/2025:12:04:43 +0000] "POST / HTTP/1.1" 404 197 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36"
# 또 몇 시간 후...
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 25
| |- Total failed: 51
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 3
|- Total banned: 3
`- Banned IP list: 78.xxx.xxx.159 66.xxx.xxx.168 167.xxx.xxx.46
---
일주일 지난 결과 46개의 IP가 걸러졌네요. 😁
그리고 404 응답 코드의 IP도 69개에서 4개로 줄었습니다.
$ sudo fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
| |- Currently failed: 50
| |- Total failed: 786
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 46
|- Total banned: 46
`- Banned IP list: ...
$ grep " 404 " /var/log/nginx/access.log | awk '{print $1}' | uniq | nl
1 218.xxx.xxx.92
2 217.xxx.xxx.125
3 106.xxx.xxx.238
4 84.xxx.xxx.138
참고 문서
https://manpages.ubuntu.com/manpages/xenial/man1/fail2ban-client.1.html
댓글