jiuhui.net API逆向:从抓包到一行命令发布

jiuhui.net API逆向:从抓包到一行命令发布
前言
jiuhui.net 是一个内容管理平台,日常运营中需要频繁发布和编辑文章。手动登录后台 → 填写表单 → 上传文件 → 提交的流程重复且低效。本文记录了对 jiuhui.net 三个核心 API 端点的逆向过程,以及最终封装为 jiuhui-publish 一行命令发布工具的全流程。目标读者为有 API 调试经验的开发者。
一、抓包与认证分析
1.1 确认鉴权方式
打开 Chrome DevTools → Network 面板,在 jiuhui.net 后台执行一次文章发布操作。观察所有请求的 Request Headers,发现一个关键的 Cookie 字段:
Cookie: adminkey=mjvJ5yjB
这个 adminkey 就是后端鉴权的唯一凭据。不同于 JWT 或 OAuth 等复杂方案,jiuhui.net 采用简单的 Cookie 认证。只要在请求中携带该 Cookie,服务端即认为请求合法。这意味着我们可以直接复用这个 Key 进行 API 调用,无需模拟登录流程。
1.2 三个核心端点
通过筛选 Network 中的 XHR/Fetch 请求,锁定三个最关键的 API:
| 方法 | 端点 | 用途 |
|---|---|---|
| POST | /index.php/admin/api/add.html | 新增文章 |
| POST | /index.php/admin/api/edit.html | 编辑文章 |
| POST | /index.php/file/upload.html | 上传文件(图片等) |
| GET | /index.php/admin/web/articleAe.html?id=N | 获取文章详情 |
二、内容获取接口逆向
2.1 GET 请求获取文章详情
编辑文章时,后台首先请求 GET /index.php/admin/web/articleAe.html?id=N(N 为文章 ID)来获取当前文章的完整数据。返回格式为 HTML 页面,其中包含一个隐藏的 <textarea>,其 value 属性中直接嵌入了文章的 JSON 数据:
<textarea id="article_data" style="display:none;">
{"id":"123","title":"示例文章","content":"<p>正文内容</p>"}
</textarea>
2.2 ?️ 踩坑一:HTML 实体二次转义
这是本次逆向中最隐蔽的坑。乍看之下返回的是标准 JSON,但仔细检查发现,JSON 中的所有特殊字符都被 HTML 实体编码了两次:
- 正常 HTML 实体:
"→" - 二次转义后:
&quot;→ 先解为",再解才得到"
也就是说,从 textarea 中拿到的字符串,需要做两次 HTML 实体解码才能得到真正的 JSON。如果只做一次解码,JSON.parse 会直接报错——因为 " 不是合法的 JSON token。
import html
raw = textarea_value # 从页面提取
first_pass = html.unescape(raw) # &quot; → "
second_pass = html.unescape(first_pass) # " → "
data = json.loads(second_pass)
根本原因推测:后端在存储时将 JSON 做了一次 HTML 实体编码(防 XSS),模板引擎渲染时又做了一次自动转义,导致双层编码。前端 JavaScript 通过 .textContent 或 jQuery .val() 读取时,浏览器自动解掉一层,所以前端不感知这个问题的存在。但当我们直接拿原始 HTML 解析时,就必须手动处理。
三、文章发布 API 逆向
3.1 POST 新增与编辑
新增和编辑两个端点的请求体结构几乎一致,仅路径不同:
POST /index.php/admin/api/add.html
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: adminkey=mjvJ5yjB
id=&title=文章标题&content=<p>正文HTML</p>&cateid=1&level=3&attribute=&status=1
参数说明:
id:新增时留空,编辑时传文章 IDtitle:文章标题content:HTML 格式的正文内容cateid:分类 ID(需先获取,通常从文章详情中提取)level:权重等级(3 为默认)attribute:特殊属性标记status:1 表示发布,0 表示草稿
返回格式为 JSON:{"code":1,"msg":"操作成功"}。code 为 1 表示成功,其他值表示失败。
四、文件上传 API 逆向
4.1 Multipart 上传
图片上传走的是 POST /index.php/file/upload.html,Content-Type 为 multipart/form-data:
POST /index.php/file/upload.html
Cookie: adminkey=mjvJ5yjB
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png
[binary data]
------WebKitFormBoundary
Content-Disposition: form-data; name="dir"
article
------WebKitFormBoundary--
成功返回:
{"code":1,"msg":"上传成功","url":"/upload/article/202605/xxx.png"}
拿到 url 后拼接到文章正文的 <img> 标签中即可。
4.2 ?️ 踩坑二:500KB 上传限制
上传稍大一些的图片时,接口返回 {"code":0,"msg":"文件大小超过限制"}。经测试,最大允许大小为 500KB。
解决方案:
- 上传前压缩图片到 500KB 以内,推荐使用
tinify(TinyPNG API)或Pillow调整质量/尺寸 - 或者接受限制,使用外部图床(如 sm.ms)等方式规避
from PIL import Image
import io
def compress_image(filepath, max_size=500*1024):
img = Image.open(filepath)
buf = io.BytesIO()
quality = 85
while True:
buf.seek(0)
buf.truncate()
img.save(buf, format='JPEG', quality=quality)
if buf.tell() <= max_size or quality <= 10:
break
quality -= 5
return buf.getvalue()
五、暗坑总结
5.1 ?️ 踩坑三:HEAD 请求 403 误判
在调试过程中,使用 requests 库发送 HEAD 请求探测端点可用性时,所有 API 都返回 403。起初怀疑是 IP 被封或需要额外鉴权,排查近半小时后发现:
- 根本原因:服务器对 HEAD 请求做了严格限制,直接返回 403 Forbidden
- 但 GET/POST 请求完全正常
这个坑非常隐蔽,因为在浏览器中我们几乎不会主动发送 HEAD 请求(DevTools 显示的是 method 列,默认不展示 HEAD)。如果你用 curl -I 或 requests.head() 测试,就会掉进这个陷阱。
六、最终成果:jiuhui-publish 一行命令发布
6.1 工具设计
综合以上逆向成果,封装为命令行工具 jiuhui-publish,支持一行命令完成文章发布:
# 发布新文章(从 Markdown 文件)
jiuhui-publish --title "文章标题" --file article.md
# 编辑已有文章
jiuhui-publish --id 123 --title "新标题" --file article.md
# 上传图片并自动插入
jiuhui-publish --upload image.png --title "图文文章" --file article.md
6.2 核心工作流
- 读取 Markdown:将本地 Markdown 文件转为 HTML(使用
markdown库),提取其中的图片引用 - 上传图片:自动将 Markdown 中的本地图片上传至 jiuhui.net,替换为远程 URL;如果是网络图片则直接引用
- 获取已有数据(编辑模式):请求
GET /index.php/admin/web/articleAe.html?id=N,提取当前分类、属性等元信息 - 发送请求:POST add.html 或 edit.html,携带 Cookie 认证
- 处理结果:解析返回 JSON,输出文章 ID 和链接
6.3 实现要点
import requests, html, json, re
from bs4 import BeautifulSoup
COOKIES = {"adminkey": "mjvJ5yjB"}
BASE = "https://jiuhui.net"
def get_article(article_id):
"""获取文章详情,处理二次HTML实体转义"""
resp = requests.get(
f"{BASE}/index.php/admin/web/articleAe.html?id={article_id}",
cookies=COOKIES
)
soup = BeautifulSoup(resp.text, "html.parser")
raw = soup.select_one("#article_data").text
# 两次 HTML 实体解码
text = html.unescape(html.unescape(raw))
return json.loads(text)
def upload_file(filepath):
"""上传文件,自动压缩到500KB以下"""
data = open(filepath, "rb").read()
if len(data) > 500 * 1024:
data = compress_image(filepath) # 见上文压缩函数
resp = requests.post(
f"{BASE}/index.php/file/upload.html",
cookies=COOKIES,
files={"file": ("image.png", data, "image/png")},
data={"dir": "article"}
)
return resp.json()["url"]
def publish(title, content, article_id=None):
"""发布或编辑文章"""
endpoint = "edit" if article_id else "add"
data = {
"id": article_id or "",
"title": title,
"content": content,
"cateid": "1",
"level": "3",
"attribute": "",
"status": "1"
}
resp = requests.post(
f"{BASE}/index.php/admin/api/{endpoint}.html",
cookies=COOKIES,
data=data
)
return resp.json()
总结
本次 jiuhui.net API 逆向实战,从最基础的抓包开始,逐步分析认证机制、四个核心端点的请求/响应格式,并踩过了三个关键坑:
- HTML 实体二次转义——页面中的 JSON 需要两次
html.unescape()才能正确解析 - 500KB 上传限制——图片上传前需要压缩到 500KB 以内
- HEAD 请求 403 误判——服务器对 HEAD 请求的限制导致可用性误判
最终封装为 jiuhui-publish 命令行工具,实现了一键从 Markdown 发布到 jiuhui.net 的自动化工流。整个逆向过程再次证明:很多看似复杂的 API,背后往往是简单的表单提交 + Cookie 认证。耐心抓包、仔细对比、留意响应中的编码细节,就能快速找到突破口。