从WordPress SQLi谈PHP格式化字符串问题

0x00 背 景

近日,WordPress爆出了一个SQLi漏洞,漏洞发生在WP的后台上传图片的位置,通过修改图片在数据库中的参数,以及利用php的 sprintf 函数的特性,在删除图片时,导致 ' 单引号的逃逸。漏洞利用较为困难,但思路非常值得学习。

0x01 漏 洞 分 析

漏洞发生在 wp-admin/upload.php 的157行,进入删除功能,

之后进入函数 wp_delete_attachment( $post_id_del ) ,$post_id_del 可控,而且没有做(int)格式转化处理。

wp_delete_attachment 位于 wp-includes\post.php 的 4863 行。其中

图片的post_id被带入查询,$wpdb->prepare 中使用了 sprintf,会做自动的类型转化,可以输入 22 payload ,会被转化为 22 ,因而可以绕过。

之后进入4898行的 delete_metadata( 'post', null, '_thumbnail_id', $post_id, true ); 函数。

delete_metadata函数位于 wp-includes\meta.php 的307行,

在这里代码拼接出了如下sql语句,meta_value为传入的media参数

SELECT meta_id FROM wp_postmeta WHERE meta_key = '_thumbnail_id' AND meta_value = 'payload'

之后这条语句会进入查询,结果为真代码才能继续,所以要修改_thumbnail_id对应的meta_value的值为payload,保证有查询结果。

因此,我们需要上传一张图片,并在写文章中设置为特色图片

在数据库的wp_postmeta表中可以看到, _thumbnail_id 即是特色图片设定的值,对应的meta_value即图片的post_id

原文通过一个 WP<4.7.5 版本的xmlrpc漏洞修改 _thumbnail_id 对应meta_value的值,或通过插件 importer 修改。这里直接在数据库里修改,修改为我们的payload。

之后在365行,此处便是漏洞的核心,问题在于代码使用了两次 sprintf 拼接语句,导致可控的payload进入了第二次的 sprintf 。输入payload为 22 %1$%s hello

代码会拼接出sql语句,带入$wpdb->prepare

SELECT post_id FROM wp_postmeta WHERE meta_key = '%s' AND meta_value = '22 %1$%s hello'

进入$wpdb->prepare后,代码会将所有 %s 转化为 '%s' ,即 meta_value = '22 %1$'%s' hello'

因为sprintf的问题 (vsprintf与sprintf类似) , '%s' 的前一个 ' 会被吃掉, %1$'%s 被格式化为 _thumbnail_id ,最后格式化字符串出来的语句会变成

单引号成功逃逸!

最后payload为

http://localhost/wp-admin/upload.php?action=delete&media[]=22%20%251%24%25s%20hello&_wpnonce=bbba5b9cd3

这个SQL注入不会报错,只能使用延时注入,而且需要后台的上传权限,所以利用起来比较困难。

0x02 漏 洞 原 理

上述WordPress的SQLi的核心问题在于在 sprintf 中, '%s' 的前一个 ' 被吃掉了,这里利用了 sprintf 的 padding 功能

单引号后的一个字符会作为padding填充字符串。

此外, sprintf 函数可以使用下面这种写法

%后的数字代表第几个参数,$后代表类型。

所以,payload %1$'%s' 中的 '% 被视为使用 % 进行 padding,导致了 ' 的逃逸。

0x03 php 格 式 化 字 符 串

但在测试过程中,还发现其他问题。php的 sprintf 或 vsprintf 函数对格式化的字符类型没做检查。

如下代码是可以执行的,显然php格式化字符串中并不存在 %y 类型,但php不会报错,也不会输出 %y ,而是输出为空

<?php $query = "%y"; $args = 'b'; echo sprintf( $query, $args ) ; ?>

通过fuzz得知,在php的格式化字符串中,%后的一个字符(除了 '%' )会被当作字符类型,而被吃掉,单引号 ' ,斜杠 \ 也不例外。

如果能提前将 %' and 1=1# 拼接入sql语句,若存在SQLi过滤,单引号会被转义成 \'

select * from user where username = '%\' and 1=1#';

然后这句sql语句如果继续进入格式化字符串, \ 会被 % 吃掉, ' 成功逃逸

<?php $sql = "select * from user where username = '%\' and 1=1#';"; $args = "admin"; echo sprintf( $sql, $args ) ; //result: select * from user where username = '' and 1=1#' ?>

不过这样容易遇到 PHP Warning: sprintf(): Too few arguments 的报错。

还可以使用 %1$ 吃掉后面的斜杠,而不引起报错。

