在 Ubuntu18.04 安装 LEMP 栈,构建本地网站
0x00 背景
1. 目的
基于 LEMP 栈 搭建本地 web 服务器,实现一个能在本地网络中访问的网页,网页中实现对数据库的简单操作。
学习目标:
- 理解 web 服务器的运行,学会手动搭建一个 web 服务器
- 学习简单的 HTML 和 PHP 知识,并编写网站前端和服务器端的代码
- 学习 MySQL 数据库的基本知识,完成数据库的基本操作,实现 PHP 和 MySQL 的连接
- 学习如何加固 LEMP 栈
2. LEMP 栈简介
LEMP 指代 Linux、Nginx、MySQL、PHP,是一个实现 web 服务器的栈,之所以简写为 LEMP 而不是 LNMP,因为Nginx 的读音同 Engine X,因此简写选的是 E 而不是 N,此外 LEMP 是实际可拼读的英文,而 LNMP 只能逐个字母发音(当然也有 LNMP 的简写,但个人比较支持 LEMP)。常见的搭建 web 服务器的组合还有 LAMP,它是 LEMP 的前辈, LAMP 中用的 web 服务器软件 为 Apache。
0x01 实现
1. 环境搭建过程
目标环境
linux+nginx+php- fpm+mysql
1.1 安装 Linux 系统
Linux 系统使用 Ubuntu-18.04 版本,使用 VMware Workstation 安装其镜像,Ubuntu 镜像在其官网上下载。
sudo apt-get update & sudo apt-get upgrade
1.2 安装 Nginx
在 Ubuntu 中安装 Nginx,只需要在终端输入以下命令即可:
sudo apt-get install nginx
在浏览器中浏览 nginx 默认网页,http://主机地址,说明安装成功
1.3 安装 Php-fpm
在终端输入以下命令即可:
sudo apt-get intall php-fpm
查看服务是否运行:
systemctl status php7.2-fpm.service
1.4 安装 Mysql
在终端输入以下命令安装 mysql 和 php 的 mysql 支持:
sudo apt-get install mysql-server mysql-client php-mysql
安装完成后,检查 mysql 服务器是否运行:
sudo systemctl mysql.service
2. 配置自己的网站
目标
搭建本地网站 www.jaylen.com,配置 Nginx 使其支持 php,本地网站显示一张图片,图片下方有个评论界面,可以输入评论,显示到网页中,评论会存到 mysql 数据库中。
2.1 域名映射
要在本地网络中,通过浏览器访问到 www.jaylen.com,需要修改主机的 hosts 文件,在 /etc/hsots
添加一行:
your-ip-add www.jaylen.com
这样就不需要 DNS 服务器解析该域名了(域名并没有注册!!!,DNS 服务器是没有记录的)。
2.2 配置 Nginx
首先需要配置 Nginx,使其支持 php,在 /etc/nginx/conf.d
目录下添加自己网站服务器的配置,jaylen.com.conf
,内容如下:
# server configuration
server {
listen 80;
listen [::]:80;
server_name jaylen.com;
root /var/www/jaylen.com; # 网站在主机的根目录
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html index.php;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
# pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
}
测试配置是否成功:
sudo nginx -t
成功的话,出现如下提示信息:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
重新加载配置:
sudo nginx -s reload
2.3 创建网站主页
首先为网站创建一个根目录 /var/www/jaylen.com
,用于存储网站内容,该目录和 Nginx 配置文件中的 root
指令的内容一致。
sudo mkdir /var/www/jaylen.com
写个简单的 html 页面和 php进行测试,index.html
,
<html>
<body>
<head>
<title>
jaylen's homepage
</title>
</head>
<h1>Welcome to Jaylen's Homepage</h1>
<p>Website is under construction, wait...</p>
</bdoy>
</html>
index.php
<html>
<body>
<head>
<title>
jaylen's homepage
</title>
</head>
<h1>Welcome to Jaylen's Homepage</h1>
<p>Website is under construction, wait...</p>
<?php
echo phpinfo();
?>
</bdoy>
</html>
通过浏览器浏览网站 www.jaylen.com
测试完成,没有问题。
进一步完善网页内容,使其显示一张图片,图片下方带有评论输入框,可以提交评论,提交的评论存到主机的 mysql 数据库中,而后显示在评论区,最后完成的大致如下图,简陋的不行。
在 mysql 中创建一个新用户,并授权 INSERT、SELECT 权限,评论内容需要存到数据库,并读取数据库内容。
mysql>
CREATE USER 'user101'@'localhost'
IDENTIFIED BY 'webdev101@Webdev102';
GRANT INSERT,SELECT
ON *.*
TO 'user101'@'localhost'
WITH GRANT OPTION;
刷新授权表,退出,
mysql> FLUSH PRIVILEGE;
mysql> quit;
以新创建的用户重新登陆
mysql -u user101 -p
创建一个数据库
mysql> create database webdb101
切换到该数据库
mysql> use webdb101
创建一个表格,用于存放评论内容
mysql> CREATE TABLE comments (
id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
comment VARCHAR(200) NOT NULL
);
插入一条数据
mysql> INSERT INTO comments(comment) VALUES ('test');
最终的 index.php
文件内容如下:
<!DOCTYPE HTML>
<html>
<head>
<style>
.error {color: #FF0000;}
img {
width: 30%;
height: auto;
}
td {
border: 1px solid black;
}
</style>
</head>
<body>
<?php
// define variables and set to empty values
include('connect-mysql.php');
$comment = "";
$commentErr = "";
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (empty($_POST["comment"])) {
$commentErr = "You must write someting...";
} else {
$comment = test_input($_POST["comment"]);
$sql = "INSERT INTO comments (comment) VALUES ('".$comment."')";
if (mysqli_query($dbcon, $sql)) {
$commentErr = "Submit comment successfully!";
}
else {
$commentErr = "Error:" . $sql . "<br>" . mysqli_error($dbcon);
}
}
}
function test_input($data) {
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data);
return $data;
}
?>
<h1>Jaylen's HomePage</h1>
<img src='image/homepage.jpeg' alt='homepage icon'>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
<hr>
<p>Wirte your comment here!</p>
<textarea name="comment" rows="5" cols="80"><?php echo $comment;?></textarea>
<span class='error'>*<?php echo $commentErr; ?></span>
<br></br>
<input type="submit" name="submit" value="Submit">
<hr>
</form>
<?php
$sqlget = "SELECT * FROM comments";
$sqldata = mysqli_query($dbcon, $sqlget) or die("Fail to connect to database!" . mysqli_error($dbcon));
echo "<table>";
echo "<tr><th>Comments</th></tr>";
while($row = mysqli_fetch_array($sqldata, MYSQLI_ASSOC)) {
echo "<tr><td>";
echo $row['comment'];
echo "</td></tr>";
}
echo "</table>";
?>
</body>
</html>
连接数据库的操作,单独放到 connec-mysql.php
文件中。
在网页中测试下提交评论,
[外链图片转存失败(img-4GV7Mzuw-1565152977001)(https://cdn.jsdelivr.net/gh/BingSlient/WebSecurityLearning/WebSecurityBasics/images/1564841954745.png)]
2.4 问题
记录搭建自己本地网络过程中遇到的问题。
-
配置 Nginx 使其支持 php 时,访问 http://localhsot/index.php,出现
502 Bad Gateway
错误,查看 Nginx 错误日志/var/log/nginx/error.log
,找到错误记录如下:2019/08/02 03:11:53 [crit] 4293#4293: *50 connect() to unix:/var/run/php/php7.0-fpm.sock failed (2: No such file or directory) while connecting to upstream, client: 192.168.47.129, server: _, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.0-fpm.sock:", host: "192.168.47.129"
原因是找不到
unix:/var/run/php/php7.0-fpm.sock
文件,而我安装的版本是 7.2 的,因此应该是配置错误了,在系统中查找php7.0-fpm.sock
发现实际路径为/run/php/php7.0-fpm.sock
修改/etc/nginx/sites-avalable/default
文件如下:# pass PHP scripts to FastCGI server location ~ \.php$ { include snippets/fastcgi-php.conf; # With php-fpm (or other unix sockets): fastcgi_pass unix:/run/php/php7.2-fpm.sock; # With php-cgi (or other tcp sockets): #fastcgi_pass 127.0.0.1:9000; }
-
在
index.php
文件中对数据库进行插入操作时,提示错误:Unknown column '' in 'field list'
原因时插入的数据是
VARCHAR
类型,需要额外添加双引号,我原来的 sql 语句如下:$sql = "INSERT INTO comments (comment) VALUES ($comment)";
这样实际导致
$comment
变量展开后是一个标识符,而不是字符串,需要在外面添加额外的双引号,具体处理如下,注意在单引号中$
不会被识别成特殊字符,因此不会展开变量,而双引号中不能直接包含双引号,所以就成了下面的结果。$sql = "INSERT INTO comments (comment) VALUES ('".$comment."')";
3.本地服务器加固
3.1 加固 Linux 系统
时刻保持系统和软件为最新版本
在 Ubuntu 中检查系统需要更新的软件包,使用如下命令,apt-get -s
命令用于模拟后面命令的操作,但实际不会改变系统的状态,所以 apt-get -s upgrade
只会模拟软件更新的过程,你会看到被更新的软件的信息,但实际并没有更新到系统上 :
sudo apt-get update && sudo apt-get -s upgrade
然后根据需要更新你想要更新的软件。如果你想更新所有软件,使用如下命令:
sudo apt-get update && sudo apt-get upgrade
加固远程登陆
在使用和管理服务器时,往往我们需要远程登陆服务器,这就需要我们保证远程登陆过程的安全性。以下步骤一定程度提高了远程登陆的安全性。
- 强制使用高强度用户密码(数字、字母、字符的组合且长度14位以上)
- 更改 SSH 默认的端口(22)为随机端口
- 禁止 root 身份的远程登陆
- 使用公钥认证机制进行远程登陆
- 使用 Linux 标准用户而不是 root 用户执行上述操作,并且该用户的权限可提升成 root 权限
现以 Ubuntu 系统为例,完成上述操作:
强制使用高强度用户密码
要强制用户使用高强度密码,需要安装额外的模块 libpam-cracklib
sudo apt-get install libpam-cracklib
在 Ubuntu 中,密码策略(规定密码的长度,字符等)定义在 /etc/pam.d/common-password
文件中,如果要规定,密码长度为 14,包含大小写字符数字和字符,在文件中,在 pam_unix.so
的前一行,添加:
password required pam_cracklib.so try_first_pass retry=3 minlen=14 lcredit=-1 ucredit=-1 dcredit=-1 ocredit=-1 difok=2 reject_username
上述配置的选项的描述如下,详情参考 libpam-cracklib 文档:
选项 | 描述 |
---|---|
retry=N | 设置密码时的,最大重试次数 |
minlen=N | 新密码的最小长度 |
lcredit=N | 最少小写字母数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1 |
ucredt=N | 最少大写字母数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1 |
dcredit=N | 最少数字的数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1 |
ocredit=N | 最少其它字符数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1 |
difok=N | 和旧密码不同的字符数 |
reject_username | 禁止用户名作为密码 |
总之规定各种字符类型的个数,要使用负数,其绝对值表示该类型字符至少有多少个。
更改 SSH 默认的端口(22)为随机端口
禁止 root 身份的远程登陆
修改 ssh-server 的配置文件:
sudo vim /etc/ssh/sshd_config
找到 # Port 22
更改为:
Port 2019
找到 #PermitRootLogin
,更改为:
PermitRootLogin no
远程登陆则需要指定该端口执行登陆,而且无法使用 root 用户登陆
sss username@ip-addr -p 2019
使用公钥认证机制进行远程登陆
SSH 登陆 提供公钥认证机制的登陆,即不需要密码的登陆方式,但是需要客户端生成公钥和私钥,并将公钥发送给服务端,服务端将公钥添加到相应用户的配置文件中。
首先客户端需要生成,密钥对:
ssh-keygen -t rsa -b 4096
使用默认文件名,一路 Enter 最后,生成的密钥对文件在 ~/.ssh/
目录下,其中 id_rsa
为私钥,id_rsa.pub
为公钥。
接着使用将公钥文件上传到服务器:
ssh-copy-id username@ip-addr -p portnum
该命令会把公钥文件的内容,写入到 /home/username/ssh/authorized_keys
文件中,所以也可以手动添加内容。如此一来就可以 username
的身份,不使用密码登陆服务器了。
ssh -p 2019 username@ip-addr
3.2 加固 Nginx
防止信息泄露
Nginx 默认开启 Server Token(显示版本号),这样使得 Nginx 的版本号很容易被获取,如下图为连接域名不存在资源时的返回页面,可以看到 Nginx 的版本号
在 /etc/nginx/nginx.conf
中 http 块中添加(去掉注释即可):
server_tokens off
关闭后,访问域名下不存在的资源,返回页面中没有了 Nginx 的版本号 信息。
增加访问控制策略
Nginx 可以使用 allow
和 deny
指令在配置文件中允许或禁止特定 IP 的访问, 编辑 Niginx 配置文件 /etc/nginx/conf.d/jaylen.com.conf
,只允许 192.168.47.129 192.168.47.130 访问 网站 www.jaylen.com
# server configuration
server {
listen 80;
listen [::]:80;
# IPs access control
allow 192.168.47.129;
allow 192.168.47.130;
deny all;
root /var/www/jaylen.com;
使用 TLS 加固 Nginx
TLS 可以加密客户端和服务端通信的数据,降低信息泄露的风险。对于本地网站可以使用 SSL 自签 证书 实现 HTTPS 连接。当然在公网中使用的网站,通常会使用 CA 认证的证书,要免费使用 SSL 证书,可参考:
How to Install Nginx with Let’s encrypt and get A+ from SSLLabs Test
要在 Nginx 配置自签证书,首先在配置目录下建一个文件夹,进到文件夹中:
sudo mkdir /etc/nginx/ssl/
cd /etc/nginx/ssl
生成密钥:
sudo openssl genrsa -aes256 -out nginx.key 1024
接着生成 CSR:
sudo openssl req -new -key nginx.key -out nginx.csr
Enter pass phrase for nginx.key:
Can't load /home/jaylen/.rnd into RNG
140067713049024:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/home/jaylen/.rnd
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Guangdong
Locality Name (eg, city) []:Guangzhou
Organization Name (eg, company) [Internet Widgits Pty Ltd]:IT
Organizational Unit Name (eg, section) []:IT
Common Name (e.g. server FQDN or YOUR name) []:www.jaylen.com
Email Address []:jaylen@gmail.com
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:admin
An optional company name []:IT
最后,签发证书:
sudo openssl x509 -req -days 365 -in nginx.csr -signkey nginx.key -out nginx.crt
成功签发:
ignkey nginx.key -out nginx.crt
Signature ok
subject=C = CN, ST = Guangdong, L = Guangzhou, O = IT, OU = IT, CN = www.jaylen.com, emailAddress = jaylen@gmail.com
Getting Private key
Enter pass phrase for nginx.key:
接着在 /etc/nginx/conf.d/jaylen.com.conf
中修改:
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
重新加载 Nginx 配置:
sudo nginx -s reload
提示输入之前步骤设置的密码,输入即可。
其它 加固措施,可仔细阅读:
Top 25 Nginx Web Server Best Security Practices
Nginx Web Server Security and Hardening Guide
3.3 加固 mysql
运行 mysql_secure_installation
工具(安装 mysql 后自带的 shell 脚本),进行 mysql 的安全检查,
根据提示,设置密码为最高级别,并为 root 用户设置密码,最后同意以下选项:
- Remove anonymous users? – 删除匿名用户
- Disallow root login remotely? – 禁止远程使用 root 用户登陆
- Remove test database and access to it? – 删除测试数据库和其访问权限
- Reload privilege tables now? – 重载授权表
3.4 加固 PHP
修改 php.ini
文件
当文件不存在时停止 PHP 处理
Nginx 对于 PHP 支持的配置文件中常常会使用如下形式的配置,该配置使得 PHP 解释器接受所有以 .php
结尾的 URI,这样一来就会存在很大的风险,存在任意代码执行漏洞,具体解释见参考资料 [6]。
修改 /etc/php/7.2/fpm/php.ini
文件(不同系统该文件位置略有不同),设置cgi.fix_pathinfo=0
,可以禁止 PHP 解释器查找文件系统中不存在的文件,使用sed 命令完成文件内容的修改:
sudo sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g' /etc/php/7.2/fpm/php.ini
禁用危险的 PHP 函数
在 php.ini
中添加:
disable_functions =exec,eval,phpinfo,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
限制文件上传功能
如果网站不需要文件上传功能,应该禁用。
file_uploads=Off
如果需要上传功能,则设置文件的大小,根据实际情况设置,如头像图片上传1M足矣。
file_uploads=On
upload_max_filesize=1M
设置 POST 方法传输数据的大小
POST 方法是当客户端需要向服务器发送数据时使用的,该方法可被用于对服务器进行 DoS 攻击等,所以需要将其能传输的数据大小设置成合理的数值,如果网站不需要上传文件等数据量大的操作,4KB也应该足够了。
post_max_size=1K
防止 PHP 信息泄露
expose_php = Off
限制 PHP 脚本的最长执行时间
# set in seconds
max_execution_time = 30 #最长执行时间 30 s
max_input_time = 30 #脚本解析输入最长时间30s
memory_limit = 40M #脚本最大使用内存40M
这样可以有效防止大规模的 DOS 攻击。
禁用未使用的 PHP 模块
查看已安装的 PHP 模块:
php -m
根据实际情况,禁用不使用模块,注释掉 php.ini
相应的配置行。
0x02 参考资料
[1] Install a LEMP Stack on Ubuntu 18.04
[2] Serve PHP with PHP-FPM and NGINX
[3] Nginx vs Apache
[4] Setting up an Nginx Reverse Proxy
[5] Getting Started with NGINX
[6] Passing Uncontrolled Requests to PHP
[7] Unknown column '' in 'field list'解决方案
[10] Use Public Key Authentication with SSH
[11] Top 25 Nginx Web Server Best Security Practices
[12] Nginx Web Server Security and Hardening Guide