概述

batchDetectClickjacking可以用于自动化批量检测和利用点击劫持类漏洞。支持

  • 对多个HTTP站点批量快速检测
  • 对存在点击劫持类漏洞的网站生成利用代码PoC
  • 指定检测时使用的并发线程数
  • 从管道或文件输入被测站点
  • 指定单个网站检测的超时时间
  • 指定检测时使用的网络代理
  • 指定检测时使用的User-Agent

常见用法

1
2
3
4
5
6
7
8
# 检测单个网站链接是否受点击劫持漏洞影响
echo https://www.qq.com | ./batchDetectClickjacking

# 检测文件urls.txt中的多个链接
cat urls.txt | ./batchDetectClickjacking

# 使用100个线程检测大量url链接,只显示存在漏洞的网站,同时为每个漏洞网站生成PoC
./batchDetectClickjacking -iL urls.txt -s -g -t 100

支持参数

1
2
3
4
5
6
7
8
-h 查看帮助
-g 为存在漏洞的网站生成PoC
-iL 指定一个文件,其中包含若干待检测的网站地址,每行一个
-s 只显示存在漏洞的网站;默认情况下还会显示不存在漏洞的网站链接并显示响应头的X-Frame-Options值。
-t 指定检测时候使用的线程数,默认情况下是50个线程
-u 指定发起请求使用的UA,默认情况下是Firefox
-w 指定每个请求的超时时间,默认情况下是30s
-x 指定发起请求使用的网络代理,可以用于和burpsuite联动

漏洞原理及危害

点击劫持攻击,也称UI覆盖攻击,允许恶意页面以用户的名义点击“受害网站”,欺骗用户进行转账、点赞、关注、转发等操作,或者窃取用户凭据。

一个典型的利用场景是欺骗用户进行转账、点赞、关注或转发。如果业务网站的某正常功能页面A存在点击劫持漏洞,攻击者可以在恶意网站B中使用iframe标签包含A页面,但将A页面置顶并设为透明,这样用户认为自己在B网站上点击时,实际上是在被欺骗着点击了A网站。从而恶意网站B能够做到以用户名义点击A网站,进行转账、转发等敏感操作。

另一种典型的利用场景是窃取用户账号密码等身份凭据。如果业务网站的某正常功能页面A存在点击劫持漏洞,攻击者可以在恶意网站B包含A页面,将A页面置底并将B网站设为透明,这样用户认为自己在A网站进行输入时,实际上输入数据会被B网站获取到。

漏洞修复方法

主要有两种方式可以防御点击劫持,第一种是X-Frame-Options头信息(推荐使用,但可能有些浏览器不支持);第二种方式是使用javascript编写嵌入阻断代码(代码编写不当时可能被绕过,但当流量经过的某些web代理可能被去掉X-Frame-Options头时可以考虑这种方式)。

X-Frame-Options HTTP响应头能用来向浏览器指明是否允许渲染在iframe标签内的页面。网站可以用它来确保网站内容不被嵌入到其他站点,从而避免遭受点击劫持攻击。X-Frame-Options头信息有三种可用的值:,推荐使用SAMEORIGIN或DENY。

  • DENY,阻止所有的域名嵌入此页面。.
  • SAMEORIGIN,只允许同源站点嵌入此页面。
  • ALLOW-FROM uri,允许指定的“URI”嵌入此页面(如ALLOW-FROM http://www.example.com ,则允许www.example.com嵌入此页面)。ALLOW-FROM选项是2012年左右添加的, 所以一些旧浏览器可能不支持这个参数。不要依赖ALLOW-FROM参数。如果你使用这个选项,但是浏览器不支持它,那相当于你没有做任何点击劫持防御。

漏洞示意demo

正常业务网站A(存在点击劫持漏洞): http://localhost:8090/sitea.html

恶意网站B(实际攻击中会将A网站完全透明化):http://localhost:8091/siteb.html

用户被吸引点击彩色按钮时实际上可能触发转账或者转发的操作。

demo源码: sitea.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>I Frame</title>
    <style>
        body {
            background-color: #93B874;
        }
    </style>
</head>
<body>
<h1 align="center">A 网站:提供正常业务,可以点击进行转账等敏感操作</h1>
    <br>
    <p align="center">
        <button style="height:100px;width:180px;background:grey">喜欢这篇文章吗?<br>点这个按钮给作者转账</button>
        <br><br><br>
        <button style="height:125px;width:250px;background:grey">不喜欢这篇文章吗?<br>点这个按钮转发文章,让更多人讨厌它</button>
    </p>
</body>
</html>

siteb

 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
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>彩票开奖网站</title>
    <style>
        #target_website {
        position:absolute;
        width:2000px;
        height:1280px;
        opacity:0.2;
        z-index:1;
      }
    
        #decoy_website {
        position:absolute;
        width:2000px;
        height:1280px;
        z-index:-1;
      }
    </style>
  </head>
  <body>
    <div id="decoy_website" align="center">
        <br><h1 >B网站:花里胡哨但会吸引人进行点击操作的恶意网站</h1>
        <p align="center">
            <button style="height:30px;width:140px;background:red">点这个按钮抽大奖</button>
            <br><br><br><br><br>
            <b style="color:blue">快来点我啊,点我有惊喜!!!</b>
            <br><br>
            <button style="height:30px;width:140px;background:green">点这个按钮领红包</button>
        </p>
    </div>
    <div id="target_website" align="center">
        <iframe src="http://localhost:8090/sitea.html" height="40%" width="40%" align="middle" allowtransparency="true"></iframe>
    </div>
  </body>
