freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

一道ctf题目走进vsprintf漏洞利用技巧
2023-09-08 14:45:01

前言

前段时间参加了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格式化字符串):
image

那么我们就格式化字符串的时候就拿到一个双引号,也就是我们可以闭合了。

那我们的注入payload就可以是

username=admin%ff%1$c||1#&password=668

\xff 是为了utf8截断从而绕过sql查询部分的admin的判断,截断后面的东西都不会被和admin比较了
image

但是还是有一个问题,怎么突破==='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

$'是什么意思我们可以看官方手册:

image

也就是上面的字符串要填充 > 但是没写数量所以填充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了 这个%也就逃逸出来了:
image

其实输出%需要%%的转义和这个原理几乎一摸一样。

后续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
image

扩展

由于vsprintf函数格式化字符串过程中%specifier是必须的
所以经常被用来逃逸单引号使用,比如在sql语句中经常有需要闭合的' 而代码转义了' 导致我们无法闭合,那么我们可以构造:

%1$'or(1=1)#

经过转义变为

%1$\'or(1=1)#

而经过vsprintf时,单引号就逃逸出来了
image

因为specifier必须,所以斜杠被当作类型吃掉了,单引号自然就出来了。此技巧也可以去逃逸其他字符出来,在sql注入中很好用。

其实php的这个vsprintf函数和c语言中的vsprintf几乎一致,以后pwn中遇到这个字符串格式化漏洞也可以和web漏洞技巧互相参考。

# web安全 # 系统安全 # CTF
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录