diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eaddc41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +contrast.jar +terraform.tfstate +terraform.tfstate.backup +.terraform +contrast_security.yaml +.terraform.tfstate.lock.info +node_modules +.idea +package-lock.json +terraform.tfvars \ No newline at end of file diff --git a/1-Build-Docker-Image.sh b/1-Build-Docker-Image.sh new file mode 100755 index 0000000..04b96a1 --- /dev/null +++ b/1-Build-Docker-Image.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker build . -t spring-petclinic:1.5.1 --no-cache \ No newline at end of file diff --git a/2-Deploy-Docker-Image-To-Docker-Hub.sh b/2-Deploy-Docker-Image-To-Docker-Hub.sh new file mode 100755 index 0000000..6549157 --- /dev/null +++ b/2-Deploy-Docker-Image-To-Docker-Hub.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo "Please log in using your Docker Hub credentials to update the container image" +docker login +docker tag spring-petclinic:1.5.1 contrastsecuritydemo/spring-petclinic:1.5.1 +docker push contrastsecuritydemo/spring-petclinic:1.5.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..86096e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM openjdk:8-jre-alpine +RUN mkdir /spring-petclinic +WORKDIR /spring-petclinic + +#Add application +ADD ./spring-petclinic-1.5.1.jar /spring-petclinic/spring-petclinic-1.5.1.jar + +#Add Contrast +RUN mkdir /opt/contrast +RUN apk --no-cache add curl +RUN curl --fail --silent --location "https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.contrastsecurity&a=contrast-agent&v=LATEST" -o /opt/contrast/contrast.jar + +#Enable Contrast +ENV JAVA_TOOL_OPTIONS='-javaagent:/opt/contrast/contrast.jar -Dcontrast.agent.java.standalone_app_name=spring-petclinic' + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "spring-petclinic-1.5.1.jar"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..8549684 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,87 @@ +env.terraform_version = '0.12.3' + +pipeline { + agent any + + stages { + stage('dependencies') { + steps { + sh """ + FILE=/usr/bin/terraform + if [ -f "\$FILE" ]; then + echo "\$FILE exists, skipping download" + else + echo "\$FILE does not exist" + cd /tmp + curl -o terraform.zip https://releases.hashicorp.com/terraform/'$terraform_version'/terraform_'$terraform_version'_linux_amd64.zip + unzip -o terraform.zip + sudo mv terraform /usr/bin + rm -rf terraform.zip + fi + """ + script { + withCredentials([file(credentialsId: env.contrast_yaml, variable: 'path')]) { + def contents = readFile(env.path) + writeFile file: 'contrast_security.yaml', text: "$contents" + } + } + sh """ + terraform init + npm i puppeteer + """ + } + } + stage('provision') { + steps { + script { + env.GIT_SHORT_COMMIT = checkout(scm).GIT_COMMIT.take(7) + env.GIT_BRANCH = checkout(scm).GIT_BRANCH + + withCredentials([azureServicePrincipal('ContrastAzureSponsored')]) { + try { + sh """ + export ARM_CLIENT_ID=$AZURE_CLIENT_ID + export ARM_CLIENT_SECRET=$AZURE_CLIENT_SECRET + export ARM_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID + export ARM_TENANT_ID=$AZURE_TENANT_ID + + terraform apply -auto-approve -var 'location=$location' -var 'initials=$initials' -var 'environment=qa' -var 'servername=jenkins' -var 'session_metadata="branchName=${env.GIT_BRANCH},buildNumber=${BUILD_NUMBER},commitHash=${env.GIT_SHORT_COMMIT},version=1.5.1"' + """ + } catch (Exception e) { + echo "Terraform refresh failed, deleting state" + sh "rm -rf terraform.tfstate" + currentBuild.result = "FAILURE" + error("Aborting the build.") + } + } + } + } + } + stage('sleeping') { + steps { + sleep 120 + } + } + stage('exercise') { + steps { + timeout(20) { + sh """ + FQDN=\$(terraform output fqdn) + BASEURL=\$FQDN node exercise.js + """ + } + } + } + stage('destroy') { + steps { + withCredentials([azureServicePrincipal('ContrastAzureSponsored')]) { + sh """export ARM_CLIENT_ID=$AZURE_CLIENT_ID + export ARM_CLIENT_SECRET=$AZURE_CLIENT_SECRET + export ARM_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID + export ARM_TENANT_ID=$AZURE_TENANT_ID + terraform destroy -auto-approve""" + } + } + } + } +} diff --git a/README.md b/README.md index c610623..a9661dd 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# demo-petclinic \ No newline at end of file +# Spring PetClinic: A deliberately insecure Java web application + +This sample application is based on https://github.com/Contrast-Security-OSS/spring-petclinic + +**Warning**: The computer running this application will be vulnerable to attacks, please take appropriate precautions. + +# Running standalone + +You can run PetClinic locally on any machine with Java 1.8 RE installed. + +1. Place a `contrast_security.yaml` file into the application's root folder. +1. Place a `contrast.jar` into the application's root folder. +1. Run the application using: +```sh +java -javaagent:contrast.jar -Dcontrast.config.path=contrast_security.yaml -Dcontrast.agent.java.standalone_app_name=spring-petclinic -jar spring-petclinic-1.5.1.jar [--server.port=8080] [--server.address=localhost] +``` +1. Browse the application at http://localhost:8080/ + +# Running in Docker + +You can run PetClinic within a Docker container. + +1. Place a `contrast_security.yaml` file into the application's root folder. +1. Build the PetClinic container image using `./1-Build-Docker-Image.sh`. The Contrast agent is added automatically during the Docker build process. +1. Run the container using `docker run -v $PWD/contrast_security.yaml:/etc/contrast/java/contrast_security.yaml -p 8080:8080 spring-petclinic:1.5.1` +1. Browse the application at http://localhost:8080/ + +# Running in Azure (Azure Container Instance): + +## Pre-Requisites + +1. Place a `contrast_security.yaml` file into the application's root folder. +1. Install Terraform from here: https://www.terraform.io/downloads.html. +1. Install PyYAML using `pip install PyYAML`. +1. Install the Azure cli tools using `brew update && brew install azure-cli`. +1. Log into Azure to make sure you cache your credentials using `az login`. +1. Edit the [variables.tf](variables.tf) file (or add a terraform.tfvars) to add your initials, preferred Azure location, app name, server name and environment. +1. Run `terraform init` to download the required plugins. +1. Run `terraform plan` and check the output for errors. +1. Run `terraform apply` to build the infrastructure that you need in Azure, this will output the web address for the application. +1. Run `terraform destroy` when you would like to stop the app service and release the resources. + +# Running automated tests + +There is a test script which you can use to reveal vulnerabilities which requires node and puppeteer. + +1. Install Node, NPM and Chrome. +1. From the app folder run `npm i puppeteer`. +1. Run `BASEURL=https://.azurewebsites.net node exercise.js` or `BASEURL=https://.azurewebsites.net DEBUG=true node exercise.js` to watch the automated script. + +## Updating the Docker Image + +You can re-build the docker image (used by Terraform) by running two scripts in order: + +* 1-Build-Docker-Image.sh +* 2-Deploy-Docker-Image-To-Docker-Hub.sh diff --git a/exercise.js b/exercise.js new file mode 100644 index 0000000..b796faf --- /dev/null +++ b/exercise.js @@ -0,0 +1,78 @@ +const puppeteer = require('puppeteer'); + +(async () => { + if (!process.env.BASEURL) { + console.log('Please specify a base url. E.g. `BASEURL=http://example.org node exercise.js`'); + } else { + var browser; + + if (process.env.DEBUG) { + browser = await puppeteer.launch({ + headless: false, + executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + }); + } else { + browser = await puppeteer.launch(); + } + + const sqliPayload = "D' OR '1%'='1" + + //home page + + console.log('visiting home page') + const page = await browser.newPage() + await page.goto(process.env.BASEURL) + await page.waitFor(2000) + + //exercising sqli vulnerability + console.log('exercising sqli vulnerability') + const page2 = await browser.newPage() + await page2.goto(process.env.BASEURL + '/owners/find') + await page2.waitFor(2000) + await page2.focus('#lastName.form-control') + await page2.keyboard.type('Davis'); + await page2.waitFor(2000) + await page2.click('button.btn.btn-default') + await page2.waitFor(2000) + + //attacking sqli vulnerability + + console.log('attacking sqli vulnerability') + const page3 = await browser.newPage() + await page3.goto(process.env.BASEURL + '/owners/find') + await page3.waitFor(2000) + await page3.focus('#lastName.form-control') + await page3.keyboard.type(sqliPayload); + await page3.waitFor(2000) + await page3.click('button.btn.btn-default') + await page3.waitFor(2000) + + //vets + console.log('visiting vets') + const page4 = await browser.newPage() + await page4.goto(process.env.BASEURL + '/vets.html') + await page4.waitFor(2000) + + //owners + console.log('visiting owners') + const page5 = await browser.newPage() + await page5.goto(process.env.BASEURL + '/owners') + await page5.waitFor(2000) + + // edit owner + + console.log('editing an owner') + const page6 = await browser.newPage() + await page6.goto(process.env.BASEURL + '/owners/1/edit') + await page6.waitFor(2000) + await page6.evaluate( () => document.getElementById("firstName").value = "David") + await page6.waitFor(2000) + await page6.click('button.btn.btn-default') + await page6.waitFor(2000) + + + + browser.close() + console.log('End') + } +})() diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..57b5ee2 --- /dev/null +++ b/main.tf @@ -0,0 +1,40 @@ +#Terraform `provider` section is required since the `azurerm` provider update to 2.0+ +provider "azurerm" { + features {} +} + +#Extract the connection from the normal yaml file to pass to the app container +data "external" "yaml" { + program = [var.python_binary, "${path.module}/parseyaml.py"] +} + +#Set up a personal resource group for the SE local to them +resource "azurerm_resource_group" "personal" { + name = "Sales-Engineer-${var.initials}" + location = var.location +} + +#Set up a container group +resource "azurerm_container_group" "app" { + name = "${var.appname}-${var.initials}" + location = azurerm_resource_group.personal.location + resource_group_name = azurerm_resource_group.personal.name + ip_address_type = "public" + dns_name_label = "${replace(var.appname, "/[^-0-9a-zA-Z]/", "-")}-${var.initials}" + os_type = "linux" + + container { + name = "web" + image = "contrastsecuritydemo/spring-petclinic:1.5.1" + cpu = "1" + memory = "1.5" + ports { + port = 8080 + protocol = "TCP" + } + environment_variables = { + JAVA_TOOL_OPTIONS = "-javaagent:/opt/contrast/contrast.jar -Dcontrast.agent.java.standalone_app_name=spring-petclinic -Dcontrast.api.url=${data.external.yaml.result.url} -Dcontrast.api.api_key=${data.external.yaml.result.api_key} -Dcontrast.api.service_key=${data.external.yaml.result.service_key} -Dcontrast.api.user_name=${data.external.yaml.result.user_name} -Dcontrast.standalone.appname=${var.appname} -Dcontrast.server.name=${var.servername} -Dcontrast.server.environment=${var.environment} -Dcontrast.application.session_metadata=${var.session_metadata} -Dcontrast.application.tags=${var.apptags} -Dcontrast.server.tags=${var.servertags}" + } + } +} + diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..db04df6 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,13 @@ +output "ip_address" { + value = azurerm_container_group.app.ip_address +} + +#the dns fqdn of the container group if dns_name_label is set +output "fqdn" { + value = "http://${azurerm_container_group.app.fqdn}:8080" +} + +output "contrast" { + value = "This app should appear in the environment ${data.external.yaml.result.url}" +} + diff --git a/parseyaml.py b/parseyaml.py new file mode 100644 index 0000000..846433a --- /dev/null +++ b/parseyaml.py @@ -0,0 +1,4 @@ +import yaml, json +with open('./contrast_security.yaml') as f: + config = yaml.load(f) + print(json.dumps(config['api'])) diff --git a/spring-petclinic-1.5.1.jar b/spring-petclinic-1.5.1.jar new file mode 100644 index 0000000..59d92a8 Binary files /dev/null and b/spring-petclinic-1.5.1.jar differ diff --git a/terraform-local/main.tf b/terraform-local/main.tf new file mode 100644 index 0000000..4285a56 --- /dev/null +++ b/terraform-local/main.tf @@ -0,0 +1,29 @@ +#Terraform `provider` section is required since the `azurerm` provider update to 2.0+ +provider "azurerm" { + features {} +} + +# Configure the Docker provider +provider "docker" { + host = "unix:///var/run/docker.sock" +} + +data "external" "yaml" { + program = [var.python_binary, "${path.module}/parseyaml.py"] +} + +# Create a container +resource "docker_container" "spring-petclinic" { + image = "contrastsecuritydemo/spring-petclinic:1.5.1" + name = "spring-petclinic" + + ports { + internal = 8080 + external = 8081 + } + + env = [ + "JAVA_TOOL_OPTIONS=-Dcontrast.api.url=${data.external.yaml.result.url} -Dcontrast.api.api_key=${data.external.yaml.result.api_key} -Dcontrast.api.service_key=${data.external.yaml.result.service_key} -Dcontrast.api.user_name=${data.external.yaml.result.user_name} -Dcontrast.standalone.appname=${var.appname} -Dcontrast.server.name=${var.servername} -Dcontrast.server.environment=${var.environment} -Dcontrast.application.session_metadata=${var.session_metadata} -Dcontrast.application.tags=${var.apptags} -Dcontrast.server.tags=${var.servertags}" + ] +} + diff --git a/terraform-local/outputs.tf b/terraform-local/outputs.tf new file mode 100644 index 0000000..f7b1077 --- /dev/null +++ b/terraform-local/outputs.tf @@ -0,0 +1,5 @@ +#the dns fqdn of the container group if dns_name_label is set +output "fqdn" { + value = "http://localhost:8081/" +} + diff --git a/terraform-local/parseyaml.py b/terraform-local/parseyaml.py new file mode 100644 index 0000000..2f50efb --- /dev/null +++ b/terraform-local/parseyaml.py @@ -0,0 +1,4 @@ +import yaml, json +with open('./contrast_security.yaml') as f: + config = yaml.load(f) + print(json.dumps(config['api'])) \ No newline at end of file diff --git a/terraform-local/variables.tf b/terraform-local/variables.tf new file mode 100644 index 0000000..7111b44 --- /dev/null +++ b/terraform-local/variables.tf @@ -0,0 +1,39 @@ +variable "initials" { + description = "Enter your initials to include in URLs. Lowercase only!!!" + default = "" +} + +variable "appname" { + description = "The name of the app to display in Contrast TeamServer. Also used for DNS, so no spaces please!" + default = "spring-petclinic" +} + +variable "servername" { + description = "The name of the server to display in Contrast TeamServer." + default = "spring-petclinic-docker" +} + +variable "environment" { + description = "The Contrast environment for the app. Valid values: development, qa or production" + default = "development" +} + +variable "session_metadata" { + description = "See https://docs.contrastsecurity.com/user-vulnerableapps.html#session" + default = "" +} + +variable "python_binary" { + description = "Path to local Python binary" + default = "python" +} + +variable "apptags" { + description = "Tags to be associated with the app in Contrast TeamServer." + default = "" +} + +variable "servertags" { + description = "Tags to be associated with the server in Contrast TeamServer." + default = "" +} diff --git a/terraform-local/versions.tf b/terraform-local/versions.tf new file mode 100644 index 0000000..d9b6f79 --- /dev/null +++ b/terraform-local/versions.tf @@ -0,0 +1,3 @@ +terraform { + required_version = ">= 0.12" +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..602825f --- /dev/null +++ b/variables.tf @@ -0,0 +1,45 @@ +variable "initials" { + description = "Enter your initials to include in URLs. Lowercase only!!!" + default = "" +} + +variable "location" { + description = "The Azure location where all resources in this example should be created, to find your nearest run `az account list-locations -o table`" + default = "" +} + +variable "appname" { + description = "The name of the app to display in Contrast TeamServer. Also used for DNS, so no spaces please!" + default = "spring-petclinic" +} + +variable "servername" { + description = "The name of the server to display in Contrast TeamServer." + default = "spring-petclinic-docker" +} + +variable "environment" { + description = "The Contrast environment for the app. Valid values: development, qa or production" + default = "development" +} + +variable "session_metadata" { + description = "See https://docs.contrastsecurity.com/user-vulnerableapps.html#session" + default = "" +} + +variable "python_binary" { + description = "Path to local Python binary" + default = "python" +} + +variable "apptags" { + description = "Tags to be associated with the app in Contrast TeamServer." + default = "" +} + +variable "servertags" { + description = "Tags to be associated with the server in Contrast TeamServer." + default = "" +} + diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..ac97c6a --- /dev/null +++ b/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +}