0x01 Feedback

题目地址,题目描述如下,提示flag保存在flag文件中:

访问页面,是一个输入界面:

随便填写内容,Send后发现在下方有回显:

抓包发现发送的是XML格式的数据:

推测考察的是XXE读取文件,换个XXE payload测试是否解析参数实体:

没问题,那就读取本地文件/etc/passwd试试:

问题来了,读取flag文件,但不知道绝对路径呀,这里file://伪协议只能读取绝对路径的文件。

那就换个php://filter伪协议吧,它可以读取相对路径,直接尝试读本目录的flag文件吧:

读到了flag,解码为Securinets{XxexXE@Ll_Th3_W@Y}。

当然,可以修改payload看看这个feed.php的内容如下:

1
2
3
4
5
6
7
8
9
10
11
<?php
libxml_disable_entity_loader (false);
//$xmlfile = $_POST["ta"];
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$feedback = simplexml_import_dom($dom);
$author = $feedback->author;
echo "<h4>Thanks For you Feedback $author</h4>"

?>

0x02 Custom Location

题目地址,题目描述如下,说是找出数据库资格证:

访问页面,没啥功能,查看页面源码也没有东西:

尝试访问robots.txt来看看是不是有某些提示,出现报错信息,看来是开启了Debug模式,从页面可看出是用了Symfony这个框架来搭建的:

随便点击一个php文件即可查看它的源代码。

那么我们就可以查看index.php了,前提是需要知道这个框架的index.php是在public目录中的:

可以看到,里面又包含了一个文件进来,访问该文件,发现里面调用了”secret_ctf_location/env”,再访问该文件在数据库配置的地址找到了flag:

0x03 SQL Injected

题目地址,题目描述如下图,标题是SQL注入了,但提示说我不喜欢这名字,而且可以下载代码:

访问页面是个登录界面,可注册:

随便注册个用户,注册成功后自动登录进界面:

点击左上角的Flags界面显示”Error! You need to be an admin to access this area”即无权访问。

在index页面编辑title和内容然后Post,即可在下面更新显示内容:

在Find Posts一栏,输入指定用户名会显示该用户发布过的内容:

大致功能了解了,现在来源码审计。

项目目录如下:

简单理下,create_db.sql是执行创建数据库表和字段内容的SQL语句;db.php配置数据库连接信息;flags.php即显示Flags页面,其中关键是判断$_SESSION[‘role’]是否为1,是则从包含的secret.php中输出flag;secret.php中保存了flag;logout.php即登出。

关键的几个文件为login.php、register.php和index.php,因为这几个是程序的主要处理逻辑,涉及到的SQL操作都在这几个文件中。

先看看register.php关键部分的SQL操作,对用户输入的username和password参数调用了mysqli_real_escape_string()函数进行了转义过滤,然后写入INSERT语句,其中role字段值写死了为0,SQL语句执行成功后即跳转至index.php界面;也就是说,这里没法进行SQL注入了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (isset($_POST['username']) && isset($_POST['password'])) {
if(!empty($_POST['username']) && !empty($_POST['password'])) {
$success = true;
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);
$sql = "INSERT INTO users (login, password, role) VALUES ('". $username ."', '". $password ."', 0)";
try {
$conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
$success = false;
}
} else {
$success = false;
}

if($success) {
$_SESSION['username'] = $username;
$_SESSION['message'] = "<div class=\"alert alert-success\">
<strong>Success!</strong> Welcome aboard ".$_SESSION['username']." !
</div>";
header('location: index.php');
}
}

同样看看login.php中进行SQL操作的代码,都是进行了mysqli_real_escape_string()函数的转义过滤,然后执行SELECT查询语句,这里也无法进行SQL注入;注意到,将查询成功后获取的用户名即login字段值赋给\$_SESSION[‘username’],将role字段值赋值给​\$_SESSION[‘role’]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (isset($_POST['username']) && !empty($_POST['username']) && isset($_POST['password']) && !empty($_POST['password'])) {
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);
$sql = "SELECT * FROM users WHERE login='". $username ."' and password='". $password ."'";
$res = $conn->query($sql);
if($res->num_rows > 0) {
$user = $res->fetch_assoc();
$_SESSION['username'] = $user['login'];
$_SESSION['role'] = $user['role'];
header('location: index.php');
die();
} else {
$success = false;
}
}

最后看看index.php,可以看到界面输入的参数post、title、post_author等都进行了转义过滤,但是注意到在拼接的SQL语句中有的含有参数$_SESSION[‘username’]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php
...

