mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-21 19:10:41 +08:00
148 lines
6.7 KiB
JavaScript
148 lines
6.7 KiB
JavaScript
import {
|
|
extensionRequest,
|
|
launchBrowserAgentContractHarness,
|
|
transformedFetchOptions,
|
|
waitFor,
|
|
} from './browser-agent-contract-harness.mjs'
|
|
|
|
const targetURL = process.env.DES_TARGET || 'http://127.0.0.1:82/'
|
|
const requestPath = '/encrypt/des.php'
|
|
const plaintext = {username: 'admin', password: '123456'}
|
|
let harness
|
|
|
|
function pathOf(url) {
|
|
return new URL(url, targetURL).pathname
|
|
}
|
|
|
|
async function performLogin(targetPage, password = 'wrong-password') {
|
|
await targetPage.locator('#username').fill('admin')
|
|
await targetPage.locator('#password').fill(password)
|
|
await targetPage.getByRole('button', {name: '登录', exact: true}).click()
|
|
return targetPage.getByRole('button', {name: 'Des规律Key', exact: true})
|
|
}
|
|
|
|
try {
|
|
harness = await launchBrowserAgentContractHarness({
|
|
profilePrefix: 'yakit-des-',
|
|
targetURL,
|
|
})
|
|
const {controlPage, tabId, targetPage} = harness
|
|
let browserRequestCount = 0
|
|
targetPage.on('request', (request) => {
|
|
if (pathOf(request.url()) === requestPath) browserRequestCount += 1
|
|
})
|
|
|
|
await extensionRequest(controlPage, 'recording.start', {
|
|
tabId, frameId: 0, captureValues: true, maxEntries: 120, maxValueBytes: 8_192,
|
|
})
|
|
await (await performLogin(targetPage)).click()
|
|
await targetPage.waitForTimeout(500)
|
|
if (browserRequestCount !== 1) throw new Error(`Initial DES recording expected one request, received ${browserRequestCount}`)
|
|
|
|
const snapshot = await extensionRequest(controlPage, 'recording.get', {tabId, frameId: 0, limit: 120})
|
|
const candidate = snapshot.profileCandidates?.find((item) => pathOf(item.request?.url || '') === requestPath)
|
|
if (!candidate || candidate.status !== 'capture-required') {
|
|
throw new Error(`Structured DES output was not routed through business-envelope capture: ${JSON.stringify(candidate)}`)
|
|
}
|
|
const matcherEvent = snapshot.events?.find((event) => event.id === candidate.capturePlan?.matcherEventId)
|
|
if (matcherEvent?.crypto?.family !== 'symmetric'
|
|
|| !matcherEvent.crypto.operation.toLowerCase().includes('des')
|
|
|| !matcherEvent.wrapperHandleId) {
|
|
throw new Error(`DES candidate has no reusable crypto matcher: ${JSON.stringify(matcherEvent)}`)
|
|
}
|
|
|
|
await extensionRequest(controlPage, 'deep.capture.start', {
|
|
tabId,
|
|
frameId: 0,
|
|
matcher: {
|
|
kind: 'crypto',
|
|
adapterId: matcherEvent.crypto.adapterId,
|
|
operation: matcherEvent.crypto.operation,
|
|
wrapperHandleId: matcherEvent.wrapperHandleId,
|
|
scriptUrl: matcherEvent.scriptUrl,
|
|
frameHints: candidate.capturePlan.frameHints,
|
|
},
|
|
})
|
|
const replayButton = await performLogin(targetPage)
|
|
let replayFailure
|
|
const replay = replayButton.click({noWaitAfter: true, timeout: 20_000})
|
|
.catch((reason) => { replayFailure = reason })
|
|
const paused = await waitFor(controlPage, 'deep.capture.status', {tabId, frameId: 0}, (value) => (
|
|
value?.state === 'paused' && value.pause?.collecting !== true
|
|
), 20_000)
|
|
const automatic = paused.pause?.automaticCapture
|
|
const frame = paused.pause?.frames?.find((item) => item.id === automatic?.frameId)
|
|
if (automatic?.state !== 'ready' || automatic.strategy !== 'request-transaction'
|
|
|| frame?.functionName !== 'encryptAndSendDataDES') {
|
|
throw new Error(`DES deep capture did not select its business request envelope: ${JSON.stringify({automatic, frame})}`)
|
|
}
|
|
const callable = await extensionRequest(controlPage, 'callable.create', {
|
|
tabId,
|
|
frameId: 0,
|
|
source: 'deep-capture',
|
|
strategy: 'request-transaction',
|
|
callFrameId: frame.id,
|
|
name: 'DES 请求事务',
|
|
candidateId: candidate.id,
|
|
})
|
|
await replay
|
|
if (replayFailure) throw replayFailure
|
|
await targetPage.waitForTimeout(250)
|
|
if (browserRequestCount !== 1) throw new Error(`DES capture leaked a terminal request; observed ${browserRequestCount}`)
|
|
|
|
const callableExecution = await extensionRequest(controlPage, 'callable.execute', {
|
|
tabId, frameId: 0, callableId: callable.id, args: [plaintext],
|
|
})
|
|
if (callableExecution.value?.username !== plaintext.username
|
|
|| !/^[a-f0-9]+$/i.test(callableExecution.value?.password || '')) {
|
|
throw new Error(`DES request transaction did not preserve the Hex envelope: ${JSON.stringify(callableExecution)}`)
|
|
}
|
|
|
|
const plainPacket = {
|
|
method: 'POST',
|
|
url: new URL(requestPath, targetURL).toString(),
|
|
headers: [{name: 'Content-Type', value: 'application/json'}],
|
|
bodyBase64: Buffer.from(JSON.stringify(plaintext)).toString('base64'),
|
|
}
|
|
const validation = await extensionRequest(controlPage, 'analysis.profile.validate', {
|
|
tabId,
|
|
frameId: 0,
|
|
candidateId: candidate.id,
|
|
callableId: callable.id,
|
|
inputPaths: ['body'],
|
|
name: 'DES 明文网关',
|
|
packet: plainPacket,
|
|
comparisonMode: 'structure',
|
|
})
|
|
if (!validation?.valid || !validation?.saveEligible || !validation.validationDraft?.id) {
|
|
throw new Error(`DES Profile validation failed: ${JSON.stringify(validation)}`)
|
|
}
|
|
const validationDraft = await extensionRequest(controlPage, 'analysis.profile.validation.latest', {tabId, frameId: 0})
|
|
const savedProfile = await extensionRequest(controlPage, 'transform.profile.save', validationDraft.profile)
|
|
const explanationText = JSON.stringify(savedProfile.explanation)
|
|
if (!explanationText.includes('DES') || explanationText.includes(plaintext.password)) {
|
|
throw new Error(`DES semantic explanation is missing or persisted plaintext: ${explanationText}`)
|
|
}
|
|
|
|
const execution = await extensionRequest(controlPage, 'transform.execute', {
|
|
profileId: savedProfile.id,
|
|
direction: 'request',
|
|
packet: plainPacket,
|
|
})
|
|
const wireBody = JSON.parse(Buffer.from(execution.bodyBase64, 'base64').toString('utf8'))
|
|
if (wireBody.username !== plaintext.username || !/^[a-f0-9]+$/i.test(wireBody.password || '')) {
|
|
throw new Error(`Saved DES Profile produced an invalid terminal body: ${JSON.stringify(wireBody)}`)
|
|
}
|
|
if (!execution.nodeTrace?.length || !execution.fieldChanges?.some((change) => change.path === 'body.password')) {
|
|
throw new Error(`Saved DES Profile did not return an explainable runtime trace: ${JSON.stringify(execution)}`)
|
|
}
|
|
if (browserRequestCount !== 1) throw new Error(`DES Profile execution leaked a browser request; observed ${browserRequestCount}`)
|
|
|
|
const response = await fetch(execution.url, transformedFetchOptions(execution, plainPacket.headers))
|
|
const result = await response.json()
|
|
if (!result.success) throw new Error(`Target server rejected the saved DES Profile output: ${JSON.stringify(result)}`)
|
|
process.stdout.write('DES transaction verified: the structured CipherParams result required business-envelope capture, replay preserved Hex serialization, runtime evidence stayed value-free, and the target accepted the final packet.\n')
|
|
} finally {
|
|
await harness?.close()
|
|
}
|