-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
1068 lines (932 loc) · 52.5 KB
/
Copy pathscript.js
File metadata and controls
1068 lines (932 loc) · 52.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
let LANG = 'en';
const matrixState = { drops: [], columns: 0, width: 0, height: 0 };
(function matrixRain(){
const canvas = document.getElementById('matrix-bg');
const ctx = canvas.getContext('2d');
const chars = "01アイウエオカキクケコサシスセソABCDEF{}<>/*;:$#@!01234567890";
function resize(){
matrixState.width = canvas.width = window.innerWidth;
matrixState.height = canvas.height = window.innerHeight;
const fontSize = 15;
matrixState.columns = Math.floor(matrixState.width / fontSize);
matrixState.drops = new Array(matrixState.columns).fill(0).map(()=> Math.floor(Math.random() * -50));
}
window.addEventListener('resize', resize);
resize();
let frame = 0;
function draw(){
frame++;
const { width, height, columns, drops } = matrixState;
ctx.fillStyle = 'rgba(3,6,8,0.10)';
ctx.fillRect(0,0,width,height);
ctx.font = '15px "JetBrains Mono", monospace';
for(let i=0;i<columns;i++){
const char = chars[Math.floor(Math.random()*chars.length)];
const x = i*15;
const y = drops[i]*15;
ctx.fillStyle = 'rgba(180,255,220,0.8)';
ctx.fillText(char, x, y);
ctx.fillStyle = Math.random() > 0.94 ? 'rgba(53,228,255,0.5)' : 'rgba(51,255,156,0.45)';
ctx.fillText(char, x, y - 15);
if(frame % 3 === 0){
if(y > height && Math.random() > 0.975){
drops[i] = 0;
} else {
drops[i]++;
}
}
}
requestAnimationFrame(draw);
}
draw();
})();
const BOOT_LINES = {
en: [
{t:'initializing cyber_lab kernel...', c:'dim'},
{t:'loading sandbox network interfaces [OK]', c:'ok'},
{t:'mounting virtual target: sandbox.local [OK]', c:'ok'},
{t:'spawning mock services: http, sql-engine, auth [OK]', c:'ok'},
{t:'WARNING: all requests are simulated, no real exploitation', c:'warn'},
{t:'loading vulnerability modules (8)... [OK]', c:'ok'},
{t:'establishing training session...', c:'dim'},
{t:'ACCESS GRANTED — welcome, operator.', c:'ok'},
],
ar: [
{t:'تهيئة نواة cyber_lab...', c:'dim'},
{t:'تحميل واجهات الشبكة الوهمية [تم]', c:'ok'},
{t:'تركيب الهدف الافتراضي: sandbox.local [تم]', c:'ok'},
{t:'تشغيل خدمات وهمية: http, sql-engine, auth [تم]', c:'ok'},
{t:'تحذير: جميع الطلبات وهمية، لا يوجد استغلال حقيقي', c:'warn'},
{t:'تحميل وحدات الثغرات (8)... [تم]', c:'ok'},
{t:'إنشاء جلسة تدريبية...', c:'dim'},
{t:'تم منح الوصول — أهلًا بك أيها المشغّل.', c:'ok'},
]
};
function runBootSequence(){
const el = document.getElementById('boot-lines');
const lines = BOOT_LINES[LANG];
el.innerHTML = '';
let out = '';
let li = 0, ci = 0;
function typeNext(){
if(li >= lines.length){
el.innerHTML = out + '<span class="boot-cursor"> </span>';
setTimeout(()=>{
document.getElementById('boot-screen').classList.add('hidden');
document.getElementById('app').classList.remove('hidden');
runIntroReveal();
}, 500);
return;
}
const line = lines[li];
ci++;
const partial = line.t.slice(0, ci);
el.innerHTML = out + `<span class="${line.c}">${partial}</span>` + '<span class="boot-cursor"> </span>';
if(ci <= line.t.length){
setTimeout(typeNext, 8 + Math.random()*10);
} else {
out += `<span class="${line.c}">${line.t}</span>\n`;
li++; ci = 0;
setTimeout(typeNext, 90);
}
}
typeNext();
}
(function clock(){
const el = document.getElementById('clock');
function tick(){
const d = new Date();
el.textContent = d.toTimeString().split(' ')[0];
}
tick();
setInterval(tick, 1000);
})();
const STR = {
en: {
brandSub: 'PENETRATION TESTING SANDBOX v2.1',
targetLabel: 'TARGET: sandbox.local',
modeLabel: 'MODE: SIMULATED',
desc: 'Hands-on training for bug bounty hunters — 8 realistic vulnerability challenges, <strong>100% simulated</strong>. Solve one to unlock the next.',
footer: 'This site is purely educational — every request and response is simulated and never touches a real server or system.',
hintLabel: 'Hint:',
reset: '↺ Reset',
send: 'Send ▶',
close: 'Close ✕',
openLab: 'Open Lab ⌁',
placeholder: '// Click "Send" to see the mock server response here',
langToggleLabel: 'العربية',
sevLabel: { crit:'Critical', high:'High', med:'Medium' },
lockedLabel: 'LOCKED',
lockedHint: 'Solve the previous challenge to unlock',
nextUnlocked: 'Next challenge unlocked — key:',
allDone: 'All 8 challenges completed!',
},
ar: {
brandSub: 'بيئة اختبار اختراق تدريبية v2.1',
targetLabel: 'الهدف: sandbox.local',
modeLabel: 'الوضع: محاكاة',
desc: 'بيئة تدريبية لصيادي الثغرات (Bug Bounty) — 8 تجارب واقعية، <strong>وهمية 100%</strong>. حل كل تجربة يفتح لك التي تليها.',
footer: 'هذا الموقع تعليمي بالكامل — جميع الطلبات والاستجابات وهمية ولا تتصل بأي خادم أو نظام حقيقي.',
hintLabel: 'التلميح:',
reset: '↺ استعادة',
send: 'إرسال ▶',
close: 'إغلاق ✕',
openLab: 'فتح المختبر ⌁',
placeholder: '// اضغط "إرسال" لترى رد الخادم الوهمي هنا',
langToggleLabel: 'English',
sevLabel: { crit:'حرجة', high:'عالية', med:'متوسطة' },
lockedLabel: 'مقفلة',
lockedHint: 'أكمل التجربة السابقة لفتح هذه',
nextUnlocked: 'تم فتح التجربة التالية — المفتاح:',
allDone: 'أتممت جميع التجارب الثمانية!',
}
};
const TITLE_WORDS = ['Vulnerability', 'Bugs', 'Injection', 'Exploit', 'Backdoor'];
function getParam(text, name){
const re = new RegExp(name + '=([^&\\s]*)');
const m = text.match(re);
if(!m) return '';
try{ return decodeURIComponent(m[1].replace(/\+/g,' ')); }
catch(e){ return m[1]; }
}
function esc(str){
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
function pick(lang, en, ar){ return lang === 'ar' ? ar : en; }
const typeSessions = new WeakMap();
function typeHTML(el, html, opts={}){
const speed = opts.speed ?? 12;
const jitter = opts.jitter ?? 10;
const showCursor = opts.cursor !== false;
const cursor = '<span class="type-cursor"> </span>';
const session = Symbol();
typeSessions.set(el, session);
let i = 0, out = '';
el.innerHTML = showCursor ? cursor : '';
function step(){
if(typeSessions.get(el) !== session) return;
if(i >= html.length){
el.innerHTML = out;
if(opts.onDone) opts.onDone();
return;
}
if(html[i] === '<'){
const close = html.indexOf('>', i);
if(close === -1){ out += html.slice(i); i = html.length; }
else { out += html.slice(i, close+1); i = close+1; }
el.innerHTML = out + (showCursor ? cursor : '');
step();
return;
}
out += html[i];
i++;
el.innerHTML = out + (showCursor ? cursor : '');
setTimeout(step, speed + Math.random()*jitter);
}
step();
}
function eraseHTML(el, opts={}, onDone){
const speed = opts.speed ?? 18;
const jitter = opts.jitter ?? 12;
const cursor = '<span class="type-cursor"> </span>';
const session = Symbol();
typeSessions.set(el, session);
let text = el.textContent;
function step(){
if(typeSessions.get(el) !== session) return;
if(text.length <= 0){
el.innerHTML = cursor;
if(onDone) onDone();
return;
}
text = text.slice(0, -1);
el.innerHTML = text + cursor;
setTimeout(step, speed + Math.random()*jitter);
}
step();
}
let titleCycleTimer = null;
function stopTitleCycle(){
if(titleCycleTimer){ clearTimeout(titleCycleTimer); titleCycleTimer = null; }
}
function startTitleCycle(glitchEl){
let idx = 0;
function loop(){
const holdMs = 5000 + Math.random() * 3000;
titleCycleTimer = setTimeout(()=>{
idx = (idx + 1) % TITLE_WORDS.length;
eraseHTML(glitchEl, {}, ()=>{
typeHTML(glitchEl, TITLE_WORDS[idx], { speed:30, jitter:20, onDone:()=>{
glitchEl.setAttribute('data-text', TITLE_WORDS[idx]);
loop();
}});
});
}, holdMs);
}
loop();
}
const FAKE_USERS = {
'1001': {name:'Sarah Al-Otaibi', email:'sarah.o@sandbox.local', phone:'05xxxxxx01', role:'customer', card:'**** **** **** 4471'},
'1002': {name:'Ahmed Al-Qahtani', email:'ahmed.q@sandbox.local', phone:'05xxxxxx02', role:'customer', card:'**** **** **** 9082'},
'1000': {name:'Fahad Al-Harbi', email:'fahad.h@sandbox.local', phone:'05xxxxxx88', role:'customer', card:'**** **** **** 1123'},
'1': {name:'root_admin', email:'admin@sandbox.local', phone:'—', role:'super_admin', card:'N/A — internal account'},
};
function fakeUserFor(id){
if(FAKE_USERS[id]) return FAKE_USERS[id];
return {name:'Mohammed Al-Zahrani', email:`user${id}@sandbox.local`, phone:'05xxxxxx' + String(id).slice(-2).padStart(2,'0'), role:'customer', card:'**** **** **** ' + (1000+ (Number(id)%9000))};
}
const MODULES = {
sqli: {
order: 1, cwe: 'CWE-89', tagClass: 'red', sev: 'crit',
badge: 'SQLi',
unlockKey: 'CL-7F2A-91B3',
name: { en:'SQLi', ar:'SQLi' },
desc: { en:'Manipulate database queries through user input to bypass authentication or leak data.',
ar:'التلاعب باستعلامات قواعد البيانات عبر مدخلات المستخدم لتجاوز التحقق أو تسريب بيانات.' },
tag: { en:'Injection', ar:'حقن' },
labTitle: { en:'SQLi Lab', ar:'SQLi Lab' },
hint: {
en:'The mock server builds a SQL query directly from the <span class="mono">username</span> and <span class="mono">password</span> values with no sanitization. Try breaking out with a quote <span class="mono">\'</span> and making the condition always true, e.g. <span class="mono">\'+OR+\'1\'=\'1</span> (use <span class="mono">+</span> instead of spaces in a GET query string).',
ar:'الخادم الوهمي يبني استعلام SQL مباشرة من قيمتي <span class="mono">username</span> و <span class="mono">password</span> بدون معالجة. جرّب تكسر الاستعلام بعلامة اقتباس <span class="mono">\'</span> ثم اجعل الشرط يتحقق دائمًا، مثل: <span class="mono">\'+OR+\'1\'=\'1</span> (استخدم <span class="mono">+</span> بدل المسافة داخل رابط GET).'
},
request:
`GET /api/v1/login?username=admin&password=1234 HTTP/1.1
Host: bank-secure.sandbox.local
User-Agent: Mozilla/5.0 (CyberLab Sandbox)
Accept: application/json
Cookie: session=a8f5f167f44f4964e6c998dee827110c
Connection: close`,
check(reqText){
const username = getParam(reqText, 'username');
const password = getParam(reqText, 'password');
const combined = `${username} ${password}`;
const hasQuote = combined.includes("'");
const hasTautology = /or\s+'?1'?\s*=\s*'?1'?/i.test(combined) || /'1'\s*=\s*'1'/i.test(combined) || /'\s*or\s*''\s*=\s*''/i.test(combined);
const hasComment = /--|#/.test(combined);
if(hasQuote && hasTautology){
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ Injection successful! You broke the query condition with a quote and made '1'='1' always true, bypassing the password check entirely — a classic Authentication Bypass via SQL Injection.",
'✅ نجح الحقن! كسرت شرط الاستعلام بعلامة الاقتباس وخليت \'1\'=\'1\' يتحقق دائمًا، فتجاوزت التحقق من كلمة المرور بالكامل — هذا مثال على تجاوز التحقق من الهوية عبر حقن SQL.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
<span class="resp-warn">${pick(LANG,'-- simulated query executed on server --','-- الاستعلام الوهمي المنفَّذ على الخادم --')}</span>
<span class="resp-dim">SELECT * FROM users WHERE username='${esc(username)}' AND password='${esc(password)}';</span>
{
"status": "<span class="resp-ok">authenticated</span>",
"message": "<span class="resp-warn">${pick(LANG,'⚠ auth check bypassed — condition always TRUE','⚠ تم تجاوز التحقق — الشرط صحيح دائمًا')}</span>",
"user": { "id": 1, "role": "<span class="resp-key">admin</span>" },
"leaked_table_preview": [
{ "id": 1, "username": "admin", "password_hash": "5f4dcc3b5aa765d61d8327deb882cf99" },
{ "id": 2, "username": "sarahs", "password_hash": "e10adc3949ba59abbe56e057f20f883e" },
{ "id": 3, "username": "finance_bot", "password_hash": "25d55ad283aa400af464c76d713c07ad" }
]
}` };
}
if(hasQuote && !hasTautology){
return { outcome:'partial', status:'500 Internal Server Error', pill:'mid',
verdict: pick(LANG,
"⚠ You broke the query syntax but didn't complete the injection. You've proven the vulnerability exists — now make the condition evaluate to true instead of just breaking it.",
'⚠ كسرت بنية الاستعلام لكنك ما أكملت الحقن. أنت أثبتّ وجود الثغرة، الخطوة الجاية إنك تخلي الشرط يرجع صحيح دائمًا بدل ما تكسره بس.'),
body:
`<span class="resp-fail">HTTP/1.1 500 Internal Server Error</span>
<span class="resp-dim">Content-Type: application/json</span>
<span class="resp-dim">SELECT * FROM users WHERE username='${esc(username)}' AND password='${esc(password)}';</span>
{
"error": "<span class="resp-fail">SQL syntax error</span>",
"detail": "unterminated quoted string near '${esc(password || username)}'",
"note": "<span class="resp-dim">${pick(LANG,'query structure is broken — confirms the input is unsanitized','بنية الاستعلام مكسورة — هذا يؤكد أن المدخل غير معالج')}</span>"
}` };
}
if(hasComment && !hasQuote){
return { outcome:'fail', status:'401 Unauthorized', pill:'bad',
verdict: pick(LANG,
"A comment marker alone isn't enough — you need to break out of the quotes first to reach the query logic.",
'رمز التعليق لوحده ما يكفي — لازم تكسر علامات الاقتباس أولًا عشان توصل لمنطق الاستعلام.'),
body:
`<span class="resp-fail">HTTP/1.1 401 Unauthorized</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "status": "<span class="resp-fail">failed</span>", "message": "Invalid username or password" }` };
}
return { outcome:'fail', status:'401 Unauthorized', pill:'bad',
verdict: pick(LANG,
"Normal response — nothing you sent changed the query logic. Try adding a quote and an always-true condition.",
'رد طبيعي — القيم المُرسلة لم تُغيّر منطق الاستعلام. جرّب تضيف علامة اقتباس وشرط صحيح دائمًا.'),
body:
`<span class="resp-fail">HTTP/1.1 401 Unauthorized</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "status": "<span class="resp-fail">failed</span>", "message": "Invalid username or password" }` };
}
},
xss: {
order: 2, cwe: 'CWE-79', tagClass: 'purple', sev: 'high',
badge: 'XSS',
unlockKey: 'CL-3D9E-44C7',
name: { en:'XSS', ar:'XSS' },
desc: { en:'Inject HTML/JavaScript that executes in another visitor\'s browser when content is displayed.',
ar:'حقن أكواد HTML/JavaScript تُنفَّذ في متصفح ضحية آخر عند عرض المحتوى.' },
tag: { en:'Injection', ar:'حقن' },
labTitle: { en:'XSS Lab', ar:'XSS Lab' },
hint: {
en:'The mock server stores the <span class="mono">comment</span> value and later shows it to every visitor with no sanitization. Try injecting a <span class="mono"><script></span> tag or an event handler like <span class="mono">onerror=</span>.',
ar:'الخادم الوهمي يخزّن قيمة <span class="mono">comment</span> ويعرضها لاحقًا لأي زائر بدون تنقية. جرّب تحقن وسم <span class="mono"><script></span> أو معالج حدث زي <span class="mono">onerror=</span>.'
},
request:
`POST /api/v1/comments HTTP/1.1
Host: forum.sandbox.local
Content-Type: application/x-www-form-urlencoded
Cookie: session=b3f9c2a41d7e4e0b9a6f2d8e5c1a9b70
Content-Length: 55
post_id=42&comment=Great article, thanks for sharing!`,
check(reqText){
const comment = getParam(reqText, 'comment');
const hasScript = /<script[\s>]/i.test(comment);
const hasEventHandler = /on(error|load|click|mouseover|focus)\s*=/i.test(comment);
const hasAnyTag = /<[a-z][^>]*>/i.test(comment);
if(hasScript || hasEventHandler){
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ Injection successful! The comment is stored and rendered for every visitor with no sanitization — a real Stored XSS. Any visitor opening this post will run your injected code automatically (e.g. cookie theft, redirects).",
'✅ نجح الحقن! التعليق يُخزَّن ويُعرض لأي زائر بدون تنقية — هذا يعتبر Stored XSS حقيقي. أي زائر يفتح المنشور سينفذ متصفحه الكود المحقون تلقائيًا (مثل سرقة الكوكيز أو التحويل لصفحة تصيد).'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{
"status": "comment_stored",
"sanitized": <span class="resp-fail">false</span>,
"rendered_html_preview": "<span class="resp-warn">${esc(comment)}</span>"
}
<span class="resp-dim">${pick(LANG,'-- another visitor\'s browser renders the page --','-- متصفح زائر آخر يعرض الصفحة --')}</span>
<span class="resp-warn">${pick(LANG,'⚠ script executed in the victim\'s browser:','⚠ تم تنفيذ الكود في متصفح الضحية:')}</span>
<span class="resp-key">document.cookie</span> → <span class="resp-dim">"session=b3f9c2a41d7e4e0b9a6f2d8e5c1a9b70"</span>
<span class="resp-warn">${pick(LANG,'→ sent to attacker-controlled endpoint (simulated)','→ تم إرسالها لخادم المهاجم (محاكاة)')}</span>` };
}
if(hasAnyTag){
return { outcome:'partial', status:'200 OK', pill:'mid',
verdict: pick(LANG,
"The comment contains an HTML tag, but nothing executable (no <script> or event handler). Try a tag that actually runs JavaScript.",
'التعليق فيه وسم HTML لكنه غير قابل للتنفيذ (لا يوجد <script> ولا معالج أحداث). جرّب وسم فعلي ينفّذ جافاسكربت.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{
"status": "comment_stored",
"sanitized": <span class="resp-dim">true</span>,
"rendered_html_preview": "<span class="resp-dim">${esc(comment)}</span>",
"note": "${pick(LANG,'tags rendered as inert markup, no executable event found','الوسوم عُرضت كنص خامل، لا يوجد حدث قابل للتنفيذ')}"
}` };
}
return { outcome:'fail', status:'200 OK', pill:'bad',
verdict: pick(LANG,
'Normal response — plain text with nothing executable. Try injecting <span class="mono"><script>alert(1)</script></span>.',
'رد طبيعي — نص عادي بدون أي محتوى قابل للتنفيذ. جرّب تحقن <span class="mono"><script>alert(1)</script></span>.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "status": "comment_stored", "rendered_html_preview": "${esc(comment)}" }` };
}
},
idor: {
order: 3, cwe: 'CWE-639', tagClass: 'cyan', sev: 'high',
badge: 'IDOR',
unlockKey: 'CL-A812-5F60',
name: { en:'IDOR', ar:'IDOR' },
desc: { en:'Access another user\'s resources by changing an object identifier, with no authorization check.',
ar:'الوصول لموارد تخص مستخدمين آخرين عبر تعديل معرّف الكائن دون التحقق من الصلاحية.' },
tag: { en:'Access Control', ar:'التحكم بالصلاحيات' },
labTitle: { en:'IDOR Lab', ar:'IDOR Lab' },
hint: {
en:'You\'re logged in as user <span class="mono">1001</span>. The mock server returns any user\'s data based only on the number in the path, without checking that the token actually belongs to that user. Try changing the number in <span class="mono">/users/1001/profile</span>.',
ar:'أنت مسجّل دخول كصاحب المعرّف <span class="mono">1001</span>. الخادم الوهمي يرجّع بيانات أي مستخدم بناءً على الرقم في المسار فقط، بدون تحقق من أن التوكن يخصه فعلًا. جرّب تغيّر الرقم في <span class="mono">/users/1001/profile</span>.'
},
request:
`GET /api/v1/users/1001/profile HTTP/1.1
Host: app.sandbox.local
Authorization: Bearer sandbox.jwt.user1001
Cookie: session=c7d29e1f0a4b4e5d9f3c1a7e2b6d8f40
Accept: application/json`,
check(reqText){
const m = reqText.match(/users\/([^\/\s]+)\/profile/i);
const id = m ? m[1] : null;
if(!id || !/^\d+$/.test(id)){
return { outcome:'fail', status:'400 Bad Request', pill:'bad',
verdict: pick(LANG,
'Invalid ID format — it needs to be a valid user number. Try a number different from 1001, like 1002 or 1.',
'صيغة المعرّف غير صالحة — لازم يكون رقم مستخدم صحيح. جرّب رقم مختلف عن 1001 زي 1002 أو 1.'),
body:
`<span class="resp-fail">HTTP/1.1 400 Bad Request</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "error": "invalid user id format" }` };
}
if(id === '1001'){
const u = FAKE_USERS['1001'];
return { outcome:'fail', status:'200 OK', pill:'mid',
verdict: pick(LANG,
"Normal response — you're still seeing only your own data because you haven't changed the user number in the path. Try a different user ID.",
'رد طبيعي — لسه تشوف بياناتك الخاصة فقط لأنك ما غيّرت رقم المستخدم في المسار. جرّب رقم مستخدم آخر.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{
"user_id": 1001,
"name": "${u.name}",
"email": "${u.email}",
"note": "<span class="resp-dim">${pick(LANG,'this is your own data (the logged-in user)','هذه بياناتك أنت (المستخدم المسجّل دخوله)')}</span>"
}` };
}
const u = fakeUserFor(id);
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
`✅ IDOR successful! You changed the ID to ${esc(id)} and the server returned another user's data with no check that the token belongs to them — meaning any attacker could scrape every user's data just by changing a number in the path.`,
`✅ IDOR ناجحة! غيّرت الرقم إلى ${esc(id)} والخادم رجّع بيانات مستخدم آخر بدون أي تحقق من أن التوكن يخصه — يعني أي مهاجم يقدر يسحب بيانات كل المستخدمين بس بتغيير رقم المسار.`),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{
"user_id": ${esc(id)},
"name": "<span class="resp-warn">${esc(u.name)}</span>",
"email": "<span class="resp-warn">${esc(u.email)}</span>",
"phone": "<span class="resp-warn">${esc(u.phone)}</span>",
"role": "${esc(u.role)}",
"card_last": "<span class="resp-key">${esc(u.card)}</span>",
"note": "<span class="resp-fail">${pick(LANG,'⚠ authorization check missing — returned another user\'s private data','⚠ لا يوجد تحقق من الصلاحية — تم إرجاع بيانات خاصة لمستخدم آخر')}</span>"
}` };
}
},
cmdi: {
order: 4, cwe: 'CWE-78', tagClass: 'red', sev: 'crit',
badge: 'CmdI',
unlockKey: 'CL-E4B7-2C18',
name: { en:'CmdI', ar:'CmdI' },
desc: { en:'Sneak OS shell commands into an input that gets passed straight to the system shell.',
ar:'تمرير أوامر نظام تشغيل داخل مدخل يُرسل مباشرة لواجهة الأوامر (shell).' },
tag: { en:'Injection', ar:'حقن' },
labTitle: { en:'CmdI Lab', ar:'CmdI Lab' },
hint: {
en:'The mock server runs something like <span class="mono">ping -c 1 <host></span> directly in a shell. Try adding a shell separator such as <span class="mono">;</span>, <span class="mono">|</span> or <span class="mono">&&</span> followed by a command like <span class="mono">whoami</span>.',
ar:'الخادم الوهمي ينفذ أمرًا شبيهًا بـ <span class="mono">ping -c 1 <host></span> مباشرة داخل الشل. جرّب تضيف فاصل أوامر زي <span class="mono">;</span> أو <span class="mono">|</span> أو <span class="mono">&&</span> متبوعًا بأمر مثل <span class="mono">whoami</span>.'
},
request:
`GET /api/v1/tools/ping?host=8.8.8.8 HTTP/1.1
Host: ops.sandbox.local
User-Agent: Mozilla/5.0 (CyberLab Sandbox)
Accept: application/json
Cookie: session=d1a4e7f2b9c8461fae0d2c6b7a9e1f33`,
check(reqText){
const host = getParam(reqText, 'host');
const hasSeparator = /[;|&`]|\$\(/.test(host);
const hasCommand = /\b(whoami|id|cat|ls|uname|pwd)\b/i.test(host);
if(hasSeparator && hasCommand){
let output = pick(LANG,'-- command output --','-- ناتج الأمر --');
if(/whoami/i.test(host)) output = 'www-data';
else if(/\bid\b/i.test(host)) output = 'uid=33(www-data) gid=33(www-data) groups=33(www-data)';
else if(/cat/i.test(host)) output = 'root:x:0:0:root:/root:/bin/bash\nwww-data:x:33:33::/var/www:/usr/sbin/nologin';
else if(/uname/i.test(host)) output = 'Linux sandbox 6.2.0-sandbox #1 SMP x86_64 GNU/Linux';
else if(/pwd/i.test(host)) output = '/var/www/ops';
else output = 'bin boot etc home var usr';
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ Injection successful! The shell separator let you chain a second command after the ping, and the server executed it and returned the output — full Remote Command Execution.",
'✅ نجح الحقن! فاصل الأوامر خلاك تضيف أمر ثاني بعد ping ونفّذه الخادم ورجّع ناتجه — هذا تنفيذ أوامر عن بُعد كامل.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
<span class="resp-dim">${pick(LANG,'-- simulated shell command --','-- الأمر الوهمي المنفَّذ --')}</span>
<span class="resp-dim">ping -c 1 ${esc(host)}</span>
{
"ping_result": "1 packets transmitted, 1 received",
"injected_output": "<span class="resp-warn">${esc(output)}</span>",
"note": "<span class="resp-fail">${pick(LANG,'⚠ shell metacharacter allowed a second command to run','⚠ فاصل الشل سمح بتشغيل أمر إضافي')}</span>"
}` };
}
if(hasSeparator && !hasCommand){
return { outcome:'partial', status:'200 OK', pill:'mid',
verdict: pick(LANG,
"⚠ You broke out of the ping command with a shell separator, but didn't supply a command to run. Add something like whoami or cat after the separator.",
'⚠ خرجت من أمر ping بفاصل الشل لكن ما زودت أمر تشغّله. أضف شيء زي whoami أو cat بعد الفاصل.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
<span class="resp-dim">ping -c 1 ${esc(host)}</span>
{
"ping_result": "1 packets transmitted, 1 received",
"injected_output": "",
"note": "${pick(LANG,'separator accepted but no payload executed','تم قبول الفاصل لكن لم يُنفَّذ أي أمر')}"
}` };
}
return { outcome:'fail', status:'200 OK', pill:'bad',
verdict: pick(LANG,
'Normal response — a plain ping with no shell separator. Try adding a separator like ; or | followed by a command.',
'رد طبيعي — عملية ping عادية بدون فاصل أوامر. جرّب تضيف فاصل زي ; أو | متبوع بأمر.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "ping_result": "1 packets transmitted, 1 received", "host": "${esc(host)}" }` };
}
},
path: {
order: 5, cwe: 'CWE-22', tagClass: 'amber', sev: 'high',
badge: 'LFI',
unlockKey: 'CL-19D3-88AF',
name: { en:'LFI', ar:'LFI' },
desc: { en:'Escape the intended folder using ../ sequences to read arbitrary files on the server.',
ar:'الخروج من المجلد المخصص باستخدام ../ للوصول لملفات حساسة على الخادم.' },
tag: { en:'File Access', ar:'الوصول للملفات' },
labTitle: { en:'LFI Lab', ar:'LFI Lab' },
hint: {
en:'The mock server reads a file by directly appending the <span class="mono">file</span> value to a fixed folder path. Try escaping that folder with <span class="mono">../</span> sequences to reach a sensitive file such as <span class="mono">/etc/passwd</span>.',
ar:'الخادم الوهمي يقرأ الملف بإلحاق قيمة <span class="mono">file</span> مباشرة لمسار مجلد ثابت. جرّب الخروج من المجلد باستخدام <span class="mono">../</span> للوصول لملف حساس مثل <span class="mono">/etc/passwd</span>.'
},
request:
`GET /api/v1/files/download?file=report_2024.pdf HTTP/1.1
Host: docs.sandbox.local
User-Agent: Mozilla/5.0 (CyberLab Sandbox)
Accept: */*
Cookie: session=e5f8a1c9d3b74620ae5f9c1d7b3a8e42`,
check(reqText){
const file = getParam(reqText, 'file');
const hasTraversal = /(\.\.\/)|(\.\.%2f)/i.test(file);
const hasSensitiveTarget = /etc\/passwd|etc\/shadow/i.test(file);
if(hasTraversal && hasSensitiveTarget){
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ Traversal successful! The ../ sequences escaped the downloads folder entirely, letting you read a system file outside the intended directory.",
'✅ نجح الاجتياز! سلسلة ../ خرجت بالكامل من مجلد التحميلات وقرأت ملف نظام خارج المسار المسموح به.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: text/plain</span>
<span class="resp-dim">${pick(LANG,'-- simulated file read --','-- قراءة الملف الوهمية --')}</span>
<span class="resp-dim">open("/var/www/downloads/${esc(file)}")</span>
<span class="resp-warn">root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33::/var/www:/usr/sbin/nologin
db_admin:x:1001:1001::/home/db_admin:/bin/bash</span>
<span class="resp-fail">${pick(LANG,'⚠ arbitrary file read outside the intended directory','⚠ قراءة ملف عشوائي خارج المجلد المخصص')}</span>` };
}
if(hasTraversal && !hasSensitiveTarget){
return { outcome:'partial', status:'404 Not Found', pill:'mid',
verdict: pick(LANG,
"⚠ You escaped the folder with ../ but pointed at a path that doesn't exist. You've confirmed the traversal works — now aim it at a real file such as /etc/passwd.",
'⚠ خرجت من المجلد بـ ../ لكن أشرت لمسار غير موجود. أثبتّ أن الاجتياز يعمل — وجّهه الآن لملف حقيقي مثل /etc/passwd.'),
body:
`<span class="resp-fail">HTTP/1.1 404 Not Found</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "error": "file not found", "resolved_path": "${esc(file)}" }` };
}
return { outcome:'fail', status:'200 OK', pill:'bad',
verdict: pick(LANG,
'Normal response — the requested file is served from inside the allowed folder. Try escaping it with ../ sequences.',
'رد طبيعي — الملف المطلوب يُقدَّم من داخل المجلد المسموح به. جرّب الخروج منه بسلسلة ../.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/pdf</span>
{ "file": "${esc(file)}", "size_kb": 214, "status": "served from /var/www/downloads/" }` };
}
},
ssrf: {
order: 6, cwe: 'CWE-918', tagClass: 'cyan', sev: 'high',
badge: 'SSRF',
unlockKey: 'CL-6C2F-703B',
name: { en:'SSRF', ar:'SSRF' },
desc: { en:'Trick the server into making requests to internal addresses on your behalf.',
ar:'خداع الخادم لتنفيذ طلبات لعناوين داخلية نيابة عنك.' },
tag: { en:'Server-Side', ar:'جهة الخادم' },
labTitle: { en:'SSRF Lab', ar:'SSRF Lab' },
hint: {
en:'The mock server fetches a preview of any <span class="mono">url</span> you give it, with no validation — including internal addresses. Try pointing it at something like <span class="mono">http://127.0.0.1/admin</span> or <span class="mono">http://169.254.169.254/</span> (the cloud metadata address).',
ar:'الخادم الوهمي يجلب معاينة لأي <span class="mono">url</span> تعطيه إياه بدون أي تحقق — حتى العناوين الداخلية. جرّب توجيهه لشيء مثل <span class="mono">http://127.0.0.1/admin</span> أو <span class="mono">http://169.254.169.254/</span> (عنوان بيانات السحابة).'
},
request:
`POST /api/v1/preview HTTP/1.1
Host: app.sandbox.local
Content-Type: application/x-www-form-urlencoded
Cookie: session=f2a6c9e1d4b83570ae1f7c9d2b6a4e85
Content-Length: 44
url=https://blog.sandbox.local/latest-post`,
check(reqText){
const url = getParam(reqText, 'url');
const isInternal = /127\.0\.0\.1|localhost|169\.254\.169\.254|0\.0\.0\.0|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+/i.test(url);
const isValidUrl = /^https?:\/\//i.test(url);
if(isInternal){
const isMetadata = /169\.254\.169\.254/.test(url);
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ SSRF successful! The server fetched an internal address on your behalf and returned data it should never expose to the outside — a network boundary bypass.",
'✅ نجحت SSRF! الخادم جلب عنوانًا داخليًا نيابة عنك ورجّع بيانات ما كان يفترض تُكشف للخارج — هذا تجاوز لحدود الشبكة.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
<span class="resp-dim">${pick(LANG,'-- simulated server-side fetch --','-- الطلب الوهمي من جهة الخادم --')}</span>
<span class="resp-dim">GET ${esc(url)}</span>
{
"fetched_from": "<span class="resp-warn">${esc(url)}</span>",
"preview": "<span class="resp-warn">${isMetadata
? pick(LANG,'{ "role":"ops-admin", "access_key":"AKIA...SANDBOX", "secret_key":"REDACTED-DEMO" }','{ "role":"ops-admin", "access_key":"AKIA...SANDBOX", "secret_key":"محجوب-تجريبي" }')
: pick(LANG,'Internal Admin Panel — service status: healthy, active sessions: 214','لوحة تحكم داخلية — حالة الخدمة: تعمل، الجلسات النشطة: 214')}</span>",
"note": "<span class="resp-fail">${pick(LANG,'⚠ server followed a request to an internal-only address','⚠ الخادم نفّذ طلبًا لعنوان داخلي فقط')}</span>"
}` };
}
if(isValidUrl){
return { outcome:'fail', status:'200 OK', pill:'bad',
verdict: pick(LANG,
'Normal response — an external URL is fetched and previewed exactly as expected. Try pointing the request at an internal address instead.',
'رد طبيعي — تم جلب رابط خارجي ومعاينته كما هو متوقع. جرّب توجيه الطلب لعنوان داخلي بدلًا من ذلك.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "fetched_from": "${esc(url)}", "preview": "Blog post preview loaded successfully." }` };
}
return { outcome:'fail', status:'400 Bad Request', pill:'bad',
verdict: pick(LANG,
"That doesn't look like a valid URL. Try a full address starting with http:// or https://.",
'هذا ما يبدو رابط صالح. جرّب عنوان كامل يبدأ بـ http:// أو https://.'),
body:
`<span class="resp-fail">HTTP/1.1 400 Bad Request</span>
<span class="resp-dim">Content-Type: application/json</span>
{ "error": "invalid url" }` };
}
},
ssti: {
order: 7, cwe: 'CWE-1336', tagClass: 'purple', sev: 'crit',
badge: 'SSTI',
unlockKey: 'CL-B045-9E71',
name: { en:'SSTI', ar:'SSTI' },
desc: { en:'Inject template syntax into user input that gets rendered server-side, leading to code execution.',
ar:'حقن صياغة قوالب داخل مدخل المستخدم الذي يُعالَج على جهة الخادم، مما يؤدي لتنفيذ أكواد.' },
tag: { en:'Injection', ar:'حقن' },
labTitle: { en:'SSTI Lab', ar:'SSTI Lab' },
hint: {
en:'The mock server renders the <span class="mono">name</span> value straight into a server-side template with no escaping. Try a template expression such as <span class="mono">{{7*7}}</span> to see if it gets evaluated.',
ar:'الخادم الوهمي يمرر قيمة <span class="mono">name</span> مباشرة داخل قالب على جهة الخادم بدون أي تهريب. جرّب صياغة قالب مثل <span class="mono">{{7*7}}</span> لترى إذا كانت تُنفَّذ.'
},
request:
`POST /api/v1/greet HTTP/1.1
Host: portal.sandbox.local
Content-Type: application/x-www-form-urlencoded
Cookie: session=f7c1a9e3b5d84620ae7f1c9d3b5a7e60
Content-Length: 14
name=Operator`,
check(reqText){
const name = getParam(reqText, 'name');
const hasArith = /\{\{\s*7\s*\*\s*7\s*\}\}/.test(name);
const hasConfigLeak = /\{\{\s*config\s*\}\}/i.test(name);
const hasBraces = /\{\{.*\}\}/.test(name);
if(hasArith || hasConfigLeak){
const leaked = hasConfigLeak ? 'SECRET_KEY=sandbox-demo-9f21, DEBUG=True' : '49';
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ Injection successful! The template engine evaluated your expression on the server instead of treating it as plain text — full Server-Side Template Injection, which usually escalates to remote code execution.",
'✅ نجح الحقن! محرك القوالب نفّذ تعبيرك على الخادم بدل ما يعامله كنص عادي — هذا Server-Side Template Injection كامل، وعادة يتصعّد لتنفيذ أوامر عن بُعد.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: text/html</span>
<span class="resp-dim">${pick(LANG,'-- simulated template render --','-- تمثيل القالب الوهمي --')}</span>
<span class="resp-dim">Hello, {{ name }}!</span>
<span class="resp-dim">${pick(LANG,'rendered output:','الناتج بعد التنفيذ:')}</span>
Hello, <span class="resp-warn">${esc(leaked)}</span>!
<span class="resp-fail">${pick(LANG,'⚠ template expression was evaluated server-side','⚠ تم تنفيذ تعبير القالب على جهة الخادم')}</span>` };
}
if(hasBraces){
return { outcome:'partial', status:'200 OK', pill:'mid',
verdict: pick(LANG,
"⚠ Your braces were rendered but the expression inside wasn't recognized. Try a known evaluable expression like {{7*7}}.",
'⚠ الأقواس ظهرت لكن التعبير بداخلها غير معروف. جرّب تعبير معروف مثل {{7*7}}.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: text/html</span>
Hello, ${esc(name)}!` };
}
return { outcome:'fail', status:'200 OK', pill:'bad',
verdict: pick(LANG,
'Normal response — plain text with no template syntax. Try injecting {{7*7}}.',
'رد طبيعي — نص عادي بدون أي صياغة قوالب. جرّب تحقن {{7*7}}.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: text/html</span>
Hello, ${esc(name)}!` };
}
},
xxe: {
order: 8, cwe: 'CWE-611', tagClass: 'amber', sev: 'crit',
badge: 'XXE',
name: { en:'XXE', ar:'XXE' },
desc: { en:'Define a malicious external entity in an XML body to make the parser read local files.',
ar:'تعريف كيان خارجي خبيث داخل جسم XML لجعل المحلل يقرأ ملفات محلية.' },
tag: { en:'File Access', ar:'الوصول للملفات' },
labTitle: { en:'XXE Lab', ar:'XXE Lab' },
hint: {
en:'The mock server parses the XML body with external entities enabled. Try defining <span class="mono"><!ENTITY xxe SYSTEM "file:///etc/passwd"></span> and referencing it with <span class="mono">&xxe;</span> inside a tag.',
ar:'الخادم الوهمي يحلل جسم XML مع تفعيل الكيانات الخارجية. جرّب تعرّف <span class="mono"><!ENTITY xxe SYSTEM "file:///etc/passwd"></span> وتستخدمه بـ <span class="mono">&xxe;</span> داخل أحد الوسوم.'
},
request:
`POST /api/v1/import HTTP/1.1
Host: docs.sandbox.local
Content-Type: application/xml
Cookie: session=a1c4f9e7b2d85310ae4f7c1d9b2a5e83
Content-Length: 96
<?xml version="1.0"?>
<request><name>quarterly-report</name></request>`,
check(reqText){
const hasDoctype = /<!DOCTYPE/i.test(reqText);
const hasEntity = /<!ENTITY\s+\w+\s+SYSTEM\s+["']file:\/\/\/etc\/passwd["']/i.test(reqText);
const hasReference = /&\w+;/.test(reqText.replace(/&|<|>|"/gi, ''));
if(hasDoctype && hasEntity && hasReference){
return { outcome:'success', status:'200 OK', pill:'ok',
verdict: pick(LANG,
"✅ Injection successful! The parser resolved your external entity and read a file from the server's filesystem — a classic XXE leading to local file disclosure.",
'✅ نجح الحقن! المحلل استجاب لتعريف الكيان الخارجي وقرأ ملفًا من نظام ملفات الخادم — هذا XXE كلاسيكي يؤدي لكشف ملفات محلية.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/xml</span>
<span class="resp-dim">${pick(LANG,'-- simulated XML parse with external entities enabled --','-- تحليل XML وهمي مع تفعيل الكيانات الخارجية --')}</span>
<result>
<span class="resp-warn">root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33::/var/www:/usr/sbin/nologin</span>
</result>
<span class="resp-fail">${pick(LANG,'⚠ external entity resolved and local file contents returned','⚠ تم تنفيذ الكيان الخارجي وإرجاع محتوى ملف محلي')}</span>` };
}
if(hasDoctype && !hasEntity){
return { outcome:'partial', status:'200 OK', pill:'mid',
verdict: pick(LANG,
"⚠ A DOCTYPE was accepted, but no entity pointing at a real file was defined. Add an ENTITY referencing file:///etc/passwd.",
'⚠ تم قبول DOCTYPE لكن ما عرّفت كيانًا يشير لملف حقيقي. أضف ENTITY يشير لـ file:///etc/passwd.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/xml</span>
<result><name>quarterly-report</name></result>` };
}
return { outcome:'fail', status:'200 OK', pill:'bad',
verdict: pick(LANG,
'Normal response — a plain XML document with no external entity. Try adding a DOCTYPE with an ENTITY pointing at a local file.',
'رد طبيعي — مستند XML عادي بدون كيان خارجي. جرّب تضيف DOCTYPE فيه ENTITY يشير لملف محلي.'),
body:
`<span class="resp-ok">HTTP/1.1 200 OK</span>
<span class="resp-dim">Content-Type: application/xml</span>
<result><name>quarterly-report</name></result>` };
}
},
};
let currentModule = null;
let lastCheckResult = null;
let unlockedCount = 1;
const TOTAL_MODULES = Object.keys(MODULES).length;
function s(key){ return STR[LANG][key]; }
function renderStaticText(){
document.documentElement.lang = LANG;
document.documentElement.dir = LANG === 'ar' ? 'rtl' : 'ltr';
document.getElementById('brand-sub').textContent = s('brandSub');
document.getElementById('target-label').textContent = s('targetLabel');
document.getElementById('mode-label').textContent = s('modeLabel');
document.getElementById('lang-toggle-label').textContent = s('langToggleLabel');
document.getElementById('intro-desc').innerHTML = s('desc');
document.getElementById('footer-text').textContent = s('footer');
document.getElementById('hint-label').textContent = s('hintLabel');
document.getElementById('btn-reset').textContent = s('reset');
document.getElementById('btn-send').textContent = s('send');
document.getElementById('lab-close').textContent = s('close');
}
function initTitle(onReady){
stopTitleCycle();
const glitchEl = document.getElementById('intro-title-glitch');
const tagEl = document.getElementById('intro-title-tag');
tagEl.classList.add('glitch-tag--in');
glitchEl.innerHTML = '';
glitchEl.setAttribute('data-text', TITLE_WORDS[0]);
typeHTML(glitchEl, TITLE_WORDS[0], { speed:30, jitter:20, onDone:()=>{
if(onReady) onReady();
startTitleCycle(glitchEl);
}});
}
function runIntroReveal(){
const descEl = document.getElementById('intro-desc');
descEl.innerHTML = '';
initTitle(()=>{
setTimeout(()=> typeHTML(descEl, s('desc'), { speed:7, jitter:8 }), 250);
});
}
function renderModuleCards(){
const wrap = document.getElementById('modules');
wrap.innerHTML = '';
const sevClass = { crit:'sev--crit', high:'sev--high', med:'sev--med' };
Object.keys(MODULES)
.sort((a,b)=> MODULES[a].order - MODULES[b].order)
.forEach(key=>{
const mod = MODULES[key];
const isLocked = mod.order > unlockedCount;
const card = document.createElement('article');
card.className = 'module-card' + (isLocked ? ' module-card--locked' : '');
card.tabIndex = isLocked ? -1 : 0;
card.dataset.module = key;
const cardBody = `
<div class="module-card__head">
<span class="module-id" dir="ltr">${String(mod.order).padStart(2,'0')}</span>
<span class="module-tag module-tag--${mod.tagClass}">${mod.tag[LANG]}</span>
</div>
<h2 class="module-name" dir="ltr">${mod.name[LANG]}</h2>
<p class="module-desc">${mod.desc[LANG]}</p>
<div class="module-meta">
<span dir="ltr">${mod.cwe}</span>
<span class="sev ${sevClass[mod.sev]}">${s('sevLabel')[mod.sev]}</span>
</div>
<button class="module-open"${isLocked ? ' disabled' : ''}>${s('openLab')}</button>
`;
if(isLocked){
card.innerHTML = `
<div class="module-card__inner">${cardBody}</div>
<div class="lock-overlay">
<span class="lock-overlay__icon">🔒</span>
<span class="lock-overlay__label">${s('lockedLabel')}</span>
<span class="lock-overlay__hint">${s('lockedHint')}</span>
</div>
`;
} else {
card.innerHTML = cardBody;
card.addEventListener('click', ()=> openModule(key));
card.addEventListener('keydown', (e)=>{
if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); openModule(key); }
});
}
wrap.appendChild(card);
});
}
const lab = ()=> document.getElementById('lab');
const labBadge = ()=> document.getElementById('lab-badge');
const labTitleText = ()=> document.getElementById('lab-title-text');
const hintText = ()=> document.getElementById('hint-text');
const requestBox = ()=> document.getElementById('request-box');
const responseBox = ()=> document.getElementById('response-box');
const respStatus = ()=> document.getElementById('resp-status');
const verdictBox = ()=> document.getElementById('verdict');
function openModule(key){
const mod = MODULES[key];
if(!mod) return;
currentModule = key;
lastCheckResult = null;
labBadge().textContent = mod.badge;
typeHTML(labTitleText(), mod.labTitle[LANG], { speed:24, jitter:18 });
typeHTML(hintText(), mod.hint[LANG], { speed:9, jitter:10 });
requestBox().value = mod.request;
responseBox().innerHTML = `<span class="placeholder">${s('placeholder')}</span>`;
respStatus().textContent = '—';