<?php $sql = "select * from user where username = '%1$\' and 1=1#' and password='%s';"; $args = "admin"; echo sprintf( $sql, $args) ; //result: select * from user where username = '' and 1=1#' and password='admin'; ?>

通过翻阅php的源码,在 ext/standard/formatted_print.c 的642行

可以发现php的 sprintf 是使用switch..case..实现,对于未知的类型 default ,php未做任何处理,直接跳过,所以导致了这个问题。

在高级php代码审核技术中的5.3.5中,提及过使用 $order_sn=substr($_GET["order_sn"], 1) 截断吃掉 \ 或 " 。

之前也有过利用iconv转化字符编码, iconv('utf-8', 'gbk', $_GET['word']) 因为utf-8和gbk的长度不同而吃掉 \ 。

几者的问题同样出现在字符串的处理,可以导致'的转义失败或其他问题,可以想到其他字符串处理函数可能存在类似的问题,值得去继续发掘。

0x04 利 用 条 件

1. 执行语句使用 sprintf 或 vsrptinf 进行拼接

2. 执行语句进行了两次拼接,第一次拼接的参数内容可控,类似如下代码

<?php $input = addslashes("%1$' and 1=1#"); $b = sprintf("AND b='%s'", $input); ... $sql = sprintf("SELECT * FROM t WHERE a='%s' $b", 'admin'); echo $sql; //result: SELECT * FROM t WHERE a='admin' AND b=' ' and 1=1#'

0x05 总 结

此次漏洞的核心还是 sprintf 的问题,同一语句的两次拼接,意味着可控的内容被带进了格式化字符串,又因为 sprintf 函数的处理问题,最终导致漏洞的发生。

此问题可能仍会出现在WordPress的插件,原文的评论中也有人提到曾在Joomla中发现过类似的问题。而其他使用 sprintf 进行字符串拼接的cms,同样可能因此导致SQL注入和代码执行等漏洞。

相 关 链 接

[1] https://medium.com/websec/wordpress-sqli-bbb2afcc8e94

[2] https://medium.com/websec/wordpress-sqli-poc-f1827c20bf8e

[3] http://php.net/manual/zh/function.sprintf.php

[4] https://github.com/php/php-src/blob/c8aa6f3a9a3d2c114d0c5e0c9fdd0a465dbb54a5/ext/standard/formatted_print.c

[5] https://www.seebug.org/vuldb/ssvid-96376

知道创宇404实验室出品

往 期 热 门

原文发布于微信公众号 - Seebug漏洞平台(seebug_org)

原文发表时间:2017-09-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏猿人谷

mybatis动态调用表名和字段名

  一直在使用Mybatis这个ORM框架,都是使用mybatis里的一些常用功能。今天在项目开发中有个业务是需要限制各个用户对某些表里的字段查询以及某些字段...

3076
来自专栏Java学习123

MyEclipse10.7安装jad反编译插件

3097
来自专栏GreenLeaves

存储过程详解

存储过程简介 什么是存储过程:存储过程可以说是一个记录集吧,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表...

21610
来自专栏哲学驱动设计

性能优化总结(三):聚合SQL在GIX4中的应用

本节主要介绍,在GIX4系统中,如何应用上篇讲的方案来改善性能,如果与现有的系统环境集成在一起。大致包含以下内容: SQL的生成 映射-数据读取方案 工厂方法-...

2076
来自专栏Java面试通关手册

深入理解单例模式

Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):https://github.com/Snailclimb/Java_G...

1876
来自专栏MasiMaro 的技术博文

OLEDB存取BLOB型数据

现代数据库系统除了支持一些标准的通用数据类型以外,大多数还支持一种称之为BLOB型的数据。 BLOB全称为big large object bytes, 大二...

1283
来自专栏跟着阿笨一起玩NET

C#设计模式学习笔记-单例模式

  最近在学设计模式,学到创建型模式的时候,碰到单例模式(或叫单件模式),现在整理一下笔记。

882
来自专栏黑泽君的专栏

day29_Hibernate学习笔记_01

  Hibernate:是一个数据持久化层的ORM框架。   Object:对象,java对象,此处特指JavaBean。   Relational:关系,二维...

702
来自专栏逸鹏说道

C# 温故而知新:Stream篇(四)下

上面的例子是将一个文件作为整体进行操作,这样会带来一个问题,当文件很大或者网络不是很稳定的时候会发生意想不到的错误 那我们该怎么解决...

3365
来自专栏Java帮帮-微信公众号-技术文章全总结

高级教程-springData-JPA第一天【悟空教程】

ORM(Object-Relational Mapping) 表示对象关系映射。在面向对象的软件开发中,通过 ORM,就可以把对象映射到关系型数据库中。只要有一...

3363

扫码关注云+社区

领取腾讯云代金券