</html>

工具源码

v2

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

v1

早期单线程版本

  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
package uispoof

import (
	"bufio"
	"crypto/tls"
	"flag"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

// banner
//https://manytools.org/hacker-tools/ascii-banner/
// Rounded font
const banner = `
 ______                   _        ______                                
(____  \         _       | |      (______)         _                 _   
 ____)  )_____ _| |_ ____| |__     _     _ _____ _| |_ _____  ____ _| |_ 
|  __  ((____ (_   _) ___)  _ \   | |   | | ___ (_   _) ___ |/ ___|_   _)
| |__)  ) ___ | | |( (___| | | |  | |__/ /| ____| | |_| ____( (___  | |_ 
|______/\_____|  \__)____)_| |_|  |_____/ |_____)  \__)_____)\____)  \__)
                                                                         
 _______ _  _       _      _             _     _                         
(_______) |(_)     | |    (_)           | |   (_)                        
 _      | | _  ____| |  _  _ _____  ____| |  _ _ ____   ____             
| |     | || |/ ___) |_/ )| (____ |/ ___) |_/ ) |  _ \ / _  |            
| |_____| || ( (___|  _ ( | / ___ ( (___|  _ (| | | | ( (_| |            
 \______)\_)_|\____)_| \_)| \_____|\____)_| \_)_|_| |_|\___ |            
                        (__/                          (_____| by findneo
`

// options
type Options struct {
	mode      string
	InputFile string
	Verbose   bool
	threads   int
	useragent string
	proxy string
}

var options *Options

func parse_options() *Options {
	options := &Options{}

	flag.StringVar(&options.mode, "m", "", "find404|fmtjs|newjs")
	flag.StringVar(&options.InputFile, "l", "", "file contains url stirngs")
	flag.BoolVar(&options.Verbose, "v", false, "show X-Frame-Options header content in response")
	flag.IntVar(&options.threads, "t", 10, "limit concurrent threads num")
	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.Parse()
	return options
}

var tr *http.Transport
var client *http.Client

func global_init(){
	//fmt.Fprintln(os.Stderr,banner)

	tr = &http.Transport{
		MaxIdleConns:       10,
		IdleConnTimeout:    30 * time.Second,
		DisableCompression: true,
		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}
}


func is_vuln_to__clickjacking(url string) int{
	req,err:=http.NewRequest("GET",url,nil)
	if err != nil{
		fmt.Fprintln(os.Stderr,"构造请求时出现错误:")
		fmt.Fprintln(os.Stderr,strings.Repeat("-",50))
		fmt.Fprintln(os.Stderr,err.Error())
		fmt.Fprintln(os.Stderr,strings.Repeat("-",50))
		return 2 // 链接请求失败,需要进一步手工检查
	}
	req.Header.Add("User-Agent",options.useragent)

	resp, err := client.Do(req)
	if err != nil {
		fmt.Fprintln(os.Stderr,"发起请求时出现错误:")
		fmt.Fprintln(os.Stderr,strings.Repeat("-",50))
		fmt.Fprintln(os.Stderr,err.Error())
		fmt.Fprintln(os.Stderr,strings.Repeat("-",50))
		return 2 // 链接请求失败,需要进一步手工检查
	}

	//被 defer 的函数在 return 之后执行,用于释放资源
	defer resp.Body.Close()

	//body, err := io.ReadAll(resp.Body)

	if len(resp.Header["X-Frame-Options"])==0 {
		fmt.Println("[vul]"+url)
		return 1 // 响应头没有XFO,确认受点击劫持漏洞影响
	}else{
		if options.Verbose==true{
			// 打印出具体的响应XFO头,便于人工判断
			fmt.Println("[nop]"+url+"\t["+fmt.Sprintf("%d",resp.StatusCode)+"]"+resp.Header["X-Frame-Options"][0])
			//fmt.Println(resp.Header)
			//fmt.Println(resp.StatusCode)
		}
		return 0 // 响应头有XFO,认为不受漏洞影响
		// todo: 这里可以细分检测,比如XFO值是什么的时候还是可能有漏洞 sameorigin则没有漏洞
	}
}

func batch_detect_clickjacking() {
	sc := bufio.NewScanner(os.Stdin)
	for sc.Scan() {
		u, err := url.Parse(sc.Text())
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to parse url %s [%s]\n", sc.Text(), err)
			continue
		}
		is_vuln_to__clickjacking(u.String())
	}
}

func main() {
	options = parse_options()
	global_init()
	batch_detect_clickjacking()
}

参考文档