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 = 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: