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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
| package main
import (
"bufio"
"crypto/md5"
"crypto/tls"
"errors"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
)
//batchDetectClickjacking.go
// usage
//sort -u /mnt/d/record/burphttp/monitor/domain | httpx | ./batchDetectClickjacking -x http://127.0.0.1:8080 -v -t 50 | tee -a res.txt
// banner
//https://manytools.org/hacker-tools/ascii-banner/
// Rounded font
const banner = `
______ _ ______
(____ \ _ | | (______) _ _
____) )_____ _| |_ ____| |__ _ _ _____ _| |_ _____ ____ _| |_
| __ ((____ (_ _) ___) _ \ | | | | ___ (_ _) ___ |/ ___|_ _)
| |__) ) ___ | | |( (___| | | | | |__/ /| ____| | |_| ____( (___ | |_
|______/\_____| \__)____)_| |_| |_____/ |_____) \__)_____)\____) \__)
_______ _ _ _ _ _ _
(_______) |(_) | | (_) | | (_)
_ | | _ ____| | _ _ _____ ____| | _ _ ____ ____
| | | || |/ ___) |_/ )| (____ |/ ___) |_/ ) | _ \ / _ |
| |_____| || ( (___| _ ( | / ___ ( (___| _ (| | | | ( (_| |
\______)\_)_|\____)_| \_)| \_____|\____)_| \_)_|_| |_|\___ |
(__/ (_____| by findneo
`
// options
type Options struct {
mode string
InputFile string
Silent bool
generatePoC bool
threads int
timeout int
useragent string
proxy string
headersFile string
}
type customHeader struct {
key string
value string
}
var options *Options
func parse_options() *Options {
options := &Options{}
//flag.StringVar(&options.mode, "m", "", "find404|fmtjs|newjs")
flag.StringVar(&options.InputFile, "iL", "", "file contains url stirngs /input can be from os.stdin or pipe")
flag.BoolVar(&options.Silent, "s", false, "show only vuln sites")
flag.BoolVar(&options.generatePoC, "g", false, "gen PoC for vuln sites")
flag.IntVar(&options.threads, "t", 50, "limit concurrent threads num")
flag.IntVar(&options.timeout, "w", 30, "timeout seconds")
flag.StringVar(&options.useragent, "u", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", "Custom User agent")
flag.StringVar(&options.proxy, "x", "", "Custom proxy,like http://127.0.0.1:8080")
flag.StringVar(&options.headersFile, "hf", "", "file contains Custom Headers,like Cookie: qwq")
flag.Parse()
return options
}
var tr *http.Transport
var client *http.Client
var PoCdir string
var customHeaders []customHeader
func global_init() {
tr = &http.Transport{
//MaxIdleConns: 10,
//MaxIdleConns: options.threads,
//IdleConnTimeout: 30 * time.Second, //no good ,即使设了这个,单个请求超时时间还是会达到130秒
//DisableCompression: true,
MaxIdleConnsPerHost: -1,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
}
// 如果指定了proxy,则所有请求都使用该proxy
if options.proxy != "" {
proxyUrl, err := url.Parse(options.proxy)
if err != nil {
fmt.Fprintln(os.Stderr, "自定义的proxy格式有误,不设置代理")
} else {
tr.Proxy = http.ProxyURL(proxyUrl)
}
}
client = &http.Client{
Transport: tr,
Timeout: time.Duration(options.timeout) * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
redirect_limit := 20
if len(via) >= redirect_limit { // 默认重定向次数是10次
return errors.New(fmt.Sprintf("stopped after %d redirects", redirect_limit))
}
return nil
},
}
if options.generatePoC {
// 如果指定要生成PoC,则新建一个文件夹。
PoCdir = "clickjackPoCs_" + time.Now().Format("20060102_150405")
if _, err := os.Stat(PoCdir); os.IsNotExist(err) {
err1 := os.Mkdir(PoCdir, 777)
if err1 != nil {
throwErr("创建PoC文件夹失败:"+PoCdir, err1)
} else {
//fmt.Fprintln(os.Stderr,"创建PoC文件夹成功:"+PoCdir)
abs, _ := filepath.Abs(PoCdir)
throwErr("生成的PoC将存放在以下文件夹:"+abs, nil)
}
}
}
//如果指定了自定义请求头的文件,则尝试解析请求头文件
if options.headersFile != "" {
finput, err := os.Open(options.headersFile)
if err != nil {
throwErr("打开自定义header文件失败", err)
}
scanner := bufio.NewScanner(finput)
for scanner.Scan() {
headerstring := scanner.Text()
colon_index := strings.Index(headerstring, ":")
if colon_index == -1 {
throwErr("自定义请求头格式有误", nil)
}
key := headerstring[:colon_index]
value := headerstring[colon_index+1:]
customHeaders = append(customHeaders, customHeader{key, value})
}
}
}
func throwErr(errdesc string, err error) {
fmt.Fprintln(os.Stderr, strings.Repeat("-", 50))
fmt.Fprintln(os.Stderr, errdesc)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
fmt.Fprintln(os.Stderr, strings.Repeat("-", 50))
}
func genClickjackingPoC(u string) string {
uu, err := url.Parse(u)
if err != nil {
fmt.Fprintln(os.Stderr, "为 %s 生成PoC时出现错误 [%s]", u, err)
}
pocName := fmt.Sprintf("%s_%x.html", uu.Host, md5.Sum([]byte(u)))
pocFilename := path.Join(PoCdir, pocName)
poc := fmt.Sprintf(`
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>PoC of Clickjacking</title>
</head>
<body>
<h1 align="center">Batch Detect Clickjacking</h1>
<p align="center">
this site is vulnerable: <br>
<a href="%s">%s</a><br><br><br>
<iframe src="%s" height="80%" width="80%" align="middle"></iframe>
</p>
</body>
</html>
`, u, u, u)
f, err := os.Create(pocFilename)
if err != nil {
throwErr("创建PoC文件时出错", err)
}
f.WriteString(poc)
f.Close()
return pocName
}
func is_vuln_to__clickjacking(url string) int {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
throwErr("构造请求时出现错误:", err)
return 2 // 链接请求失败,需要进一步手工检查
}
req.Header.Add("User-Agent", options.useragent)
for _, header := range customHeaders {
req.Header.Add(header.key, header.value)
}
resp, err := client.Do(req)
if err != nil {
throwErr("发起请求时出现错误:", err)
return 2 // 链接请求失败,需要进一步手工检查
}
//被 defer 的函数在 return 之后执行,用于释放资源
defer resp.Body.Close()
//body, err := io.ReadAll(resp.Body)
if len(resp.Header["X-Frame-Options"]) == 0 {
if options.generatePoC {
fmt.Println("[vul]\t" + url + "\t" + genClickjackingPoC(url))
} else {
fmt.Println("[vul]\t" + url)
}
return 1 // 响应头没有XFO,确认受点击劫持漏洞影响
} else {
if options.Silent == false {
// 打印出具体的响应XFO头,便于人工判断
fmt.Println(fmt.Sprintf("[nop]\t%s\t[%d]%s", url, resp.StatusCode, resp.Header["X-Frame-Options"][0]))
//fmt.Println(resp.Header)
//fmt.Println(resp.StatusCode)
}
return 0 // 响应头有XFO,认为不受漏洞影响
// todo: 这里可以细分检测,比如XFO值是什么的时候还是可能有漏洞 sameorigin则没有漏洞
// todo: 有时域名无法解析,但响应头居然有xfo:deny,不知道是不是net/http自己加的,这个现象有点怪。
}
}
func batch_detect_clickjacking() {
batchurl_filename := options.InputFile
var scanner *bufio.Scanner
if batchurl_filename != "" {
finput, _ := os.Open(batchurl_filename)
scanner = bufio.NewScanner(finput)
} else {
scanner = bufio.NewScanner(os.Stdin)
}
var wg sync.WaitGroup
var ch = make(chan struct{}, options.threads)
for scanner.Scan() {
u, err := url.Parse(scanner.Text())
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse url %s [%s]\n", scanner.Text(), err)
continue
}
wg.Add(1)
ch <- struct{}{} // acquire a token
go func(url string) {
defer wg.Done()
is_vuln_to__clickjacking(url)
<-ch // release the token
}(u.String())
}
wg.Wait()
}
func main() {
fmt.Fprintln(os.Stderr, banner)
options = parse_options()
global_init()
batch_detect_clickjacking()
}
|