文件上传代码分析

目录

不同类型的语言

脚本语⾔/解释型语⾔

代表就是php/jsp/asp这种,他们的运⾏模式是⼀套动态解释引擎+脚本代码,在这种运⾏模式

下,我们⼀旦能够获取cmdshell或者物理设备,等同于我们能看到全部的代码。

所以对于php/jsp/asp⽽⾔,简单记为我们能看到所有的代码,是纯⽩盒审计。

⼀次编译到处运⾏

代表就是java/python/.net

他们的运⾏模式是将代码编译成中间语

⾔,并且在不同的操作系统上运⾏有不同的虚拟机,由虚拟机来完成将中间语⾔(ir)run起

来的操作。因为其强⼤的跨平台能⼒,也迅速得到推⼴。

对于java/python/.net这种虚拟机+字节码的运⾏模式,我们简单记为,可以恢复并看到

80%以上的代码来进⾏审计,基本等于⽩盒审计。

编译型语⾔

C/go/c++

对于编译型语⾔,基本看不到代码。

不同语⾔的webshell上传差异

脚本语⾔/解释型语⾔

可以上传webshell并运⾏

⼀次编译到处运⾏

视情况⽽定:

python基本不能直接上传webshell,因为没办法路由到webshell⽂件。

java要看中间件,有的中间件解析jsp的情况下,是可以通过上传jsp的webshell,有的中间件

不解析jsp就没办法了。

.net也是⼀样的道理,但是从经验来看,.net搭建的⽹站很多可以直接上传aspx的webshell。

编译型语⾔

没有办法上传webshell,但是在某些情况下可以通过上传+其他利⽤来进⾏rce。

⽂件上传到webshell

⽂件上传能够到webshell,归根结底是2个层⾯的问题:后缀处理、运⾏环境(操作系统、中

间件)

任意⽂件上传

php 复制代码
<?php
// 设置内容类型和字符编码
header("Content-type: text/html;charset=utf-8");
// 关闭错误报告
error_reporting(0);
 
// 设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
// 设置上传目录的URL路径
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
 
$is_upload = false;
 
// 检查上传目录是否存在,如果不存在则创建
if (!file_exists(UPLOAD_PATH)) {
    mkdir(UPLOAD_PATH, 0755);
}
 
// 检查是否提交了表单
if (!empty($_POST['submit'])) {
    // 检查是否有文件上传
    if (!$_FILES['file']['size']) {
        echo "<script>alert('请添加上传文件')</script>";
    } else {
        // 获取上传文件的文件名
        $name = basename($_FILES['file']['name']);
        
        // 移动上传的文件到指定目录
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
            $is_upload = true;
        } else {
            echo "<script>alert('上传失败')</script>";
        }
    }
}
?>

这是⼀个最简单的任意⽂件上传的原型,上传来的⽂件只是取了下⽂件名,就通过

move_uploaded_file写⼊到

UPLOAD_PATH . $name 去了。没有任何检查。

js检测

php 复制代码
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
 
// 设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
 
$is_upload = false;
 
// 检查上传目录是否存在,如果不存在则创建
if (!file_exists(UPLOAD_PATH)) {
    mkdir(UPLOAD_PATH, 0755, true); // 添加了第三个参数true,以允许递归创建目录(如果需要的话)
}
 
