前言
前段时间参加了DownunderCTF,有一道题目做了很久,最终也没有解决,赛后看了wp,发现这个题目很有意思,中间涉及了vsprintf的一些技巧,特意写出来分享一下。
正文
我们直接来看下这个题目smooth-jazz
smooth-jazz
题目代码如下:
<?php
function mysql_fquery($mysqli, $query, $params) {
return mysqli_query($mysqli, vsprintf($query, $params));
}
if (isset($_POST['username']) && isset($_POST['password'])) {
$mysqli = mysqli_connect('db', 'challuser', 'challpass', 'challenge');
$username = strtr($_POST['username'], ['"' => '\\"', '\\' => '\\\\']);
$password = sha1($_POST['password']);
$res = mysql_fquery($mysqli, 'SELECT * FROM users WHERE username = "%s"', [$username]);
if (!mysqli_fetch_assoc($res)) {
$message = "Username not found.";
goto fail;
}
$res = mysql_fquery($mysqli, 'SELECT * FROM users WHERE username = "'.$username.'" AND password = "%s"', [$password]);
if (!mysqli_fetch_assoc($res)) {
$message = "Invalid password.";
goto fail;
}
$htmlsafe_username = htmlspecialchars($username, ENT_COMPAT | ENT_SUBSTITUTE);
$greeting = $username === "admin"
? "Hello $htmlsafe_username, the server time is %s and the flag is %s"
: "Hello $htmlsafe_username, the server time is %s";
$message = vsprintf($greeting, [date('Y-m-d H:i:s'), getenv('FLAG')]);
fail:
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Smooth Jazz</title>
<style>
body {
background-color: #f8f8f8;
font-family: Arial, sans-serif;
}
.container {
max-width: 400px;
margin: 100px auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
text-align: center;
}
h1 {
color: #333;
}
form {
margin-top: 20px;
}
label, input {
display: block;
margin-bottom: 10px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background-color: #4287f5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.music-player {
margin-top: 20px;
}
h2 {
color: #333;
}
audio {
width: 100%;
margin-top: 10px;
}
.message {
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>Smooth Jazz</h1>
<form method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" placeholder="Enter your username">
<label for="password">Password:</label>
<input type="password" id="password" name="password" placeholder="Enter your password">
<input type="submit" value="Login">
</form>
<div class="music-player">
<audio src="/offering-larry-stephens.mp3" id="audio"></audio>
If you are stuck, you can <a href="javascript:document.getElementById('audio').play()">listen to some smooth jazz</a>.
</div>
<div id="message" class="message">
<p><?= $message ?? '' ?></p>
</div>
</div>
</body>
</html>
先说下整体逻辑,
1、先判断用户名是否存在
2、sql查询对应用户名、密码是否存在在数据库
3、最后$username还要强等于(===)admin 才能拿到flag
其实一开始我以为是宽字节注入呢,因为我构造admin%df'进入发现可以绕过第一步,但是怎么都无法注入,后续查看数据库编码也没有gbk的编码形式。其实这里涉及的是另一个考点:
UTF截断:UTF截断会截断无效内容,比如 admin和admin%ff 应该是一样的。
所以我这里可以通过这种方法绕过第一步,但这里不是宽字节注入。而且后续还要强等于admin,这个===印象中在非特殊环境下是不可绕过的。而且根据代码来看,注入点应该在第二个查询语句中。
现在好像是无解的,但是代码最开头的地方有一个vsprintf,比赛中我也忽略了,我以为考点在下面,这里的vsprintf 根据格式字符串中的格式指示符,常见用法就是:
# %[argnum$][flags][width][.precision]specifier.
# 必须参数就是%specifier 中间都是可选参数
#一些用法
# %表示要被格式的参数 后面跟类型 %s就是字符串
$string = vsprintf('Hello, %s! Today is %s.', ['John', 'Monday']);
# %1$s 表示第一个字符要被格式成字符串,%2$d 表示第二个字符要被格式成数字。
$result = vsprintf('Name: %1$s, Age: %2$d', ['John', 25]);
# %1$'a10 表示单引号后的第一个字符要填充 填充10次。然后拼接后面的3
$result = vsprintf("Like %1\$'a10s", ["3"]);
当然还是建议去看下官方文档,详细了解所有用法:
https://www.php.net/manual/zh/function.vsprintf.php
如果我们插入一个%1$c 然后把password 的 sha1 生成一个34开头的字符串(char类型只拿前面的一个char格式化字符串):
那么我们就格式化字符串的时候就拿到一个双引号,也就是我们可以闭合了。
那我们的注入payload就可以是
username=admin%ff%1$c||1#&password=668
\xff 是为了utf8截断从而绕过sql查询部分的admin的判断,截断后面的东西都不会被和admin比较了
但是还是有一个问题,怎么突破==='admin'强等判断打印flag呢?
$htmlsafe_username = htmlspecialchars($username, ENT_COMPAT | ENT_SUBSTITUTE);
$greeting = $username === "admin"
? "Hello $htmlsafe_username, the server time is %s and the flag is %s"
: "Hello $htmlsafe_username, the server time is %s";
$message = vsprintf($greeting, [date('Y-m-d H:i:s'), getenv('FLAG')]);
这里仍然利用vsprintf,我们既然绕不过==="admin" 能不能在格式化字符串的时候再创建一个变量放置位把getenv('FLAG') 打印出来?
如果我们直接插入%2$s 那么在sql注入格式化字符串的时候就会报错,因为那里的参数只有一个[password],我们需要的是成功执行sql注入并在最后一个vsprintf中能拿到类似%2$s的字段来把flag格式化写进去,那么我们需要第二个变量位置它在拼接sql语句时不进行格式化,在最后一个vsprintf处把flag格式化进去:
这步的关键在于htmlspecialchars
$htmlsafe_username = htmlspecialchars($username, ENT_COMPAT | ENT_SUBSTITUTE);
//这行代码将 $username 变量的值进行 HTML 转义,并将转义后的结果赋给 $htmlsafe_username 变量。
//第一个参数 $username 是需要进行转义的字符串,它的值被传递给 htmlspecialchars() 函数进行处理。
//第二个参数 ENT_COMPAT | ENT_SUBSTITUTE 是转义模式,用于指定转义的规则。
//ENT_COMPAT 表示只转义双引号,将双引号转换为 "。其他特殊字符不转义。
//ENT_SUBSTITUTE 表示将无法转义的字符用 Unicode 替代符号替代,而不是忽略或删除它们。
利用 > 被编码为&gt 我们可以构造 %1$'>%2$s
$'是什么意思我们可以看官方手册:
也就是上面的字符串要填充 > 但是没写数量所以填充0个就是空了,%1$'>在vsprintf处理时就会变成空,只剩下%2$s
sql语句变成:
SELECT * FROM users WHERE username = "admin"||1#%2$s" AND password = "34c66477519b949b09b45e131347c17b5822a30a"SELECT * FROM users WHERE username = "admin"||1#%2$s" AND password =
这里解释下为啥%1$'>%2$s 不是按照我们理解的应该两个参数都格式化进去,关键还是文档。
%[argnum$][flags][width][.precision]specifier
.
官方文档说明这个函数的使用方式如下,其中%和specifier是必须的 也就是说%s是极简模式。我们使用%1$'< 时 有没有发现这个格式的specifier应该是什么?
没有,对没有specifier,所以%1$'>%2$s 就把% 当作specifier了 这个%也就逃逸出来了:
其实输出%需要%%的转义和这个原理几乎一摸一样。
后续username需要传入htmlspecialchars($username):
admin%1$c||1#%1$'>%2$s
此时拼接完的参数是:
$greeting = Hello admin%1$c||1#%1$'>%2$s, the server time is %s
$message = vsprintf($greeting, [date('Y-m-d H:i:s'), getenv('FLAG')]);
1、%1$c 被一个字符填充
2、%1$'&g 被 2023 填充,因为&后面没有数字,所以没有使用&填充,后面specifier标志为g表示通用格式。可以看官方手册了解详情
3、%2$s 被真正的flag填充 最后的%s第一个时间字符串填充
最终我们拿到flag
扩展
由于vsprintf函数格式化字符串过程中%specifier是必须的
所以经常被用来逃逸单引号使用,比如在sql语句中经常有需要闭合的' 而代码转义了' 导致我们无法闭合,那么我们可以构造:
%1$'or(1=1)#
经过转义变为
%1$\'or(1=1)#
而经过vsprintf时,单引号就逃逸出来了
因为specifier必须,所以斜杠被当作类型吃掉了,单引号自然就出来了。此技巧也可以去逃逸其他字符出来,在sql注入中很好用。
其实php的这个vsprintf函数和c语言中的vsprintf几乎一致,以后pwn中遇到这个字符串格式化漏洞也可以和web漏洞技巧互相参考。