文前漫谈

文章首发在Freebuf

研究几种webshell管理工具时遇见,发现几款常见的webshell管理工具都有对php://input的利用

本文记录我对php://input原理的学习和思考,如有疏漏失错,还请联系告知

$HTTP_RAW_POST_DATA的作用和php://input的作用相同,但是PHP 5.6.0. 被废弃了,后面的版本中就是用php://input替代。所有本文省略

官方文档的解释

$_POST数组

当 HTTP POST 请求的 Content-Type 是 application/x-www-form-urlencodedmultipart/form-data时,会将变量以关联数组形式传入当前脚本。

php://input 伪协议

php://input 是个可以访问请求的原始数据的只读流。当请求方式是post,并且Content-Type不等于"multipart/form-data"时,可以使用php://input来获取原始请求的数据。

从POST的几种Content-Type看PHP的$_POST []与 php://input

通常的,在php中我们用$POST[]数组来处理封装好的POST请求正文中的内容,下面从http post上传时几种不同的表单编码格式分析$POST[]数组和php://input伪协议在处理post内容时的差别

POST表单上传时常见有这几种编码:

  • multipart/form-data
  • application/x-www-data-urlencoded
  • application/json
  • text/plain

multipart/form-data (文件上传时的编码)

服务端代码

<html>
<form enctype="multipart/form-data" method="post">
<input type="text" name="name" />
<input type="file" name="upload_file" />
<button type="submit" name="submit" value="Submit">Submit</button>
</form>
<?php
echo "\$_POST[]: ";
var_dump($_POST);
echo "<br>";echo "<br>";
echo "php://input: ";
var_dump(file_get_contents("php://input"));
?>
</html>

上传 upload.txt , form表单如下,文件上传的post包就不多解释了

boundary=—-WebKitFormBoundaryHwjtGxAgxib9xeFe就是multipart分块传输的分界线

这个http包我精简过,注意Content-Type即可

POST /test/demo.php HTTP/1.1
Host: 192.168.1.136
Content-Length: 398
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.1.136
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryHwjtGxAgxib9xeFe
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.1.136/test/demo.php
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
Connection: close

------WebKitFormBoundaryHwjtGxAgxib9xeFe
Content-Disposition: form-data; name="name"

123456
------WebKitFormBoundaryHwjtGxAgxib9xeFe
Content-Disposition: form-data; name="upload_file"; filename="upload.txt"
Content-Type: text/plain

upload test
------WebKitFormBoundaryHwjtGxAgxib9xeFe
Content-Disposition: form-data; name="submit"

Submit
------WebKitFormBoundaryHwjtGxAgxib9xeFe--

看见后端只有$POST[] 数组接收了form表单上传的内容,正如上面说的,当Content-Type: multipart/form-data;,php://input并不起作用

image-20230403225438175

application/x-www-data-urlencoded

服务端代码

<html>
<form enctype="application/x-www-data-urlencoded" method="post">
<input type="text" name="name" />
<button type="submit" name="submit" value="Submit">Submit</button>
</form>
<?php
echo "\$_POST[]: ";
var_dump($_POST);
echo "<br>";echo "<br>";
echo "php://input: ";
var_dump(file_get_contents("php://input"));
?>
</html>

form表单的内容

POST /test/demo.php HTTP/1.1
Host: 192.168.1.136
Content-Length: 23
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.1.136
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.1.136/test/demo.php
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
Connection: close

name=demo&submit=Submit

$_POST[] 和 php://input都能接收到form表单的内容,也正如上面所说,php://input访问的时原始数据的只读流,后端接收到的和http传输的是相同的,但是解析的结果依然不同

image-20230403230323508

这里我突发奇想看看试试如果url编码的结果是什么

把 ‘demo’ 进行再一次编码url全编码,相当于url二次编码了

demo
->urlencode
%31%32%37%2e%30%2e%30%2e%31

image-20230403231001651

这里两者之间就有些差别了,具体原因就是浏览器发包时还会对post请求进行一次url编码,$_POST[]数组的内容是urldecode()之后的,而php://input访问的还是原始数据,也就没有解码

image-20230403233824446

application/json

json格式通常在传输数据时使用,我直接在post包中发送json格式的数据了。其实理所应当的能想到$_POST[]数组是处理不了json格式的数据的,这种情况php只能用php://input读post的数据

image-20230403234723112

总的来说

php://input会接收除了Content_Type: multipart/form-data情况下的内容

而$_POST[]数组只处理类型为application/x-www-form-urlencodedmultipart/form-data的情况,因为只有这两种能被直接写成关联数组的形式(就是字典的形式)

php://input 命令执行的原理

请求头中的Content-Type字段告诉服务器后端用户交互上传的是什么类型的数据,$_POST能处理form表单上传的键值对格式(类似字典)的内容,但是处理如上的json格式或者其他格式内容并不会生效。何况php的Content-Type类型且不止这三种

上面的三个例子也更好的说明了php://input是怎样和为什么要从原始数据的只读流处理post请求包的内容,但这不是php://input能进行命令执行的关键

它能进行命令执行的关键还是看它和什么函数联动,它本身并不能进行RCE,是处理php://input内容的函数出了问题

比如经典的远程文件包含

<?php
include($_GET['file']);
?>

file接收php://input的内容,post body里就是完整的php文件内容,这里的关键就是include()函数,它会解析<?php标签,并把后面的代码当作php代码执行,直到读到?>

image-20230404001117124

或者说直接是

<?php
eval(file_get_contents("php://input"));
?>

这种情况根据上面所说,直接在post body里输入php代码就行,连标签都不用加。(注意php://input和全局变量$_POST不同,它通常配合file_get_contents()函数使用)

image-20230404001948080

比如冰蝎4的默认shell

image-20230404145214389