// 处理文件上传
if (!empty($_POST['submit'])) {
    if ($_FILES['file']['size'] == 0) {
        echo "<script>alert('请添加上传文件')</script>";
    } else {
        $name = basename($_FILES['file']['name']);
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
            $is_upload = true;
        } else {
            echo "<script>alert('上传失败')</script>";
        }
    }
}
?>
 
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>JavaScript 绕过</title>
    <link href="./attachs/bootstrap.sketchy.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="jumbotron">
            <h1 class="text-center">永远不要相信用户的输入</h1>
            <img src="./imgs/js.png" class="rounded mx-auto d-block" width="auto"><br>
            <p class="lead">
                "永远不要相信用户的输入" 是进行安全设计和安全编码的重要准则。换句话说,
                任何输入数据在证明其无害之前,都是有害的。许多危险的漏洞就是因为过于相信用户的输入是善意的而导致的。
            </p><br>
            <div>
                <?php
                if ($is_upload) {
                    echo '<img src="./upload/' . $name . '" class="rounded mx-auto d-block" width="100px">';
                }
                ?>
            </div>
            <form action="" method="post" enctype="multipart/form-data" onsubmit="return checkfilesuffix()">
                <div class="form-group">
                    <label for="exampleFormControlFile1">文章插入图片</label>
                    <input type="file" class="form-control-file" name="file" id="file">
                    <input type="submit" name="submit" value="Upload">
                </div>
            </form>
        </div>
    </div>
 
    <script>
        function checkfilesuffix() {
            var file = document.getElementsByName('file')[0].value;
            if (file == "" || file == null) {
                swal("请添加上传文件", "", "error");
                return false;
            } else {
                var whitelist = new Array(".jpg", ".png", ".gif", ".jpeg");
                var file_suffix = file.substring(file.lastIndexOf("."));
                if (whitelist.indexOf(file_suffix) == -1) {
                    swal("只允许上传图片类型的文件!", "", "error");
                    return false;
                }
            }
            return true; // 添加了返回true,以确保在文件后缀合法时表单能够提交
        }
 
        function error() {
            swal("上传失败", "", "error");
        }
    </script>
    <script src="./attachs/sweetalert.min.js"></script>
</body>
</html>

在整个输⼊流转中,真正决定⽂件是否写⼊的是后端代码路由,⽽从它往前数,⽹络层⾯我们

是可控的,所以完全不必要去管js的事情。

解析规则

不同中间件不同版本有不同的利⽤⽅式

MIME

php 复制代码
<?php
header("Content-type: text/html;charset=utf-8");
error_reporting(0); // 通常不建议在生产环境中完全关闭错误报告
 
// 设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
// 注意:UPLOAD_URL_PATH的定义可能不正确,它应该是一个相对于网站的URL路径
// 但当前代码尝试从服务器路径中移除DOCUMENT_ROOT,这通常不会得到正确的URL
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
 
if (!file_exists(UPLOAD_PATH)) {
    mkdir(UPLOAD_PATH, 0755); // 创建上传目录,权限设置为755
}
 
$is_upload = false;
 
if (!empty($_POST['submit'])) {
    // 检查文件类型是否为允许的图像类型
    if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) {
        // 注意:echo中的black()函数未定义,可能是想调用alert()或其他函数
        echo "<script>alert('文件类型不允许');</script>"; // 修改为alert,并给出明确的错误信息
        exit();
    } else {
        $name = basename($_FILES['file']['name']); // 获取上传文件的原始名称
        // 将文件从临时目录移动到指定上传目录
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
            $is_upload = true; // 标记文件上传成功
        } else {
            echo "<script>alert('上传失败');</script>"; // 文件上传失败时给出提示
        }
    }
}
?>

⽂件头

php 复制代码
<?php
header("Content-type: text/html;charset=utf-8");
// 关闭错误报告(不推荐在生产环境中这样做)
error_reporting(0);
 
// 设置上传目录
define("UPLOAD_PATH", dirname(__FILE__) . "/upload/");
// 注意:UPLOAD_URL_PATH的定义可能不正确,应该根据实际情况设置
define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH));
 
// 如果上传目录不存在,则创建它
if (!file_exists(UPLOAD_PATH)) {
    mkdir(UPLOAD_PATH, 0755);
}
 
$is_upload = false;
 
if (!empty($_POST['submit'])) {
    // 检查文件大小是否为0
    if (!$_FILES['file']['size']) {
        echo "<script>alert('文件大小为0,上传失败');</script>";
        exit();
    }
 
    // 读取文件的前4个字节(文件头)
    $file = fopen($_FILES['file']['tmp_name'], "rb");
    $bin = fread($file, 4);
    fclose($file);
 
    // 检查文件类型(通过MIME类型和文件头)
    $allowedMimeTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
    $allowedFileHeaders = ["89504E47", "FFD8FFE0", "47494638"]; // 分别对应PNG, JPEG, GIF的文件头
 
    if (!in_array($_FILES['file']['type'], $allowedMimeTypes)) {
        echo "<script>alert('文件类型不允许');</script>";
        exit();
    } elseif (!in_array(bin2hex($bin), $allowedFileHeaders)) {
        echo "<script>alert('文件头不匹配,可能是非法文件');</script>";
        exit();
    }
 
    // 获取原始文件名
    $name = basename($_FILES['file']['name']);
 
    // 移动上传的文件到指定目录
    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        $is_upload = true;
    } else {
        echo "<script>alert('文件上传失败');</script>";
    }
}
?>

我们知道,⽂件头是⽂件格式的普适约定,⼀般在⼀个⽂件的前⼏个字节。

但是对于php/asp/jsp这种解释型语⾔⽽⾔,灵活性很强,并不要求脚本代码位于⽂件的开头,这样的话,我们就可以在开头使⽤图⽚⽂件的⽂件头,在中间或者后⾯的位置插⼊脚本语⾔,进⾏getshell。

与上⾯类似的,这种检测⽅式也没有触及到核⼼,并没有去检测filename字段的后缀。

后缀检测失效

后缀检测失效,指的是程序员已经想到要检测filename字段的后缀了,但是因为业务能⼒不熟练,在检测过程中被绕过了,导致webshell上传。

php 复制代码
<?php
// ...(之前的代码,如header设置、错误报告关闭、上传目录定义等)
 
if (!empty($_POST['submit'])) {
    // 获取上传文件的原始名称
    $name = basename($_FILES['file']['name']);
 
    // 定义黑名单,包含不允许上传的文件扩展名
    $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
    $name = str_ireplace($blacklist, "", $name);
 
    // 检查文件是否成功上传
    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        $is_upload = true;
    } else {
        // 上传失败,显示错误消息
        echo "<script>alert('文件上传失败');</script>";
    }
}
?>

将敏感后缀字符替换为空,那么就可以⽤pasxhp-->php来绕过

php 复制代码
<?php
// 假设 UPLOAD_PATH 已经被定义为一个安全的上传目录路径
 
if (!empty($_POST['submit'])) {
    // 获取上传文件的原始名称
    $name = basename($_FILES['file']['name']);
 
    // 定义黑名单,包含不允许上传的文件扩展名
    $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
 
    // 逐个替换黑名单中的扩展名为空格(注意:这种方法不安全,应该使用白名单)
    foreach ($blacklist as $extension) {
        $name = str_replace('.' . $extension, ' ', $name); // 注意添加点号来匹配完整的扩展名
    }
 
    // 移除文件名中多余的空格(可能由于多个扩展名被替换而产生)
    $name = preg_replace('/\s+/', '_', $name); // 使用下划线替换空格,或者您可以选择删除它们
 
    // 为了安全起见,这里应该添加额外的逻辑来确保 $name 只包含允许的字符和格式
    // 例如,您可以使用 pathinfo 来获取文件的扩展名,并检查它是否在您的白名单中
    // 但由于我们坚持使用黑名单方法(尽管不推荐),我们将跳过这一步
 
    // 检查文件是否成功上传
    if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
        $is_upload = true;
    } else {
        // 上传失败,显示错误消息
        // 注意:在实际应用中,应该避免使用 JavaScript alert 来显示错误消息,因为这可能会暴露敏感信息
        // 更好的做法是在服务器端记录错误,并向用户显示一个通用的错误页面
        echo "<script>alert('文件上传失败,请重试。');</script>";
    }
}
?>

将敏感后缀字符替换成空格,那么以上的利⽤不能绕过了。但是这⾥忽略了运⾏

环境的因素,在windows下⼤⼩写通⽤,所以可以上传PHP后缀进⾏绕过。

php 复制代码
<?php
// 定义允许上传的文件扩展名白名单
$whitelist = array("jpg", "jpeg", "png", "gif");
 
// 假设 UPLOAD_PATH 是一个已定义的常量,指向安全的上传目录
define('UPLOAD_PATH', '/path/to/upload/directory/');
 
// 确保上传目录存在且可写
if (!is_dir(UPLOAD_PATH) || !is_writable(UPLOAD_PATH)) {
    die('上传目录不存在或不可写。');
}
 
// 检查是否提交了上传表单
if (!empty($_POST['submit'])) {
    // 获取上传文件的原始名称
    $originalName = basename($_FILES['file']['name']);
 
    // 获取文件的扩展名(不区分大小写)
    $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
 
    // 检查文件的扩展名是否在白名单中
    if (in_array($ext, $whitelist)) {
        // 生成一个新的文件名(避免使用原始文件名,以防止安全问题)
        $newName = uniqid() . '.' . $ext;
 
        // 检查文件是否成功上传
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $newName)) {
            $is_upload = true;
            echo "<script>alert('文件上传成功!');</script>";
        } else {
            // 上传失败,显示错误消息
            echo "<script>alert('文件上传失败,请重试。');</script>";
        }
    } else {
        // 文件扩展名不在白名单中,显示错误消息
        echo "<script>alert('不允许的文件类型。');</script>";
    }
} else {
    // 没有提交上传表单,可能显示一个上传表单
    echo '<form action="" method="post" enctype="multipart/form-data">
            <input type="file" name="file">
            <input type="submit" name="submit" value="上传">
          </form>';
}
?>

NTFS Tricks

这个技巧对于⼀些,限制⽐较严格的⿊名单检测,特别有效。

php 复制代码
<?php
// 定义上传路径常量(请确保这是一个安全的路径)
define('UPLOAD_PATH', '/path/to/your/upload/directory/');
 
// 确保上传目录存在且可写
if (!is_dir(UPLOAD_PATH) || !is_writable(UPLOAD_PATH)) {
    die('上传目录不存在或不可写。');
}
 
// 检查是否提交了文件上传表单
if (!empty($_FILES['file']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
    // 获取上传文件的原始名称
    $originalName = basename($_FILES['file']['name']);
 
    // 获取文件的扩展名(转换为小写)
    $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
 
    // 定义黑名单数组
    $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
 
    // 如果文件扩展名不在黑名单中
    if (!in_array($ext, $blacklist)) {
        // 为了安全起见,生成一个新的文件名(避免使用原始文件名)
        $name = uniqid() . '.' . $ext;
 
        // 尝试移动上传的文件到指定目录
        if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
            $is_upload = true; // 文件上传成功标志(虽然在这个脚本中没有被进一步使用)
            echo "<script>alert('文件上传成功!');</script>"; // 显示成功消息(注意:在实际应用中,应避免在客户端显示敏感信息)
        } else {
            // 上传失败,显示错误消息
            echo "<script>alert('文件上传失败,请重试。');</script>"; // 显示失败消息(同样,应避免在客户端显示敏感信息)
        }
    } else {
        // 文件扩展名在黑名单中,显示错误消息
        echo "<script>alert('不允许的文件类型。');</script>"; // 显示黑名单错误消息(同样,应避免在客户端显示敏感信息)
    }
} else {
    // 没有提交文件上传表单或请求方法不是POST
    // 这里可以显示一个文件上传表单或其他内容
    echo '<form action="" method="post" enctype="multipart/form-data">
            <input type="file" name="file">
            <input type="submit" value="上传">
          </form>';
}
?>

此时,如果在windwos下可以上传shell.php::$DATA即可绕过。最终shell为shell.php。

相关推荐
龙虎榜小红牛系统1 分钟前
WordCloud参数的用法:
python·wordcloud
hummhumm6 分钟前
第 36 章 - Go语言 服务网格
java·运维·前端·后端·python·golang·java-ee
微澜-9 分钟前
编译以前项目更改在x64下面时报错:函数“PVOID GetCurrentFiber(void)”已有主体
c++
YuanLiu_22715 分钟前
代码随想录算法训练营第十三天(递归遍历;迭代遍历;统一迭代;层序遍历)
java·数据结构·笔记·算法·leetcode
闻缺陷则喜何志丹17 分钟前
【C++动态规划】1411. 给 N x 3 网格图涂色的方案数|1844
c++·算法·动态规划·力扣·网格·数目·涂色
achaoyang18 分钟前
【Python中while循环】
开发语言·python
呆呆小雅20 分钟前
C# 封装
java·开发语言·c#
linmoo198629 分钟前
java脚手架系列16-AI大模型集成
java·人工智能·ai·大模型·通义千问·qwen·脚手架
对酒当歌丶人生几何29 分钟前
Mybatis控制台打印SQL执行信息(执行方法、执行SQL、执行时间)
java·数据库·sql·mybatis
uncleqiao38 分钟前
5.Feign与ReflectiveFeign
java·feign