From 4ea584d04df501efba1b9ea8ebbc46f59b6e247d Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 31 Oct 2023 12:52:29 -0600 Subject: [PATCH 1/3] Update workflows --- .github/workflows/kaakaww.yml | 11 +- stackhawk.d/active/api1:2019-tennant-check.js | 76 ++++++ stackhawk.d/authentication/form-auth-multi.js | 249 ++++++++++++++++++ stackhawk.d/httpsender/custom-sender.kts | 17 ++ stackhawk.d/stackhawk-custom-params.yml | 57 ++++ stackhawk.d/stackhawk-github-pr.yml | 5 + 6 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 stackhawk.d/active/api1:2019-tennant-check.js create mode 100644 stackhawk.d/authentication/form-auth-multi.js create mode 100644 stackhawk.d/httpsender/custom-sender.kts create mode 100644 stackhawk.d/stackhawk-custom-params.yml create mode 100644 stackhawk.d/stackhawk-github-pr.yml diff --git a/.github/workflows/kaakaww.yml b/.github/workflows/kaakaww.yml index 849f263..3841ccb 100644 --- a/.github/workflows/kaakaww.yml +++ b/.github/workflows/kaakaww.yml @@ -14,13 +14,18 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: stackhawk-hawkscan: + name: Runs the application and the HawkScan test of the app runs-on: ubuntu-latest steps: - - name: Check out Repo! + - name: Checkout code uses: actions/checkout@v2 - name: Build and Run Vulny! run: docker-compose build && docker-compose up -d - name: HawkScan - uses: stackhawk/hawkscan-action@v1.3.2 + uses: stackhawk/hawkscan-action@v2.0.0 with: - apiKey: ${{ secrets.HAWK_API_TOKEN }} + apiKey: ${{ secrets.HAWK_API_KEY }} + configurationFiles: stackhawk.d/stackhawk-custom-params.yml stackhawk.d/stackhawk-github-pr.yml + env: + COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + BRANCH_NAME: ${{ github.head_ref }} diff --git a/stackhawk.d/active/api1:2019-tennant-check.js b/stackhawk.d/active/api1:2019-tennant-check.js new file mode 100644 index 0000000..74c9ac8 --- /dev/null +++ b/stackhawk.d/active/api1:2019-tennant-check.js @@ -0,0 +1,76 @@ +var RISK = 3; // 0: info, 1: low, 2: medium, 3: high +// StackHawk doesn't display INFO alerts +var TITLE = "ScottyCo Brewing Custom Tenant Check"; +var DESCRIPTION = `A ScottyCo user was able to obtain information about another user it should not have been able to. + Please see internal WIKI about ScottyCo tenancy checks at + https://example.com/wiki/ssdlc/tennancy`; +var SOLUTION = "Implement Correct Spring AuthZ Annotation on user object."; +var REFERENCE = "https://something.that.shows.the.problem.com/OMG"; +var OTHER = "If you run into ploblems, reach out to the security team on slack #appsechalp"; + +function log(msg) { + print("[" + this["zap.script.name"] + "]" + msg); +} + + +function alert(as, msg, evidence) { + as.newAlert() + .setPluginId(1000012) + .setRisk(RISK) + .setName(TITLE) + .setDescription(DESCRIPTION) + .setEvidence(evidence) + .setOtherInfo(OTHER) + .setSolution(SOLUTION) + .setReference(REFERENCE) + .setMessage(msg) + .raise(); + + } + + + function scan(as, msg, param, value) { + + } + + function scanNode(as, msg) { + var uri = msg.getRequestHeader().getURI(); + + log("scanning ", uri); + + // copy requests before using them or bad things + msg = msg.cloneRequest(); + + var request_header = msg.getRequestHeader(); + uri = request_header.getURI(); + + var path = ""; + if (uri.getPath() != null && uri.getPath().length() >1) { + path = uri.getPath().toString() + "/user"; + } else { + path = "/user"; + } + uri.setPath(path); + + log("scanning 2" + uri); + + request_header.setHeader("Content-Type", "application-json"); + + as.sendAndReceive(msg, false, false); + + var response_header = msg.getResponseHeader(); + var response_body = msg.getResponseBody(); + + log("response body: ", response_body); + log("response header: ", response_header); + + //check for evidence of problem + var evidence_idx = response_body.toString().indexOf("12345678"); + + log(msg); + + // Test the response here and make other requests as needed + if (response_header.getStatusCode() == 200 && evidence_idx >=0) { + alert(as, msg, "12345678"); + } + } \ No newline at end of file diff --git a/stackhawk.d/authentication/form-auth-multi.js b/stackhawk.d/authentication/form-auth-multi.js new file mode 100644 index 0000000..e961242 --- /dev/null +++ b/stackhawk.d/authentication/form-auth-multi.js @@ -0,0 +1,249 @@ + +/** TEMPLATE BEGIN **/ +/** Use this boilerplate code in all auth scripts. + * NOTE: This script requires hawkscan version 2.6.0 or higher to support the getHawkConf() function. + * **/ + +/** Import Java classes required for many authentication functions **/ +const URLEncoder = Java.type("java.net.URLEncoder"); +const URI = Java.type("org.apache.commons.httpclient.URI"); +const LogManager = Java.type("org.apache.log4j.LogManager"); + +const ScriptVars = Java.type("org.zaproxy.zap.extension.script.ScriptVars"); +const AuthenticationHelper = Java.type("org.zaproxy.zap.authentication.AuthenticationHelper"); +const ExtensionAntiCSRF = Java.type("org.zaproxy.zap.extension.anticsrf.ExtensionAntiCSRF"); +const Control = Java.type("org.parosproxy.paros.control.Control"); + +const HttpHeader = Java.type("org.parosproxy.paros.network.HttpHeader"); +const HttpRequestHeader = Java.type("org.parosproxy.paros.network.HttpRequestHeader"); + +/** Create logger and helper init functions **/ +const logger = LogManager.getLogger("custom-form-auth"); +let loggingEnabled = false; + +// log if enabled +const log = (str) => { + if (loggingEnabled) { + logger.info(str) + } +} + +// initialize logging based on parameters from hawk config +const initLogging = (paramsValues) => { + const loggingParam = paramsValues.get("logging"); + if (loggingParam != null && loggingParam === "true") { + loggingEnabled = true; + } +} + +/** Reference to the configured hawkscan conf file as a Javascript Object **/ +const getHawkConf = () => { + return JSON.parse(ScriptVars.getGlobalVar("hawkConf")); +} +const hawkConf = getHawkConf(); + +/** Helper functions **/ +// build a URL from supplied params and hawkscan conf's app.host +const paramUrl = (paramsValues, paramName) => { + return `${hawkConf["app"]["host"]}${paramsValues.get(paramName)}` +} + +// build a form-urlencoded string from a Javascript Object +const buildForm = (obj) => { + let str = ""; + let i = 0; + for (const key in obj) { + if (i > 0) { + str += "&"; + } + str += `${urlEncode(key)}=${urlEncode(obj[key])}`; + i++; + } + return str; +} + +// Url encode a string +const urlEncode = (str) => { + return URLEncoder.encode(str, "UTF-8"); +} + +// List of credential names from the configured hawkscan conf +const credentialNames = () => { + let names = []; + for (const name in hawkConf["app"]["authentication"]["script"]["credentials"]) { + names.push(name); + } + return names; +} + +// Add all credentials to the form Object +const addCredentials = (formObj, credentials) => { + credentialNames().forEach((name) => { + formObj[name] = credentials.getParam(name); + }); +} + +/** Reference to the ExtensionCsrf AddOn for csrf parsing utilities **/ +const extCsrf = Control.getSingleton() + .getExtensionLoader() + .getExtension(ExtensionAntiCSRF.class); + +/** + * Make a GET request like a browser would following redirects and accumulating cookies into the "jar" + * The returned object will contain and array of HttpMessage's in chronological order of each redirect. + * The HttpMessage contains the requestHeaders, requestBody, responseHeader, and responseBody. + * **/ +const getRequestBrowserLike = (helper, url, jar) => { + if (jar == null) { + jar = {cookies: {}, messages: []}; + } + + let requestUri = new URI(url, false); + let requestHeader = new HttpRequestHeader(HttpRequestHeader.GET, requestUri, HttpHeader.HTTP11); + + let msg = helper.prepareMessage(); + msg.setRequestHeader(requestHeader); + + // Make the HTTP request + helper.sendAndReceive(msg); + + log(`req/resp: ${logMsg(msg)}`) + msg.responseHeader.getHttpCookies().forEach((cookie) => { + if (cookie.name in jar.cookies) { + jar.cookies[cookie.name].push(cookie); + } else { + jar.cookies[cookie.name] = [cookie]; + } + }); + + jar.messages.push(msg); + const locationHdr = msg.responseHeader.getHeader("Location"); + if (locationHdr != null) { + return getRequestBrowserLike(helper, locationHdr, jar); + } else { + return jar; + } + +} + +const jsonMsg = (msg) => { + return { + requestHeader: msg.requestHeader.toString(), + requestBody: msg.requestBody.toString(), + responseHeader: msg.responseHeader.toString(), + responseBody: msg.responseBody.toString() + } +} + +const logMsg = (msg) => { + return `${msg.requestHeader}\r\n\r\n${msg.requestBody}\r\n---\r\n${msg.responseHeader}\r\n\r\n${msg.responseBody}`; +} + +/** TEMPLATE END **/ + +function authenticate(helper, paramsValues, credentials) { + initLogging(paramsValues); + log("Authenticating with custom script..."); + + // Login form page url + let url = paramUrl(paramsValues, "loginPagePath"); + + // Make request to login form page following redirects and accumulating cookies + let jar = getRequestBrowserLike(helper, url); + + /* Enable for demo + log("jar response headers") + jar.messages.forEach((jarMsg) => { + log(`${jarMsg.responseHeader.toString()}`) + });*/ + + // The last message in the jar should contain our login form + let msg = jar.messages[jar.messages.length - 1]; + + // Add login form page response to the auth message history to aid cookie session tracking + AuthenticationHelper.addAuthMessageToHistory(msg); + + // Add special extra csrf like token + extCsrf.addAntiCsrfTokenName(paramsValues.get("csrfExtra")); + + // Extract acsrf tokens from the response + const acsrfTokens = extCsrf.getTokensFromResponse(msg); + + // Accumulate the tokens and credentials for the form post on an Object. + let formObj = {}; + log(`csrf tokens count: ${acsrfTokens.size()}`); + acsrfTokens.forEach((token) => { + formObj[token.name] = token.value; + log(`adding csrf token ${token.name} = ${token.value}`); + }); + addCredentials(formObj, credentials); + formObj["remember"] = paramsValues.get("remember"); + + // Build the request body from the form Object + let requestBody = ""; + const formType = paramsValues.get("formType"); + log(`formObj: ${JSON.stringify(formObj)}`) + // Encode as JSON or form + if (formType != null && formType === "JSON") { + requestBody = JSON.stringify(formObj); + } else { + requestBody = buildForm(formObj); + } + + log(`POST: ${requestBody}`); + + // Prepare the request to the login form + url = paramUrl(paramsValues, "loginPage"); + requestUri = new URI(url, false); + requestHeader = new HttpRequestHeader(HttpRequestHeader.POST, requestUri, HttpHeader.HTTP11) + msg = helper.prepareMessage(); + msg.setRequestHeader(requestHeader); + msg.setRequestBody(requestBody); + + // Set the contentLength header from the length of the encoded request body + requestHeader.contentLength = msg.requestBody.length(); + + // Make the HTTP POST request to the login form + helper.sendAndReceive(msg); + + log(`req/resp: ${logMsg(msg)}`) + + /** + * Return response message from the authentication form post. + * The response header and body will be evaluated against the app.authentication.testPath + * as well as the app.loggedIn/OutIndicators. If the response is determined to be + * valid the response will also be passed to the session mgmt method + * for evaluation. + */ + return msg; +} + +/** + * The list of required parameters, the script will fail if these are not present. + * They are available on the paramsValues map in the authenticate() function + * @returns {string[]} + */ +function getRequiredParamsNames() { + return ["loginPagePath", "loginPage", "remember"]; +} + +/** + * The list of credential parameters. + * They are available on the credentials map in the authenticate() function + * These parameters MUSt match the names in app.authentication.script.credentials. + * @returns {string[]} + */ +function getCredentialsParamsNames() { + return ["username", "password"]; +} + +/** + * The list of optional parameters/ + * They are available on the paramsValues map in the authenticate() function. + * A parameter MUST be included here if passed in the app.authentication.script.parameters map + * or it will not be available on the paramsValues map. + * @returns {string[]} + */ +function getOptionalParamsNames() { + return ["logging", "formType", "csrfExtra"]; +} diff --git a/stackhawk.d/httpsender/custom-sender.kts b/stackhawk.d/httpsender/custom-sender.kts new file mode 100644 index 0000000..18d4e15 --- /dev/null +++ b/stackhawk.d/httpsender/custom-sender.kts @@ -0,0 +1,17 @@ +import org.apache.log4j.LogManager +import org.parosproxy.paros.network.HttpMessage +import org.zaproxy.zap.extension.script.HttpSenderScriptHelper +import org.zaproxy.zap.extension.script.ScriptVars + +val logger = LogManager.getLogger("sender1") + +// modify a request before it's sent to the web application +fun sendingRequest(msg: HttpMessage, initiator: Int, helper: HttpSenderScriptHelper) { + // logger.info("custom-sender script $initiator") + msg.requestHeader.setHeader("X-ZAP-Initiator", "$initiator") +} + +// modify the response from the web application before sending to the client +fun responseReceived(msg: HttpMessage, initiator: Int, helper: HttpSenderScriptHelper) { + msg.responseHeader.setHeader("X-ZAP-Initiator", "$initiator") +} diff --git a/stackhawk.d/stackhawk-custom-params.yml b/stackhawk.d/stackhawk-custom-params.yml new file mode 100644 index 0000000..79484ab --- /dev/null +++ b/stackhawk.d/stackhawk-custom-params.yml @@ -0,0 +1,57 @@ +app: + applicationId: ${APP_ID:458599ed-6493-44c1-9b9d-73ba0eac5bc8} + env: ${APP_ENV:custom-params} + host: ${APP_HOST:https://localhost:9000} + excludePaths: + - "/logout" + openApiConf: + path: /openapi + fakerEnabled: true #turn on the faker library to generate custom data + includeAllMethods: true # turns on Deletes which is off by default + customVariables: + - field: text + values: + - "pants" + - "hosen" + - field: searchText + values: + - "best" + - field: username + values: + - $faker:email + + authentication: + usernamePassword: + type: JSON + loginPath: /api/jwt/auth/signin + usernameField: username + passwordField: password + scanUsername: "user" + scanPassword: "password" + tokenExtraction: + type: TOKEN_PATH + value: "token" + tokenAuthorization: + type: HEADER + value: Authorization + tokenType: Bearer + testPath: + path: /api/jwt/items/search/i + success: ".*200.*" + loggedInIndicator: "\\QSign Out\\E" + loggedOutIndicator: ".*Location:.*/login.*" + + + +hawk: + failureThreshold: high + spider: + base: false + +hawkAddOn: + scripts: + - name: api1:2019-tennant-check.js + id: 1000012 + type: active + path: stackhawk.d + language: JAVASCRIPT diff --git a/stackhawk.d/stackhawk-github-pr.yml b/stackhawk.d/stackhawk-github-pr.yml new file mode 100644 index 0000000..9344cdd --- /dev/null +++ b/stackhawk.d/stackhawk-github-pr.yml @@ -0,0 +1,5 @@ +tags: + - name: _STACKHAWK_GIT_COMMIT_SHA + value: ${COMMIT_SHA} + - name: _STACKHAWK_GIT_BRANCH + value: ${BRANCH_NAME} \ No newline at end of file From b4243ddd45faa36e411ad50c4908f600f4836121 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 31 Oct 2023 12:54:39 -0600 Subject: [PATCH 2/3] Update action --- .github/workflows/kaakaww.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/kaakaww.yml b/.github/workflows/kaakaww.yml index 3841ccb..7ffd672 100644 --- a/.github/workflows/kaakaww.yml +++ b/.github/workflows/kaakaww.yml @@ -6,7 +6,6 @@ name: KAAKAWW on: # Triggers the workflow on push or pull request events but only for the main branch pull_request: - branches: [ Main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From 38c281cc484c8185adf197a2d08f55264bc9c01a Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 31 Oct 2023 13:00:50 -0600 Subject: [PATCH 3/3] Up to date build --- Dockerfile | 2 +- .../application-postgresql.properties | 37 +++++++++++++++++++ src/main/resources/application.properties | 33 ++++------------- stackhawk.d/stackhawk-custom-params.yml | 2 +- 4 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 src/main/resources/application-postgresql.properties diff --git a/Dockerfile b/Dockerfile index d6b8fb7..76578c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM openjdk:11.0.10-jdk-slim RUN mkdir /javavulny /app COPY . /javavulny/ -RUN sed -i 's/localhost\:5432/db\:5432/' /javavulny/src/main/resources/application.properties +RUN sed -i 's/localhost\:5432/db\:5432/' /javavulny/src/main/resources/application-postgresql.properties RUN cd /javavulny \ && ./gradlew --no-daemon build \ diff --git a/src/main/resources/application-postgresql.properties b/src/main/resources/application-postgresql.properties new file mode 100644 index 0000000..54e7835 --- /dev/null +++ b/src/main/resources/application-postgresql.properties @@ -0,0 +1,37 @@ +spring.datasource.platform=postgres +spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.username=postgresql +spring.datasource.password=postgresql +spring.datasource.driverClassName=org.postgresql.Driver +spring.jpa.database=POSTGRESQL +spring.jpa.show_sql=false +spring.jpa.properties.hibernate.use_sql_comments=false +spring.jpa.properties.hibernate.format_sql=false +spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQL81Dialect +spring.jpa.generate-ddl=true +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +logging.level.org.hibernate.SQL=debug +logging.level.org.hibernate.type=trace +logging.level.org.hibernate.type.descriptor.sql=trace +logging.level.org.springframework=info +logging.level.org.baeldung=info + +server.error.whitelabel.enabled=false +server.error.include-stacktrace=always + +server.port=9000 + +springdoc.api-docs.path=/openapi + +server.ssl.key-store-type=PKCS12 +#server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store=classpath:javavulny.p12 +server.ssl.key-store-password=stackhawk +server.ssl.key-alias=JavaVulny + +payload.startSize=3096 +payload.count=20 + +management.endpoints.web.exposure.include=* +management.endpoints.jmx.exposure.include=* \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 72f794d..7e0c785 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,43 +1,26 @@ -spring.datasource.platform=postgres -spring.datasource.url=jdbc:postgresql://localhost:5432/postgres -spring.datasource.username=postgresql -spring.datasource.password=postgresql -spring.jpa.database=POSTGRESQL -spring.jpa.show_sql=false -spring.jpa.properties.hibernate.use_sql_comments=false -spring.jpa.properties.hibernate.format_sql=false -spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQL81Dialect +spring.datasource.url=jdbc:h2:file:${PWD}/db/vulny;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE +spring.datasource.username=sa +spring.datasource.password=password +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.generate-ddl=true spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true -logging.level.org.hibernate.SQL=debug -logging.level.org.hibernate.type=trace -logging.level.org.hibernate.type.descriptor.sql=trace -logging.level.org.springframework=info -logging.level.org.baeldung=info - -server.error.whitelabel.enabled=false -server.error.include-stacktrace=always - server.port=9000 springdoc.api-docs.path=/openapi -server.ssl.enabled=false +server.ssl.enabled = true server.ssl.key-store-type=PKCS12 -server.ssl.key-store=classpath:keystore.p12 +#server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store=classpath:javavulny.p12 server.ssl.key-store-password=stackhawk server.ssl.key-alias=JavaVulny -server.ssl.protocol=TLS -server.ssl.enabled-protocols=TLSv1.2 payload.startSize=3096 payload.count=20 -management.endpoints.web.exposure.include=* -management.endpoints.jmx.exposure.include=* - #large payload testing #payload.startSize=2048000 #payload.count=100 diff --git a/stackhawk.d/stackhawk-custom-params.yml b/stackhawk.d/stackhawk-custom-params.yml index 79484ab..32534ea 100644 --- a/stackhawk.d/stackhawk-custom-params.yml +++ b/stackhawk.d/stackhawk-custom-params.yml @@ -1,5 +1,5 @@ app: - applicationId: ${APP_ID:458599ed-6493-44c1-9b9d-73ba0eac5bc8} + applicationId: ${APP_ID:44d63acb-a50b-4ab5-baa3-9508bb12691f} env: ${APP_ENV:custom-params} host: ${APP_HOST:https://localhost:9000} excludePaths: