freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

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

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

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

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

前言

前段时间参加了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
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 The die so alive 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
The die so alive LV.2
这家伙太懒了,还未填写个人描述!
  • 2 文章数
  • 1 关注者
原创投稿-python开发中执行速度的技术路线选择
2023-10-11
文章目录