if (isset($_POST['post']) && isset($_POST['title'])) {
if(!empty($_POST['post']) && !empty($_POST['title'])) {
$success = true;
$post = mysqli_real_escape_string($conn, $_POST['post']);
$title = mysqli_real_escape_string($conn, $_POST['title']);
$sql = "INSERT INTO posts (title, content, date, author) VALUES ('". $title ."', '". $post ."', CURDATE(), '". $_SESSION['username'] ."')";
try {
$conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
$success = false;
}
} else {
$success = false;
}

if($success) {
$_SESSION['message'] = "<div class=\"alert alert-success\">
<strong>Success!</strong> Your post has been saved!
</div>";
}
}
if (isset($_POST['post_author'])) {
$sql = "SELECT * FROM posts WHERE author = '". mysqli_real_escape_string($conn, $_POST['post_author']) ."'";
try {
$posts = $conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
}
} else {
$sql = "SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";
try {
$posts = $conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
}
}
?>

<!DOCTYPE html>
<html>
...

<div class="content">
<?php
if(isset($_SESSION['message']) && $_SESSION['message']) {
echo $_SESSION['message'];
$_SESSION['message'] = null;
}
?>
<div>
<form class="post-form" action="" method="post">
<input class="form-control" placeholder="Title" name="title" style="margin-bottom: 10px;" />
<textarea class="form-control" placeholder="Express yourself ..." name="post"></textarea>
<input type="submit" class="btn btn-primary post-btn" value="Post">
</form>
</div>
<h5 style="color: gray;">Find Posts</h5>
<form class="post-search" action="" method="post">
<input class="form-control" placeholder="username" style="width: 250px;" name="post_author" value="<?php echo $_POST['post_author'] ?>"/>
<button class="btn btn-outline-success" type="submit"> Find </button>
</form>
<?php
echo "<h5 class=\"results-count\">Results: $posts->num_rows</h5>";
if($posts->num_rows > 0) {
while($post = $posts->fetch_assoc()) {
?>
<div style="padding-bottom: 20px">
<div>
<h5 style="display: inline"> <?php echo $post['title'] ?></h5>
<h6 class="float-right"> <?php echo $post['date'] ?></h6>
</div>
<h6> <?php echo $post['content'] ?></h6>
<div class="float-right"> By: <?php echo $post['author'] ?> </div>
</div>
<hr/>
<?php
}
}
?>
</div>
</body>
</html>

因为以上几个文件中外界输入的参数都进行了mysqli_real_escape_string()函数的转义过滤,因此无法从输入的参数进行直接的SQL注入。但是前面注意到index.php中有些SQL语句含有拼接$_SESSION[‘username’]的写法,先列下出现的语句吧:

1
2
3
$sql = "INSERT INTO posts (title, content, date, author) VALUES ('". $title ."', '". $post ."', CURDATE(), '". $_SESSION['username'] ."')";

$sql = "SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";

只有两句,一个为INSERT一个为SELECT语句。前面我们知道$_SESSION[‘username’]是从login.php中查询表的login字段即用户名得来的。

再回看题目,flag就在Flags界面,但只有role为1的用户才能访问,那就需要SQL注入dump下role为1的用户名/密码登录访问来获取flag了。

那么就清晰了,在注册时往username进行SQL注入,虽然注册时调用mysqli_real_escape_string()函数转移过滤了,但是在存储进数据库的时候是你输入时的内容而不包含转义符\,因此在$_SESSION[‘username’]从users表中提取login字段时就是注入时的原格式,在拼接SQL语句时会直接造成SQL注入。

确认一下,使用M7’作为用户名注册登录,可以看到注册成功后直接跳转过去的index.php界面显示的用户名中的单引号前是有转移符\的,这是因为此时的$_SESSION[‘username’]是由注册时调用mysqli_real_escape_string()函数后直接赋值过来的结果:

其实存储在数据库中的内容是没有转移符\的,我们登出再登录,会发现转移符不见了,这是因为此时的$_SESSION[‘username’]是从数据库中查询得来的:

OK,那剩下的就是如何进行SQL注入了。

相比之下INSERT语句作用不大,但SELECT语句可以列出用户名和密码等字段值,因此利用SELECT语句进行SQL注入来dump role为1的用户信息。

构造payload前先看下create_db.sql中的posts表是存在5个字段,而users表是存在id、login、passwod和role等4个字段:

1
2
3
create database webn;
create table users (id int auto_increment primary key, login varchar(100), password varchar(100), role boolean default 0);
create table posts (id int auto_increment primary key, title varchar(50), content text, date Date, author varchar(100));

注册输入如下构造的用户名:

1
'union select id,login,password,role,5 from users where role=1#

注册完新用户后登出再登录,可以看到输出了role为1、login即用户名为root的5个输出字段信息:

用root/jjLLgTGk3uif2rKBVwqH登录再访问Flags界面即可拿到flag: