Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Profile with SSTI and SSRF Bugs #655

Merged
merged 36 commits into from Nov 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
36cdb10
Server side view rendering setup
CaptainFreak Jul 20, 2018
baf6e6e
Update profile fuctionality with SSTi Bug
CaptainFreak Jul 21, 2018
960857d
Profile image upload functionality
CaptainFreak Jul 21, 2018
79b6346
Image upload with url functionality (SSRF Bug)
CaptainFreak Jul 21, 2018
ae48273
Fixed code style
CaptainFreak Jul 21, 2018
a01e656
Jades latest version
CaptainFreak Jul 22, 2018
d85ea84
request package for SSRF
CaptainFreak Jul 24, 2018
9d7c567
SSTI and SSRF Challenge descriptions
CaptainFreak Jul 26, 2018
29716bf
SSTi and SSRF challenges added
CaptainFreak Jul 30, 2018
d8b0ab2
refactored
CaptainFreak Aug 2, 2018
04dd386
E2E tests for SSTi and SSRF
CaptainFreak Aug 3, 2018
fbd2eb3
Moved captcha verification to captcha module
J12934 Aug 4, 2018
692bd8d
Properly respond to challenge solve verification requests if the solv…
J12934 Aug 6, 2018
1a95e17
Added validation so that user can try multiple payloads for ssti
CaptainFreak Aug 7, 2018
a8ee5fc
Updated malware link
CaptainFreak Aug 7, 2018
1225dd5
Merge remote-tracking branch 'remotes/origin/gsoc-challenges' into gs…
CaptainFreak Aug 7, 2018
d1bd52f
Disabled e2e tests
CaptainFreak Aug 8, 2018
9c7547d
Merge remote-tracking branch 'remotes/upstream/gsoc-challenges' into …
CaptainFreak Aug 9, 2018
5622933
Fixed code style
CaptainFreak Aug 9, 2018
ea5c7b8
Merge remote-tracking branch 'remotes/upstream/gsoc-challenges' into …
CaptainFreak Aug 9, 2018
a57613a
Fb ctf country mappings
CaptainFreak Aug 9, 2018
9f4475f
Replaced variables assined with var
J12934 Aug 16, 2018
0b23d97
Simplified promise handling and removed console.logs
J12934 Aug 16, 2018
3548613
Standard fix
J12934 Aug 16, 2018
dd5eacd
More promise simplification
J12934 Aug 16, 2018
f0bd542
Fix SSTi and SSRF challenge descriptions and links
bkimminich Aug 20, 2018
e46a3c7
Fix typo "comand"
bkimminich Aug 20, 2018
edf1c5c
Merge branch 'release-v8' into gsoc-challenges
bkimminich Oct 2, 2018
8ab8f7c
Remove obsolete captcha function
bkimminich Oct 2, 2018
35b4524
Add missing spaces before braces
bkimminich Oct 2, 2018
b336695
Add missing spaces before braces
bkimminich Oct 2, 2018
2976fec
Add spaces before braces
bkimminich Oct 2, 2018
6cc208b
Corrected router link
J12934 Oct 3, 2018
13a3dfd
Added api test for profile image upload
J12934 Oct 3, 2018
1fcdc39
Revert "Added api test for profile image upload"
J12934 Oct 3, 2018
a5cbb7b
Merge branch 'develop' into gsoc-challenges
bkimminich Oct 15, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/fbctf.yml
Expand Up @@ -218,4 +218,10 @@ ctf:
httpHeaderXssChallenge:
name: Puerto Rico
code: PR
sstiChallenge:
name: Indonesia
code: ID
ssrfChallenge:
name: Thailand
code: TH

17 changes: 17 additions & 0 deletions data/static/challenges.yml
Expand Up @@ -564,3 +564,20 @@
hint: 'Punctuality is the politeness of kings.'
hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/race-condition.html#like-any-review-any-number-of-times'
key: timingAttackChallenge
-
name: 'SSTi'
category: 'Injection'
description: 'Infect the server with malware by abusing arbitrary command execution.'
difficulty: 6
hint: 'The search for a personal identity is the life of a man.'
hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#infect-the-server-with-malware-by-abusing-arbitrary-command-execution'
key: sstiChallenge
-
name: 'SSRF'
category: 'Broken Access Control'
description: 'Request a hidden resource on server through server.'
difficulty: 6
hint: 'Reverse the Bad and it gets Good again.'
hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#request-a-hidden-resource-on-server-through-server'
key: ssrfChallenge

4 changes: 4 additions & 0 deletions frontend/src/app/navbar/navbar.component.html
Expand Up @@ -12,6 +12,10 @@
</span>
</button>
<mat-menu #userMenu="matMenu">
<button mat-menu-item (click)="goToProfilePage()" *ngIf="isLoggedIn()">
<i class="fas fa-user-circle fa-lg"></i>
{{userEmail}}
</button>
<button mat-menu-item routerLink="/recycle" *ngIf="isLoggedIn()">
<i class="fas fa-recycle fa-lg"></i>
{{"NAV_RECYCLE" | translate}}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/navbar/navbar.component.ts
Expand Up @@ -147,4 +147,8 @@ export class NavbarComponent implements OnInit {
}, (err) => console.log(err))
}

goToProfilePage () {
window.location.replace('/profile')
}

}
19 changes: 19 additions & 0 deletions frontend/src/assets/public/images/uploads/default.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 0 additions & 11 deletions lib/insecurity.js
Expand Up @@ -6,7 +6,6 @@ const sanitizeHtml = require('sanitize-html')
const z85 = require('z85')
const utils = require('./utils')
const fs = require('fs')
const models = require('../models/index')

exports.publicKey = fs.readFileSync('encryptionkeys/jwt.pub', 'utf8')
const privateKey = '-----BEGIN RSA PRIVATE KEY-----\r\nMIICXAIBAAKBgQDNwqLEe9wgTXCbC7+RPdDbBbeqjdbs4kOPOIGzqLpXvJXlxxW8iMz0EaM4BKUqYsIa+ndv3NAn2RxCd5ubVdJJcX43zO6Ko0TFEZx/65gY3BE0O6syCEmUP4qbSd6exou/F+WTISzbQ5FBVPVmhnYhG/kpwt/cIxK5iUn5hm+4tQIDAQABAoGBAI+8xiPoOrA+KMnG/T4jJsG6TsHQcDHvJi7o1IKC/hnIXha0atTX5AUkRRce95qSfvKFweXdJXSQ0JMGJyfuXgU6dI0TcseFRfewXAa/ssxAC+iUVR6KUMh1PE2wXLitfeI6JLvVtrBYswm2I7CtY0q8n5AGimHWVXJPLfGV7m0BAkEA+fqFt2LXbLtyg6wZyxMA/cnmt5Nt3U2dAu77MzFJvibANUNHE4HPLZxjGNXN+a6m0K6TD4kDdh5HfUYLWWRBYQJBANK3carmulBwqzcDBjsJ0YrIONBpCAsXxk8idXb8jL9aNIg15Wumm2enqqObahDHB5jnGOLmbasizvSVqypfM9UCQCQl8xIqy+YgURXzXCN+kwUgHinrutZms87Jyi+D8Br8NY0+Nlf+zHvXAomD2W5CsEK7C+8SLBr3k/TsnRWHJuECQHFE9RA2OP8WoaLPuGCyFXaxzICThSRZYluVnWkZtxsBhW2W8z1b8PvWUE7kMy7TnkzeJS2LSnaNHoyxi7IaPQUCQCwWU4U+v4lD7uYBw00Ga/xt+7+UqFPlPVdz1yyr4q24Zxaw0LgmuEvgU5dycq8N7JxjTubX0MIRR+G9fmDBBl8=\r\n-----END RSA PRIVATE KEY-----'
Expand Down Expand Up @@ -93,13 +92,3 @@ exports.isRedirectAllowed = url => {
}
return allowed
}

exports.verifyCaptcha = () => (req, res, next) => {
models.Captcha.findOne({ where: { captchaId: req.body.captchaId } }).then(captcha => {
if (req.body.captcha === captcha.dataValues.answer) {
next()
} else {
res.status(401).send('Wrong answer to CAPTCHA. Please try again.')
}
})
}
4 changes: 2 additions & 2 deletions models/captcha.js
@@ -1,8 +1,8 @@
module.exports = (sequelize, { INTEGER, STRING }) => {
const Challenge = sequelize.define('Captcha', {
const Captcha = sequelize.define('Captcha', {
captchaId: INTEGER,
captcha: STRING,
answer: STRING
})
return Challenge
return Captcha
}
8 changes: 8 additions & 0 deletions models/user.js
Expand Up @@ -5,6 +5,10 @@ const challenges = require('../data/datacache').challenges

module.exports = (sequelize, { STRING, BOOLEAN }) => {
const User = sequelize.define('User', {
username: {
type: STRING,
defaultValue: 'Anonymous'
},
email: {
type: STRING,
unique: true,
Expand All @@ -28,6 +32,10 @@ module.exports = (sequelize, { STRING, BOOLEAN }) => {
lastLoginIp: {
type: STRING,
defaultValue: '0.0.0.0'
},
profileImage: {
type: STRING,
defaultValue: 'default.svg'
}
})

Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -97,7 +97,8 @@
"sqlite3": "^4.0.2",
"swagger-ui-express": "~4.0.1",
"unzipper": "0.8.12",
"z85": "~0.0"
"z85": "~0.0",
"jade": "0.16.0"
},
"devDependencies": {
"chai": "~4.1",
Expand Down
14 changes: 13 additions & 1 deletion routes/captcha.js
@@ -1,6 +1,6 @@
const models = require('../models/index')

module.exports = function captchas () {
function captchas () {
return (req, res) => {
var captchaId = req.app.locals.captchaId++
var operators = ['*', '+', '-']
Expand All @@ -26,3 +26,15 @@ module.exports = function captchas () {
})
}
}

captchas.verifyCaptcha = () => (req, res, next) => {
models.Captcha.findOne({ where: { captchaId: req.body.captchaId } }).then(captcha => {
if (req.body.captcha === captcha.dataValues.answer) {
next()
} else {
res.status(401).send('Wrong answer to CAPTCHA. Please try again.')
}
})
}

module.exports = captchas
36 changes: 36 additions & 0 deletions routes/profileImageFileUpload.js
@@ -0,0 +1,36 @@
const utils = require('../lib/utils')
const fs = require('fs')
const models = require('../models/index')
const insecurity = require('../lib/insecurity')

module.exports = function fileUpload () {
return (req, res, next) => {
const file = req.file
if (utils.endsWith(file.originalname.toLowerCase(), '.jpg')) {
const loggedInUser = insecurity.authenticatedUsers.get(req.cookies.token)
if (loggedInUser) {
const buffer = file.buffer
fs.open('frontend/dist/frontend/assets/public/images/uploads/' + loggedInUser.data.id + '.jpg', 'w', function (err, fd) {
if (err) {
console.log('error opening file: ' + err)
}
fs.write(fd, buffer, 0, buffer.length, null, function (err) {
if (err) console.log('error opening file: ' + err)
fs.close(fd, function () {
})
})
})
models.User.findById(loggedInUser.data.id).then(user => {
return user.updateAttributes({ profileImage: loggedInUser.data.id + '.jpg' })
}).catch(error => {
next(error)
})
} else {
next(new Error('Blocked illegal activity by ' + req.connection.remoteAddress))
}
res.location('/profile')
res.redirect('/profile')
}
res.status(204).end()
}
}
33 changes: 33 additions & 0 deletions routes/profileImageUrlUpload.js
@@ -0,0 +1,33 @@
const fs = require('fs')
const models = require('../models/index')
const insecurity = require('../lib/insecurity')
const request = require('request')

module.exports = function profileImageUrlUpload () {
return (req, res, next) => {
if (req.body.imageUrl !== undefined) {
const url = req.body.imageUrl
if (url.match(/(.)*solve\/challenges\/server-side(.)*/) !== null) {
req.app.locals.abused_ssrf_bug = true
}
const loggedInUser = insecurity.authenticatedUsers.get(req.cookies.token)
if (loggedInUser) {
request
.get(url)
.on('error', function (err) {
console.log(err)
})
.pipe(fs.createWriteStream('frontend/dist/frontend/assets/public/images/uploads/' + loggedInUser.data.id + '.jpg'))
models.User.findById(loggedInUser.data.id).then(user => {
return user.updateAttributes({ profileImage: loggedInUser.data.id + '.jpg' })
}).catch(error => {
next(error)
})
} else {
next(new Error('Blocked illegal activity by ' + req.connection.remoteAddress))
}
}
res.location('/profile')
res.redirect('/profile')
}
}
19 changes: 19 additions & 0 deletions routes/updateUserProfile.js
@@ -0,0 +1,19 @@
const models = require('../models/index')
const insecurity = require('../lib/insecurity')

module.exports = function updateUserProfile () {
return (req, res, next) => {
const loggedInUser = insecurity.authenticatedUsers.get(req.cookies.token)
if (loggedInUser) {
models.User.findById(loggedInUser.data.id).then(user => {
return user.updateAttributes({ username: req.body.username, email: req.body.email })
}).catch(error => {
next(error)
})
} else {
next(new Error('Blocked illegal activity by ' + req.connection.remoteAddress))
}
res.location('/profile')
res.redirect('/profile')
}
}
34 changes: 34 additions & 0 deletions routes/userProfile.js
@@ -0,0 +1,34 @@
const fs = require('fs')
const models = require('../models/index')
const insecurity = require('../lib/insecurity')
const jade = require('jade')

module.exports = function getUserProfile () {
return (req, res, next) => {
fs.readFile('views/userProfile.jade', function (err, buf) {
if (err) throw err
const loggedInUser = insecurity.authenticatedUsers.get(req.cookies.token)
if (loggedInUser) {
models.User.findById(loggedInUser.data.id).then(user => {
const templateString = buf.toString()
let username = user.dataValues.username
if (username.match(/#\{(.*)\}/) !== null) {
req.app.locals.abused_ssti_bug = true
const code = username.substring(2, username.length - 1)
try {
eval(code) // eslint-disable-line no-eval
} catch (err) {
username = '\\' + username
}
}
const fn = jade.compile(templateString.replace('usrname', username))
res.send(fn(user.dataValues))
}).catch(error => {
next(error)
})
} else {
next(new Error('Blocked illegal activity by ' + req.connection.remoteAddress))
}
})
}
}
17 changes: 17 additions & 0 deletions routes/verify.js
Expand Up @@ -77,6 +77,23 @@ exports.jwtChallenges = () => (req, res, next) => {
next()
}

exports.serverSideChallenges = () => (req, res, next) => {
if (req.query.key === 'tRy_H4rd3r_n0thIng_iS_Imp0ssibl3') {
if (utils.notSolved(challenges.sstiChallenge) && req.app.locals.abused_ssti_bug === true) {
utils.solve(challenges.sstiChallenge)
res.status(204).send()
return
}

if (utils.notSolved(challenges.ssrfChallenge) && req.app.locals.abused_ssrf_bug === true) {
utils.solve(challenges.ssrfChallenge)
res.status(204).send()
return
}
}
next()
}

function jwtChallenge (challenge, req, algorithm, email) {
const decoded = jwt.decode(utils.jwtFrom(req), { complete: true, json: true })
if (hasAlgorithm(decoded, algorithm) && hasEmail(decoded, email)) {
Expand Down
20 changes: 18 additions & 2 deletions server.js
Expand Up @@ -20,6 +20,8 @@ const swaggerUi = require('swagger-ui-express')
const RateLimit = require('express-rate-limit')
const swaggerDocument = yaml.load(fs.readFileSync('./swagger.yml', 'utf8'))
const fileUpload = require('./routes/fileUpload')
const profileImageFileUpload = require('./routes/profileImageFileUpload')
const profileImageUrlUpload = require('./routes/profileImageUrlUpload')
const redirect = require('./routes/redirect')
const angular = require('./routes/angular')
const easterEgg = require('./routes/easterEgg')
Expand Down Expand Up @@ -58,6 +60,8 @@ const trackOrder = require('./routes/trackOrder')
const countryMapping = require('./routes/countryMapping')
const basketItems = require('./routes/basketItems')
const saveLoginIp = require('./routes/saveLoginIp')
const userProfile = require('./routes/userProfile')
const updateUserProfile = require('./routes/updateUserProfile')
const config = require('config')

errorhandler.title = 'Juice Shop (Express ' + utils.version('express') + ')'
Expand All @@ -69,6 +73,8 @@ require('./lib/startup/cleanupFtpFolder')()
app.locals.captchaId = 0
app.locals.captchaReqId = 1
app.locals.captchaBypassReqTimes = []
app.locals.abused_ssti_bug = false
app.locals.abused_ssrf_bug = false

/* Bludgeon solution for possible CORS problems: Allow everything! */
app.options('*', cors())
Expand Down Expand Up @@ -101,6 +107,9 @@ app.use('/assets/public/images/tracking', verify.accessControlChallenges())
app.use('/assets/public/images/products', verify.accessControlChallenges())
app.use('/assets/i18n', verify.accessControlChallenges())

/* Checks for challenges solved by abusing SSTi and SSRF bugs */
app.use('/solve/challenges/server-side', verify.serverSideChallenges())

/* /ftp directory browsing and file download */
app.use('/ftp', serveIndex('ftp', { 'icons': true }))
app.use('/ftp/:file', fileServer())
Expand All @@ -117,8 +126,11 @@ app.use(express.static(path.join(__dirname, '/frontend/dist/frontend')))

app.use(cookieParser('kekse'))

app.use(bodyParser.urlencoded({ extended: true }))
/* File Upload */
app.post('/file-upload', upload.single('file'), fileUpload())
app.post('/profile/image/file', upload.single('file'), profileImageFileUpload())
app.post('/profile/image/url', upload.single('file'), profileImageUrlUpload())

app.use(bodyParser.text({ type: '*/*' }))
app.use(function jsonParser (req, res, next) {
Expand All @@ -130,7 +142,6 @@ app.use(function jsonParser (req, res, next) {
}
next()
})

/* HTTP request logging */
let accessLogStream = require('file-stream-rotator').getStream({ filename: './access.log', frequency: 'daily', verbose: false, max_logs: '2d' })
app.use(morgan('combined', { stream: accessLogStream }))
Expand Down Expand Up @@ -183,7 +194,7 @@ app.use('/rest/basket/:id/order', insecurity.isAuthorized())
/* Challenge evaluation before epilogue takes over */
app.post('/api/Feedbacks', verify.forgedFeedbackChallenge())
/* Captcha verification before epilogue takes over */
app.post('/api/Feedbacks', insecurity.verifyCaptcha())
app.post('/api/Feedbacks', captcha.verifyCaptcha())
/* Captcha Bypass challenge verification */
app.post('/api/Feedbacks', verify.captchaBypassChallenge())
/* Register admin challenge verification */
Expand Down Expand Up @@ -252,6 +263,11 @@ app.post('/b2b/v2/orders', b2bOrder())
/* File Serving */
app.get('/the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg', easterEgg())
app.get('/this/page/is/hidden/behind/an/incredibly/high/paywall/that/could/only/be/unlocked/by/sending/1btc/to/us', premiumReward())

/* Routes for profile page */
app.get('/profile', userProfile())
app.post('/profile', updateUserProfile())

app.use(angular())

/* Error Handling */
Expand Down