juice-shop

Форк
0
/
antiCheat.ts 
165 строк · 8.7 Кб
1
/*
2
 * Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
3
 * SPDX-License-Identifier: MIT
4
 */
5

6
import config from 'config'
7
import colors from 'colors/safe'
8
import { retrieveCodeSnippet } from '../routes/vulnCodeSnippet'
9
import { readFixes } from '../routes/vulnCodeFixes'
10
import { type Challenge } from '../data/types'
11
import { getCodeChallenges } from './codingChallenges'
12
import logger from './logger'
13
import { type NextFunction, type Request, type Response } from 'express'
14
import * as utils from './utils'
15
// @ts-expect-error FIXME due to non-existing type definitions for median
16
import median from 'median'
17

18
const coupledChallenges = { // TODO prevent also near-identical challenges (e.g. all null byte file access or dom xss + bonus payload etc.) from counting as cheating
19
  loginAdminChallenge: ['weakPasswordChallenge'],
20
  nullByteChallenge: ['easterEggLevelOneChallenge', 'forgottenDevBackupChallenge', 'forgottenBackupChallenge', 'misplacedSignatureFileChallenge'],
21
  deprecatedInterfaceChallenge: ['uploadTypeChallenge', 'xxeFileDisclosureChallenge', 'xxeDosChallenge'],
22
  uploadSizeChallenge: ['uploadTypeChallenge', 'xxeFileDisclosureChallenge', 'xxeDosChallenge'],
23
  uploadTypeChallenge: ['uploadSizeChallenge', 'xxeFileDisclosureChallenge', 'xxeDosChallenge']
24
}
25
const trivialChallenges = ['errorHandlingChallenge', 'privacyPolicyChallenge', 'closeNotificationsChallenge']
26

27
const solves: Array<{ challenge: any, phase: string, timestamp: Date, cheatScore: number }> = [{ challenge: {}, phase: 'server start', timestamp: new Date(), cheatScore: 0 }] // seed with server start timestamp
28

29
const preSolveInteractions: Array<{ challengeKey: any, urlFragments: string[], interactions: boolean[] }> = [
30
  { challengeKey: 'missingEncodingChallenge', urlFragments: ['/assets/public/images/uploads/%F0%9F%98%BC-'], interactions: [false] },
31
  { challengeKey: 'directoryListingChallenge', urlFragments: ['/ftp'], interactions: [false] },
32
  { challengeKey: 'easterEggLevelOneChallenge', urlFragments: ['/ftp', '/ftp/eastere.gg'], interactions: [false, false] },
33
  { challengeKey: 'easterEggLevelTwoChallenge', urlFragments: ['/ftp', '/gur/qrif/ner/fb/shaal/gurl/uvq/na/rnfgre/rtt/jvguva/gur/rnfgre/rtt'], interactions: [false, false] },
34
  { challengeKey: 'forgottenDevBackupChallenge', urlFragments: ['/ftp', '/ftp/package.json.bak'], interactions: [false, false] },
35
  { challengeKey: 'forgottenBackupChallenge', urlFragments: ['/ftp', '/ftp/coupons_2013.md.bak'], interactions: [false, false] },
36
  { challengeKey: 'loginSupportChallenge', urlFragments: ['/ftp', '/ftp/incident-support.kdbx'], interactions: [false, false] },
37
  { challengeKey: 'misplacedSignatureFileChallenge', urlFragments: ['/ftp', '/ftp/suspicious_errors.yml'], interactions: [false, false] },
38
  { challengeKey: 'recChallenge', urlFragments: ['/api-docs', '/b2b/v2/orders'], interactions: [false, false] },
39
  { challengeKey: 'rceOccupyChallenge', urlFragments: ['/api-docs', '/b2b/v2/orders'], interactions: [false, false] }
40
]
41

42
export const checkForPreSolveInteractions = () => ({ url }: Request, res: Response, next: NextFunction) => {
43
  preSolveInteractions.forEach((preSolveInteraction) => {
44
    for (let i = 0; i < preSolveInteraction.urlFragments.length; i++) {
45
      if (utils.endsWith(url, preSolveInteraction.urlFragments[i])) {
46
        preSolveInteraction.interactions[i] = true
47
      }
48
    }
49
  })
50
  next()
51
}
52

53
export const calculateCheatScore = (challenge: Challenge) => {
54
  const timestamp = new Date()
55
  let cheatScore = 0
56
  let timeFactor = 2
57
  timeFactor *= (config.get('challenges.showHints') ? 1 : 1.5)
58
  timeFactor *= (challenge.tutorialOrder && config.get('hackingInstructor.isEnabled') ? 0.5 : 1)
59
  if (areCoupled(challenge, previous().challenge) || isTrivial(challenge)) {
60
    timeFactor = 0
61
  }
62

63
  const minutesExpectedToSolve = challenge.difficulty * timeFactor
64
  const minutesSincePreviousSolve = (timestamp.getTime() - previous().timestamp.getTime()) / 60000
65
  cheatScore += Math.max(0, 1 - (minutesSincePreviousSolve / minutesExpectedToSolve))
66

67
  const preSolveInteraction = preSolveInteractions.find((preSolveInteraction) => preSolveInteraction.challengeKey === challenge.key)
68
  let percentPrecedingInteraction = -1
69
  if (preSolveInteraction) {
70
    percentPrecedingInteraction = preSolveInteraction.interactions.filter(Boolean).length / (preSolveInteraction.interactions.length)
71
    const multiplierForMissingExpectedInteraction = 1 + Math.max(0, 1 - percentPrecedingInteraction) / 2
72
    cheatScore *= multiplierForMissingExpectedInteraction
73
    cheatScore = Math.min(1, cheatScore)
74
  }
75

76
  logger.info(`Cheat score for ${areCoupled(challenge, previous().challenge) ? 'coupled ' : (isTrivial(challenge) ? 'trivial ' : '')}${challenge.tutorialOrder ? 'tutorial ' : ''}${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)}min (expected ~${minutesExpectedToSolve}min) with${config.get('challenges.showHints') ? '' : 'out'} hints allowed${percentPrecedingInteraction > -1 ? (' and ' + percentPrecedingInteraction * 100 + '% expected preceding URL interaction') : ''}: ${cheatScore < 0.33 ? colors.green(cheatScore.toString()) : (cheatScore < 0.66 ? colors.yellow(cheatScore.toString()) : colors.red(cheatScore.toString()))}`)
77
  solves.push({ challenge, phase: 'hack it', timestamp, cheatScore })
78
  return cheatScore
79
}
80

