皮卡丘靶场解题记录-文件上传

clientcheck:

  1. 漏洞原理总结:

    这关的一个核心要点是,一切在前端做的安全措施都是不靠谱的

    就像小区门口的保安只检查”看起来像不像住户”,但后门根本没锁。

    你从正门走,保安(前端 JS)会拦住你:”你拿的是 .php 文件,不许进!”

    但你绕到后门(Burp 直接发包),后门压根没锁(后端不检查),你大摇大摆把 .php 木马搬进了服务器。
    核心教训:一切在浏览器里做的安全措施,都等于没有安全措施。 因为攻击者用的是 Burp Suite,不是浏览器。

  2. 我的测试过程(截图):

    这里分两种方法,一种绕过、一种禁掉前端防护

    方法1:

    先造一个假的jpg,内容如下图,修改文件名为shell.jpg(注意,要放在防火墙的排除文件夹里面,不然会被本机清理掉)

    抓包之后,修改请求。

    image-20260815192249317

    将此处的shell.jpg修改为shell.php

    这出修改起到一个什么作用?

    从后端源码可以发现,这个地方是将filename作为存在服务器上的文件名。所以修改这里之后,就将原来的文件名成功覆盖为php了(具体代码在哪里?)正规网站的写法是白名单校验后缀 + 重命名,这个修改filename的操作就没用了。

    然后放行。

    这里遇到了一个小问题,我自己的靶场是搭建在服务器上的,但是当时我为了防止别人也进我的靶场给顺手加了一个简单的HTTP Basic Auth,就需要在连蚂蚁剑的时候加上请求头。

    ==补充:==

    HTTP Basic Auth是什么?

    HTTP Basic Auth 是网站 最古老的一种”门卫” :你访问时浏览器弹出一个账号密码框,输进去之后,浏览器把 账号:密码 做一次 Base64 编码 ,塞进一个叫 Authorization: Basic xxx 的请求头里,之后每次请求都带着它。服务器看到这个头对得上,就放你进去

    比如这次,我就添加的

    1
    Authorization: Basic bulabula(账号:密码做了base64编码之后的内容)

    在实际过程中,这种basic auth对外不常见,但**”webshell 藏在需要认证才能访问的目录后面”这件事非常常见**。所以蚁剑、哥斯拉、冰蝎这些工具”能加请求头/Cookie”是标配——不带认证,连 404/401 都看不到,更别说 getshell。

    查了一下:

    image-20260816111419348

    注意:Base64 只是”编码”不是”加密” ,别人一眼能解开。所以 Basic Auth 一定要配 HTTPS,否则密码等于裸奔

    目前这个地方是http,那如果换成https怎么办?

    直接改 https 就行,蚁剑完全支持,不需要额外配置。它内部自己会处理 SSL/TLS 加密握手。把url从http://xxx/shell.phphttps://xxx/shell.php就可以了。可能踩的坑:如果目标网站的 https 证书是 自签名/过期/无效 的,蚁剑可能报”证书错误”连不上。这时去蚁剑设置里勾选**「忽略 SSL 证书校验」**就行。

方法2:

因为是前端验证,所以,我们直接在控制台废掉前端验证也可以

image-20260816111940713

废掉之后可以直接上传php(注意,不要刷新,临时改的,刷新会恢复)

image-20260816112954904

拼接出urlhttp://101.43.30.86/pikachu/vul/unsafeupload/uploads/123.php

然后用自己的shell文件里的密码,前面的帖子里讲过,此处就省略掉

如图添加请求头

image-20260816113829006
  1. 核心漏洞代码分析:

    image-20260813165434464

    前端对后缀进行了处理,但是

    image-20260813165809894

    这个处理函数是绑定在输入框的onchange事件上的,选完文件就检查一下后缀。意味着什么呢?不走浏览器就可以绕过这个检查。它就像你家门口的保安,只在你走正门时管用,绕到后门(Burp 直接发包)保安根本看不见。

    文件传入后端叫uploadfile

    image-20260813170054960

    用函数upload-client进行处理

    image-20260813170130998

  2. 实战延伸(价值所在):

【实战价值与门槛】

image-20260813170634211

MIME type

  1. 漏洞原理总结:

    这一关(服务端 check):门卫学聪明了一点,开始查 你递上来的名片 ( Content-Type )。但—— 名片是你自己印的呀!
    把一个 .php 木马递进去,名片上印着”我是图片 image/jpeg”,门卫只看名片不拆包,就放你进去了。
    类比快递: Content-Type 就像包裹上的”申报单”,自己填”衣物”;真实文件内容才是”包裹里的东西”。安检只看申报单、不拆包,所以把危险品申报成”衣物”就混进去了

    总结:前端校验不可信,同样,在后端校验也不能把校验内容设置为前端可更改的东西。

  2. 我的测试过程(截图):

    image-20260819160417634

    改成image

    image-20260819160523895

    成功上传,接下来步骤类似前面,省略

    如果设置了basic auth或者别的校验记得加请求头

    image-20260819160915815
  3. 核心漏洞代码分析:

    image-20260819155009600

    使用了upload_sick()函数对上传进行处理

    找到函数本体:

    image-20260819155130151

    看起来很周全对吧?对MIME进行了验证,但是

    它校验的$_FILES[‘type’]这个值,是客户端在请求里面自己填写的,是可以被修改的。

    即为,抓包里面的

    1
    Content-Type: image/jpeg

    跟文件的真实内容是没关系的。类似于名片

    如何防御?

    验证真实文件类型,读文件内容。比如getimagesize() 、 finfo_file()这些函数

  4. 实战延伸(价值所在):

image-20260819155627870

getimagesize

  1. 漏洞原理总结: 这一关安检确实开箱了,但他只瞄了一眼箱子里图片的开头, 没翻到图片底下夹层里藏的那把刀(PHP 代码),把木马代码塞在图片末尾,前面是张真图,他就放行了。

  2. 我的测试过程(截图):

    在powershell拼接出所需图片马:

    image-20260819221842452

    可以看到内容被成功拼接在文件末尾

    image-20260819221745378

    直接开始上传。

    image-20260819222002447

    翻车了,这个图片选得有点大,换个小点的。

    换了个小点的图片,成功了。

    image-20260819222445422

    上传之后,图片马的路径在:http://101.43.30.86/pikachu/vul/unsafeupload/uploads/shell2.png

    下一步利用,有点小技巧,需要结合前面的文件包含漏洞。因为这一关卡,木马落盘存在服务器上的后缀被后端的白名单锁死成图片的jpg/jpeg/png了。而文件的后缀,决定了它会不会被php执行。 前面两关绕过之后存在服务器上的文件后缀名都是php,直接访问就执行,蚂蚁剑直连。而图片后缀直接访问是不会被执行的,需要依靠文件包含。

    前面文件包含的关卡中,fi_local.php中有:

    1
    include "include/$filename";   // 把文件内容直接"塞进 PHP 里执行"

    fi_local.php 在 vul/fileinclude/ 目录,它包含时带了 include/ 前缀。要够到 vul/unsafeupload/uploads/shell2.png ,用目录穿越 ../../ 退两级到vul/然后再进入。

    因此,拼好的文件包含url为:http://101.43.30.86/pikachu/vul/fileinclude/fi_local.php?filename=../../unsafeupload/uploads/shell2.png&submit=1

    (这里后缀的submit在前面本地文件包含处已经讲过,此处略)

    然后按照流程,用这关文件包含url连蚂蚁剑,填密码,测试连通性。

    连接成功

    image-20260819224558051

    ==注:==

    1
    cmd /c "copy /b 2.png + 123.php shell2.png"

    详解:

    cmd /c 调用 真正的 cmd 命令行 , /c 表示”执行完后面的命令就自动退出”

    copy cmd里的复制/合并命令

    /b 二进制模式,不加 /b , copy 默认按 文本模式 处理,遇到图片里的特殊字节(比如 0x1A 这个”文件结束标记”、换行符等)会被它”自作主张”地转换/截断, 图片就被破坏了 。

    2.png 第1份文件,真实图片,提供文件头

    +拼接符号,把后面的符号追加到前面的末尾

    123.php 第 2 份文件:木马代码

    shell2.png 输出文件:拼接出的图片马

    1
    shell2.png = [2.png 的图片数据] + [123.php 的木马代码]

    url末尾的&submit=1

    因为前文fi_local.php中,

    1
    2
    3
    4
    if(isset($_GET['submit']) && $_GET['filename']!=null){
    $filename = $_GET['filename'];
    include "include/$filename"; // 只有进了这个 if 才会执行包含
    }

    &submit=1 就是给 submit 参数塞个值, 满足第 1 个条件,把 if 的门打开。

  3. 核心漏洞代码分析:

    这里处理上传调用的函数是upload()

    image-20260819215618444

    找到函数代码,函数内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    function upload($key,$size,$type=array(),$mime=array(),$save_path){
    $arr_errors=array(
    1=>'上传的文件超过了 php.ini中 upload_max_filesize 选项限制的值',
    2=>'上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值',
    3=>'文件只有部分被上传',
    4=>'没有文件被上传',
    6=>'找不到临时文件夹',
    7=>'文件写入失败'
    );
    // var_dump($_FILES);
    if(!isset($_FILES[$key]['error'])){
    $return_data['error']='请选择上传文件!';
    $return_data['return']=false;
    return $return_data;
    }
    if ($_FILES[$key]['error']!=0) {
    $return_data['error']=$arr_errors[$_FILES[$key]['error']];
    $return_data['return']=false;
    return $return_data;
    }
    //验证上传方式
    if(!is_uploaded_file($_FILES[$key]['tmp_name'])){
    $return_data['error']='您上传的文件不是通过 HTTP POST方式上传的!';
    $return_data['return']=false;
    return $return_data;
    }
    //获取后缀名,如果不存在后缀名,则将变量设置为空
    $arr_filename=pathinfo($_FILES[$key]['name']);
    if(!isset($arr_filename['extension'])){
    $arr_filename['extension']='';
    }
    //先验证后缀名
    if(!in_array(strtolower($arr_filename['extension']),$type)){//转换成小写,在比较
    $return_data['error']='上传文件的后缀名不能为空,且必须是'.implode(',',$type).'中的一个';
    $return_data['return']=false;
    return $return_data;
    }

    //验证MIME类型,MIME类型可以被绕过
    if(!in_array($_FILES[$key]['type'], $mime)){
    $return_data['error']='你上传的是个假图片,不要欺骗我xxx!';
    $return_data['return']=false;
    return $return_data;
    }
    //通过getimagesize来读取图片的属性,从而判断是不是真实的图片,还是可以被绕过的
    if(!getimagesize($_FILES[$key]['tmp_name'])){
    $return_data['error']='你上传的是个假图片,不要欺骗我!';
    $return_data['return']=false;
    return $return_data;
    }
    //验证大小
    if($_FILES[$key]['size']>$size){
    $return_data['error']='上传文件的大小不能超过'.$size.'byte(500kb)';
    $return_data['return']=false;
    return $return_data;
    }

    //把上传的文件给他搞一个新的路径存起来
    if(!file_exists($save_path)){
    if(!mkdir($save_path,0777,true)){
    $return_data['error']='上传文件保存目录创建失败,请检查权限!';
    $return_data['return']=false;
    return $return_data;
    }
    }
    //生成一个新的文件名,并将新的文件名和之前获取的扩展名合起来,形成文件名称
    $new_filename=str_replace('.','',uniqid(mt_rand(100000,999999),true));
    if($arr_filename['extension']!=''){
    $arr_filename['extension']=strtolower($arr_filename['extension']);//小写保存
    $new_filename.=".{$arr_filename['extension']}";
    }
    //将tmp目录里面的文件拷贝到指定目录下并使用新的名称
    $save_path=rtrim($save_path,'/').'/';
    if(!move_uploaded_file($_FILES[$key]['tmp_name'],$save_path.$new_filename)){
    $return_data['error']='临时文件移动失败,请检查权限!';
    $return_data['return']=false;
    return $return_data;
    }
    //如果以上都通过了,则返回这些值,存储的路径,新的文件名(不要暴露出去)
    $return_data['save_path']=$save_path.$new_filename;
    $return_data['filename']=$new_filename;
    $return_data['return']=true;
    return $return_data;
    }

    可以看到这里做了五种验证,看起来很严密。

    检查 说明
    上传方式 必须是 HTTP POST 传的
    后缀白名单 只能是 jpg/jpeg/png
    MIME类型 上一关的内容,可控
    getimagesize读内容 新增的”拆包验货”
    文件大小 ≤ 512000 字节(500KB)

    getimagesize() 会读文件真实内容 ,判断它是不是一张真图片。但是,getimagesize() 只读文件开头的图片头来判断类型,它不管图片数据后面还跟着什么。所以经典绕法就是: 一张真图片 + 在末尾拼接 PHP 代码 ,做成”图片马”

  4. 实战延伸(价值所在):

