从前端解决下载无后缀文件出现后缀的问题.txt

从前端解决下载无后缀文件出现后缀的问题.txt

RocketDev

发现bug.txt

最近我们的新生训练赛就要开始了,题目陆陆续续到了测试阶段,对于pwn题, 一些简单题就只需要提供一个binary文件,没有后缀,下载下来就能直接打。结果上传一看, 限制了文件类型。好不容易能上传任意文件了,下载下来却发现多了一个.txt后缀。 ELF怎么可能有后缀呢?检查http响应,没有问题:

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Date: Sun, 31 Aug 2025 13:20:35 GMT
Server: Apache/2.4.52 (Ubuntu)
Accept-Ranges: bytes
Content-Disposition: attachment; filename="shellcode"
Content-Length: 16552
Content-Type: application/zip
Last-Modified: Sun, 31 Aug 2025 13:13:15 GMT
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive

然而实际下载的文件却变成了shellcode.txt

于是我跑到我自己的网站上, 有一个putenv文件,尝试下载,http响应是这样的:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Date: Sun, 31 Aug 2025 13:24:26 GMT
Server: openresty
Content-Disposition: inline; filename=putenv
Content-Length: 16552
Content-Type: application/octet-stream
Last-Modified: Sat, 08 Feb 2025 06:30:59 GMT

下载下来是没有后缀名的。这不是都正确声明Content-Disposition了吗, 为什么还是不能下载无后缀名的文件呢?

尝试复现.txt

尝试修改自己的python服务,使响应和平台的一致,并没有任何效果。既不是因为Content-Type, 又不是因为Content-Disposition,真正的原因是什么呢?

硬调chromium.txt

为了深入理解其中发生了什么,只能调试浏览器了,看看下载文件中发生了什么。由于chromium 是开源的,可以就着源码,拿符号对着看。问题是,浏览器这么多进程,怎么调试呢? 阅读官方的调试指南, 里面提到可以关闭沙箱,并限制renderer进程,然而这仍然不够,还需要搭配上gdb调试的一些设置, 它们是:

stub
1
2
3
set breakpoint always-inserted on
set detach-on-fork off
set non-stop on

这样就能让所有进程同时运行,同时将断点下在所有进程中,并且监控所有进程。

那么方法有了,调试什么函数呢?直接使用copilot读取仓库并寻找下载时指定文件名的函数, 定位到了net::GenerateFileNameImpl函数,然后直接跟到GetSuggestedFilenameImpl, 其中的重要内容大致是

/net/base/filename_util_internal.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
std::u16string GetSuggestedFilenameImpl(
const GURL& url,
const std::string& content_disposition,
const std::string& referrer_charset,
const std::string& suggested_name,
const std::string& mime_type,
const std::string& default_name,
bool should_replace_extension,
ReplaceIllegalCharactersFunction replace_illegal_characters_function) {

...
std::string filename; // In UTF-8
bool overwrite_extension = false;
bool is_name_from_content_disposition = false;
// Try to extract a filename from content-disposition first.
if (!content_disposition.empty()) {
HttpContentDisposition header(content_disposition, referrer_charset);
filename = header.filename();
if (!filename.empty())
is_name_from_content_disposition = true;
}
// Then try to use the suggested name.
if (filename.empty() && !suggested_name.empty())
filename = suggested_name;

...

// extension should not appended to filename derived from
// content-disposition, if it does not have one.
// Hence mimetype and overwrite_extension values are not used.
if (is_name_from_content_disposition)
GenerateSafeFileName("", false, &result);
else
GenerateSafeFileName(mime_type, overwrite_extension, &result);

std::u16string result16;
if (!FilePathToString16(result, &result16)) {
result = base::FilePath(default_name_str);
if (!FilePathToString16(result, &result16)) {
result = base::FilePath(kFinalFallbackName);
FilePathToString16(result, &result16);
}
}
return result16;
}

接着我们使用gdb -x stub -args /usr/lib/chromium/chromium --no-sandbox --single-process 启动浏览器调试,把断点下在net::GenerateFileNameImpl,下载调试符号,让浏览器跑起来。 启动完成以后尝试下载一下文件,观察到此时进入调试态,注意寄存器的状态是什么样的。

虽然Arch提供了chromium的调试符号包,但是调试符号中只有函数名,这意味着我们没有源码可以看,只能盲调。 通过pwndbg的nextcall命令,可以很方便地观察调用了哪些函数,就着源码看也还行。

例如以下是下载我网站上文件时寄存器的状态,根据源码,rdx是指向Content-Disposition的字符串, rsi是指向url的字符串,r9是指向mime_type的字符串,即application/octet-stream

regs

接下来不断使用nextcall去找我们感兴趣的函数,以此侧面观察控制流(记得在 GetSuggestedFilenameImpl步入)。在这里我们就能观察到解析了Content-Disposition

header

之后调用了net::EnsureSafeExtension函数,把参数对应过去,mime_type是空字符串, 后缀名被Content-Disposition强制指定了,因此没有添加后缀名。(#L32)

extension

接着看看平台上下载文件的例子,首先观察mime_type是text/plain,而且链接前面是blob, 并且没有Content-Disposition头的信息。

regs

继续执行,直接到了EnsureSafeExtension的地方,此时我们发现走了另一个分支( !is_name_from_content_disposition),阅读源码可知在这里由于传入的mime_type是text/plain, 因此后续发现prefered extension是.txt,导致被强制加上了.txt后缀。(#L34)

extension

在chromium查找扩展名的过程中,由于application/octet-stream是“通用二进制数据”, 因此检查直接返回了,不要求后缀名。

错误设置的下载方式

原先平台上下载方式是把请求响应包在blob里,然后模拟链接点击下载, mime_type是在链接中设置的,查阅 Mozilla HTTP 文档 发现需要在blob中设置才行。

修复这个小bug

给blob加上mime_type就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@@ -418,14 +418,13 @@
}

// 创建 blob URL
- const blob = new Blob([response.data]);
+ const blob = new Blob([response.data], { type: 'application/octet-stream' });
const url = window.URL.createObjectURL(blob);

// 创建临时下载链接
const link = document.createElement('a');
link.href = url;
link.download = filename;
- link.type = 'application/octet-stream';
document.body.appendChild(link);
link.click();

参考

  1. 2025 新年火箭杯 | RocketDevlog
  2. Chromium Docs - Tips for debugging on Linux
  3. net::GetSuggestedFilenameImpl | GitHub
  4. <a>:锚元素
  • 标题: 从前端解决下载无后缀文件出现后缀的问题.txt
  • 作者: RocketDev
  • 创建于 : 2025-08-31 21:14:00
  • 更新于 : 2025-08-31 23:22:00
  • 链接: https://rocketma.dev/2025/08/31/discard.txt/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
从前端解决下载无后缀文件出现后缀的问题.txt