81
export const calculateFindItCheatScore = async (challenge: Challenge) => {
82
  const timestamp = new Date()
83
  let timeFactor = 0.001
84
  timeFactor *= (challenge.key === 'scoreBoardChallenge' && config.get('hackingInstructor.isEnabled') ? 0.5 : 1)
85
  let cheatScore = 0
86

87
  const codeSnippet = await retrieveCodeSnippet(challenge.key)
88
  if (codeSnippet == null) {
89
    return 0
90
  }
91
  const { snippet, vulnLines } = codeSnippet
92
  timeFactor *= vulnLines.length
93
  const identicalSolved = await checkForIdenticalSolvedChallenge(challenge)
94
  if (identicalSolved) {
95
    timeFactor = 0.8 * timeFactor
96
  }
97
  const minutesExpectedToSolve = Math.ceil(snippet.length * timeFactor)
98
  const minutesSincePreviousSolve = (timestamp.getTime() - previous().timestamp.getTime()) / 60000
99
  cheatScore += Math.max(0, 1 - (minutesSincePreviousSolve / minutesExpectedToSolve))
100

101
  logger.info(`Cheat score for "Find it" phase of ${challenge.key === 'scoreBoardChallenge' && config.get('hackingInstructor.isEnabled') ? 'tutorial ' : ''}${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)}min (expected ~${minutesExpectedToSolve}min): ${cheatScore < 0.33 ? colors.green(cheatScore.toString()) : (cheatScore < 0.66 ? colors.yellow(cheatScore.toString()) : colors.red(cheatScore.toString()))}`)
102
  solves.push({ challenge, phase: 'find it', timestamp, cheatScore })
103

104
  return cheatScore
105
}
106

107
export const calculateFixItCheatScore = async (challenge: Challenge) => {
108
  const timestamp = new Date()
109
  let cheatScore = 0
110

111
  const { fixes } = readFixes(challenge.key)
112
  const minutesExpectedToSolve = Math.floor(fixes.length / 2)
113
  const minutesSincePreviousSolve = (timestamp.getTime() - previous().timestamp.getTime()) / 60000
114
  cheatScore += Math.max(0, 1 - (minutesSincePreviousSolve / minutesExpectedToSolve))
115

116
  logger.info(`Cheat score for "Fix it" phase of ${colors.cyan(challenge.key)} solved in ${Math.round(minutesSincePreviousSolve)}min (expected ~${minutesExpectedToSolve}min): ${cheatScore < 0.33 ? colors.green(cheatScore.toString()) : (cheatScore < 0.66 ? colors.yellow(cheatScore.toString()) : colors.red(cheatScore.toString()))}`)
117
  solves.push({ challenge, phase: 'fix it', timestamp, cheatScore })
118
  return cheatScore
119
}
120

121
export const totalCheatScore = () => {
122
  return solves.length > 1 ? median(solves.map(({ cheatScore }) => cheatScore)) : 0
123
}
124

125
function areCoupled (challenge: Challenge, previousChallenge: Challenge) {
126
  // @ts-expect-error FIXME any type issues
127
  return coupledChallenges[challenge.key]?.indexOf(previousChallenge.key) > -1 || coupledChallenges[previousChallenge.key]?.indexOf(challenge.key) > -1
128
}
129

130
function isTrivial (challenge: Challenge) {
131
  return trivialChallenges.includes(challenge.key)
132
}
133

134
function previous () {
135
  return solves[solves.length - 1]
136
}
137

138
const checkForIdenticalSolvedChallenge = async (challenge: Challenge): Promise<boolean> => {
139
  const codingChallenges = await getCodeChallenges()
140
  if (!codingChallenges.has(challenge.key)) {
141
    return false
142
  }
143

144
  const codingChallengesToCompareTo = codingChallenges.get(challenge.key)
145
  if (!codingChallengesToCompareTo?.snippet) {
146
    return false
147
  }
148
  const snippetToCompareTo = codingChallengesToCompareTo.snippet
149

150
  for (const [challengeKey, { snippet }] of codingChallenges.entries()) {
151
    if (challengeKey === challenge.key) {
152
      // don't compare to itself
153
      continue
154
    }
155

156
    if (snippet === snippetToCompareTo) {
157
      for (const solvedChallenges of solves) {
158
        if (solvedChallenges.phase === 'find it') {
159
          return true
160
        }
161
      }
162
    }
163
  }
164
  return false
165
}
166

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.