diff --git a/Dockerfile b/Dockerfile index 2d4280e..9cd14c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ FROM maven:3.8-jdk-8 as builder COPY . /usr/src/easybuggy4sb/ WORKDIR /usr/src/easybuggy4sb/ -RUN mvn -B package +RUN mvn -B package -Dmaven.test.skip=true FROM openjdk:8-slim +RUN apt-get update && apt-get install curl -y WORKDIR /opt/easybuggy4sb COPY --from=builder /usr/src/easybuggy4sb/target/ROOT.war . CMD ["java", "-XX:MaxMetaspaceSize=128m", "-Xloggc:logs/gc_%p_%t.log", "-XX:NativeMemoryTracking=summary", "-Xmx256m", "-XX:MaxDirectMemorySize=90m", "-XX:+UseSerialGC", "-XX:+PrintHeapAtGC", "-XX:+PrintGCDetails", "-XX:+PrintGCDateStamps", "-XX:+UseGCLogFileRotation", "-XX:NumberOfGCLogFiles=5", "-XX:GCLogFileSize=10M", "-XX:GCTimeLimit=15", "-XX:GCHeapFreeLimit=50", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=logs/", "-XX:ErrorFile=logs/hs_err_pid%p.log", "-agentlib:jdwp=transport=dt_socket,server=y,address=9009,suspend=n", "-Dderby.stream.error.file=logs/derby.log", "-Dderby.infolog.append=true", "-Dderby.language.logStatementText=true", "-Dderby.locks.deadlockTrace=true", "-Dderby.locks.monitor=true", "-Dderby.storage.rowLocking=true", "-Dcom.sun.management.jmxremote", "-Dcom.sun.management.jmxremote.port=7900", "-Dcom.sun.management.jmxremote.ssl=false", "-Dcom.sun.management.jmxremote.authenticate=false", "-ea", "-jar", "ROOT.war"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8e6e8f0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3' + +volumes: + mysql_data: + driver: local + +services: + db: + image: mysql:5.7 + container_name: db + volumes: + - mysql_data:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: keycloak + MYSQL_USER: keycloak + MYSQL_PASSWORD: password + ports: + - 3306:3306 + keycloak: + image: quay.io/keycloak/keycloak:legacy + environment: + DB_VENDOR: MYSQL + DB_ADDR: db + DB_DATABASE: keycloak + DB_USER: keycloak + DB_PASSWORD: password + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: admin + KEYCLOAK_FRONTEND_URL: http://localhost:8180/auth + # Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the MySQL JDBC driver documentation in order to use it. + #JDBC_PARAMS: "connectTimeout=30000" + ports: + - 8180:8080 + healthcheck: + test: "curl -f http://keycloak:8080/auth/realms/master/.well-known/openid-configuration || exit 1" + depends_on: + - db + easybuggy: + build: + context: . + dockerfile: Dockerfile + ports: + - 8080:8080 + - 9009:9009 + depends_on: + keycloak: + condition: service_healthy + environment: + db_url: jdbc:mysql://db:3306/keycloak?useSSL=false + db_class_name: com.mysql.jdbc.Driver + db_platform: mysql + db_username: keycloak + db_password: password + oidc_op_name: Keycloak + oidc_config_endpoint: http://keycloak:8080/auth/realms/master/.well-known/openid-configuration + oidc_dynamic_client_registration_enabled: 'true' diff --git a/src/main/java/org/t246osslab/easybuggy4sb/errors/StackOverflowErrorController.java b/src/main/java/org/t246osslab/easybuggy4sb/errors/StackOverflowErrorController.java index 5cb57ec..0de8d2a 100644 --- a/src/main/java/org/t246osslab/easybuggy4sb/errors/StackOverflowErrorController.java +++ b/src/main/java/org/t246osslab/easybuggy4sb/errors/StackOverflowErrorController.java @@ -8,13 +8,6 @@ public class StackOverflowErrorController { @RequestMapping(value = "/sofe") public void process() { - new S().toString(); - } - - public class S { - @Override - public String toString() { - return "" + this; - } + process(); } } diff --git a/src/main/java/org/t246osslab/easybuggy4sb/vulnerabilities/VulnerableOIDCRPController.java b/src/main/java/org/t246osslab/easybuggy4sb/vulnerabilities/VulnerableOIDCRPController.java index 4c63ec5..1e2f94d 100644 --- a/src/main/java/org/t246osslab/easybuggy4sb/vulnerabilities/VulnerableOIDCRPController.java +++ b/src/main/java/org/t246osslab/easybuggy4sb/vulnerabilities/VulnerableOIDCRPController.java @@ -1,33 +1,9 @@ package org.t246osslab.easybuggy4sb.vulnerabilities; -import java.io.IOException; -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.spec.RSAPublicKeySpec; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.ModelAndView; -import org.t246osslab.easybuggy4sb.controller.AbstractController; - import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest; import com.google.api.client.auth.oauth2.RefreshTokenRequest; +import com.google.api.client.auth.oauth2.TokenRequest; import com.google.api.client.auth.oauth2.TokenResponse; import com.google.api.client.auth.oauth2.TokenResponseException; import com.google.api.client.auth.openidconnect.IdToken; @@ -41,10 +17,28 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.gson.Gson; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; +import org.t246osslab.easybuggy4sb.controller.AbstractController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.*; @Controller public class VulnerableOIDCRPController extends AbstractController { @@ -59,6 +53,8 @@ public class VulnerableOIDCRPController extends AbstractController { protected String endSessionEndpoint; + protected String registration_endpoint; + protected String jwksUri; protected String issuer; @@ -69,31 +65,36 @@ public class VulnerableOIDCRPController extends AbstractController { @Value("${oidc.client.secret}") protected String clientSecret; - @Value("${oidc.redirect.uri}") - protected String redirectUri; - @Value("${oidc.op.name}") protected String opName; + @Value("${oidc.dynamic.client.registration.enabled}") + protected boolean clientRegistrationEnabled = false; + @Value("${oidc.configuration.endpoint}") public void setOPConfig(String configEndpoint) { + log.debug("OP Config Endpoint: " + configEndpoint); try { HttpRequestFactory requestFactory = (new NetHttpTransport()).createRequestFactory(); HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(configEndpoint)); HttpResponse response = request.execute(); Map opConfig = new Gson().fromJson(response.parseAsString(), Map.class); + log.debug("OP Config: " + opConfig.toString()); authzEndpoint = (String) opConfig.get("authorization_endpoint"); tokenEndpoint = (String) opConfig.get("token_endpoint"); userinfoEndpoint = (String) opConfig.get("userinfo_endpoint"); endSessionEndpoint = (String) opConfig.get("end_session_endpoint"); + registration_endpoint = (String) opConfig.get("registration_endpoint"); issuer = (String) opConfig.get("issuer"); jwksUri = (String) opConfig.get("jwks_uri"); - if (!(StringUtils.isEmpty(clientId) || StringUtils.isEmpty(clientSecret) || StringUtils.isEmpty(redirectUri) - || StringUtils.isEmpty(opName))) { + if (clientRegistrationEnabled) { + tryRegisterClient(); + } + if (!(StringUtils.isEmpty(clientId) || StringUtils.isEmpty(clientSecret) || StringUtils.isEmpty(opName))) { isSettingsReady = true; } } catch (IOException e) { - log.debug("OP configuration request failed.", e); + log.error("OP configuration request failed.", e); } } @@ -151,7 +152,7 @@ public ModelAndView start(ModelAndView mav, HttpServletRequest req, HttpServletR url.setScopes(Arrays.asList("openid", "profile")); url.setState(state); url.set("nonce", nonce); - url.setRedirectUri(new GenericUrl(redirectUri).build()); + url.setRedirectUri(new GenericUrl(req.getRequestURL().toString().replace("/start", "/callback")).build()); res.sendRedirect(url.build()); } catch (IOException e) { log.error("Authorization code request failed.", e); @@ -197,12 +198,13 @@ public ModelAndView callback(ModelAndView mav, HttpServletRequest req, HttpSessi /* Access the token endpoint and get ID and access token */ AuthorizationCodeTokenRequest authzReq = new AuthorizationCodeTokenRequest(new NetHttpTransport(), new JacksonFactory(), new GenericUrl(tokenEndpoint), code); - authzReq.setRedirectUri(redirectUri) + authzReq.setRedirectUri(req.getRequestURL().toString()) .setClientAuthentication(new BasicAuthentication(clientId, clientSecret)); HttpResponse httpRes = authzReq.executeUnparsed(); IdTokenResponse idTokenRes = httpRes.parseAs(IdTokenResponse.class); String accessToken = idTokenRes.getAccessToken(); - IdToken idToken = IdToken.parse(idTokenRes.getFactory(), idTokenRes.getIdToken()); + String idTokenStr = idTokenRes.getIdToken(); + IdToken idToken = IdToken.parse(idTokenRes.getFactory(), idTokenStr); // Verify nonce if (!nonce.equals(idToken.getPayload().getNonce())) { @@ -234,7 +236,9 @@ public ModelAndView callback(ModelAndView mav, HttpServletRequest req, HttpSessi } ses.setAttribute("accessToken", accessToken); + ses.setAttribute("idToken", idTokenStr); ses.setAttribute("refreshToken", idTokenRes.getRefreshToken()); + ses.setAttribute("state", req.getParameter("state")); userInfo = getUserInfo(ses); changeNextPageToUserInfo(mav, locale, userInfo); ses.setAttribute("sub", userInfo.get("sub")); @@ -249,26 +253,17 @@ public ModelAndView callback(ModelAndView mav, HttpServletRequest req, HttpSessi } @RequestMapping(value = "/oidclogout") - public String logout(HttpSession ses) { + public void logout(HttpServletRequest req, HttpServletResponse res, HttpSession ses) { try { - HttpRequestFactory requestFactory = (new NetHttpTransport()).createRequestFactory(); - HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(endSessionEndpoint)); - request.setRequestMethod(HttpMethods.POST); - Map params = new HashMap<>(); - params.put("client_id", clientId); - params.put("client_secret", clientSecret); - params.put("refresh_token", (String) ses.getAttribute("refreshToken")); - HttpContent content = new UrlEncodedContent(params); - request.setContent(content); - HttpResponse response = request.execute(); - if (response.isSuccessStatusCode()) { - log.error("Logout request to OP failed. Response: ", response.parseAsString()); - } + GenericUrl url = new GenericUrl(endSessionEndpoint); + url.set("id_token_hint", (String) ses.getAttribute("idToken")); + url.set("post_logout_redirect_uri", req.getRequestURL().toString().replace("/oidclogout", "/")); + url.set("state", ses.getAttribute("state")); + res.sendRedirect(url.build()); } catch (IOException e) { log.error("Logout request to OP failed.", e); } ses.invalidate(); - return "redirect:/"; } private void changeNextPageToUserInfo(ModelAndView mav, Locale locale, Map userInfo) { @@ -277,7 +272,7 @@ private void changeNextPageToUserInfo(ModelAndView mav, Locale locale, Map } private Map getUserInfo(HttpSession ses) { - + String accessToken = (String) ses.getAttribute("accessToken"); if (accessToken == null) { return null; @@ -307,6 +302,40 @@ private void changeNextPageToUserInfo(ModelAndView mav, Locale locale, Map } return null; } + private void tryRegisterClient() { + log.info("Try Register Client."); + + try { + TokenRequest tokenReq = new TokenRequest(new NetHttpTransport(), new JacksonFactory(), + new GenericUrl(tokenEndpoint), "password"); + tokenReq.put("client_id", "admin-cli"); + tokenReq.put("username", "admin"); + tokenReq.put("password", "admin"); + TokenResponse tokenResponse = tokenReq.execute(); + + HttpRequestFactory requestFactory = (new NetHttpTransport()).createRequestFactory(); + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(registration_endpoint)); + request.setRequestMethod(HttpMethods.POST); + HttpHeaders headers = new HttpHeaders(); + headers.setAuthorization("bearer " + tokenResponse.getAccessToken()); + headers.setContentType("application/json"); + headers.setAccept("application/json"); + request.setHeaders(headers); + Map params = new HashMap<>(); + params.put("redirect_uris", Arrays.asList("*")); + params.put("post_logout_redirect_uris", Arrays.asList("*")); + HttpContent content = new JsonHttpContent(new JacksonFactory(), params); + request.setContent(content); + HttpResponse response = request.execute(); + Map map = new Gson().fromJson(response.parseAsString(), Map.class); + clientId = (String) map.get("client_id"); + clientSecret = (String) map.get("client_secret"); + log.info("Client {} is registered.", clientId); + + } catch (IOException e) { + log.error("Registration request to OP failed.", e); + } + } private TokenResponse refreshTokens(String refreshToken) { if (refreshToken != null) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4e27000..20b7150 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,11 +4,12 @@ spring.messages.encoding=UTF-8 spring.thymeleaf.cache=false -spring.datasource.url=jdbc:derby:memory:demo;create=true -spring.datasource.username=demo -spring.datasource.password=demo -spring.datasource.driver-class-name=org.apache.derby.jdbc.EmbeddedDriver -spring.datasource.platform= +spring.profiles.active=docker-compose +spring.datasource.url=${db_url:jdbc:derby:memory:demo;create=true} +spring.datasource.driver-class-name=${db_class_name:org.apache.derby.jdbc.EmbeddedDriver} +spring.datasource.username=${db_username:demo} +spring.datasource.password=${db_password:demo} +spring.datasource.platform=${db_platform:} spring.datasource.continue-on-error=true spring.mail.host=localhost @@ -49,9 +50,8 @@ account.lock.count=5 mail.admin.address=root@localhost ### OpenID Connect feature -oidc.client.id=clientappname -oidc.client.secret=b179fc27-1de8-4904-bd26-be628816b2c2 -oidc.redirect.uri=http://localhost:8080/callback -oidc.op.name=Keyclaok -oidc.configuration.endpoint=http://localhost:8180/auth/realms/master/.well-known/openid-configuration - +oidc.client.id= +oidc.client.secret= +oidc.op.name=${oidc_op_name:Keycloak} +oidc.configuration.endpoint=${oidc_config_endpoint:http://localhost:8180/auth/realms/master/.well-known/openid-configuration} +oidc.dynamic.client.registration.enabled=${oidc_dynamic_client_registration_enabled:false}