背景
ZenPhoto 是一款小巧的相册软件,具备 RSS 输出、FTP 上传方式、Tag、评论回复等功能。基于 PHP+MySQL 环境,它的主旨是简单,这也就是说不管是用户还是网站管理员,都可以很容易的使用它。
关于二阶注入,上一篇文章有写过Second Order SQL Injection
漏洞复现
访问
http://localhost:8888/zenphoto-zenphoto-1.4.8/zp-core/admin-options.php?saved&tab=gallery
找到Sort gallery by字段,设置为Custom,并在custom filed字段后面输入id,extractvalue(0x0a,concat(0x0a,(select version())))%23,最后点Apply

访问
http://localhost:8888/zenphoto-zenphoto-1.4.8/zp-core/admin-upload.php?page=upload&tab=http&type=images
如果服务端开启了报错,那么就能在这个页面找到报错消息。当然一般线上服务我们都会关闭掉报错,这也没关系,继续访问
http://localhost:8888/zenphoto-zenphoto-1.4.8/zp-core/admin-logs.php?page=logs
就能看到报错日志了,其中就抛出了数据库版本号

审计
那么在点Apply的时候,发生了什么呢?我拦下了数据包
|
|
定位到/zenphoto-zenphoto-1.4.8/zp-core/admin-options.php文件,找到savegalleryoptions相关代码
|
|
定位到gallery_sorttype参数, $st = strtolower(sanitize($_POST['gallery_sorttype'], 3));,留意到这里对$_POST进来的值传到了sanitize函数。该函数的在文件/zenphoto-zenphoto-1.4.8/zp-core/functions-common.php
|
|
这个函数对传入$input_string先判断是否是数组,如果是,就遍历数组,过滤每个值。如果不是,就直接将$input_string丢入sanitize_string函数去做过滤,最后返回过滤后的值。其实这里有点不严谨,因为如果传入是数组的话,仅仅对value做了过滤,而key是原样保留的,会有安全隐患。我简单改造了下这个函数
|
|
继续在该文件里跟踪sanitize_string函数
|
|
这个函数在服务器开启gpc的情况下,会先对参数stripslashes一下,sanitize_level为3的情况下,将$input传入了getBare函数,并返回该函数执行结果。跟进getBare函数
|
|
可以看到仅仅是替换了一些html标签,并将HTML实体转换为字符,然后就返回内容了,看来做得并不多。
回到savegalleryoptions代码块
|
|
strtolower函数将输入的内容都转换为了小写,$_POST[‘customalbumsort’]的内容也经过了sanitize函数的过滤,最后就调用了_zp_galleryd的setSortType方法,继续跟进,找到_zp_gallery实例

$_zp_gallery即为Gallery类的一个实例,定义了一些protected变量

可以看到setSortType函数直接把参数传递给了set函数,在文件中找到set函数的定义
|
|
这里将参数值赋值给了受保护的变量$data,接下来继续回到savegalleryoptions代码块
|
|
$_zp_gallery实例调用了save方法,从Gallery类中找到save方法,将$this->data值serialize序列化后传递给了setOption函数
|
|
跟进setOption函数,在文件/zenphoto-zenphoto-1.4.8/zp-core/functions-basic.php
|
|
这里我们传入$key是gallery_data,$value是序列化后的$this->data,从代码中可以看到这两个在INSERT INTO前都要经过db_quote函数处理,跟进db_quote,我本地选择的mysqli扩展,所以定位到此文件/zenphoto-zenphoto-1.4.8/zp-core/functions-db-MySQLi.php
|
|
这里的$_zp_DB_connection即为我mysqli连接串,接着$_zp_DB_connection->real_escape_string($string)其实是一种面向对象的写法,面向过程的写法是这样的string mysqli_real_escape_string ( mysqli $link , string $escapestr ),官方文档里有说明该函数过滤哪些字符。

看来db_quote还是有用的,毕竟单双引号都被干掉了,所以这个输入点的是无法构造出带单双引号的payload的,回到漏洞复现的步骤,当时填入的payload是id,extractvalue(0x0a,concat(0x0a,(select version())))%23,这个payload是不受到过滤函数影响的,从数据库也可以看到payload被完整的插入进去了

到这里可以看到该二阶注入的第一步已经成功了,下面继续分析
访问
http://localhost:8888/zenphoto-zenphoto-1.4.8/zp-core/admin-upload.php?page=upload&tab=http&type=images
能触发漏洞,找到相关位置,/zenphoto-zenphoto-1.4.8/zp-core/admin-upload.php,66行
genAlbumList($albumlist);
找到该函数实现的位置,文件/zenphoto-zenphoto-1.4.8/zp-core/admin-functions.php
|
|
$albumsprime = $_zp_gallery->getAlbums(0);,这里有个我们熟悉的类实例$_zp_gallery,跟进/zenphoto-zenphoto-1.4.8/zp-core/class-gallery.php
|
|
$sorttype变量值为NULL,注意这里的$key参数,这个就是被我们污染了的变量,跟进下getAlbumSortKey的由来,/zenphoto-zenphoto-1.4.8/zp-core/class-gallery.php
|
|
由于传入的$sorttype为NULL,所以进入了if判断,调用getSortType函数去了,跟进/zenphoto-zenphoto-1.4.8/zp-core/class-gallery.php
|
|
这里回去调用get方法,并返回结果,所以找get方法去
|
|
这里传入一个$field,会返回$this->data值,而这个$this->data在构造函数中的实现为/zenphoto-zenphoto-1.4.8/zp-core/class-gallery.php
|
|
跟进getOption函数/zenphoto-zenphoto-1.4.8/zp-core/functions-basic.php
|
|
它会去数据库的options表里面去取对应$key字段的value并返回,而我们传入的$key=gallery_data,得到value字段值为
a:21:{s:21:"gallery_sortdirection";i:0;s:16:"gallery_sorttype";s:56:"id,extractvalue(0x0a,concat(0x0a,(select version())))%23";s:13:"gallery_title";s:31:"a:1:{s:5:"en_US";s:6:"相馆";}";s:19:"Gallery_description";s:99:"a:1:{s:5:"en_US";s:73:"You can insert your Gallery description on the Admin Options Gallery tab.";}";s:16:"gallery_password";N;s:12:"gallery_user";N;s:12:"gallery_hint";N;s:10:"hitcounter";N;s:13:"current_theme";s:7:"default";s:13:"website_title";s:0:"";s:11:"website_url";s:0:"";s:16:"gallery_security";s:6:"public";s:16:"login_user_field";N;s:24:"album_use_new_image_date";i:0;s:19:"thumb_select_images";i:0;s:17:"unprotected_pages";s:43:"a:2:{i:0;s:8:"register";i:1;s:7:"contact";}";s:13:"album_publish";i:1;s:13:"image_publish";i:1;s:30:"multilevel_thumb_select_images";i:0;s:14:"sort_direction";i:0;s:9:"codeblock";s:6:"a:0:{}”;}

回到构造函数,这个值赋值给$data,然后传入getSerializedArray函数,跟进一下/zenphoto-zenphoto-1.4.8/zp-core/functions-common.php
|
|
这里就是做一下简单的反序列化处理,以数组形式将结果返回。
这样就得到$this->data[‘gallery_sorttype’]的值id,extractvalue(0x0a,concat(0x0a,(select version())))%23,也就是$sorttype的值为id,extractvalue(0x0a,concat(0x0a,(select version())))%23,即$key的值。
接下来又调用了sortAlbumArray函数,找到该函数/zenphoto-zenphoto-1.4.8/zp-core/class-gallery.php
|
|
至此,二阶注入的第二步分析完,完整的过程就是这样,最后看下mysql的执行log,之前存入的payload被执行了即触发了漏洞。

总结
正如exploit-db上所说的,ZenPhoto1.4.8存在很多处二阶注入的地方。本质上仍然是在对用户可控的输入点过滤不够,过分依赖系统函数。