三种典型的软件漏洞攻击
如果网站应用对用户的输入数据不加以进一步验证,就有可能遭受攻击。我们将在一下部分看到这一点。
网页应用程序一般运行于远端的虚拟机上,攻击的过程一般是一个远程攻击。
- 命令行注入 Command injection
- 跨站脚本注入 Cross Site Scripting
- SQL数据库语句注入 SQL Injection
实验需要启动一个虚拟机,并在上面安装python 3。所有的网络应用将使用Flask包进行实现,这个包允许我们使用最小的代价实现一个网页应用[1]。
1 : 命令行注入 Command injection
命令行注入攻击通常是由于对输入命令的验证不足导致。这可能会导致计算机执行额外的命令,让攻击者访问未经允许的内容。
首先使用
$ ifconfig
输出:
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
检查自己的loopback地址---->127.0.0.1
Ps:127.0.0.1,通常被称为本地回环地址(Loopback Address),不属于任何一个有类别地址类。它代表设备的本地虚拟接口,所以默认被看作是永远不会宕掉的接口。
写一个python文件,放到当前目录下:
# cmd_injection.py
import os
import subprocess
import argparse
# https://docs.python.org/3/library/argparse.html
def test_reachability(address, safe=False):
if safe:
out = subprocess.Popen(['ping', '-c', '1', address])
else:
cmd = 'ping -c 1 ' + address
os.system(cmd)
if __name__ == '__main__':
# Add command line arguments
parser = argparse.ArgumentParser()
#这里的-s表示安全模式的缩写 写成-sssss都没关系
#这里的--safe表示的是安全模式的全称
#调用的时候写-s 和 --safe效果一致,都是将输入的safe变量设置为True
parser.add_argument('-s', '--safe', action = 'store_true', help = "select safe scan")
#这里的--address required = True意思是一定要输入一个ip地址存放到args.address中
parser.add_argument('--address', required = True, help = "目标ipv4地址")
# 从parser中取出命令对象到args中
args = parser.parse_args()
# Execute the command
print("测试连通性,目标地址: ", args.address)
test_reachability(address = args.address, safe = args.safe)
测试在正常状态下使用该文件
$ python3 cmd_injection.py --address 127.0.0.1
args.address:127.0.0.1 args.safe: False
Testing reachability to 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.042 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.042/0.042/0.042/0.000 ms
使用恶意的输入内容运行该文件:
python3 cmd_injection.py --address "127.0.0.1; ls -l; ip route"
此时输出了路由表文件
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.029/0.029/0.029/0.000 ms
total 28
-rw-rw-r-- 1 student student 133 Nov 19 19:30 acl.csv
-rw-r--r-- 1 student student 8192 Mar 15 19:30 database.db
drwxrwxr-x 10 student student 4096 Oct 4 15:45 flowmanager
drwxrwxr-x 2 student student 4096 Mar 15 17:38 python_file
drwxrwxr-x 3 student student 4096 Nov 19 18:23 sdn
drwxrwxr-x 2 student student 4096 Mar 16 14:53 sec
default via 10.0.104.1 dev ens160 proto static
10.0.104.0/24 dev ens160 proto kernel scope link src 10.0.104.190
上述命令实际上执行了ls -l; ip route,即用户不应当有权限访问的命令。
如果加上安全操作选项 --safe,此时输出的内容则不带有敏感信息。
student@sec-lab:~$ python3 sec/cmd_injection.py --address 127.0.0.1 -s
args.address:127.0.0.1 args.safe: True
Testing reachability to 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.055 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.055/0.055/0.055/0.000 ms
subprocess 相对于 os.system(cmd)的安全性
使用subprocess可以让应用更加安全[3] [4]。使用subprocess可以检验你的输入从而防止执行恶意代码。
subprocess.run()、subprocess.call()、subprocess.check_call()和subprocess.check_output()都是通过对subprocess.Popen的封装来实现的高级函数,因此如果我们需要更复杂功能时,可以通过subprocess.Popen来完成。
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False,
startup_info=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())
-
args: 要执行的shell命令,可以是字符串,也可以是命令各个参数组成的序列。当该参数的值是一个字符串时,该命令的解释过程是与平台相关的,因此通常建议将args参数作为一个序列传递。
-
bufsize: 指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。
-
stdin, stdout, stderr: 分别表示程序标准输入、输出、错误句柄。
-
preexec_fn: 用于指定一个将在子进程运行之前被调用的可执行对象,只在Unix平台下有效。
-
close_fds: 如果该参数的值为True,则除了0,1和2之外的所有文件描述符都将会在子进程执行之前被关闭。
-
shell: 该参数用于标识是否使用shell作为要执行的程序,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。
-
cwd: 如果该参数值不是None,则该函数将会在执行这个子进程之前改变当前工作目录。
-
env: 用于指定子进程的环境变量,如果env=None,那么子进程的环境变量将从父进程中继承。如果env!=None,它的值必须是一个映射对象。
-
universal_newlines: 如果该参数值为True,则该文件对象的stdin,stdout和stderr将会作为文本流被打开,否则他们将会被作为二进制流被打开。
-
startupinfo和creationflags: 这两个参数只在Windows下有效,它们将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如主窗口的外观,进程优先级等。
出于显而易见的原因,使用shell执行命令的任何操作都是不安全的(您不希望有人在shell中运行rm -rf /
:)。 os.system
和os.popen
都使用外壳程序。
为了安全起见,请将subprocess
模块与shell = False
一起使用
2: 跨站脚本注入 Cross Site Scripting —Cross-Site Scripting (XSS)
这种漏洞通常出现于需要输入信息的地方。漏洞产生的原因通常是未正確的對使用者輸入資料作驗證與過濾,而允許攻擊者將程式注入至網站中,使得使用者連至網站時載入被植入的惡意代碼,而讓瀏覽器自動執行,後果可能導致使用者資料暴露或是下載其他惡意代碼[5]。
https://josephjsf2.github.io/security/2019/09/19/cross-site-scripting.html
主要的分类有:
-
- Stored XSS attacks 未正常過濾使用者所輸入的資料,導致攻擊的script被儲存至server端造成後續的攻擊
-
- Reflected XSS attacks 與Stored XSS不同, Reflexted XSS不需要儲存於Server端。主要發生原因是Server端未正確過濾前端傳入內容,在未適當過濾使用者輸入資料後,直接將使用者輸入之資料寫回response中導致的漏洞。
-
- DOM based Xss 所以與前兩項主要的差異在於,Stored XSS與Reflected XSS是Server端未正常處理資料,將惡意代碼一同返回Response中而導致使用者瀏覽器運行惡意代碼。
以下部分是两个关于Reflected XSS attacks和Stored XSS attacks的例子:
2.1 Reflected XSS Attacks
Reflected XSS Attacks是指注入的脚本在Web服务器上反映出来的攻击,例如在错误消息,搜索结果或任何其他响应中,这些响应包括作为请求一部分发送给服务器的部分或全部输入。 反映出来的攻击是通过另一种途径(例如,通过电子邮件或其他网站)传递给受害者的。 当用户被诱骗点击恶意链接,提交特制表格或什至只是浏览到恶意网站时,注入的代码将传播到易受攻击的网站,这会将攻击反映回用户的浏览器。 浏览器然后执行代码,因为它来自“受信任”服务器。 反射XSS有时也称为非持久XSS型或II型XSS。
下面的示例显示了一种安全和不安全的方式来将给定名称呈现回页面。 在不安全的输入中,first_name不会被转义,从而使页面容易受到跨站点脚本攻击的攻击。 安全输入转义了first_name(删除了非法字符),因此它不受跨站点脚本攻击的影响。
###xss_reflect.py
#导入用于轻量化构建网站的包
from flask import Flask, request, make_response, escape, render_template
app = Flask(__name__)
#网页的html代码部分
html = """
<form name="form" action="/form" method="post" onSubmit="return false">
<label for="fname">Name: </label>
<input type="text" id="fname" name="name">
<input type="button" name="button" value="Submit"
onClick="document.form.submit()">
<input type="checkbox" name="safe" {}>
<label for="ch1"> Make Safe</label>
</form>
<p>Your Name is: {}</p>
<script>document.cookie = "username=John Doe";</script>
"""
@app.route('/')
def default():
return make_response(html.format('',''))
@app.route('/form', methods=['GET', 'POST'])
def form():
if request.method == 'POST':
user_name = request.form.get('name', '')
if not request.form.getlist('safe'):
return make_response(html.format('', user_name))
else:
return make_response(html.format('checked', escape(user_name)))
else:
return default()
if __name__ == '__main__':
app.run(host="0.0.0.0", debug=True)
运行后使用本地浏览器打开http://:5000
在name一栏输入以下恶意代码:
<button onClick="location.href='www.baidu.com'">Click Me</button>
<b onmouseover=alert('Oops!')>click me!</b>
<img src=https://new.bobon900.com/wq/20190510074110_80392.png onerror=alert(document.cookie);>
<script>alert(document.cookie)</script>
提交后会看到不可描述的图片。点击click会转跳到不明网站。
3: SQL数据库语句注入 SQL Injection
SQL注入可以获取未授权访问的一些信息
#SQL injection
import os
import argparse
import sqlite3 as sql
# Database path
DATABASE = os.path.join(os. getcwd(), 'database.db')
# Data to populate the database
USERS = [("A_people", 12000, "Tony"), ("B_people", 57000, "Tony"),
("C_people", 56000, "Bob"), ("D_people", 57000, "Bob"),
("E_people", 78000, "Jenny"), ("F_people", 87000, "Jenny")]
def get_salaries(manager, safe=False):
"""
获取一个主管下所有人的收入
"""
con = sql.connect(DATABASE)
cur = con.cursor()
if safe:
# 使用参数化请求来获取信息是更为安全的方式
cur.execute("SELECT * FROM employees WHERE manager = (?)", (manager, ))
else:
# 使用字符串拼接构造命令,这是一种不安全的方式
statement = "SELECT * FROM employees WHERE manager = '" + manager +"';"
print("SQL statement executed:\n", statement)
cur.execute(statement)
row = cur.fetchall()
con.close()
# 返回雇员信息
return [{"Employee": r[0], "Salary": r[1]} for r in row]
def create_db():
"""
创建雇员信息数据库
"""
con = sql.connect(DATABASE)
# 创建数据表 指明表中的项目
con.execute("CREATE TABLE IF NOT EXISTS employees\
(employee varchar(30), salary real, manager varchar(30));")
# 插入用户
cur = con.cursor()
cur.execute("SELECT * FROM employees;")
if not cur.fetchone():
con.executemany("INSERT INTO employees (employee, salary, manager) \
VALUES (?,?,?);", USERS)
con.commit()
con.close()
# 如果仅仅执行当前文件,执行以下语句
if __name__ == '__main__':
# Create a database if it does not exist.
create_db()
# 接收命令行输入
parser = argparse.ArgumentParser()
# 全称为safe,缩写为s的变量,复制为True9(如果命令中有--safe或者-s的话)
parser.add_argument('-s', '--safe', action = 'store_true', help = "select safe query")
parser.add_argument('--name', required = True, help = "manager's name")
#将这些属性放进args对象中,可以用args.name, args.safe的方式访问这些变量的值
args = parser.parse_args()
# Query the database
report = get_salaries(args.name, args.safe)
print("Output:\n", report)
例如,执行python3 sql_injection_test.py --name Bob
会正常返回主管为Bob的人的所有手下。
$ python3 sql_injection_test.py --name Bob
SQL statement executed:
SELECT * FROM employees WHERE manager = 'Bob';
Output:
[{'Employee': 'C_people', 'Salary': 56000.0}, {'Employee': 'D_people', 'Salary': 57000.0}]
如果执行以下恶意代码,则返回了所有雇员的信息[6].
因为此时执行的sql请求是SELECT * FROM employees WHERE manager = ''OR 1=1;
–代表注释掉后面的内容。
since OR 1=1 is always TRUE.
https://www.w3schools.com/sql/sql_injection.asp
$ python3 sql_injection_test.py --name "'OR 1=1; --"
SQL statement executed:
SELECT * FROM employees WHERE manager = ''OR 1=1; --';
Output:
[{'Employee': 'A_people', 'Salary': 12000.0}, {'Employee': 'B_people', 'Salary': 57000.0}, {'Employee': 'C_people', 'Salary': 56000.0}, {'Employee': 'D_people', 'Salary': 57000.0}, {'Employee': 'E_people', 'Salary': 78000.0}, {'Employee': 'F_people', 'Salary': 87000.0}]
References
[1] https://flask.palletsprojects.com/en/1.1.x/installation/#python-version
[2] https://baike.baidu.com/item/%E6%9C%AC%E5%9C%B0%E5%9B%9E%E7%8E%AF%E5%9C%B0%E5%9D%80
[3] https://stackoverflow.com/questions/44730935/advantages-of-subprocess-over-os-system
[4] https://www.cnblogs.com/yyds/p/7288916.html
[5] https://josephjsf2.github.io/security/2019/09/19/cross-site-scripting.html
[6] https://www.w3schools.com/sql/sql_injection.asp