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

作者:SeaFood@知道创宇404实验室

发表时间:2017年9月8日

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'的前一个'被吃掉了,这里利用了sprintfpadding功能

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

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

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

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

0x03 php格式化字符串

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

如下代码是可以执行的,显然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. 执行语句使用sprintfvsrptinf进行拼接
  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注入和代码执行等漏洞。

0x06 参考链接

https://medium.com/websec/wordpress-sqli-bbb2afcc8e94

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

http://php.net/manual/zh/function.sprintf.php

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

https://www.seebug.org/vuldb/ssvid-96376

------------------------------------2017.11.01 更新------------------------------------

0x07 WordPress 4.8.2补丁问题

国外安全研究人员Anthony Ferrara给出了另一种此漏洞的利用方式,并指出了WordPress 4.8.2补丁存在的问题。

如下代码

<?php

$input1 = '%1$c) OR 1 = 1 /*';
$input2 = 39;
$sql = "SELECT * FROM foo WHERE bar IN ('$input1') AND baz = %s";
$sql = sprintf($sql, $input2);
echo $sql;
//result: SELECT * FROM foo WHERE bar IN ('') OR 1 = 1 /*') AND baz = 39

%c起到了类似chr()的效果,将数字39转化为',从而导致了sql注入。

对此,WordPress 4.8.2补丁在WPDB::prepare()中加入

$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query );

从而,禁用了除%d%s%F之外的格式,这种方法导致了三个问题。

1.大量开发者在开发过程中使用了例如%1$s的格式,此次补丁导致代码出错。

2.在例如以下代码中

   $db->prepare("SELECT * FROM foo WHERE name= '%4s' AND user_id = %d", $_GET['name'], get_current_user_id());

%4s会被替换成%%4s%%在sprintf中代表字符%,没有格式化功能。所以,$_GET['name']会被写到%d处,攻击者可以控制user id,可能导致越权问题的出现。

3.补丁可以被绕过

meta.php的漏洞处

   if ( $delete_all ) {
     $value_clause = '';
     if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) {
       $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value );
     }
     $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) );
   }

如果输入

   $meta_value = ' %s ';
   $meta_key = ['dump', ' OR 1=1 /*'];

之后两次进入prepare(),因为

   $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); 

使得%s变为''%s''

最后结果

   SELECT type FROM table WHERE meta_key = 'dump' AND meta_value = '' OR 1=1 /*''

WordPress也承认这是一个错误的修复

在WordPress 4.8.3的补丁中,一是修改了meta.php中两次使用prepare()的问题,二是使用随机生成的占位符替换%,在进入数据库前再替换回来。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java技术

面试中有哪些经典的数据库问题?

1、如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择主键作为聚集索引、如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯...

721
来自专栏生信技能树

构建shell脚本一文就够

非常多的朋友在看我们公众号过往转录组,WES,等流程分享的时候发现很难理解我们的代码,其实就是缺乏shell脚本知识,那么这篇教程你就不容错过。 内容 使用多个...

3034
来自专栏绿巨人专栏

设计模式大集合

3499
来自专栏数据小魔方

左手用R右手Python系列——循环中的错误异常规避

上一讲讲了R语言与Pyhton中的异常捕获与错误处理基本知识,今天以一个小案例来进行实战演练,让你的程序遇水搭桥,畅通无阻。 本案例目标网址,今日头条的头条指数...

3236
来自专栏python3

python3--文件操作

rb,只读,以bytes类型读取(非文字类的文件时,用rb,比如图片,音频文件等)

762
来自专栏一个会写诗的程序员的博客

TypeScript 之父简介:TS Anders Hejlsberg: Introducing TypeScript参考资料TypeScript入门指南(JavaScript的超集)

https://channel9.msdn.com/posts/Anders-Hejlsberg-Introducing-TypeScript

742
来自专栏瓜大三哥

UVM(十二)之各register model

UVM(十二)之各register model 1. register model的必要性 考虑一个问题,当验证平台向DUT发了某些激励后,我们期望DUT中的某...

22010
来自专栏草根专栏

使用 Moq 测试.NET Core 应用 -- Mock 行为

第一篇文章, 关于Mock的概念介绍:https://cloud.tencent.com/developer/article/1172536

562
来自专栏Golang语言社区

【Go 语言社区】epoll详解

什么是epoll epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的...

41912
来自专栏PHP技术

史上最好用的免费翻蔷利器

awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件逐行的读入,以空格为...

34611

扫码关注云+社区