image-20260819221017025

章节疑问:

1、黑盒情况下,第一眼拿到文件上传入口,怎么判断是否存在漏洞,以及漏洞类型用什么方法判断、怎么找出验证、利用?

黑盒看不到源码,就上传不同的”探针文件”,根据服务器给不给过,反推它校验了什么

image-20260819234300892

提示语是情报:

  • 提示” 只允许图片格式 “ → 在查后缀/MIME。

  • 提示” 文件类型不符 “ → 在查 MIME。

  • 提示” 不是真实图片 “ → 在查 getimagesize。

  • 提示” 文件过大 “ → 有大小限制。

    不同提示语直接暴露了后端校验逻辑 ,要仔细看。

    实战中需要注意的点:

    先用无害探针 ,如 ,确认能执行再升级。先用真图片摸清正常响应长啥样。大厂有 WAF, @eval 、 <?php 特征可能直接被拦,要变着法子传。需要控制频率,别触发封号。

2、实际过程中上传文件之后界面可能不会返回文件在服务器存放的位置吧?那要怎么get shell?

情况A:返回了 URL/路径,最常见

很多上传点 其实会返回路径 :头像上传、富文本编辑器、图片上传接口,通常返回类似:

1
{"url": "/uploads/2026/08/14/xxxx.jpg"}

或者直接回显”上传成功,路径:xxx”。 拿到路径直接蚁剑连 。

情况B:返回了文件名,但不知道目录

  • 看页面源码 :上传后页面如果显示图片,F12 看 , src 就是完整路径。
  • 右键图片”复制图片地址” :直接拿到 URL。
  • 目录爆破 :用 Burp Intruder 爆破常见上传目录 + 你的文件名:
1
2
/upload/   /uploads/   /files/   /images/   /
avatar/ /static/ /temp/ /pic/

情况C:

  • 先看 响应头 和 报错信息 ,有时会泄露绝对路径。
  • 猜命名规律 :如果知道是”时间戳+随机名”,可以缩小范围爆破。
  • 组合拳 :如果上传目录被配置成”不执行脚本”( .php 传上去也不跑),就要配合别的漏洞:
  • 文件包含 (你刚学的,把图片马 include 进来执行)
  • 解析漏洞 (如 IIS/旧 nginx 的 .jpg/.php 解析)
  • 路径穿越 (文件名里带 ../../ 把文件传到可执行目录)

3、大厂普遍做了”白名单 + 重命名 + 内容校验 + 上传目录禁执行”四件套,具体是什么?

image-20260819234825789 image-20260819234912168

4、内容校验,大厂也是只查文件头?还是头和尾都查?那如果把图片马后面尾部再拼一个图片,真正的马放在中间不是也行?

分两种情况:

情况A:

这类 看文件开头的”魔法字节” (图片签名),后面内容是啥它根本不管 。

  • 马放 末尾 → 能过 (靶场就是这种)
  • 马放 中间 、前后拼图片 → 也能过 (因为只看头,没必要)

情况B:

大厂往往 不是”查”,而是”重写” :
服务器把图片 解码成像素 → 再重新编码存成新图 。
这个过程 只保留像素数据 ,图片里一切非像素字节(藏的马、EXIF 注释、附加数据) 统统被丢掉 。

5、二次渲染能绕过吗?

能,属于进阶内容。CTF 常见、真实 SRC 里有难度

思路是 把代码藏进”重新编码后仍然保留的字节”里 ——比如图片的 EXIF 段、ICC 配置段、或某些图片格式在渲染时不会重写的特定区域。这需要逆向分析目标具体用了哪个图片处理库、哪些字节段不会被洗掉,再精准地把马塞进去。

这个笔记主要是记录皮卡丘靶场的,所以暂时略过,后续补充学习。

找到一篇帖子【文件上传绕过】——二次渲染漏洞_二次渲染绕过-CSDN博客

然后我之前看包师傅的帖子里也有二次渲染的脚本,感兴趣的可以去看看。